Branch data Line data Source code
1 : : /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
2 : : *
3 : : * Copyright 2025 GNOME Foundation, Inc.
4 : : *
5 : : * SPDX-License-Identifier: GPL-2.0-or-later
6 : : *
7 : : * This program is free software; you can redistribute it and/or modify
8 : : * it under the terms of the GNU General Public License as published by
9 : : * the Free Software Foundation; either version 2 of the License, or
10 : : * (at your option) any later version.
11 : : *
12 : : * This program is distributed in the hope that it will be useful,
13 : : * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 : : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 : : * GNU General Public License for more details.
16 : : *
17 : : * You should have received a copy of the GNU General Public License
18 : : * along with this program; if not, see <http://www.gnu.org/licenses/>.
19 : : *
20 : : * Authors:
21 : : * - Ignacy Kuchciński <ignacykuchcinski@gnome.org>
22 : : */
23 : :
24 : : #include "config.h"
25 : :
26 : : #include <glib/gi18n.h>
27 : : #include <stdint.h>
28 : :
29 : : #include "screen-time-statistics-row.h"
30 : :
31 : : /**
32 : : * MctScreenTimeStatisticsRow:
33 : : *
34 : : * A widget which shows screen time bar chart for screen time usage
35 : : * records for the selected user.
36 : : *
37 : : * Since: 0.14.0
38 : : */
39 : : struct _MctScreenTimeStatisticsRow
40 : : {
41 : : CcScreenTimeStatisticsRow parent;
42 : :
43 : : GDBusConnection *connection; /* (owned) */
44 : : uid_t uid;
45 : : };
46 : :
47 [ # # # # : 0 : G_DEFINE_TYPE (MctScreenTimeStatisticsRow, mct_screen_time_statistics_row, CC_TYPE_SCREEN_TIME_STATISTICS_ROW)
# # ]
48 : :
49 : : typedef enum
50 : : {
51 : : PROP_CONNECTION = 1,
52 : : PROP_UID,
53 : : } MctScreenTimeStatisticsRowProperty;
54 : :
55 : : static GParamSpec *properties[PROP_UID + 1];
56 : :
57 : : static void
58 : 0 : mct_screen_time_statistics_row_get_property (GObject *object,
59 : : unsigned int property_id,
60 : : GValue *value,
61 : : GParamSpec *spec)
62 : : {
63 : 0 : MctScreenTimeStatisticsRow *self = MCT_SCREEN_TIME_STATISTICS_ROW (object);
64 : :
65 [ # # # ]: 0 : switch ((MctScreenTimeStatisticsRowProperty) property_id)
66 : : {
67 : 0 : case PROP_CONNECTION:
68 : 0 : g_value_set_object (value, self->connection);
69 : 0 : break;
70 : :
71 : 0 : case PROP_UID:
72 : 0 : g_value_set_uint (value, self->uid);
73 : 0 : break;
74 : :
75 : 0 : default:
76 : 0 : G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, spec);
77 : 0 : break;
78 : : }
79 : 0 : }
80 : :
81 : : static void
82 : 0 : mct_screen_time_statistics_row_set_property (GObject *object,
83 : : unsigned int property_id,
84 : : const GValue *value,
85 : : GParamSpec *spec)
86 : : {
87 : 0 : MctScreenTimeStatisticsRow *self = MCT_SCREEN_TIME_STATISTICS_ROW (object);
88 : :
89 [ # # # ]: 0 : switch ((MctScreenTimeStatisticsRowProperty) property_id)
90 : : {
91 : 0 : case PROP_CONNECTION:
92 : : /* Construct-only. May not be %NULL. */
93 : 0 : g_assert (self->connection == NULL);
94 : 0 : self->connection = g_value_dup_object (value);
95 : 0 : g_assert (self->connection != NULL);
96 : 0 : break;
97 : :
98 : 0 : case PROP_UID:
99 : : /* Construct-only. */
100 : 0 : g_assert (self->uid == 0);
101 : 0 : self->uid = g_value_get_uint (value);
102 : 0 : g_assert (self->uid != 0);
103 : 0 : break;
104 : :
105 : 0 : default:
106 : 0 : G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, spec);
107 : 0 : break;
108 : : }
109 : 0 : }
110 : :
111 : : static void
112 : 0 : mct_screen_time_statistics_row_constructed (GObject *object)
113 : : {
114 : 0 : MctScreenTimeStatisticsRow *self = MCT_SCREEN_TIME_STATISTICS_ROW (object);
115 : :
116 : 0 : g_assert (self->connection != NULL);
117 : 0 : g_assert (self->uid != 0);
118 : :
119 : 0 : G_OBJECT_CLASS (mct_screen_time_statistics_row_parent_class)->constructed (object);
120 : 0 : }
121 : :
122 : : static void
123 : 0 : mct_screen_time_statistics_row_dispose (GObject *object)
124 : : {
125 : 0 : MctScreenTimeStatisticsRow *self = MCT_SCREEN_TIME_STATISTICS_ROW (object);
126 : :
127 [ # # ]: 0 : g_clear_object (&self->connection);
128 : :
129 : 0 : G_OBJECT_CLASS (mct_screen_time_statistics_row_parent_class)->dispose (object);
130 : 0 : }
131 : :
132 : : typedef struct
133 : : {
134 : : GDate *start_date;
135 : : size_t n_days;
136 : : double *screen_time_per_day;
137 : : } LoadDataData;
138 : :
139 : : static void
140 : 0 : load_data_data_free (LoadDataData *data)
141 : : {
142 : 0 : g_date_free (data->start_date);
143 : 0 : g_free (data->screen_time_per_day);
144 : 0 : g_free (data);
145 : 0 : }
146 : :
147 [ # # ]: 0 : G_DEFINE_AUTOPTR_CLEANUP_FUNC (LoadDataData, load_data_data_free)
148 : :
149 : : /**
150 : : * load_data_data_new:
151 : : * @start_date: (transfer none): an initialized #GDate
152 : : * @n_days: number of days
153 : : * @screen_time_per_day: (transfer full): screen time per day
154 : : */
155 : : static LoadDataData *
156 : 0 : load_data_data_new (GDate *start_date,
157 : : size_t n_days,
158 : : double *screen_time_per_day)
159 : : {
160 : 0 : g_autoptr(LoadDataData) data = g_new0 (LoadDataData, 1);
161 : :
162 : 0 : data->start_date = g_date_copy (start_date);
163 : 0 : data->n_days = n_days;
164 : 0 : data->screen_time_per_day = screen_time_per_day;
165 : :
166 : 0 : return g_steal_pointer (&data);
167 : : }
168 : :
169 : : static void
170 : : query_usage_cb (GObject *object,
171 : : GAsyncResult *result,
172 : : void *user_data);
173 : :
174 : : static void
175 : 0 : mct_screen_time_statistics_row_load_data_async (CcScreenTimeStatisticsRow *screen_time_statistics_row,
176 : : GCancellable *cancellable,
177 : : GAsyncReadyCallback callback,
178 : : void *user_data)
179 : : {
180 : 0 : MctScreenTimeStatisticsRow *self = MCT_SCREEN_TIME_STATISTICS_ROW (screen_time_statistics_row);
181 : 0 : g_autoptr(GTask) task = NULL;
182 : :
183 : 0 : task = g_task_new (self, cancellable, callback, user_data);
184 [ # # ]: 0 : g_task_set_source_tag (task, mct_screen_time_statistics_row_load_data_async);
185 : :
186 : : /* Load and parse the screen time usage periods of the child account using
187 : : * the org.freedesktop.MalcontentTimer1.Parent malcontent-timerd interface.
188 : : * See `timeLimitsManager.js` in gnome-shell for the code which records the
189 : : * usage using org.freedesktop.MalcontentTimer1.Child interface. */
190 : 0 : g_dbus_connection_call (self->connection,
191 : : "org.freedesktop.MalcontentTimer1",
192 : : "/org/freedesktop/MalcontentTimer1",
193 : : "org.freedesktop.MalcontentTimer1.Parent",
194 : : "QueryUsage",
195 : : g_variant_new ("(uss)",
196 : : self->uid,
197 : : "login-session",
198 : : ""),
199 : : (const GVariantType *) "(a(tt))",
200 : : G_DBUS_CALL_FLAGS_NONE,
201 : : -1,
202 : : cancellable,
203 : : query_usage_cb,
204 : : g_steal_pointer (&task));
205 : 0 : }
206 : :
207 : : static void allocate_duration_to_days (const GDate *model_start_date,
208 : : GArray *model_screen_time_per_day,
209 : : uint64_t start_wall_time_secs,
210 : : uint64_t duration_secs);
211 : :
212 : : static void
213 : 0 : query_usage_cb (GObject *object,
214 : : GAsyncResult *result,
215 : : void *user_data)
216 : : {
217 : 0 : GDBusConnection *connection = G_DBUS_CONNECTION (object);
218 [ # # ]: 0 : g_autoptr(GTask) task = G_TASK (g_steal_pointer (&user_data));
219 : : GDate new_model_start_date;
220 : : size_t new_model_n_days;
221 [ # # ]: 0 : g_autoptr(GArray) new_model_screen_time_per_day = NULL; /* (element-type double) */
222 [ # # ]: 0 : g_autoptr(GVariant) result_variant = NULL;
223 [ # # ]: 0 : g_autoptr(GError) local_error = NULL;
224 [ # # ]: 0 : g_autoptr(GVariantIter) entries_iter = NULL;
225 : : uint64_t start_wall_time_secs, end_wall_time_secs;
226 : :
227 : 0 : g_date_clear (&new_model_start_date, 1);
228 : :
229 : 0 : result_variant = g_dbus_connection_call_finish (connection, result, &local_error);
230 : :
231 [ # # ]: 0 : if (result_variant == NULL)
232 : : {
233 : 0 : g_task_return_error (task, g_steal_pointer (&local_error));
234 : 0 : return;
235 : : }
236 : :
237 : 0 : g_variant_get (result_variant, "(a(tt))", &entries_iter);
238 [ # # ]: 0 : while (g_variant_iter_loop (entries_iter, "(tt)", &start_wall_time_secs, &end_wall_time_secs))
239 : : {
240 : : /* Set up the model if this is the first iteration */
241 [ # # ]: 0 : if (!g_date_valid (&new_model_start_date))
242 : : {
243 : 0 : g_date_set_time_t (&new_model_start_date, start_wall_time_secs);
244 : 0 : new_model_screen_time_per_day = g_array_new (FALSE, TRUE, sizeof (double));
245 : : }
246 : :
247 : : /* Interpret the data */
248 : 0 : uint64_t duration_secs = end_wall_time_secs - start_wall_time_secs;
249 : 0 : allocate_duration_to_days (&new_model_start_date, new_model_screen_time_per_day,
250 : : start_wall_time_secs, duration_secs);
251 : : }
252 : :
253 : : /* Was the data empty? */
254 [ # # # # ]: 0 : if (new_model_screen_time_per_day == NULL || new_model_screen_time_per_day->len == 0)
255 : : {
256 : 0 : g_set_error (&local_error, G_FILE_ERROR, G_FILE_ERROR_NOENT,
257 : 0 : _("Failed to load session history data: %s"),
258 : : _("Data is empty"));
259 : 0 : g_task_return_error (task, g_steal_pointer (&local_error));
260 : 0 : return;
261 : : }
262 : :
263 : 0 : new_model_n_days = new_model_screen_time_per_day->len;
264 : :
265 : 0 : LoadDataData *data = load_data_data_new (&new_model_start_date,
266 : : new_model_n_days,
267 : 0 : (double *) g_array_free (g_steal_pointer (&new_model_screen_time_per_day), FALSE));
268 : 0 : g_task_return_pointer (task, data, (GDestroyNotify) load_data_data_free);
269 : : }
270 : :
271 : : static void allocate_duration_to_day (const GDate *model_start_date,
272 : : GArray *model_screen_time_per_day,
273 : : GDateTime *start_date_time,
274 : : uint64_t duration_secs);
275 : :
276 : : /* Take the time period [start_wall_time_secs, start_wall_time_secs + duration_secs]
277 : : * and add it to the model, splitting it between day boundaries if needed, and
278 : : * extending the `GArray` if needed. */
279 : : static void
280 : 0 : allocate_duration_to_days (const GDate *model_start_date,
281 : : GArray *model_screen_time_per_day,
282 : : uint64_t start_wall_time_secs,
283 : : uint64_t duration_secs)
284 : : {
285 : 0 : g_autoptr(GDateTime) start_date_time = NULL;
286 : :
287 : 0 : start_date_time = g_date_time_new_from_unix_local (start_wall_time_secs);
288 : :
289 [ # # ]: 0 : while (duration_secs > 0)
290 : : {
291 : 0 : g_autoptr(GDateTime) start_of_day = NULL, start_of_next_day = NULL;
292 : 0 : g_autoptr(GDateTime) new_start_date_time = NULL;
293 : : GTimeSpan span_usecs;
294 : : uint64_t span_secs;
295 : :
296 : 0 : start_of_day = g_date_time_new_local (g_date_time_get_year (start_date_time),
297 : : g_date_time_get_month (start_date_time),
298 : : g_date_time_get_day_of_month (start_date_time),
299 : : 0, 0, 0);
300 : 0 : g_assert (start_of_day != NULL);
301 : 0 : start_of_next_day = g_date_time_add_days (start_of_day, 1);
302 : 0 : g_assert (start_of_next_day != NULL);
303 : :
304 : 0 : span_usecs = g_date_time_difference (start_of_next_day, start_date_time);
305 : 0 : span_secs = span_usecs / G_USEC_PER_SEC;
306 [ # # ]: 0 : if (span_secs > duration_secs)
307 : 0 : span_secs = duration_secs;
308 : :
309 : 0 : allocate_duration_to_day (model_start_date, model_screen_time_per_day,
310 : : start_date_time, span_secs);
311 : :
312 : 0 : duration_secs -= span_secs;
313 : 0 : new_start_date_time = g_date_time_add_seconds (start_date_time, span_secs);
314 : 0 : g_date_time_unref (start_date_time);
315 : 0 : start_date_time = g_steal_pointer (&new_start_date_time);
316 : : }
317 : 0 : }
318 : :
319 : : /* Take the time period [start_date_time, start_date_time + duration_secs]
320 : : * and add it to the model, extending the `GArray` if needed. The time period
321 : : * *must not* cross a day boundary, i.e. it’s invalid to call this function
322 : : * with `start_date_time` as 23:00 on a day, and `duration_secs` as 2h.
323 : : *
324 : : * Note that @model_screen_time_per_day is in minutes, whereas @duration_secs
325 : : * is in seconds. */
326 : : static void
327 : 0 : allocate_duration_to_day (const GDate *model_start_date,
328 : : GArray *model_screen_time_per_day,
329 : : GDateTime *start_date_time,
330 : : uint64_t duration_secs)
331 : : {
332 : : GDate start_date;
333 : : int diff_days;
334 : : double *element;
335 : :
336 : 0 : g_date_clear (&start_date, 1);
337 : 0 : g_date_set_dmy (&start_date,
338 : 0 : g_date_time_get_day_of_month (start_date_time),
339 : 0 : g_date_time_get_month (start_date_time),
340 : 0 : g_date_time_get_year (start_date_time));
341 : :
342 : 0 : diff_days = g_date_days_between (model_start_date, &start_date);
343 : 0 : g_assert (diff_days >= 0);
344 : :
345 : : /* If the new day is outside the range of the model, insert it at the right
346 : : * index. This will automatically create the indices between, and initialise
347 : : * them to zero, which is what we want. */
348 [ # # ]: 0 : if ((unsigned int) diff_days >= model_screen_time_per_day->len)
349 : : {
350 : 0 : const double new_val = 0.0;
351 : 0 : g_array_insert_val (model_screen_time_per_day, diff_days, new_val);
352 : : }
353 : :
354 : 0 : element = &g_array_index (model_screen_time_per_day, double, diff_days);
355 : 0 : *element += duration_secs / 60.0;
356 : 0 : }
357 : :
358 : : static gboolean
359 : 0 : mct_screen_time_statistics_row_load_data_finish (CcScreenTimeStatisticsRow *screen_time_statistics_row,
360 : : GAsyncResult *result,
361 : : GDate *out_new_model_start_date,
362 : : size_t *out_new_model_n_days,
363 : : double **out_new_model_screen_time_per_day,
364 : : GError **error)
365 : : {
366 : 0 : g_autoptr(LoadDataData) data = NULL;
367 : :
368 : 0 : g_return_val_if_fail (g_task_is_valid (result, screen_time_statistics_row), FALSE);
369 : :
370 : : /* Set up in case of error. */
371 [ # # ]: 0 : if (out_new_model_start_date != NULL)
372 : 0 : g_date_clear (out_new_model_start_date, 1);
373 [ # # ]: 0 : if (out_new_model_n_days != NULL)
374 : 0 : *out_new_model_n_days = 0;
375 [ # # ]: 0 : if (out_new_model_screen_time_per_day != NULL)
376 : 0 : *out_new_model_screen_time_per_day = NULL;
377 : :
378 : 0 : data = g_task_propagate_pointer (G_TASK (result), error);
379 : :
380 [ # # ]: 0 : if (data == NULL)
381 : 0 : return FALSE;
382 : :
383 : : /* Success! */
384 [ # # ]: 0 : if (out_new_model_start_date != NULL)
385 : 0 : *out_new_model_start_date = *data->start_date;
386 [ # # ]: 0 : if (out_new_model_n_days != NULL)
387 : 0 : *out_new_model_n_days = data->n_days;
388 [ # # ]: 0 : if (out_new_model_screen_time_per_day != NULL)
389 : 0 : *out_new_model_screen_time_per_day = g_steal_pointer (&data->screen_time_per_day);
390 : :
391 : 0 : return TRUE;
392 : : }
393 : :
394 : : static void
395 : 0 : mct_screen_time_statistics_row_class_init (MctScreenTimeStatisticsRowClass *klass)
396 : : {
397 : 0 : GObjectClass *object_class = G_OBJECT_CLASS (klass);
398 : 0 : CcScreenTimeStatisticsRowClass *screen_time_statistics_row_class = CC_SCREEN_TIME_STATISTICS_ROW_CLASS (klass);
399 : :
400 : 0 : object_class->get_property = mct_screen_time_statistics_row_get_property;
401 : 0 : object_class->set_property = mct_screen_time_statistics_row_set_property;
402 : 0 : object_class->constructed = mct_screen_time_statistics_row_constructed;
403 : 0 : object_class->dispose = mct_screen_time_statistics_row_dispose;
404 : :
405 : 0 : screen_time_statistics_row_class->load_data_async = mct_screen_time_statistics_row_load_data_async;
406 : 0 : screen_time_statistics_row_class->load_data_finish = mct_screen_time_statistics_row_load_data_finish;
407 : :
408 : : /**
409 : : * MctScreenTimeStatisticsRow:connection: (not nullable)
410 : : *
411 : : * A connection to the system bus, where malcontent-timerd runs. It’s
412 : : * provided to allow an existing connection to be re-used and for testing
413 : : * purposes.
414 : : *
415 : : * Since 0.14.0
416 : : */
417 : 0 : properties[PROP_CONNECTION] = g_param_spec_object ("connection", NULL, NULL,
418 : : G_TYPE_DBUS_CONNECTION,
419 : : G_PARAM_READWRITE |
420 : : G_PARAM_CONSTRUCT_ONLY |
421 : : G_PARAM_STATIC_STRINGS);
422 : :
423 : : /**
424 : : * MctScreenTimeStatisticsRow:uid:
425 : : *
426 : : * The selected user’s UID.
427 : : *
428 : : * Since 0.14.0
429 : : */
430 : 0 : properties[PROP_UID] = g_param_spec_uint ("uid", NULL, NULL,
431 : : 0, G_MAXUINT, 0,
432 : : G_PARAM_READWRITE |
433 : : G_PARAM_CONSTRUCT_ONLY |
434 : : G_PARAM_STATIC_STRINGS);
435 : :
436 : 0 : g_object_class_install_properties (object_class, G_N_ELEMENTS (properties), properties);
437 : 0 : }
438 : :
439 : : static void
440 : 0 : mct_screen_time_statistics_row_init (MctScreenTimeStatisticsRow *self)
441 : : {
442 : 0 : }
443 : :
444 : : /**
445 : : * mct_screen_time_statistics_row_new:
446 : : * @connection: (transfer none): a #GDBusConnection to use
447 : : * @uid: user ID to use
448 : : *
449 : : * Create a new #MctScreenTimeStatisticsRow widget.
450 : : *
451 : : * Returns: (transfer full): a new screen time statistics row
452 : : * Since: 0.14.0
453 : : */
454 : : MctScreenTimeStatisticsRow *
455 : 0 : mct_screen_time_statistics_row_new (GDBusConnection *connection,
456 : : uid_t uid)
457 : : {
458 : 0 : g_return_val_if_fail (G_IS_DBUS_CONNECTION (connection), NULL);
459 : :
460 : 0 : return g_object_new (MCT_TYPE_SCREEN_TIME_STATISTICS_ROW,
461 : : "connection", connection,
462 : : "uid", uid,
463 : : NULL);
464 : : }
|