Branch data Line data Source code
1 : : /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
2 : : /*
3 : : * Copyright 2024 GNOME Foundation, Inc.
4 : : *
5 : : * This library is free software; you can redistribute it and/or
6 : : * modify it under the terms of the GNU Lesser General Public
7 : : * License as published by the Free Software Foundation; either
8 : : * version 2 of the License, or (at your option) any later version.
9 : : *
10 : : * This library is distributed in the hope that it will be useful,
11 : : * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 : : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 : : * Lesser General Public License for more details.
14 : : *
15 : : * You should have received a copy of the GNU Lesser General
16 : : * Public License along with this library; if not, see <http://www.gnu.org/licenses/>.
17 : : *
18 : : * Authors:
19 : : * - Philip Withnall <pwithnall@gnome.org>
20 : : *
21 : : * SPDX-License-Identifier: GPL-3.0-or-later
22 : : */
23 : :
24 : : #include "config.h"
25 : :
26 : : #include <adwaita.h>
27 : : #include <glib.h>
28 : : #include <glib/gi18n.h>
29 : : #include <glib-object.h>
30 : : #include <gtk/gtk.h>
31 : : #include <json-glib/json-glib.h>
32 : :
33 : : #ifdef HAVE__NL_TIME_FIRST_WEEKDAY
34 : : #include <langinfo.h>
35 : : #include <locale.h>
36 : : #endif
37 : :
38 : : #include "cc-bar-chart.h"
39 : : #include "cc-screen-time-statistics-row.h"
40 : :
41 : : /* Copied from panels/common/cc-util.c in gnome-control-center */
42 : : static char *
43 : 0 : cc_util_time_to_string_text (gint64 msecs)
44 : : {
45 : 0 : g_autofree gchar *hours = NULL;
46 : 0 : g_autofree gchar *mins = NULL;
47 : 0 : g_autofree gchar *secs = NULL;
48 : : gint sec, min, hour, _time;
49 : :
50 : 0 : _time = (int) (msecs / 1000);
51 : 0 : sec = _time % 60;
52 : 0 : _time = _time - sec;
53 : 0 : min = (_time % (60*60)) / 60;
54 : 0 : _time = _time - (min * 60);
55 : 0 : hour = _time / (60*60);
56 : :
57 : 0 : hours = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE, "%d hour", "%d hours", hour), hour);
58 : 0 : mins = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE, "%d minute", "%d minutes", min), min);
59 : 0 : secs = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE, "%d second", "%d seconds", sec), sec);
60 : :
61 [ # # ]: 0 : if (hour > 0)
62 : : {
63 [ # # # # ]: 0 : if (min > 0 && sec > 0)
64 : : {
65 : : /* 5 hours 2 minutes 12 seconds */
66 : 0 : return g_strdup_printf (C_("hours minutes seconds", "%s %s %s"), hours, mins, secs);
67 : : }
68 [ # # ]: 0 : else if (min > 0)
69 : : {
70 : : /* 5 hours 2 minutes */
71 : 0 : return g_strdup_printf (C_("hours minutes", "%s %s"), hours, mins);
72 : : }
73 : : else
74 : : {
75 : : /* 5 hours */
76 : 0 : return g_strdup_printf (C_("hours", "%s"), hours);
77 : : }
78 : : }
79 [ # # ]: 0 : else if (min > 0)
80 : : {
81 [ # # ]: 0 : if (sec > 0)
82 : : {
83 : : /* 2 minutes 12 seconds */
84 : 0 : return g_strdup_printf (C_("minutes seconds", "%s %s"), mins, secs);
85 : : }
86 : : else
87 : : {
88 : : /* 2 minutes */
89 : 0 : return g_strdup_printf (C_("minutes", "%s"), mins);
90 : : }
91 : : }
92 [ # # ]: 0 : else if (sec > 0)
93 : : {
94 : : /* 10 seconds */
95 : 0 : return g_strdup (secs);
96 : : }
97 : : else
98 : : {
99 : : /* 0 seconds */
100 : 0 : return g_strdup (_("0 seconds"));
101 : : }
102 : : }
103 : :
104 : : /**
105 : : * CcScreenTimeStatisticsRow:
106 : : *
107 : : * An #AdwPreferencesRow used to display the user’s screen time statistics.
108 : : *
109 : : * This presents some summary statistics of their screen time usage, plus an
110 : : * interactive graph of their usage per day in the last few weeks.
111 : : *
112 : : * If no data is available, a placeholder will be displayed until some data is
113 : : * available.
114 : : *
115 : : * Bars in the graph can be selected to show summary statistics relating to that
116 : : * day. If data is available, a day must always be selected.
117 : : *
118 : : * The data is loaded from a file specified using
119 : : * #CcScreenTimeStatisticsRow:history-file. The data is automatically reloaded
120 : : * if the file changes, and as time passes.
121 : : */
122 : : typedef struct {
123 : : /* Child widgets */
124 : : CcBarChart *bar_chart;
125 : : GtkLabel *selected_date_label;
126 : : GtkLabel *selected_screen_time_label;
127 : : GtkLabel *selected_average_label;
128 : : GtkLabel *selected_average_value_label;
129 : : GtkLabel *week_date_label;
130 : : GtkLabel *week_screen_time_label;
131 : : GtkLabel *week_average_value_label;
132 : :
133 : : GtkButton *previous_week_button;
134 : : GtkButton *next_week_button;
135 : :
136 : : GtkStack *data_stack;
137 : :
138 : : /* Model data */
139 : : struct
140 : : {
141 : : GDate start_date; /* inclusive; invalid when unset */
142 : : size_t n_days;
143 : : double *screen_time_per_day; /* minutes for each day; (nullable) (array length=n_days) (owned) */
144 : : } model;
145 : :
146 : : /* UI state */
147 : : GFile *history_file; /* (nullable) (owned) */
148 : : GFileMonitor *history_file_monitor; /* (nullable) (owned) */
149 : : gulong history_file_monitor_changed_id;
150 : : GSource *update_timeout_source; /* (nullable) (owned) */
151 : : GCancellable *cancellable; /* (owned) */
152 : :
153 : : GDate selected_date; /* invalid when unset */
154 : : unsigned int daily_limit_minutes;
155 : : } CcScreenTimeStatisticsRowPrivate;
156 : :
157 [ # # # # : 0 : G_DEFINE_TYPE_WITH_PRIVATE (CcScreenTimeStatisticsRow, cc_screen_time_statistics_row, ADW_TYPE_PREFERENCES_ROW)
# # ]
158 : :
159 : : typedef enum {
160 : : PROP_HISTORY_FILE = 1,
161 : : PROP_SELECTED_DATE,
162 : : PROP_DAILY_LIMIT,
163 : : } CcScreenTimeStatisticsRowProperty;
164 : :
165 : : static GParamSpec *props[PROP_DAILY_LIMIT + 1];
166 : :
167 : : static void cc_screen_time_statistics_row_get_property (GObject *object,
168 : : guint property_id,
169 : : GValue *value,
170 : : GParamSpec *pspec);
171 : : static void cc_screen_time_statistics_row_set_property (GObject *object,
172 : : guint property_id,
173 : : const GValue *value,
174 : : GParamSpec *pspec);
175 : : static void cc_screen_time_statistics_row_constructed (GObject *object);
176 : : static void cc_screen_time_statistics_row_dispose (GObject *object);
177 : : static void cc_screen_time_statistics_row_finalize (GObject *object);
178 : : static void cc_screen_time_statistics_row_map (GtkWidget *widget);
179 : : static void cc_screen_time_statistics_row_unmap (GtkWidget *widget);
180 : : static void cc_screen_time_statistics_row_real_load_data_async (CcScreenTimeStatisticsRow *self,
181 : : GCancellable *cancellable,
182 : : GAsyncReadyCallback callback,
183 : : void *user_data);
184 : : static gboolean cc_screen_time_statistics_row_real_load_data_finish (CcScreenTimeStatisticsRow *self,
185 : : GAsyncResult *result,
186 : : GDate *out_new_model_start_date,
187 : : size_t *out_new_model_n_days,
188 : : double **out_new_model_screen_time_per_day,
189 : : GError **error);
190 : :
191 : : static void get_today (GDate *today);
192 : : static unsigned int get_week_start (void);
193 : : static gboolean load_session_active_history_data (CcScreenTimeStatisticsRow *self,
194 : : GDate *out_new_model_start_date,
195 : : size_t *out_new_model_n_days,
196 : : double **out_new_model_screen_time_per_day,
197 : : GError **error);
198 : : static void update_model (CcScreenTimeStatisticsRow *self);
199 : : static void load_data_cb (GObject *object,
200 : : GAsyncResult *result,
201 : : void *user_data);
202 : : static void update_ui_for_model_or_selected_date (CcScreenTimeStatisticsRow *self);
203 : : static char *bar_chart_continuous_axis_label_cb (CcBarChart *chart,
204 : : double value,
205 : : void *user_data);
206 : : static double bar_chart_continuous_axis_grid_line_cb (CcBarChart *chart,
207 : : unsigned int idx,
208 : : void *user_data);
209 : : static void bar_chart_update_accessible_description (CcScreenTimeStatisticsRow *self);
210 : : static void bar_chart_notify_selected_index_cb (GObject *object,
211 : : GParamSpec *pspec,
212 : : gpointer user_data);
213 : : static void previous_week_button_clicked_cb (GtkButton *button,
214 : : gpointer user_data);
215 : : static void next_week_button_clicked_cb (GtkButton *button,
216 : : gpointer user_data);
217 : : static void maybe_enable_update_timeout (CcScreenTimeStatisticsRow *self);
218 : :
219 : : static void
220 : 0 : cc_screen_time_statistics_row_class_init (CcScreenTimeStatisticsRowClass *klass)
221 : : {
222 : 0 : GObjectClass *object_class = G_OBJECT_CLASS (klass);
223 : 0 : GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
224 : :
225 : 0 : object_class->get_property = cc_screen_time_statistics_row_get_property;
226 : 0 : object_class->set_property = cc_screen_time_statistics_row_set_property;
227 : 0 : object_class->constructed = cc_screen_time_statistics_row_constructed;
228 : 0 : object_class->dispose = cc_screen_time_statistics_row_dispose;
229 : 0 : object_class->finalize = cc_screen_time_statistics_row_finalize;
230 : :
231 : 0 : widget_class->map = cc_screen_time_statistics_row_map;
232 : 0 : widget_class->unmap = cc_screen_time_statistics_row_unmap;
233 : :
234 : 0 : klass->load_data_async = cc_screen_time_statistics_row_real_load_data_async;
235 : 0 : klass->load_data_finish = cc_screen_time_statistics_row_real_load_data_finish;
236 : :
237 : : /**
238 : : * CcScreenTimeStatisticsRow:history-file: (nullable)
239 : : *
240 : : * File containing the screen time history to display.
241 : : *
242 : : * If %NULL, the widget will show a ‘no data available’ placeholder message.
243 : : */
244 : 0 : props[PROP_HISTORY_FILE] =
245 : 0 : g_param_spec_object ("history-file",
246 : : NULL, NULL,
247 : : G_TYPE_FILE,
248 : : G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
249 : :
250 : : /**
251 : : * CcScreenTimeStatisticsRow:selected-date: (nullable)
252 : : *
253 : : * Currently selected date.
254 : : *
255 : : * The data shown will be the week containing this date.
256 : : *
257 : : * This will be %NULL if no data is available. If any data is available, a
258 : : * date will always be selected.
259 : : */
260 : 0 : props[PROP_SELECTED_DATE] =
261 : 0 : g_param_spec_boxed ("selected-date",
262 : : NULL, NULL,
263 : : G_TYPE_DATE,
264 : : G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
265 : :
266 : : /**
267 : : * CcScreenTimeStatisticsRow:daily-limit:
268 : : *
269 : : * Daily usage limit for the user, in minutes.
270 : : *
271 : : * If set, this results in a threshold line being drawn on the usage graph.
272 : : * Zero if unset.
273 : : */
274 : 0 : props[PROP_DAILY_LIMIT] =
275 : 0 : g_param_spec_uint ("daily-limit",
276 : : NULL, NULL,
277 : : 0, G_MAXUINT, 0,
278 : : G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
279 : :
280 : 0 : g_object_class_install_properties (object_class, G_N_ELEMENTS (props), props);
281 : :
282 : 0 : g_type_ensure (CC_TYPE_BAR_CHART);
283 : :
284 : 0 : gtk_widget_class_set_template_from_resource (widget_class, "/org/freedesktop/MalcontentControl/ui/cc-screen-time-statistics-row.ui");
285 : :
286 : 0 : gtk_widget_class_bind_template_child_private (widget_class, CcScreenTimeStatisticsRow, bar_chart);
287 : 0 : gtk_widget_class_bind_template_child_private (widget_class, CcScreenTimeStatisticsRow, selected_date_label);
288 : 0 : gtk_widget_class_bind_template_child_private (widget_class, CcScreenTimeStatisticsRow, selected_screen_time_label);
289 : 0 : gtk_widget_class_bind_template_child_private (widget_class, CcScreenTimeStatisticsRow, selected_average_label);
290 : 0 : gtk_widget_class_bind_template_child_private (widget_class, CcScreenTimeStatisticsRow, selected_average_value_label);
291 : 0 : gtk_widget_class_bind_template_child_private (widget_class, CcScreenTimeStatisticsRow, week_date_label);
292 : 0 : gtk_widget_class_bind_template_child_private (widget_class, CcScreenTimeStatisticsRow, week_screen_time_label);
293 : 0 : gtk_widget_class_bind_template_child_private (widget_class, CcScreenTimeStatisticsRow, week_average_value_label);
294 : 0 : gtk_widget_class_bind_template_child_private (widget_class, CcScreenTimeStatisticsRow, previous_week_button);
295 : 0 : gtk_widget_class_bind_template_child_private (widget_class, CcScreenTimeStatisticsRow, next_week_button);
296 : 0 : gtk_widget_class_bind_template_child_private (widget_class, CcScreenTimeStatisticsRow, data_stack);
297 : :
298 : 0 : gtk_widget_class_bind_template_callback (widget_class, bar_chart_notify_selected_index_cb);
299 : 0 : gtk_widget_class_bind_template_callback (widget_class, previous_week_button_clicked_cb);
300 : 0 : gtk_widget_class_bind_template_callback (widget_class, next_week_button_clicked_cb);
301 : :
302 : 0 : gtk_widget_class_set_accessible_role (widget_class, GTK_ACCESSIBLE_ROLE_GROUP);
303 : 0 : }
304 : :
305 : : static void
306 : 0 : cc_screen_time_statistics_row_init (CcScreenTimeStatisticsRow *self)
307 : : {
308 : 0 : CcScreenTimeStatisticsRowPrivate *priv = cc_screen_time_statistics_row_get_instance_private (self);
309 : :
310 : 0 : priv->cancellable = g_cancellable_new ();
311 : :
312 : 0 : gtk_widget_init_template (GTK_WIDGET (self));
313 : :
314 : : /* Bar chart weekday labels. These need to take into account the user’s
315 : : * preferred starting day of the week. */
316 : 0 : const char * const weekdays[] = {
317 : 0 : C_("abbreviated weekday name for Sunday", "S"),
318 : 0 : C_("abbreviated weekday name for Monday", "M"),
319 : 0 : C_("abbreviated weekday name for Tuesday", "T"),
320 : 0 : C_("abbreviated weekday name for Wednesday", "W"),
321 : 0 : C_("abbreviated weekday name for Thursday", "T"),
322 : 0 : C_("abbreviated weekday name for Friday", "F"),
323 : 0 : C_("abbreviated weekday name for Saturday", "S"),
324 : : };
325 : : const char *labels[G_N_ELEMENTS (weekdays) + 1 /* NULL terminator */];
326 : 0 : unsigned int week_start = get_week_start (); /* 0 = Sunday, 1 = Monday, 2 = Tuesday, etc. */
327 [ # # ]: 0 : for (size_t i = 0; i < G_N_ELEMENTS (weekdays); i++)
328 : 0 : labels[i] = weekdays[(week_start + i) % G_N_ELEMENTS (weekdays)];
329 : 0 : labels[G_N_ELEMENTS (labels) - 1] = NULL;
330 : :
331 : 0 : cc_bar_chart_set_discrete_axis_labels (priv->bar_chart, labels);
332 : :
333 : 0 : cc_bar_chart_set_continuous_axis_label_callback (priv->bar_chart, bar_chart_continuous_axis_label_cb, NULL, NULL);
334 : 0 : cc_bar_chart_set_continuous_axis_grid_line_callback (priv->bar_chart, bar_chart_continuous_axis_grid_line_cb, NULL, NULL);
335 : 0 : }
336 : :
337 : : static void
338 : 0 : cc_screen_time_statistics_row_get_property (GObject *object,
339 : : guint property_id,
340 : : GValue *value,
341 : : GParamSpec *pspec)
342 : : {
343 : 0 : CcScreenTimeStatisticsRow *self = CC_SCREEN_TIME_STATISTICS_ROW (object);
344 : :
345 [ # # # # ]: 0 : switch ((CcScreenTimeStatisticsRowProperty) property_id)
346 : : {
347 : 0 : case PROP_HISTORY_FILE:
348 : 0 : g_value_set_object (value, cc_screen_time_statistics_row_get_history_file (self));
349 : 0 : break;
350 : 0 : case PROP_SELECTED_DATE:
351 : 0 : g_value_set_boxed (value, cc_screen_time_statistics_row_get_selected_date (self));
352 : 0 : break;
353 : 0 : case PROP_DAILY_LIMIT:
354 : 0 : g_value_set_uint (value, cc_screen_time_statistics_row_get_daily_limit (self));
355 : 0 : break;
356 : 0 : default:
357 : 0 : G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
358 : 0 : break;
359 : : }
360 : 0 : }
361 : :
362 : : static void
363 : 0 : cc_screen_time_statistics_row_set_property (GObject *object,
364 : : guint property_id,
365 : : const GValue *value,
366 : : GParamSpec *pspec)
367 : : {
368 : 0 : CcScreenTimeStatisticsRow *self = CC_SCREEN_TIME_STATISTICS_ROW (object);
369 : :
370 [ # # # # ]: 0 : switch ((CcScreenTimeStatisticsRowProperty) property_id)
371 : : {
372 : 0 : case PROP_HISTORY_FILE:
373 : 0 : cc_screen_time_statistics_row_set_history_file (self, g_value_get_object (value));
374 : 0 : break;
375 : 0 : case PROP_SELECTED_DATE:
376 : 0 : cc_screen_time_statistics_row_set_selected_date (self, g_value_get_boxed (value));
377 : 0 : break;
378 : 0 : case PROP_DAILY_LIMIT:
379 : 0 : cc_screen_time_statistics_row_set_daily_limit (self, g_value_get_uint (value));
380 : 0 : break;
381 : 0 : default:
382 : 0 : G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
383 : : }
384 : 0 : }
385 : :
386 : : static void
387 : 0 : cc_screen_time_statistics_row_constructed (GObject *object)
388 : : {
389 : 0 : CcScreenTimeStatisticsRow *self = CC_SCREEN_TIME_STATISTICS_ROW (object);
390 : :
391 : : /* Load initial data and show it in the UI. */
392 : 0 : update_model (self);
393 : :
394 : 0 : G_OBJECT_CLASS (cc_screen_time_statistics_row_parent_class)->constructed (object);
395 : 0 : }
396 : :
397 : : static void
398 : 0 : cc_screen_time_statistics_row_dispose (GObject *object)
399 : : {
400 : 0 : CcScreenTimeStatisticsRow *self = CC_SCREEN_TIME_STATISTICS_ROW (object);
401 : 0 : CcScreenTimeStatisticsRowPrivate *priv = cc_screen_time_statistics_row_get_instance_private (self);
402 : :
403 : 0 : g_cancellable_cancel (priv->cancellable);
404 [ # # ]: 0 : g_clear_object (&priv->cancellable);
405 : :
406 [ # # ]: 0 : if (priv->history_file_monitor != NULL)
407 : 0 : g_file_monitor_cancel (priv->history_file_monitor);
408 [ # # ]: 0 : if (priv->history_file_monitor_changed_id != 0)
409 : 0 : g_signal_handler_disconnect (priv->history_file_monitor, priv->history_file_monitor_changed_id);
410 : 0 : priv->history_file_monitor_changed_id = 0;
411 [ # # ]: 0 : g_clear_object (&priv->history_file_monitor);
412 [ # # ]: 0 : g_clear_object (&priv->history_file);
413 : :
414 : 0 : gtk_widget_dispose_template (GTK_WIDGET (object), CC_TYPE_SCREEN_TIME_STATISTICS_ROW);
415 : :
416 : 0 : G_OBJECT_CLASS (cc_screen_time_statistics_row_parent_class)->dispose (object);
417 : 0 : }
418 : :
419 : : static void
420 : 0 : cc_screen_time_statistics_row_finalize (GObject *object)
421 : : {
422 : 0 : CcScreenTimeStatisticsRow *self = CC_SCREEN_TIME_STATISTICS_ROW (object);
423 : 0 : CcScreenTimeStatisticsRowPrivate *priv = cc_screen_time_statistics_row_get_instance_private (self);
424 : :
425 [ # # ]: 0 : g_clear_pointer (&priv->model.screen_time_per_day, g_free);
426 : :
427 : : /* Should have been freed on unmap */
428 : 0 : g_assert (priv->update_timeout_source == NULL);
429 : :
430 : 0 : G_OBJECT_CLASS (cc_screen_time_statistics_row_parent_class)->finalize (object);
431 : 0 : }
432 : :
433 : : static void
434 : 0 : cc_screen_time_statistics_row_map (GtkWidget *widget)
435 : : {
436 : 0 : CcScreenTimeStatisticsRow *self = CC_SCREEN_TIME_STATISTICS_ROW (widget);
437 : :
438 : 0 : GTK_WIDGET_CLASS (cc_screen_time_statistics_row_parent_class)->map (widget);
439 : :
440 : 0 : maybe_enable_update_timeout (self);
441 : 0 : }
442 : :
443 : : static void
444 : 0 : cc_screen_time_statistics_row_unmap (GtkWidget *widget)
445 : : {
446 : 0 : CcScreenTimeStatisticsRow *self = CC_SCREEN_TIME_STATISTICS_ROW (widget);
447 : :
448 : 0 : GTK_WIDGET_CLASS (cc_screen_time_statistics_row_parent_class)->unmap (widget);
449 : :
450 : 0 : maybe_enable_update_timeout (self);
451 : 0 : }
452 : :
453 : : typedef struct
454 : : {
455 : : GDate *start_date;
456 : : size_t n_days;
457 : : double *screen_time_per_day;
458 : : } LoadDataData;
459 : :
460 : : static void
461 : 0 : load_data_data_free (LoadDataData *data)
462 : : {
463 : 0 : g_date_free (data->start_date);
464 : 0 : g_free (data->screen_time_per_day);
465 : 0 : g_free (data);
466 : 0 : }
467 : :
468 [ # # ]: 0 : G_DEFINE_AUTOPTR_CLEANUP_FUNC (LoadDataData, load_data_data_free)
469 : :
470 : : /**
471 : : * load_data_data_new:
472 : : * @start_date: (transfer none): an initialized #GDate
473 : : * @n_days: number of days
474 : : * @screen_time_per_day: (transfer full): screen time per day
475 : : */
476 : : static LoadDataData *
477 : 0 : load_data_data_new (GDate *start_date,
478 : : size_t n_days,
479 : : double *screen_time_per_day)
480 : : {
481 : 0 : g_autoptr(LoadDataData) data = g_new0 (LoadDataData, 1);
482 : :
483 : 0 : data->start_date = g_date_copy (start_date);
484 : 0 : data->n_days = n_days;
485 : 0 : data->screen_time_per_day = screen_time_per_day;
486 : :
487 : 0 : return g_steal_pointer (&data);
488 : : }
489 : :
490 : : static void
491 : 0 : cc_screen_time_statistics_row_load_data_async (CcScreenTimeStatisticsRow *self,
492 : : GCancellable *cancellable,
493 : : GAsyncReadyCallback callback,
494 : : void *user_data)
495 : : {
496 : 0 : CC_SCREEN_TIME_STATISTICS_ROW_GET_CLASS (self)->load_data_async (self,
497 : : cancellable,
498 : : callback,
499 : : user_data);
500 : 0 : }
501 : :
502 : : static void
503 : 0 : cc_screen_time_statistics_row_real_load_data_async (CcScreenTimeStatisticsRow *self,
504 : : GCancellable *cancellable,
505 : : GAsyncReadyCallback callback,
506 : : void *user_data)
507 : : {
508 : 0 : g_autoptr(GTask) task = NULL;
509 : : GDate new_model_start_date;
510 : : size_t new_model_n_days;
511 : 0 : g_autofree double *new_model_screen_time_per_day = NULL;
512 : 0 : g_autoptr(GError) local_error = NULL;
513 : :
514 : 0 : task = g_task_new (self, cancellable, callback, user_data);
515 [ # # ]: 0 : g_task_set_source_tag (task, cc_screen_time_statistics_row_real_load_data_async);
516 : :
517 [ # # ]: 0 : if (!load_session_active_history_data (self, &new_model_start_date,
518 : : &new_model_n_days, &new_model_screen_time_per_day,
519 : : &local_error))
520 : : {
521 : 0 : g_task_return_error (task, g_steal_pointer (&local_error));
522 : : }
523 : : else
524 : : {
525 : 0 : LoadDataData *data = load_data_data_new (&new_model_start_date,
526 : : new_model_n_days,
527 : 0 : g_steal_pointer (&new_model_screen_time_per_day));
528 : 0 : g_task_return_pointer (task, data, (GDestroyNotify) load_data_data_free);
529 : : }
530 : 0 : }
531 : :
532 : : static gboolean
533 : 0 : cc_screen_time_statistics_row_load_data_finish (CcScreenTimeStatisticsRow *self,
534 : : GAsyncResult *result,
535 : : GDate *out_new_model_start_date,
536 : : size_t *out_new_model_n_days,
537 : : double **out_new_model_screen_time_per_day,
538 : : GError **error)
539 : : {
540 : 0 : return CC_SCREEN_TIME_STATISTICS_ROW_GET_CLASS (self)->load_data_finish (self,
541 : : result,
542 : : out_new_model_start_date,
543 : : out_new_model_n_days,
544 : : out_new_model_screen_time_per_day,
545 : : error);
546 : : }
547 : :
548 : : static gboolean
549 : 0 : cc_screen_time_statistics_row_real_load_data_finish (CcScreenTimeStatisticsRow *self,
550 : : GAsyncResult *result,
551 : : GDate *out_new_model_start_date,
552 : : size_t *out_new_model_n_days,
553 : : double **out_new_model_screen_time_per_day,
554 : : GError **error)
555 : : {
556 : 0 : g_autoptr(LoadDataData) data = NULL;
557 : :
558 : 0 : g_return_val_if_fail (g_task_is_valid (result, self), FALSE);
559 : :
560 : : /* Set up in case of error. */
561 [ # # ]: 0 : if (out_new_model_start_date != NULL)
562 : 0 : g_date_clear (out_new_model_start_date, 1);
563 [ # # ]: 0 : if (out_new_model_n_days != NULL)
564 : 0 : *out_new_model_n_days = 0;
565 [ # # ]: 0 : if (out_new_model_screen_time_per_day != NULL)
566 : 0 : *out_new_model_screen_time_per_day = NULL;
567 : :
568 : 0 : data = g_task_propagate_pointer (G_TASK (result), error);
569 : :
570 [ # # ]: 0 : if (data == NULL)
571 : 0 : return FALSE;
572 : :
573 : : /* Success! */
574 [ # # ]: 0 : if (out_new_model_start_date != NULL)
575 : 0 : *out_new_model_start_date = *data->start_date;
576 [ # # ]: 0 : if (out_new_model_n_days != NULL)
577 : 0 : *out_new_model_n_days = data->n_days;
578 [ # # ]: 0 : if (out_new_model_screen_time_per_day != NULL)
579 : 0 : *out_new_model_screen_time_per_day = g_steal_pointer (&data->screen_time_per_day);
580 : :
581 : 0 : return TRUE;
582 : : }
583 : :
584 : : static gboolean
585 : 0 : is_day_in_model (CcScreenTimeStatisticsRow *self,
586 : : const GDate *day)
587 : : {
588 : 0 : CcScreenTimeStatisticsRowPrivate *priv = cc_screen_time_statistics_row_get_instance_private (self);
589 : 0 : int days_diff = g_date_days_between (&priv->model.start_date, day);
590 [ # # # # ]: 0 : return (days_diff >= 0 && (unsigned int) days_diff < priv->model.n_days);
591 : : }
592 : :
593 : : static guint
594 : 0 : get_screen_time_for_day (CcScreenTimeStatisticsRow *self,
595 : : const GDate *day)
596 : : {
597 : 0 : CcScreenTimeStatisticsRowPrivate *priv = cc_screen_time_statistics_row_get_instance_private (self);
598 : 0 : int days_diff = g_date_days_between (&priv->model.start_date, day);
599 : 0 : g_assert (is_day_in_model (self, day));
600 : 0 : return priv->model.screen_time_per_day[(unsigned int) days_diff];
601 : : }
602 : :
603 : : static char *
604 : 0 : format_hours_and_minutes (unsigned int minutes,
605 : : gboolean omit_minutes_if_zero)
606 : : {
607 : 0 : unsigned int hours = minutes / 60;
608 : 0 : minutes %= 60;
609 : :
610 : : /* Technically we should be formatting these units as per the SI Brochure,
611 : : * table 8 and §5.4.3: with a 0+00A0 (non-breaking space) between the value
612 : : * and unit; and using ‘min’ as the unit for minutes, not ‘m’.
613 : : *
614 : : * However, space is very restricted here, so we’re optimising for that.
615 : : * Given that the whole panel is about screen *time*, hopefully the meaning of
616 : : * the numbers should be obvious. */
617 : :
618 [ # # # # ]: 0 : if (hours == 0 && minutes > 0)
619 : : {
620 : : /* Translators: This is a duration in minutes, for example ‘15m’ for 15 minutes.
621 : : * Use whatever shortest unit label is used for minutes in your locale. */
622 : 0 : return g_strdup_printf (_("%um"), minutes);
623 : : }
624 [ # # ]: 0 : else if (minutes == 0)
625 : : {
626 : : /* Translators: This is a duration in hours, for example ‘2h’ for 2 hours.
627 : : * Use whatever shortest unit label is used for hours in your locale. */
628 : 0 : return g_strdup_printf (_("%uh"), hours);
629 : : }
630 : : else
631 : : {
632 : : /* Translators: This is a duration in hours and minutes, for example
633 : : * ‘3h 15m’ for 3 hours and 15 minutes. Use whatever shortest unit label
634 : : * is used for hours and minutes in your locale. */
635 : 0 : return g_strdup_printf (_("%uh %um"), hours, minutes);
636 : : }
637 : : }
638 : :
639 : : static void
640 : 0 : label_set_text_hours_and_minutes (GtkLabel *label,
641 : : unsigned int minutes)
642 : : {
643 : 0 : g_autofree char *text = format_hours_and_minutes (minutes, FALSE);
644 : 0 : gtk_label_set_text (label, text);
645 : 0 : }
646 : :
647 : : /**
648 : : * get_week_start:
649 : : *
650 : : * Gets the first week day for the current locale, expressed as a
651 : : * number in the range 0..6, representing week days from Sunday to
652 : : * Saturday.
653 : : *
654 : : * Returns: A number representing the first week day for the current
655 : : * locale
656 : : */
657 : : /* Copied from gtkcalendar.c and shell-util.c */
658 : : static unsigned int
659 : 0 : get_week_start (void)
660 : : {
661 : : int week_start;
662 : : #ifdef HAVE__NL_TIME_FIRST_WEEKDAY
663 : : union { unsigned int word; char *string; } langinfo;
664 : : int week_1stday = 0;
665 : : int first_weekday = 1;
666 : : guint week_origin;
667 : : #else
668 : : char *gtk_week_start;
669 : : #endif
670 : :
671 : : #ifdef HAVE__NL_TIME_FIRST_WEEKDAY
672 : : langinfo.string = nl_langinfo (_NL_TIME_FIRST_WEEKDAY);
673 : : first_weekday = langinfo.string[0];
674 : : langinfo.string = nl_langinfo (_NL_TIME_WEEK_1STDAY);
675 : : week_origin = langinfo.word;
676 : : if (week_origin == 19971130) /* Sunday */
677 : : week_1stday = 0;
678 : : else if (week_origin == 19971201) /* Monday */
679 : : week_1stday = 1;
680 : : else
681 : : g_warning ("Unknown value of _NL_TIME_WEEK_1STDAY.\n");
682 : :
683 : : week_start = (week_1stday + first_weekday - 1) % 7;
684 : : #else
685 : : /* Use a define to hide the string from xgettext */
686 : : # define GTK_WEEK_START "calendar:week_start:0"
687 : 0 : gtk_week_start = dgettext ("gtk40", GTK_WEEK_START);
688 : :
689 [ # # ]: 0 : if (strncmp (gtk_week_start, "calendar:week_start:", 20) == 0)
690 : 0 : week_start = *(gtk_week_start + 20) - '0';
691 : : else
692 : 0 : week_start = -1;
693 : :
694 [ # # # # ]: 0 : if (week_start < 0 || week_start > 6)
695 : : {
696 : 0 : g_warning ("Whoever translated calendar:week_start:0 for GTK+ "
697 : : "did so wrongly.\n");
698 : 0 : return 0;
699 : : }
700 : : #endif
701 : :
702 : 0 : return week_start;
703 : : }
704 : :
705 : : static void
706 : 0 : get_first_day_of_week (const GDate *date,
707 : : GDate *out_new_date)
708 : : {
709 : 0 : unsigned int week_start = get_week_start (); /* 0 = Sunday, 1 = Monday, 2 = Tuesday etc. */
710 [ # # ]: 0 : GDateWeekday week_start_as_weekday = (week_start == 0) ? G_DATE_SUNDAY : (GDateWeekday) week_start;
711 : 0 : GDateWeekday date_weekday = g_date_get_weekday (date);
712 : : int weekday_diff;
713 : :
714 : 0 : *out_new_date = *date;
715 : 0 : weekday_diff = date_weekday - week_start_as_weekday;
716 [ # # ]: 0 : if (weekday_diff >= 0)
717 : 0 : g_date_subtract_days (out_new_date, weekday_diff);
718 : : else
719 : 0 : g_date_subtract_days (out_new_date, 7 + weekday_diff);
720 : :
721 : : /* The first day of the week must be no later than @date */
722 : 0 : g_assert (g_date_days_between (out_new_date, date) >= 0);
723 : 0 : }
724 : :
725 : : static void
726 : 0 : get_last_day_of_week (const GDate *date,
727 : : GDate *out_new_date)
728 : : {
729 : 0 : get_first_day_of_week (date, out_new_date);
730 : :
731 : 0 : g_date_add_days (out_new_date, 6);
732 : :
733 : : /* The last day of the week must be no earlier than @date */
734 : 0 : g_assert (g_date_days_between (date, out_new_date) >= 0);
735 : 0 : }
736 : :
737 : : static void
738 : 0 : get_today (GDate *today)
739 : : {
740 : 0 : time_t now = time (NULL);
741 : 0 : g_assert (now != (time_t) -1); /* can only happen if the argument is non-NULL */
742 : 0 : g_date_set_time_t (today, now);
743 : 0 : }
744 : :
745 : : static gboolean
746 : 0 : is_today (const GDate *date)
747 : : {
748 : : GDate today;
749 : 0 : get_today (&today);
750 : 0 : return (g_date_compare (&today, date) == 0);
751 : : }
752 : :
753 : : /* We can’t just use g_date_get_{monday,sunday}_week_of_year() because there
754 : : * are some countries (such as Egypt) where the week starts on a Saturday.
755 : : *
756 : : * FIXME: date_get_week_of_year() can be replaced with new API from GLib once
757 : : * that’s implemented; see https://gitlab.gnome.org/GNOME/glib/-/issues/3617 */
758 : : static unsigned int
759 : 0 : date_get_week_of_year (const GDate *date,
760 : : GDateWeekday first_day_of_week)
761 : : {
762 : : GDate first_day_of_year;
763 : : unsigned int n_days_before_first_week;
764 : :
765 : 0 : g_return_val_if_fail (g_date_valid (date), 0);
766 : :
767 : 0 : g_date_clear (&first_day_of_year, 1);
768 : 0 : g_date_set_dmy (&first_day_of_year, 1, 1, g_date_get_year (date));
769 : :
770 : 0 : n_days_before_first_week = (first_day_of_week - g_date_get_weekday (&first_day_of_year) + 7) % 7;
771 : 0 : return (g_date_get_day_of_year (date) + 6 - n_days_before_first_week) / 7;
772 : : }
773 : :
774 : : static unsigned int
775 : 0 : get_week_of_year (const GDate *date)
776 : : {
777 : 0 : unsigned int week_start = get_week_start (); /* 0 = Sunday, 1 = Monday, 2 = Tuesday etc. */
778 [ # # ]: 0 : GDateWeekday week_start_as_weekday = (week_start == 0) ? G_DATE_SUNDAY : (GDateWeekday) week_start;
779 : 0 : unsigned int week_of_year = date_get_week_of_year (date, week_start_as_weekday);
780 : :
781 : : /* Safety checks */
782 [ # # ]: 0 : if (week_start == 0)
783 : 0 : g_assert (week_of_year == g_date_get_sunday_week_of_year (date));
784 [ # # ]: 0 : else if (week_start == 1)
785 : 0 : g_assert (week_of_year == g_date_get_monday_week_of_year (date));
786 : :
787 : 0 : return week_of_year;
788 : : }
789 : :
790 : : static gboolean
791 : 0 : is_this_week (const GDate *date)
792 : : {
793 : : GDate today;
794 : : unsigned int todays_week, dates_week;
795 : :
796 : 0 : get_today (&today);
797 : 0 : todays_week = get_week_of_year (&today);
798 : 0 : dates_week = get_week_of_year (date);
799 : :
800 : 0 : return (todays_week == dates_week);
801 : : }
802 : :
803 : : /* Behaviour is undefined if model is unset. If there is no data for the given
804 : : * @day_of_week (which can happen if the model is set but relatively
805 : : * unpopulated), the result (@out_average) is undefined and FALSE is returned. */
806 : : static gboolean
807 : 0 : calculate_average_screen_time_for_day_of_week (CcScreenTimeStatisticsRow *self,
808 : : GDateWeekday day_of_week,
809 : : unsigned int *out_average)
810 : : {
811 : 0 : CcScreenTimeStatisticsRowPrivate *priv = cc_screen_time_statistics_row_get_instance_private (self);
812 : 0 : GDateWeekday start_day_of_week = g_date_get_weekday (&priv->model.start_date);
813 : : size_t offset;
814 : : unsigned int n;
815 : : double sum;
816 : :
817 : 0 : g_assert (start_day_of_week != G_DATE_BAD_WEEKDAY);
818 : 0 : g_assert (day_of_week != G_DATE_BAD_WEEKDAY);
819 : :
820 : : /* add 7 to the difference to ensure it’s positive */
821 : 0 : offset = (7 + (day_of_week - start_day_of_week)) % 7;
822 : 0 : sum = 0.0;
823 : 0 : n = 0;
824 : :
825 [ # # ]: 0 : for (size_t i = offset; i < priv->model.n_days; i += 7)
826 : : {
827 : 0 : sum += priv->model.screen_time_per_day[i];
828 : 0 : n++;
829 : : }
830 : :
831 [ # # ]: 0 : if (out_average != NULL)
832 [ # # ]: 0 : *out_average = (n != 0) ? sum / n : 0;
833 : :
834 : 0 : return (n != 0);
835 : : }
836 : :
837 : : /* Behaviour is undefined if model is unset. If the model is set, but the
838 : : * chosen week lies partially outside the model, then zero will be assumed as
839 : : * the screen time for the days outside the model. */
840 : : static unsigned int
841 : 0 : calculate_total_screen_time_for_week (CcScreenTimeStatisticsRow *self,
842 : : const GDate *first_day_of_week)
843 : : {
844 : 0 : CcScreenTimeStatisticsRowPrivate *priv = cc_screen_time_statistics_row_get_instance_private (self);
845 : : double sum;
846 : 0 : const int offset = g_date_days_between (&priv->model.start_date, first_day_of_week);
847 : :
848 : 0 : sum = 0.0;
849 [ # # ]: 0 : for (int i = 0; i < 7; i++)
850 [ # # # # ]: 0 : sum += (offset + i >= 0 && (unsigned int) (offset + i) < priv->model.n_days) ? priv->model.screen_time_per_day[offset + i] : 0;
851 : :
852 : 0 : return sum;
853 : : }
854 : :
855 : : /* Behaviour is undefined if model is empty. */
856 : : static unsigned int
857 : 0 : calculate_average_screen_time_per_week (CcScreenTimeStatisticsRow *self)
858 : : {
859 : 0 : CcScreenTimeStatisticsRowPrivate *priv = cc_screen_time_statistics_row_get_instance_private (self);
860 : : /* Theoretically we want to group the screen_time_per_day values into complete
861 : : * weeks, and then average that set of weeks. In practice, this equates to
862 : : * summing all the screen_time_per_day values except any from the final
863 : : * incomplete week, and then dividing by the number of whole weeks.
864 : : *
865 : : * If there’s less than one week of data, just use the sum of all the data. */
866 : 0 : const unsigned int n_days_rounded = priv->model.n_days - (priv->model.n_days % 7);
867 : 0 : const unsigned int n_complete_weeks = n_days_rounded / 7;
868 : : double sum;
869 : :
870 : 0 : sum = 0.0;
871 [ # # ]: 0 : for (size_t i = 0; i < n_days_rounded; i++)
872 : 0 : sum += priv->model.screen_time_per_day[i];
873 : :
874 [ # # ]: 0 : return (n_complete_weeks != 0) ? sum / n_complete_weeks : sum;
875 : : }
876 : :
877 : : typedef enum
878 : : {
879 : : USER_STATE_INACTIVE = 0,
880 : : USER_STATE_ACTIVE = 1,
881 : : } UserState;
882 : :
883 : : static void allocate_duration_to_days (const GDate *model_start_date,
884 : : GArray *model_screen_time_per_day,
885 : : uint64_t start_wall_time_secs,
886 : : uint64_t duration_secs);
887 : : static void allocate_duration_to_day (const GDate *model_start_date,
888 : : GArray *model_screen_time_per_day,
889 : : GDateTime *start_date_time,
890 : : uint64_t duration_secs);
891 : :
892 : : static gboolean
893 : 0 : set_json_error (const char *history_file_path,
894 : : GError **error)
895 : : {
896 : 0 : g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
897 : 0 : _("Failed to load session history file ‘%s’: %s"),
898 : : history_file_path, _("Invalid file structure"));
899 : 0 : return FALSE;
900 : : }
901 : :
902 : : static gboolean
903 : 0 : load_session_active_history_data (CcScreenTimeStatisticsRow *self,
904 : : GDate *out_new_model_start_date,
905 : : size_t *out_new_model_n_days,
906 : : double **out_new_model_screen_time_per_day,
907 : : GError **error)
908 : : {
909 : 0 : CcScreenTimeStatisticsRowPrivate *priv = cc_screen_time_statistics_row_get_instance_private (self);
910 : 0 : g_autofree char *history_file_path = NULL;
911 : 0 : g_autoptr(JsonParser) parser = NULL;
912 : : JsonNode *root;
913 : : JsonArray *root_array;
914 : 0 : uint64_t now_secs = g_get_real_time () / G_USEC_PER_SEC;
915 : 0 : uint64_t prev_wall_time_secs = 0;
916 : 0 : UserState prev_new_state = USER_STATE_INACTIVE;
917 : : GDate new_model_start_date;
918 : 0 : g_autoptr(GArray) new_model_screen_time_per_day = NULL; /* (element-type double) */
919 : :
920 : 0 : g_date_clear (&new_model_start_date, 1);
921 : :
922 : : /* Set up in case of error. */
923 [ # # ]: 0 : if (out_new_model_start_date != NULL)
924 : 0 : g_date_clear (out_new_model_start_date, 1);
925 [ # # ]: 0 : if (out_new_model_n_days != NULL)
926 : 0 : *out_new_model_n_days = 0;
927 [ # # ]: 0 : if (out_new_model_screen_time_per_day != NULL)
928 : 0 : *out_new_model_screen_time_per_day = NULL;
929 : :
930 : : /* Load and parse the session active history file, written by gnome-shell.
931 : : * See `timeLimitsManager.js` in gnome-shell for the code which writes this
932 : : * file, and a description of the format. */
933 [ # # ]: 0 : if (priv->history_file == NULL)
934 : : {
935 : 0 : g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_NOENT,
936 : 0 : _("Failed to load session history file: %s"),
937 : : _("File is empty"));
938 : 0 : return FALSE;
939 : : }
940 : :
941 : 0 : history_file_path = g_file_get_path (priv->history_file);
942 : 0 : parser = json_parser_new_immutable ();
943 [ # # ]: 0 : if (!json_parser_load_from_mapped_file (parser, history_file_path, error))
944 : 0 : return FALSE;
945 : :
946 : 0 : root = json_parser_get_root (parser);
947 [ # # ]: 0 : if (!JSON_NODE_HOLDS_ARRAY (root))
948 : 0 : return set_json_error (history_file_path, error);
949 : :
950 : 0 : root_array = json_node_get_array (root);
951 : 0 : g_assert (root_array != NULL);
952 : :
953 [ # # ]: 0 : for (unsigned int i = 0; i < json_array_get_length (root_array); i++)
954 : : {
955 : 0 : JsonNode *element = json_array_get_element (root_array, i);
956 : : JsonObject *element_object;
957 : : JsonNode *old_state_member, *new_state_member, *wall_time_secs_member;
958 : : int64_t old_state, new_state, wall_time_secs;
959 : :
960 [ # # ]: 0 : if (!JSON_NODE_HOLDS_OBJECT (element))
961 : 0 : return set_json_error (history_file_path, error);
962 : :
963 : 0 : element_object = json_node_get_object (element);
964 : 0 : g_assert (element_object != NULL);
965 : :
966 : 0 : old_state_member = json_object_get_member (element_object, "oldState");
967 : 0 : new_state_member = json_object_get_member (element_object, "newState");
968 : 0 : wall_time_secs_member = json_object_get_member (element_object, "wallTimeSecs");
969 : :
970 [ # # # # : 0 : if (old_state_member == NULL || !JSON_NODE_HOLDS_VALUE (old_state_member) ||
# # ]
971 [ # # # # ]: 0 : new_state_member == NULL || !JSON_NODE_HOLDS_VALUE (new_state_member) ||
972 [ # # ]: 0 : wall_time_secs_member == NULL || !JSON_NODE_HOLDS_VALUE (wall_time_secs_member))
973 : 0 : return set_json_error (history_file_path, error);
974 : :
975 : 0 : old_state = json_node_get_int (old_state_member);
976 : 0 : new_state = json_node_get_int (new_state_member);
977 : 0 : wall_time_secs = json_node_get_int (wall_time_secs_member);
978 : :
979 [ # # # # ]: 0 : if (old_state == new_state ||
980 : 0 : wall_time_secs < 0 ||
981 [ # # ]: 0 : (uint64_t) wall_time_secs <= prev_wall_time_secs ||
982 [ # # # # ]: 0 : (uint64_t) wall_time_secs > now_secs ||
983 [ # # # # ]: 0 : (old_state != USER_STATE_INACTIVE && old_state != USER_STATE_ACTIVE) ||
984 [ # # ]: 0 : (new_state != USER_STATE_INACTIVE && new_state != USER_STATE_ACTIVE))
985 : 0 : return set_json_error (history_file_path, error);
986 : :
987 : : /* Set up the model if this is the first iteration */
988 [ # # ]: 0 : if (!g_date_valid (&new_model_start_date))
989 : : {
990 : 0 : g_date_set_time_t (&new_model_start_date, wall_time_secs);
991 : 0 : new_model_screen_time_per_day = g_array_new (FALSE, TRUE, sizeof (double));
992 : : }
993 : :
994 : : /* Interpret the data */
995 [ # # # # ]: 0 : if (new_state == USER_STATE_INACTIVE && prev_wall_time_secs > 0)
996 : : {
997 : 0 : uint64_t duration_secs = wall_time_secs - prev_wall_time_secs;
998 : 0 : allocate_duration_to_days (&new_model_start_date, new_model_screen_time_per_day,
999 : : prev_wall_time_secs, duration_secs);
1000 : : }
1001 : :
1002 : 0 : prev_wall_time_secs = wall_time_secs;
1003 : 0 : prev_new_state = new_state;
1004 : : }
1005 : :
1006 : : /* Was the final transition open-ended? */
1007 [ # # # # ]: 0 : if (prev_wall_time_secs > 0 && prev_new_state == USER_STATE_ACTIVE)
1008 : : {
1009 : 0 : uint64_t duration_secs = now_secs - prev_wall_time_secs;
1010 : 0 : allocate_duration_to_days (&new_model_start_date, new_model_screen_time_per_day,
1011 : : prev_wall_time_secs, duration_secs);
1012 : : }
1013 : :
1014 : : /* Was the file empty? */
1015 [ # # # # ]: 0 : if (new_model_screen_time_per_day == NULL || new_model_screen_time_per_day->len == 0)
1016 : : {
1017 : 0 : g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_NOENT,
1018 : 0 : _("Failed to load session history file ‘%s’: %s"),
1019 : : history_file_path, _("File is empty"));
1020 : 0 : return FALSE;
1021 : : }
1022 : :
1023 : : /* Success! */
1024 [ # # ]: 0 : if (out_new_model_start_date != NULL)
1025 : 0 : *out_new_model_start_date = new_model_start_date;
1026 [ # # ]: 0 : if (out_new_model_n_days != NULL)
1027 : 0 : *out_new_model_n_days = new_model_screen_time_per_day->len;
1028 [ # # ]: 0 : if (out_new_model_screen_time_per_day != NULL)
1029 : 0 : *out_new_model_screen_time_per_day = (double *) g_array_free (g_steal_pointer (&new_model_screen_time_per_day), FALSE);
1030 : :
1031 : 0 : return TRUE;
1032 : : }
1033 : :
1034 : : /* Take the time period [start_wall_time_secs, start_wall_time_secs + duration_secs]
1035 : : * and add it to the model, splitting it between day boundaries if needed, and
1036 : : * extending the `GArray` if needed. */
1037 : : static void
1038 : 0 : allocate_duration_to_days (const GDate *model_start_date,
1039 : : GArray *model_screen_time_per_day,
1040 : : uint64_t start_wall_time_secs,
1041 : : uint64_t duration_secs)
1042 : : {
1043 : 0 : g_autoptr(GDateTime) start_date_time = NULL;
1044 : :
1045 : 0 : start_date_time = g_date_time_new_from_unix_local (start_wall_time_secs);
1046 : :
1047 [ # # ]: 0 : while (duration_secs > 0)
1048 : : {
1049 : 0 : g_autoptr(GDateTime) start_of_day = NULL, start_of_next_day = NULL;
1050 : 0 : g_autoptr(GDateTime) new_start_date_time = NULL;
1051 : : GTimeSpan span_usecs;
1052 : : uint64_t span_secs;
1053 : :
1054 : 0 : start_of_day = g_date_time_new_local (g_date_time_get_year (start_date_time),
1055 : : g_date_time_get_month (start_date_time),
1056 : : g_date_time_get_day_of_month (start_date_time),
1057 : : 0, 0, 0);
1058 : 0 : g_assert (start_of_day != NULL);
1059 : 0 : start_of_next_day = g_date_time_add_days (start_of_day, 1);
1060 : 0 : g_assert (start_of_next_day != NULL);
1061 : :
1062 : 0 : span_usecs = g_date_time_difference (start_of_next_day, start_date_time);
1063 : 0 : span_secs = span_usecs / G_USEC_PER_SEC;
1064 [ # # ]: 0 : if (span_secs > duration_secs)
1065 : 0 : span_secs = duration_secs;
1066 : :
1067 : 0 : allocate_duration_to_day (model_start_date, model_screen_time_per_day,
1068 : : start_date_time, span_secs);
1069 : :
1070 : 0 : duration_secs -= span_secs;
1071 : 0 : new_start_date_time = g_date_time_add_seconds (start_date_time, span_secs);
1072 : 0 : g_date_time_unref (start_date_time);
1073 : 0 : start_date_time = g_steal_pointer (&new_start_date_time);
1074 : : }
1075 : 0 : }
1076 : :
1077 : : /* Take the time period [start_date_time, start_date_time + duration_secs]
1078 : : * and add it to the model, extending the `GArray` if needed. The time period
1079 : : * *must not* cross a day boundary, i.e. it’s invalid to call this function
1080 : : * with `start_date_time` as 23:00 on a day, and `duration_secs` as 2h.
1081 : : *
1082 : : * Note that @model_screen_time_per_day is in minutes, whereas @duration_secs
1083 : : * is in seconds. */
1084 : : static void
1085 : 0 : allocate_duration_to_day (const GDate *model_start_date,
1086 : : GArray *model_screen_time_per_day,
1087 : : GDateTime *start_date_time,
1088 : : uint64_t duration_secs)
1089 : : {
1090 : : GDate start_date;
1091 : : int diff_days;
1092 : : double *element;
1093 : :
1094 : 0 : g_date_clear (&start_date, 1);
1095 : 0 : g_date_set_dmy (&start_date,
1096 : 0 : g_date_time_get_day_of_month (start_date_time),
1097 : 0 : g_date_time_get_month (start_date_time),
1098 : 0 : g_date_time_get_year (start_date_time));
1099 : :
1100 : 0 : diff_days = g_date_days_between (model_start_date, &start_date);
1101 : 0 : g_assert (diff_days >= 0);
1102 : :
1103 : : /* If the new day is outside the range of the model, insert it at the right
1104 : : * index. This will automatically create the indices between, and initialise
1105 : : * them to zero, which is what we want. */
1106 [ # # ]: 0 : if ((unsigned int) diff_days >= model_screen_time_per_day->len)
1107 : : {
1108 : 0 : const double new_val = 0.0;
1109 : 0 : g_array_insert_val (model_screen_time_per_day, diff_days, new_val);
1110 : : }
1111 : :
1112 : 0 : element = &g_array_index (model_screen_time_per_day, double, diff_days);
1113 : 0 : *element += duration_secs / 60.0;
1114 : 0 : }
1115 : :
1116 : : static void
1117 : 0 : update_model (CcScreenTimeStatisticsRow *self)
1118 : : {
1119 : 0 : CcScreenTimeStatisticsRowPrivate *priv = cc_screen_time_statistics_row_get_instance_private (self);
1120 : :
1121 : 0 : cc_screen_time_statistics_row_load_data_async (self, priv->cancellable, load_data_cb, self);
1122 : 0 : }
1123 : :
1124 : : static void
1125 : 0 : load_data_cb (GObject *object,
1126 : : GAsyncResult *result,
1127 : : void *user_data)
1128 : : {
1129 : 0 : CcScreenTimeStatisticsRow *self = CC_SCREEN_TIME_STATISTICS_ROW (user_data);
1130 : 0 : CcScreenTimeStatisticsRowPrivate *priv = cc_screen_time_statistics_row_get_instance_private (self);
1131 : : GDate new_model_start_date;
1132 : 0 : size_t new_model_n_days = 0;
1133 [ # # ]: 0 : g_autofree double *new_model_screen_time_per_day = NULL;
1134 [ # # ]: 0 : g_autoptr(GError) local_error = NULL;
1135 : :
1136 [ # # ]: 0 : if (!cc_screen_time_statistics_row_load_data_finish (self, result, &new_model_start_date,
1137 : : &new_model_n_days, &new_model_screen_time_per_day,
1138 : : &local_error))
1139 : : {
1140 : : /* Not sure if it helps to display this error in the UI, so just log it
1141 : : * for now. `G_FILE_ERROR_NOENT` is used when the file doesn’t exist, or
1142 : : * exists but is empty, which could happen on new systems before the shell
1143 : : * logs anything. */
1144 [ # # ]: 0 : if (!g_error_matches (local_error, G_FILE_ERROR, G_FILE_ERROR_NOENT))
1145 : 0 : g_warning ("Error loading session history JSON: %s", local_error->message);
1146 : 0 : return;
1147 : : }
1148 : :
1149 : : /* Commit the new model. */
1150 : 0 : g_free (priv->model.screen_time_per_day);
1151 : :
1152 : 0 : priv->model.start_date = new_model_start_date;
1153 : 0 : priv->model.n_days = new_model_n_days;
1154 : 0 : priv->model.screen_time_per_day = g_steal_pointer (&new_model_screen_time_per_day);
1155 : :
1156 : : /* If this is the first time the model is updated, set the initial selected
1157 : : * date to today. */
1158 [ # # ]: 0 : if (!g_date_valid (&priv->selected_date))
1159 : : {
1160 : : GDate today_date;
1161 : 0 : get_today (&today_date);
1162 : 0 : cc_screen_time_statistics_row_set_selected_date (self, &today_date);
1163 : : }
1164 : : else
1165 : : {
1166 : 0 : update_ui_for_model_or_selected_date (self);
1167 : : }
1168 : : }
1169 : :
1170 : : static void
1171 : 0 : update_ui_for_model_or_selected_date (CcScreenTimeStatisticsRow *self)
1172 : : {
1173 : 0 : CcScreenTimeStatisticsRowPrivate *priv = cc_screen_time_statistics_row_get_instance_private (self);
1174 : : size_t retval;
1175 : 0 : char selected_date_text[100] = { 0, };
1176 : : unsigned int screen_time_for_selected_date;
1177 : 0 : const char * const average_weekday_labels[] = {
1178 : : NULL, /* G_DATE_BAD_WEEKDAY */
1179 : 0 : _("Average Monday"),
1180 : 0 : _("Average Tuesday"),
1181 : 0 : _("Average Wednesday"),
1182 : 0 : _("Average Thursday"),
1183 : 0 : _("Average Friday"),
1184 : 0 : _("Average Saturday"),
1185 : 0 : _("Average Sunday"),
1186 : : };
1187 : : unsigned int average_screen_time_for_selected_day_of_week;
1188 : : GDate today;
1189 : : GDate first_day_of_selected_week;
1190 : : GDate last_day_of_selected_week;
1191 [ # # ]: 0 : g_autofree char *week_date_text = NULL;
1192 : : unsigned int screen_time_for_selected_week;
1193 : : unsigned int screen_time_for_average_week;
1194 : : gboolean data_available;
1195 [ # # ]: 0 : g_autofree double *data_slice = NULL;
1196 : : int model_offset;
1197 : :
1198 : : /* The only way it’s possible to *not* have a date selected is if no data is
1199 : : * available. */
1200 [ # # # # ]: 0 : data_available = (g_date_valid (&priv->selected_date) && priv->model.n_days > 0);
1201 [ # # ]: 0 : gtk_stack_set_visible_child_name (priv->data_stack, data_available ? "main" : "no-data");
1202 : 0 : bar_chart_update_accessible_description (self);
1203 : :
1204 [ # # ]: 0 : if (!data_available)
1205 : 0 : return;
1206 : :
1207 : 0 : get_today (&today);
1208 : 0 : get_first_day_of_week (&priv->selected_date, &first_day_of_selected_week);
1209 : 0 : get_last_day_of_week (&priv->selected_date, &last_day_of_selected_week);
1210 : :
1211 : : /* Do we need to change the data in the chart because the selected date is
1212 : : * outside the currently shown range? */
1213 : 0 : model_offset = g_date_days_between (&priv->model.start_date, &first_day_of_selected_week);
1214 : :
1215 : : /* If we naively took a slice of size 7 starting at
1216 : : * `priv->model.screen_time_per_day + model_offset`, there would potentially
1217 : : * be out-of-bounds accesses at either end of the model. Allocate a temporary
1218 : : * buffer to avoid that, and initialise its values to NAN to indicate
1219 : : * unknown/unset data values. */
1220 : 0 : data_slice = g_new (double, 7);
1221 [ # # ]: 0 : for (int i = 0; i < 7; i++)
1222 [ # # # # ]: 0 : data_slice[i] = (model_offset + i >= 0 && (unsigned int) (model_offset + i) < priv->model.n_days) ? priv->model.screen_time_per_day[model_offset + i] : NAN;
1223 : :
1224 : 0 : cc_bar_chart_set_data (priv->bar_chart, data_slice, 7);
1225 : :
1226 : : /* Update UI */
1227 : 0 : cc_bar_chart_set_selected_index (priv->bar_chart, TRUE,
1228 : 0 : g_date_days_between (&first_day_of_selected_week, &priv->selected_date));
1229 : :
1230 [ # # ]: 0 : if (is_today (&priv->selected_date))
1231 : : {
1232 : 0 : g_strlcpy (selected_date_text, _("Today"), sizeof (selected_date_text));
1233 : : }
1234 : : else
1235 : : {
1236 : : /* Translators: This a medium-length date, for example ‘15 April’ */
1237 : 0 : retval = g_date_strftime (selected_date_text, sizeof (selected_date_text), _("%-d %B"), &priv->selected_date);
1238 : 0 : g_assert (retval != 0);
1239 : : }
1240 : :
1241 : 0 : gtk_label_set_label (priv->selected_date_label, selected_date_text);
1242 : :
1243 [ # # ]: 0 : if (is_day_in_model (self, &priv->selected_date))
1244 : : {
1245 : 0 : screen_time_for_selected_date = get_screen_time_for_day (self, &priv->selected_date);
1246 : 0 : label_set_text_hours_and_minutes (priv->selected_screen_time_label, screen_time_for_selected_date);
1247 : : }
1248 : : else
1249 : : {
1250 : 0 : gtk_label_set_label (priv->selected_screen_time_label, _("No Data"));
1251 : : }
1252 : :
1253 : : /* We can’t use g_date_strftime() for this, as in some locales weekdays have
1254 : : * different grammatical genders, and the ‘Average’ prefix needs to match that. */
1255 : 0 : gtk_label_set_text (priv->selected_average_label,
1256 : 0 : average_weekday_labels[g_date_get_weekday (&priv->selected_date)]);
1257 : :
1258 [ # # ]: 0 : if (calculate_average_screen_time_for_day_of_week (self, g_date_get_weekday (&priv->selected_date), &average_screen_time_for_selected_day_of_week))
1259 : 0 : label_set_text_hours_and_minutes (priv->selected_average_value_label, average_screen_time_for_selected_day_of_week);
1260 : : else
1261 : 0 : gtk_label_set_text (priv->selected_average_value_label, _("No Data"));
1262 : :
1263 [ # # ]: 0 : if (is_this_week (&priv->selected_date))
1264 : : {
1265 : 0 : week_date_text = g_strdup_printf (_("This Week"));
1266 : : }
1267 [ # # ]: 0 : else if (g_date_get_month (&first_day_of_selected_week) == g_date_get_month (&last_day_of_selected_week))
1268 : : {
1269 : 0 : char month_name[100] = { 0, };
1270 : :
1271 : 0 : retval = g_date_strftime (month_name, sizeof (month_name), "%B", &first_day_of_selected_week);
1272 : 0 : g_assert (retval != 0);
1273 : :
1274 : : /* Translators: This is a range of days within a given month.
1275 : : * For example ‘20–27 April’. The dash is an en-dash. */
1276 : 0 : week_date_text = g_strdup_printf (_("%u–%u %s"),
1277 : 0 : g_date_get_day (&first_day_of_selected_week),
1278 : 0 : g_date_get_day (&last_day_of_selected_week),
1279 : : month_name);
1280 : : }
1281 : : else
1282 : : {
1283 : 0 : char first_month_name[100] = { 0, };
1284 : 0 : char last_month_name[100] = { 0, };
1285 : :
1286 : 0 : retval = g_date_strftime (first_month_name, sizeof (first_month_name), "%B", &first_day_of_selected_week);
1287 : 0 : g_assert (retval != 0);
1288 : 0 : retval = g_date_strftime (last_month_name, sizeof (last_month_name), "%B", &last_day_of_selected_week);
1289 : 0 : g_assert (retval != 0);
1290 : :
1291 : : /* Translators: This is a range of days spanning two months.
1292 : : * For example, ‘27 April–4 May’. The dash is an en-dash. */
1293 : 0 : week_date_text = g_strdup_printf (_("%u %s–%u %s"),
1294 : 0 : g_date_get_day (&first_day_of_selected_week),
1295 : : first_month_name,
1296 : 0 : g_date_get_day (&last_day_of_selected_week),
1297 : : last_month_name);
1298 : : }
1299 : :
1300 : 0 : gtk_label_set_label (priv->week_date_label, week_date_text);
1301 : :
1302 : 0 : screen_time_for_selected_week = calculate_total_screen_time_for_week (self, &first_day_of_selected_week);
1303 : 0 : label_set_text_hours_and_minutes (priv->week_screen_time_label, screen_time_for_selected_week);
1304 : :
1305 : 0 : screen_time_for_average_week = calculate_average_screen_time_per_week (self);
1306 : 0 : label_set_text_hours_and_minutes (priv->week_average_value_label, screen_time_for_average_week);
1307 : :
1308 : : /* Update button sensitivity. */
1309 : 0 : gtk_widget_set_sensitive (GTK_WIDGET (priv->previous_week_button),
1310 : 0 : g_date_days_between (&priv->model.start_date, &first_day_of_selected_week) > 0);
1311 : 0 : gtk_widget_set_sensitive (GTK_WIDGET (priv->next_week_button),
1312 : 0 : g_date_days_between (&last_day_of_selected_week, &today) > 0);
1313 : : }
1314 : :
1315 : : static char *
1316 : 0 : bar_chart_continuous_axis_label_cb (CcBarChart *chart,
1317 : : double value,
1318 : : void *user_data)
1319 : : {
1320 [ # # ]: 0 : if (isnan (value))
1321 : 0 : return g_strdup ("");
1322 : :
1323 : : /* @value is in minutes already */
1324 : 0 : return format_hours_and_minutes (value, TRUE);
1325 : : }
1326 : :
1327 : : static double
1328 : 0 : bar_chart_continuous_axis_grid_line_cb (CcBarChart *chart,
1329 : : unsigned int idx,
1330 : : void *user_data)
1331 : : {
1332 : : /* A grid line every 2h */
1333 : 0 : return idx * 2 * 60;
1334 : : }
1335 : :
1336 : : static void
1337 : 0 : bar_chart_update_accessible_description (CcScreenTimeStatisticsRow *self)
1338 : : {
1339 : 0 : CcScreenTimeStatisticsRowPrivate *priv = cc_screen_time_statistics_row_get_instance_private (self);
1340 : 0 : g_autofree char *description = NULL;
1341 : :
1342 [ # # # # ]: 0 : if (g_date_valid (&priv->selected_date) && priv->daily_limit_minutes != 0)
1343 : 0 : {
1344 : : char date_str[200];
1345 : : size_t retval;
1346 : 0 : g_autofree char *daily_limit_str = NULL;
1347 : : GDate first_day_of_week;
1348 : :
1349 : 0 : get_first_day_of_week (&priv->selected_date, &first_day_of_week);
1350 : 0 : retval = g_date_strftime (date_str, sizeof (date_str), "%x", &first_day_of_week);
1351 : 0 : g_assert (retval != 0);
1352 : :
1353 : 0 : daily_limit_str = cc_util_time_to_string_text (priv->daily_limit_minutes * 60 * 1000);
1354 : :
1355 : : /* Translators: The first placeholder is a formatted date string
1356 : : * (formatted using the `%x` strftime placeholder, which gives the
1357 : : * preferred date representation for the current locale without the time).
1358 : : * The second placeholder is a formatted time duration (for example,
1359 : : * ‘3 hours’ or ‘25 minutes’). */
1360 : 0 : description = g_strdup_printf (_("Bar chart of screen time usage over the "
1361 : : "week starting %s. A line is overlayed at "
1362 : : "the %s mark to indicate the "
1363 : : "configured screen time limit."),
1364 : : date_str, daily_limit_str);
1365 : : }
1366 [ # # ]: 0 : else if (g_date_valid (&priv->selected_date))
1367 : : {
1368 : : char date_str[200];
1369 : : size_t retval;
1370 : : GDate first_day_of_week;
1371 : :
1372 : 0 : get_first_day_of_week (&priv->selected_date, &first_day_of_week);
1373 : 0 : retval = g_date_strftime (date_str, sizeof (date_str), "%x", &first_day_of_week);
1374 : 0 : g_assert (retval != 0);
1375 : :
1376 : : /* Translators: The placeholder is a formatted date string (formatted
1377 : : * using the `%x` strftime placeholder, which gives the preferred date
1378 : : * representation for the current locale without the time). */
1379 : 0 : description = g_strdup_printf (_("Bar chart of screen time usage over the "
1380 : : "week starting %s."),
1381 : : date_str);
1382 : : }
1383 : : else
1384 : : {
1385 : 0 : description = g_strdup_printf (_("Placeholder for a bar chart of screen "
1386 : : "time usage. No data is currently "
1387 : : "available."));
1388 : : }
1389 : :
1390 : 0 : gtk_accessible_update_property (GTK_ACCESSIBLE (priv->bar_chart),
1391 : : GTK_ACCESSIBLE_PROPERTY_DESCRIPTION, description,
1392 : : -1);
1393 : 0 : }
1394 : :
1395 : : static void
1396 : 0 : bar_chart_notify_selected_index_cb (GObject *object,
1397 : : GParamSpec *pspec,
1398 : : gpointer user_data)
1399 : : {
1400 : 0 : CcScreenTimeStatisticsRow *self = CC_SCREEN_TIME_STATISTICS_ROW (user_data);
1401 : 0 : CcScreenTimeStatisticsRowPrivate *priv = cc_screen_time_statistics_row_get_instance_private (self);
1402 : : GDate new_selected_date;
1403 : : GDate *new_selected_date_ptr;
1404 : 0 : size_t idx = 0;
1405 : :
1406 [ # # ]: 0 : if (cc_bar_chart_get_selected_index (priv->bar_chart, &idx))
1407 : : {
1408 : 0 : get_first_day_of_week (&priv->selected_date, &new_selected_date);
1409 : 0 : g_date_add_days (&new_selected_date, idx);
1410 : 0 : new_selected_date_ptr = &new_selected_date;
1411 : : }
1412 : : else
1413 : : {
1414 : 0 : new_selected_date_ptr = NULL;
1415 : : }
1416 : :
1417 : 0 : cc_screen_time_statistics_row_set_selected_date (self, new_selected_date_ptr);
1418 : 0 : }
1419 : :
1420 : : static void
1421 : 0 : previous_week_button_clicked_cb (GtkButton *button,
1422 : : gpointer user_data)
1423 : : {
1424 : 0 : CcScreenTimeStatisticsRow *self = CC_SCREEN_TIME_STATISTICS_ROW (user_data);
1425 : 0 : CcScreenTimeStatisticsRowPrivate *priv = cc_screen_time_statistics_row_get_instance_private (self);
1426 : : GDate first_day_of_previous_week;
1427 : :
1428 : 0 : get_first_day_of_week (&priv->selected_date, &first_day_of_previous_week);
1429 : 0 : g_date_subtract_days (&first_day_of_previous_week, 7);
1430 : :
1431 : 0 : cc_screen_time_statistics_row_set_selected_date (self, &first_day_of_previous_week);
1432 : 0 : }
1433 : :
1434 : : static void
1435 : 0 : next_week_button_clicked_cb (GtkButton *button,
1436 : : gpointer user_data)
1437 : : {
1438 : 0 : CcScreenTimeStatisticsRow *self = CC_SCREEN_TIME_STATISTICS_ROW (user_data);
1439 : 0 : CcScreenTimeStatisticsRowPrivate *priv = cc_screen_time_statistics_row_get_instance_private (self);
1440 : : GDate first_day_of_next_week;
1441 : :
1442 : 0 : get_first_day_of_week (&priv->selected_date, &first_day_of_next_week);
1443 : 0 : g_date_add_days (&first_day_of_next_week, 7);
1444 : :
1445 : 0 : cc_screen_time_statistics_row_set_selected_date (self, &first_day_of_next_week);
1446 : 0 : }
1447 : :
1448 : : static void
1449 : 0 : history_file_monitor_changed_cb (GFileMonitor *monitor,
1450 : : GFile *file,
1451 : : GFile *other_file,
1452 : : GFileMonitorEvent event_type,
1453 : : gpointer user_data)
1454 : : {
1455 : 0 : CcScreenTimeStatisticsRow *self = CC_SCREEN_TIME_STATISTICS_ROW (user_data);
1456 : 0 : CcScreenTimeStatisticsRowPrivate *priv = cc_screen_time_statistics_row_get_instance_private (self);
1457 : :
1458 : 0 : g_assert (priv->history_file != NULL);
1459 : :
1460 : 0 : g_debug ("%s: Reloading history file ‘%s’ as it’s changed", G_STRFUNC, g_file_peek_path (priv->history_file));
1461 : :
1462 : 0 : update_model (self);
1463 : 0 : }
1464 : :
1465 : : static gboolean
1466 : 0 : history_file_update_timeout_cb (gpointer user_data)
1467 : : {
1468 : 0 : CcScreenTimeStatisticsRow *self = CC_SCREEN_TIME_STATISTICS_ROW (user_data);
1469 : :
1470 : 0 : g_debug ("%s: Reloading history data due to the passage of time", G_STRFUNC);
1471 : :
1472 : 0 : update_model (self);
1473 : :
1474 : 0 : return G_SOURCE_CONTINUE;
1475 : : }
1476 : :
1477 : : static void
1478 : 0 : maybe_enable_update_timeout (CcScreenTimeStatisticsRow *self)
1479 : : {
1480 : 0 : CcScreenTimeStatisticsRowPrivate *priv = cc_screen_time_statistics_row_get_instance_private (self);
1481 [ # # # # ]: 0 : gboolean should_be_enabled = (priv->history_file != NULL && gtk_widget_get_mapped (GTK_WIDGET (self)));
1482 : 0 : gboolean is_enabled = (priv->update_timeout_source != NULL);
1483 : :
1484 [ # # # # ]: 0 : if (should_be_enabled && !is_enabled)
1485 : : {
1486 : 0 : priv->update_timeout_source = g_timeout_source_new_seconds (60 * 60);
1487 : 0 : g_source_set_callback (priv->update_timeout_source, G_SOURCE_FUNC (history_file_update_timeout_cb), self, NULL);
1488 : 0 : g_source_attach (priv->update_timeout_source, NULL);
1489 : : }
1490 [ # # # # ]: 0 : else if (is_enabled && !should_be_enabled)
1491 : : {
1492 : 0 : g_source_destroy (priv->update_timeout_source);
1493 [ # # ]: 0 : g_clear_pointer (&priv->update_timeout_source, g_source_unref);
1494 : : }
1495 : 0 : }
1496 : :
1497 : : /**
1498 : : * cc_screen_time_statistics_row_new:
1499 : : *
1500 : : * Create a new #CcScreenTimeStatisticsRow.
1501 : : *
1502 : : * Returns: (transfer full): the new #CcScreenTimeStatisticsRow
1503 : : */
1504 : : CcScreenTimeStatisticsRow *
1505 : 0 : cc_screen_time_statistics_row_new (void)
1506 : : {
1507 : 0 : return g_object_new (CC_TYPE_SCREEN_TIME_STATISTICS_ROW, NULL);
1508 : : }
1509 : :
1510 : : /**
1511 : : * cc_screen_time_statistics_row_get_history_file:
1512 : : * @self: a #CcScreenTimeStatisticsRow
1513 : : *
1514 : : * Get the value of #CcScreenTimeStatisticsRow:history-file.
1515 : : *
1516 : : * Returns: (transfer none) (nullable): history file which has been loaded, or
1517 : : * %NULL if not set
1518 : : */
1519 : : GFile *
1520 : 0 : cc_screen_time_statistics_row_get_history_file (CcScreenTimeStatisticsRow *self)
1521 : : {
1522 : 0 : CcScreenTimeStatisticsRowPrivate *priv = cc_screen_time_statistics_row_get_instance_private (self);
1523 : :
1524 : 0 : g_return_val_if_fail (CC_IS_SCREEN_TIME_STATISTICS_ROW (self), NULL);
1525 : :
1526 : 0 : return priv->history_file;
1527 : : }
1528 : :
1529 : : /**
1530 : : * cc_screen_time_statistics_row_set_history_file:
1531 : : * @self: a #CcScreenTimeStatisticsRow
1532 : : * @selected_date: (transfer none) (nullable): new history file to load, or
1533 : : * %NULL to clear it
1534 : : *
1535 : : * Set the value of #CcScreenTimeStatisticsRow:history-file.
1536 : : */
1537 : : void
1538 : 0 : cc_screen_time_statistics_row_set_history_file (CcScreenTimeStatisticsRow *self,
1539 : : GFile *history_file)
1540 : : {
1541 : 0 : CcScreenTimeStatisticsRowPrivate *priv = cc_screen_time_statistics_row_get_instance_private (self);
1542 [ # # ]: 0 : g_autoptr(GError) local_error = NULL;
1543 : :
1544 : 0 : g_return_if_fail (CC_IS_SCREEN_TIME_STATISTICS_ROW (self));
1545 : 0 : g_return_if_fail (history_file == NULL || G_IS_FILE (history_file));
1546 : :
1547 [ # # ]: 0 : if (g_set_object (&priv->history_file, history_file))
1548 : : {
1549 [ # # ]: 0 : g_debug ("%s: Loading history file ‘%s’", G_STRFUNC, (history_file != NULL) ? g_file_peek_path (history_file) : "(unset)");
1550 : :
1551 : 0 : update_model (self);
1552 : :
1553 : : /* Monitor the file for changes. */
1554 [ # # ]: 0 : if (priv->history_file_monitor_changed_id != 0)
1555 : 0 : g_signal_handler_disconnect (priv->history_file_monitor, priv->history_file_monitor_changed_id);
1556 : 0 : priv->history_file_monitor_changed_id = 0;
1557 [ # # ]: 0 : if (priv->history_file_monitor != NULL)
1558 : 0 : g_file_monitor_cancel (priv->history_file_monitor);
1559 [ # # ]: 0 : g_clear_object (&priv->history_file_monitor);
1560 : :
1561 [ # # ]: 0 : if (priv->history_file != NULL)
1562 : : {
1563 : 0 : g_autoptr(GFileMonitor) monitor = NULL;
1564 : :
1565 : 0 : monitor = g_file_monitor_file (priv->history_file, G_FILE_MONITOR_NONE,
1566 : : NULL, &local_error);
1567 [ # # ]: 0 : if (local_error != NULL)
1568 : 0 : g_warning ("Error monitoring history file ‘%s’: %s",
1569 : : g_file_peek_path (priv->history_file), local_error->message);
1570 : : else
1571 : 0 : priv->history_file_monitor_changed_id = g_signal_connect (monitor, "changed", G_CALLBACK (history_file_monitor_changed_cb), self);
1572 : :
1573 : 0 : g_set_object (&priv->history_file_monitor, monitor);
1574 : : }
1575 : :
1576 : : /* Periodically reload the data so the graph is updated with the passage
1577 : : * of time. */
1578 : 0 : maybe_enable_update_timeout (self);
1579 : :
1580 : 0 : g_object_notify_by_pspec (G_OBJECT (self), props[PROP_HISTORY_FILE]);
1581 : : }
1582 : : }
1583 : :
1584 : : /**
1585 : : * cc_screen_time_statistics_row_get_selected_date:
1586 : : * @self: a #CcScreenTimeStatisticsRow
1587 : : *
1588 : : * Get the value of #CcScreenTimeStatisticsRow:selected-date.
1589 : : *
1590 : : * Returns: (nullable) (transfer none): currently selected date, or %NULL if no
1591 : : * data is available
1592 : : */
1593 : : const GDate *
1594 : 0 : cc_screen_time_statistics_row_get_selected_date (CcScreenTimeStatisticsRow *self)
1595 : : {
1596 : 0 : CcScreenTimeStatisticsRowPrivate *priv = cc_screen_time_statistics_row_get_instance_private (self);
1597 : :
1598 : 0 : g_return_val_if_fail (CC_IS_SCREEN_TIME_STATISTICS_ROW (self), NULL);
1599 : :
1600 [ # # ]: 0 : return g_date_valid (&priv->selected_date) ? &priv->selected_date : NULL;
1601 : : }
1602 : :
1603 : : /**
1604 : : * cc_screen_time_statistics_row_set_selected_date:
1605 : : * @self: a #CcScreenTimeStatisticsRow
1606 : : * @selected_date: (transfer none) (nullable): new selected date, or %NULL if no
1607 : : * data is available
1608 : : *
1609 : : * Set the value of #CcScreenTimeStatisticsRow:selected-date.
1610 : : */
1611 : : void
1612 : 0 : cc_screen_time_statistics_row_set_selected_date (CcScreenTimeStatisticsRow *self,
1613 : : const GDate *selected_date)
1614 : : {
1615 : 0 : CcScreenTimeStatisticsRowPrivate *priv = cc_screen_time_statistics_row_get_instance_private (self);
1616 : :
1617 : 0 : g_return_if_fail (CC_IS_SCREEN_TIME_STATISTICS_ROW (self));
1618 : :
1619 [ # # # # : 0 : if ((!g_date_valid (&priv->selected_date) && selected_date == NULL) ||
# # ]
1620 [ # # # # ]: 0 : (g_date_valid (&priv->selected_date) && selected_date != NULL &&
1621 : 0 : g_date_compare (&priv->selected_date, selected_date) == 0))
1622 : 0 : return;
1623 : :
1624 : : /* Log the selected date */
1625 : : {
1626 : : char date_str[200];
1627 [ # # ]: 0 : if (selected_date != NULL)
1628 : 0 : g_date_strftime (date_str, sizeof (date_str), "%x", selected_date);
1629 [ # # ]: 0 : g_debug ("%s: %s", G_STRFUNC, (selected_date != NULL) ? date_str : "(unset)");
1630 : : }
1631 : :
1632 : 0 : priv->selected_date = *selected_date;
1633 : :
1634 : 0 : update_ui_for_model_or_selected_date (self);
1635 : :
1636 : 0 : g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SELECTED_DATE]);
1637 : : }
1638 : :
1639 : : /**
1640 : : * cc_screen_time_statistics_row_get_daily_limit:
1641 : : * @self: a #CcScreenTimeStatisticsRow
1642 : : *
1643 : : * Get the value of #CcScreenTimeStatisticsRow:daily-limit.
1644 : : *
1645 : : * Returns: the daily computer usage time limit, in minutes, or `0` if unset
1646 : : */
1647 : : unsigned int
1648 : 0 : cc_screen_time_statistics_row_get_daily_limit (CcScreenTimeStatisticsRow *self)
1649 : : {
1650 : 0 : CcScreenTimeStatisticsRowPrivate *priv = cc_screen_time_statistics_row_get_instance_private (self);
1651 : :
1652 : 0 : g_return_val_if_fail (CC_IS_SCREEN_TIME_STATISTICS_ROW (self), 0);
1653 : :
1654 : 0 : return priv->daily_limit_minutes;
1655 : : }
1656 : :
1657 : : /**
1658 : : * cc_screen_time_statistics_row_set_daily_limit:
1659 : : * @self: a #CcScreenTimeStatisticsRow
1660 : : * @daily_limit_minutes: the daily computer usage time limit, in minutes, or
1661 : : * `0` to unset it
1662 : : *
1663 : : * Set #CcScreenTimeStatisticsRow:daily-limit.
1664 : : */
1665 : : void
1666 : 0 : cc_screen_time_statistics_row_set_daily_limit (CcScreenTimeStatisticsRow *self,
1667 : : unsigned int daily_limit_minutes)
1668 : : {
1669 : 0 : CcScreenTimeStatisticsRowPrivate *priv = cc_screen_time_statistics_row_get_instance_private (self);
1670 : :
1671 : 0 : g_return_if_fail (CC_IS_SCREEN_TIME_STATISTICS_ROW (self));
1672 : :
1673 [ # # ]: 0 : if (priv->daily_limit_minutes == daily_limit_minutes)
1674 : 0 : return;
1675 : :
1676 [ # # ]: 0 : cc_bar_chart_set_overlay_line_value (priv->bar_chart, (daily_limit_minutes > 0) ? daily_limit_minutes : NAN);
1677 : 0 : bar_chart_update_accessible_description (self);
1678 : :
1679 : 0 : g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DAILY_LIMIT]);
1680 : : }
|