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 : : unsigned long usage_changed_id;
44 : :
45 : : GDBusConnection *connection; /* (owned) */
46 : : uid_t uid;
47 : : };
48 : :
49 [ # # # # : 0 : G_DEFINE_TYPE (MctScreenTimeStatisticsRow, mct_screen_time_statistics_row, CC_TYPE_SCREEN_TIME_STATISTICS_ROW)
# # ]
50 : :
51 : : typedef enum
52 : : {
53 : : PROP_CONNECTION = 1,
54 : : PROP_UID,
55 : : } MctScreenTimeStatisticsRowProperty;
56 : :
57 : : static GParamSpec *properties[PROP_UID + 1];
58 : :
59 : : static void
60 : 0 : mct_screen_time_statistics_row_get_property (GObject *object,
61 : : unsigned int property_id,
62 : : GValue *value,
63 : : GParamSpec *spec)
64 : : {
65 : 0 : MctScreenTimeStatisticsRow *self = MCT_SCREEN_TIME_STATISTICS_ROW (object);
66 : :
67 [ # # # ]: 0 : switch ((MctScreenTimeStatisticsRowProperty) property_id)
68 : : {
69 : 0 : case PROP_CONNECTION:
70 : 0 : g_value_set_object (value, self->connection);
71 : 0 : break;
72 : :
73 : 0 : case PROP_UID:
74 : 0 : g_value_set_uint (value, self->uid);
75 : 0 : break;
76 : :
77 : 0 : default:
78 : 0 : G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, spec);
79 : 0 : break;
80 : : }
81 : 0 : }
82 : :
83 : : static void
84 : 0 : mct_screen_time_statistics_row_set_property (GObject *object,
85 : : unsigned int property_id,
86 : : const GValue *value,
87 : : GParamSpec *spec)
88 : : {
89 : 0 : MctScreenTimeStatisticsRow *self = MCT_SCREEN_TIME_STATISTICS_ROW (object);
90 : :
91 [ # # # ]: 0 : switch ((MctScreenTimeStatisticsRowProperty) property_id)
92 : : {
93 : 0 : case PROP_CONNECTION:
94 : : /* Construct-only. May not be %NULL. */
95 : 0 : g_assert (self->connection == NULL);
96 : 0 : self->connection = g_value_dup_object (value);
97 : 0 : g_assert (self->connection != NULL);
98 : 0 : break;
99 : :
100 : 0 : case PROP_UID:
101 : : /* Construct-only. */
102 : 0 : g_assert (self->uid == 0);
103 : 0 : self->uid = g_value_get_uint (value);
104 : 0 : g_assert (self->uid != 0);
105 : 0 : break;
106 : :
107 : 0 : default:
108 : 0 : G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, spec);
109 : 0 : break;
110 : : }
111 : 0 : }
112 : :
113 : : static void
114 : 0 : mct_screen_time_statistics_row_constructed (GObject *object)
115 : : {
116 : 0 : MctScreenTimeStatisticsRow *self = MCT_SCREEN_TIME_STATISTICS_ROW (object);
117 : :
118 : 0 : g_assert (self->connection != NULL);
119 : 0 : g_assert (self->uid != 0);
120 : :
121 : 0 : G_OBJECT_CLASS (mct_screen_time_statistics_row_parent_class)->constructed (object);
122 : 0 : }
123 : :
124 : : static void
125 : 0 : mct_screen_time_statistics_row_dispose (GObject *object)
126 : : {
127 : 0 : MctScreenTimeStatisticsRow *self = MCT_SCREEN_TIME_STATISTICS_ROW (object);
128 : :
129 [ # # # # ]: 0 : if (self->connection != NULL && self->usage_changed_id != 0)
130 : : {
131 : 0 : g_dbus_connection_signal_unsubscribe (self->connection, self->usage_changed_id);
132 : 0 : self->usage_changed_id = 0;
133 : : }
134 : :
135 [ # # ]: 0 : g_clear_object (&self->connection);
136 : :
137 : 0 : G_OBJECT_CLASS (mct_screen_time_statistics_row_parent_class)->dispose (object);
138 : 0 : }
139 : :
140 : : typedef struct
141 : : {
142 : : GDate *start_date;
143 : : size_t n_days;
144 : : double *screen_time_per_day;
145 : : } LoadDataData;
146 : :
147 : : static void
148 : 0 : load_data_data_free (LoadDataData *data)
149 : : {
150 : 0 : g_date_free (data->start_date);
151 : 0 : g_free (data->screen_time_per_day);
152 : 0 : g_free (data);
153 : 0 : }
154 : :
155 [ # # ]: 0 : G_DEFINE_AUTOPTR_CLEANUP_FUNC (LoadDataData, load_data_data_free)
156 : :
157 : : /**
158 : : * load_data_data_new:
159 : : * @start_date: (transfer none): an initialized [struct@GLib.Date]
160 : : * @n_days: number of days
161 : : * @screen_time_per_day: (transfer full): screen time per day
162 : : */
163 : : static LoadDataData *
164 : 0 : load_data_data_new (GDate *start_date,
165 : : size_t n_days,
166 : : double *screen_time_per_day)
167 : : {
168 : 0 : g_autoptr(LoadDataData) data = g_new0 (LoadDataData, 1);
169 : :
170 : 0 : data->start_date = g_date_copy (start_date);
171 : 0 : data->n_days = n_days;
172 : 0 : data->screen_time_per_day = screen_time_per_day;
173 : :
174 : 0 : return g_steal_pointer (&data);
175 : : }
176 : :
177 : : static void
178 : : query_usage_cb (GObject *object,
179 : : GAsyncResult *result,
180 : : void *user_data);
181 : : static void
182 : : usage_changed_cb (GDBusConnection *connection,
183 : : const char *sender_name,
184 : : const char *object_path,
185 : : const char *interface_name,
186 : : const char *signal_name,
187 : : GVariant *parameters,
188 : : void *user_data);
189 : :
190 : : static void
191 : 0 : mct_screen_time_statistics_row_load_data_async (CcScreenTimeStatisticsRow *screen_time_statistics_row,
192 : : GCancellable *cancellable,
193 : : GAsyncReadyCallback callback,
194 : : void *user_data)
195 : : {
196 : 0 : MctScreenTimeStatisticsRow *self = MCT_SCREEN_TIME_STATISTICS_ROW (screen_time_statistics_row);
197 : 0 : g_autoptr(GTask) task = NULL;
198 : :
199 : 0 : task = g_task_new (self, cancellable, callback, user_data);
200 [ # # ]: 0 : g_task_set_source_tag (task, mct_screen_time_statistics_row_load_data_async);
201 : :
202 : : /* Load and parse the screen time usage periods of the child account using
203 : : * the org.freedesktop.MalcontentTimer1.Parent malcontent-timerd interface.
204 : : * See `timeLimitsManager.js` in gnome-shell for the code which records the
205 : : * usage using org.freedesktop.MalcontentTimer1.Child interface. */
206 : 0 : g_dbus_connection_call (self->connection,
207 : : "org.freedesktop.MalcontentTimer1",
208 : : "/org/freedesktop/MalcontentTimer1",
209 : : "org.freedesktop.MalcontentTimer1.Parent",
210 : : "QueryUsage",
211 : : g_variant_new ("(uss)",
212 : : self->uid,
213 : : "login-session",
214 : : ""),
215 : : (const GVariantType *) "(a(tt))",
216 : : G_DBUS_CALL_FLAGS_NONE,
217 : : -1,
218 : : cancellable,
219 : : query_usage_cb,
220 : : g_steal_pointer (&task));
221 : :
222 : : /* Start watching for screen time usage changes coming from the daemon. */
223 : 0 : self->usage_changed_id =
224 : 0 : g_dbus_connection_signal_subscribe (self->connection,
225 : : "org.freedesktop.MalcontentTimer1",
226 : : "org.freedesktop.MalcontentTimer1.Parent",
227 : : "UsageChanged",
228 : : "/org/freedesktop/MalcontentTimer1",
229 : : NULL,
230 : : G_DBUS_SIGNAL_FLAGS_NONE,
231 : : usage_changed_cb,
232 : : self,
233 : : NULL);
234 : 0 : }
235 : :
236 : : static void allocate_duration_to_days (const GDate *model_start_date,
237 : : GArray *model_screen_time_per_day,
238 : : uint64_t start_wall_time_secs,
239 : : uint64_t duration_secs);
240 : :
241 : : static void
242 : 0 : query_usage_cb (GObject *object,
243 : : GAsyncResult *result,
244 : : void *user_data)
245 : : {
246 : 0 : GDBusConnection *connection = G_DBUS_CONNECTION (object);
247 [ # # ]: 0 : g_autoptr(GTask) task = G_TASK (g_steal_pointer (&user_data));
248 : : GDate new_model_start_date;
249 : : size_t new_model_n_days;
250 [ # # ]: 0 : g_autoptr(GArray) new_model_screen_time_per_day = NULL; /* (element-type double) */
251 [ # # ]: 0 : g_autoptr(GVariant) result_variant = NULL;
252 [ # # ]: 0 : g_autoptr(GError) local_error = NULL;
253 [ # # ]: 0 : g_autoptr(GVariantIter) entries_iter = NULL;
254 : : uint64_t start_wall_time_secs, end_wall_time_secs;
255 : :
256 : 0 : g_date_clear (&new_model_start_date, 1);
257 : :
258 : 0 : result_variant = g_dbus_connection_call_finish (connection, result, &local_error);
259 : :
260 [ # # ]: 0 : if (result_variant == NULL)
261 : : {
262 : 0 : g_task_return_error (task, g_steal_pointer (&local_error));
263 : 0 : return;
264 : : }
265 : :
266 : 0 : g_variant_get (result_variant, "(a(tt))", &entries_iter);
267 [ # # ]: 0 : while (g_variant_iter_loop (entries_iter, "(tt)", &start_wall_time_secs, &end_wall_time_secs))
268 : : {
269 : : /* Set up the model if this is the first iteration */
270 [ # # ]: 0 : if (!g_date_valid (&new_model_start_date))
271 : : {
272 : 0 : g_date_set_time_t (&new_model_start_date, start_wall_time_secs);
273 : 0 : new_model_screen_time_per_day = g_array_new (FALSE, TRUE, sizeof (double));
274 : : }
275 : :
276 : : /* Interpret the data */
277 : 0 : uint64_t duration_secs = end_wall_time_secs - start_wall_time_secs;
278 : 0 : allocate_duration_to_days (&new_model_start_date, new_model_screen_time_per_day,
279 : : start_wall_time_secs, duration_secs);
280 : : }
281 : :
282 : : /* Was the data empty? */
283 [ # # # # ]: 0 : if (new_model_screen_time_per_day == NULL || new_model_screen_time_per_day->len == 0)
284 : : {
285 : 0 : g_set_error (&local_error, G_FILE_ERROR, G_FILE_ERROR_NOENT,
286 : 0 : _("Failed to load session history data: %s"),
287 : : _("Data is empty"));
288 : 0 : g_task_return_error (task, g_steal_pointer (&local_error));
289 : 0 : return;
290 : : }
291 : :
292 : 0 : new_model_n_days = new_model_screen_time_per_day->len;
293 : :
294 : 0 : LoadDataData *data = load_data_data_new (&new_model_start_date,
295 : : new_model_n_days,
296 : 0 : (double *) g_array_free (g_steal_pointer (&new_model_screen_time_per_day), FALSE));
297 : 0 : g_task_return_pointer (task, data, (GDestroyNotify) load_data_data_free);
298 : : }
299 : :
300 : : static void allocate_duration_to_day (const GDate *model_start_date,
301 : : GArray *model_screen_time_per_day,
302 : : GDateTime *start_date_time,
303 : : uint64_t duration_secs);
304 : :
305 : : /* Take the time period [start_wall_time_secs, start_wall_time_secs + duration_secs]
306 : : * and add it to the model, splitting it between day boundaries if needed, and
307 : : * extending the `GArray` if needed. */
308 : : static void
309 : 0 : allocate_duration_to_days (const GDate *model_start_date,
310 : : GArray *model_screen_time_per_day,
311 : : uint64_t start_wall_time_secs,
312 : : uint64_t duration_secs)
313 : : {
314 : 0 : g_autoptr(GDateTime) start_date_time = NULL;
315 : :
316 : 0 : start_date_time = g_date_time_new_from_unix_local (start_wall_time_secs);
317 : :
318 [ # # ]: 0 : while (duration_secs > 0)
319 : : {
320 : 0 : g_autoptr(GDateTime) start_of_day = NULL, start_of_next_day = NULL;
321 : 0 : g_autoptr(GDateTime) new_start_date_time = NULL;
322 : : GTimeSpan span_usecs;
323 : : uint64_t span_secs;
324 : :
325 : 0 : start_of_day = g_date_time_new_local (g_date_time_get_year (start_date_time),
326 : : g_date_time_get_month (start_date_time),
327 : : g_date_time_get_day_of_month (start_date_time),
328 : : 0, 0, 0);
329 : 0 : g_assert (start_of_day != NULL);
330 : 0 : start_of_next_day = g_date_time_add_days (start_of_day, 1);
331 : 0 : g_assert (start_of_next_day != NULL);
332 : :
333 : 0 : span_usecs = g_date_time_difference (start_of_next_day, start_date_time);
334 : 0 : span_secs = span_usecs / G_USEC_PER_SEC;
335 [ # # ]: 0 : if (span_secs > duration_secs)
336 : 0 : span_secs = duration_secs;
337 : :
338 : 0 : allocate_duration_to_day (model_start_date, model_screen_time_per_day,
339 : : start_date_time, span_secs);
340 : :
341 : 0 : duration_secs -= span_secs;
342 : 0 : new_start_date_time = g_date_time_add_seconds (start_date_time, span_secs);
343 : 0 : g_date_time_unref (start_date_time);
344 : 0 : start_date_time = g_steal_pointer (&new_start_date_time);
345 : : }
346 : 0 : }
347 : :
348 : : /* Take the time period [start_date_time, start_date_time + duration_secs]
349 : : * and add it to the model, extending the `GArray` if needed. The time period
350 : : * *must not* cross a day boundary, i.e. it’s invalid to call this function
351 : : * with `start_date_time` as 23:00 on a day, and `duration_secs` as 2h.
352 : : *
353 : : * Note that @model_screen_time_per_day is in minutes, whereas @duration_secs
354 : : * is in seconds. */
355 : : static void
356 : 0 : allocate_duration_to_day (const GDate *model_start_date,
357 : : GArray *model_screen_time_per_day,
358 : : GDateTime *start_date_time,
359 : : uint64_t duration_secs)
360 : : {
361 : : GDate start_date;
362 : : int diff_days;
363 : : double *element;
364 : :
365 : 0 : g_date_clear (&start_date, 1);
366 : 0 : g_date_set_dmy (&start_date,
367 : 0 : g_date_time_get_day_of_month (start_date_time),
368 : 0 : g_date_time_get_month (start_date_time),
369 : 0 : g_date_time_get_year (start_date_time));
370 : :
371 : 0 : diff_days = g_date_days_between (model_start_date, &start_date);
372 : 0 : g_assert (diff_days >= 0);
373 : :
374 : : /* If the new day is outside the range of the model, insert it at the right
375 : : * index. This will automatically create the indices between, and initialise
376 : : * them to zero, which is what we want. */
377 [ # # ]: 0 : if ((unsigned int) diff_days >= model_screen_time_per_day->len)
378 : : {
379 : 0 : const double new_val = 0.0;
380 : 0 : g_array_insert_val (model_screen_time_per_day, diff_days, new_val);
381 : : }
382 : :
383 : 0 : element = &g_array_index (model_screen_time_per_day, double, diff_days);
384 : 0 : *element += duration_secs / 60.0;
385 : 0 : }
386 : :
387 : : static gboolean
388 : 0 : mct_screen_time_statistics_row_load_data_finish (CcScreenTimeStatisticsRow *screen_time_statistics_row,
389 : : GAsyncResult *result,
390 : : GDate *out_new_model_start_date,
391 : : size_t *out_new_model_n_days,
392 : : double **out_new_model_screen_time_per_day,
393 : : GError **error)
394 : : {
395 : 0 : g_autoptr(LoadDataData) data = NULL;
396 : :
397 : 0 : g_return_val_if_fail (g_task_is_valid (result, screen_time_statistics_row), FALSE);
398 : :
399 : : /* Set up in case of error. */
400 [ # # ]: 0 : if (out_new_model_start_date != NULL)
401 : 0 : g_date_clear (out_new_model_start_date, 1);
402 [ # # ]: 0 : if (out_new_model_n_days != NULL)
403 : 0 : *out_new_model_n_days = 0;
404 [ # # ]: 0 : if (out_new_model_screen_time_per_day != NULL)
405 : 0 : *out_new_model_screen_time_per_day = NULL;
406 : :
407 : 0 : data = g_task_propagate_pointer (G_TASK (result), error);
408 : :
409 [ # # ]: 0 : if (data == NULL)
410 : 0 : return FALSE;
411 : :
412 : : /* Success! */
413 [ # # ]: 0 : if (out_new_model_start_date != NULL)
414 : 0 : *out_new_model_start_date = *data->start_date;
415 [ # # ]: 0 : if (out_new_model_n_days != NULL)
416 : 0 : *out_new_model_n_days = data->n_days;
417 [ # # ]: 0 : if (out_new_model_screen_time_per_day != NULL)
418 : 0 : *out_new_model_screen_time_per_day = g_steal_pointer (&data->screen_time_per_day);
419 : :
420 : 0 : return TRUE;
421 : : }
422 : :
423 : : static void
424 : 0 : usage_changed_cb (GDBusConnection *connection,
425 : : const char *sender_name,
426 : : const char *object_path,
427 : : const char *interface_name,
428 : : const char *signal_name,
429 : : GVariant *parameters,
430 : : void *user_data)
431 : : {
432 : 0 : MctScreenTimeStatisticsRow *self = MCT_SCREEN_TIME_STATISTICS_ROW (user_data);
433 : :
434 : 0 : cc_screen_time_statistics_row_update_model (CC_SCREEN_TIME_STATISTICS_ROW (self));
435 : 0 : }
436 : :
437 : : static void
438 : 0 : mct_screen_time_statistics_row_class_init (MctScreenTimeStatisticsRowClass *klass)
439 : : {
440 : 0 : GObjectClass *object_class = G_OBJECT_CLASS (klass);
441 : 0 : CcScreenTimeStatisticsRowClass *screen_time_statistics_row_class = CC_SCREEN_TIME_STATISTICS_ROW_CLASS (klass);
442 : :
443 : 0 : object_class->get_property = mct_screen_time_statistics_row_get_property;
444 : 0 : object_class->set_property = mct_screen_time_statistics_row_set_property;
445 : 0 : object_class->constructed = mct_screen_time_statistics_row_constructed;
446 : 0 : object_class->dispose = mct_screen_time_statistics_row_dispose;
447 : :
448 : 0 : screen_time_statistics_row_class->load_data_async = mct_screen_time_statistics_row_load_data_async;
449 : 0 : screen_time_statistics_row_class->load_data_finish = mct_screen_time_statistics_row_load_data_finish;
450 : :
451 : : /**
452 : : * MctScreenTimeStatisticsRow:connection: (not nullable)
453 : : *
454 : : * A connection to the system bus, where malcontent-timerd runs.
455 : : *
456 : : * It’s provided to allow an existing connection to be re-used and for testing
457 : : * purposes.
458 : : *
459 : : * Since 0.14.0
460 : : */
461 : 0 : properties[PROP_CONNECTION] = g_param_spec_object ("connection", NULL, NULL,
462 : : G_TYPE_DBUS_CONNECTION,
463 : : G_PARAM_READWRITE |
464 : : G_PARAM_CONSTRUCT_ONLY |
465 : : G_PARAM_STATIC_STRINGS);
466 : :
467 : : /**
468 : : * MctScreenTimeStatisticsRow:uid:
469 : : *
470 : : * The selected user’s UID.
471 : : *
472 : : * Since 0.14.0
473 : : */
474 : 0 : properties[PROP_UID] = g_param_spec_uint ("uid", NULL, NULL,
475 : : 0, G_MAXUINT, 0,
476 : : G_PARAM_READWRITE |
477 : : G_PARAM_CONSTRUCT_ONLY |
478 : : G_PARAM_STATIC_STRINGS);
479 : :
480 : 0 : g_object_class_install_properties (object_class, G_N_ELEMENTS (properties), properties);
481 : 0 : }
482 : :
483 : : static void
484 : 0 : mct_screen_time_statistics_row_init (MctScreenTimeStatisticsRow *self)
485 : : {
486 : 0 : }
487 : :
488 : : /**
489 : : * mct_screen_time_statistics_row_new:
490 : : * @connection: (transfer none): a D-Bus connection to use
491 : : * @uid: user ID to use
492 : : *
493 : : * Create a new [class@Malcontent.ScreenTimeStatisticsRow] widget.
494 : : *
495 : : * Returns: (transfer full): a new screen time statistics row
496 : : * Since: 0.14.0
497 : : */
498 : : MctScreenTimeStatisticsRow *
499 : 0 : mct_screen_time_statistics_row_new (GDBusConnection *connection,
500 : : uid_t uid)
501 : : {
502 : 0 : g_return_val_if_fail (G_IS_DBUS_CONNECTION (connection), NULL);
503 : :
504 : 0 : return g_object_new (MCT_TYPE_SCREEN_TIME_STATISTICS_ROW,
505 : : "connection", connection,
506 : : "uid", uid,
507 : : NULL);
508 : : }
|