1 /* gtd-plugin-background.c
2  *
3  * Copyright (C) 2017-2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com>
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program 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
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 #define G_LOG_DOMAIN "GtdPluginBackground"
20 
21 #include "config.h"
22 
23 #include "gtd-debug.h"
24 #include "gtd-plugin-background.h"
25 
26 #include <glib/gi18n.h>
27 #include <gdk/gdk.h>
28 #include <gtk/gtk.h>
29 
30 #include <libportal/portal.h>
31 #include <libportal/portal-gtk4.h>
32 
33 #define AUTOSTART_NOTIFICATION_ID      "Gtd::BackgroundPlugin::autostart_notification"
34 #define AUTOSTART_NOTIFICATION_TIMEOUT 3  /* seconds */
35 #define MAX_BODY_LENGTH                50 /* chars */
36 
37 #define DESKTOP_PORTAL_BUS_NAME "org.freedesktop.portal.Desktop"
38 #define DESKTOP_PORTAL_OBJECT_PATH "/org/freedesktop/portal/desktop"
39 #define DESKTOP_PORTAL_BACKGROUND_INTERFACE "org.freedesktop.portal.Background"
40 
41 struct _GtdPluginBackground
42 {
43   PeasExtensionBase   parent;
44 
45   GtkWidget          *preferences_panel;
46 
47   GDBusProxy         *desktop_portal;
48 
49   GSettings          *settings;
50   gboolean            startup_notification;
51   gboolean            show_notifications;
52 
53   gboolean            enabled;
54   gboolean            autostart;
55 
56   XdpPortal          *portal;
57 
58   guint               startup_notification_timeout_id;
59 };
60 
61 static void          on_tasklist_notified                        (GtdPluginBackground      *self);
62 
63 static void          gtd_activatable_iface_init                  (GtdActivatableInterface  *iface);
64 
65 G_DEFINE_DYNAMIC_TYPE_EXTENDED (GtdPluginBackground, gtd_plugin_background, PEAS_TYPE_EXTENSION_BASE,
66                                 0,
67                                 G_IMPLEMENT_INTERFACE_DYNAMIC (GTD_TYPE_ACTIVATABLE,
68                                                                gtd_activatable_iface_init))
69 
70 enum {
71   PROP_0,
72   PROP_PREFERENCES_PANEL,
73   N_PROPS
74 };
75 
76 /*
77  * Auxiliary methods
78  */
79 
80 static inline GtkWindow*
get_window(void)81 get_window (void)
82 {
83   GtkApplication *app = GTK_APPLICATION (g_application_get_default ());
84 
85   return GTK_WINDOW (gtk_application_get_active_window (app));
86 }
87 
88 static gboolean
is_gnome_todo_active(void)89 is_gnome_todo_active (void)
90 {
91   GListModel *toplevels;
92   guint i;
93 
94   toplevels = gtk_window_get_toplevels ();
95   for (i = 0; i < g_list_model_get_n_items (toplevels); i++)
96     {
97       g_autoptr (GtkWidget) toplevel = g_list_model_get_item (toplevels, i);
98 
99       if (GTK_IS_WINDOW (toplevel) && gtk_window_is_active (GTK_WINDOW (toplevel)))
100         return TRUE;
101     }
102 
103   return FALSE;
104 }
105 
106 static void
on_request_background_called_cb(GObject * object,GAsyncResult * result,gpointer user_data)107 on_request_background_called_cb (GObject      *object,
108                                  GAsyncResult *result,
109                                  gpointer      user_data)
110 {
111   g_autoptr (GError) error = NULL;
112 
113   GTD_ENTRY;
114 
115   xdp_portal_request_background_finish (XDP_PORTAL (object), result, &error);
116 
117   if (error)
118     g_warning ("Error requesting background: %s", error->message);
119 
120   GTD_EXIT;
121 }
122 
123 static void
update_background_portal(GtdPluginBackground * self)124 update_background_portal (GtdPluginBackground *self)
125 {
126   g_autoptr (GPtrArray) commandline = NULL;
127   XdpBackgroundFlags background_flags;
128   XdpParent *parent = NULL;
129   GtkWindow *window;
130 
131   GTD_ENTRY;
132 
133   window = get_window ();
134   parent = xdp_parent_new_gtk (window);
135   background_flags = XDP_BACKGROUND_FLAG_ACTIVATABLE;
136 
137   if (self->autostart)
138     {
139       commandline = g_ptr_array_new_with_free_func (g_free);
140       g_ptr_array_add (commandline, g_strdup ("gnome-todo"));
141       g_ptr_array_add (commandline, g_strdup ("--gapplication-service"));
142 
143       background_flags |= XDP_BACKGROUND_FLAG_AUTOSTART;
144     }
145 
146   xdp_portal_request_background (self->portal,
147                                  parent,
148                                  NULL,
149                                  commandline,
150                                  background_flags,
151                                  NULL,
152                                  on_request_background_called_cb,
153                                  self);
154 
155   xdp_parent_free (parent);
156 
157   GTD_EXIT;
158 }
159 
160 static void
on_window_active_changed_cb(GtkWindow * window,GParamSpec * pspec,GtdPluginBackground * self)161 on_window_active_changed_cb (GtkWindow           *window,
162                              GParamSpec          *pspec,
163                              GtdPluginBackground *self)
164 {
165   GTD_ENTRY;
166 
167   g_signal_handlers_disconnect_by_func (window, on_window_active_changed_cb, self);
168   update_background_portal (self);
169 
170   GTD_EXIT;
171 }
172 
173 static void
start_update(GtdPluginBackground * self)174 start_update (GtdPluginBackground *self)
175 {
176   GtkWindow *window;
177 
178   GTD_ENTRY;
179 
180   window = get_window ();
181   if (gtk_widget_get_visible (GTK_WIDGET (window)) && is_gnome_todo_active ())
182     {
183       update_background_portal (self);
184     }
185   else
186     {
187       g_signal_connect_object (window,
188                                "notify::is-active",
189                                G_CALLBACK (on_window_active_changed_cb),
190                                self,
191                                G_CONNECT_AFTER);
192     }
193 
194   GTD_EXIT;
195 }
196 
197 static gchar*
format_notification_body(GList * tasks,guint n_tasks)198 format_notification_body (GList *tasks,
199                           guint  n_tasks)
200 {
201   GString *string, *aux;
202   GList *l;
203   guint current_task, length;
204 
205   aux = g_string_new ("");
206   string = g_string_new ("");
207   current_task = length = 0;
208 
209   for (l = tasks; l != NULL; l = l->next)
210     {
211       GtdTask *task = l->data;
212 
213       length += g_utf8_strlen (gtd_task_get_title (task), -1);
214 
215       if (length > MAX_BODY_LENGTH)
216         break;
217 
218       /* Prepend comma */
219       if (current_task > 0)
220         g_string_append (aux, ", ");
221 
222       g_string_append (aux, gtd_task_get_title (task));
223       current_task++;
224     }
225 
226   if (current_task == 0)
227     {
228       /* The first task has a huge title. Let's ellipsize it */
229       g_string_append (string, gtd_task_get_title (tasks->data));
230       g_string_truncate (string, MAX_BODY_LENGTH - 1);
231       g_string_append (string, "…");
232     }
233   else if (current_task < n_tasks)
234     {
235       /* Some tasks fit, but we need to append a text explaining there are more */
236       g_string_append_printf (string,
237                               g_dngettext (GETTEXT_PACKAGE,
238                                            "%1$s and one more task",
239                                            "%1$s and %2$d other tasks",
240                                            n_tasks - current_task),
241                               aux->str,
242                               n_tasks - current_task);
243     }
244   else
245     {
246       /* We were able to print all task titles */
247       g_string_append (string, aux->str);
248     }
249 
250   g_string_free (aux, TRUE);
251 
252   return g_string_free (string, FALSE);
253 }
254 
255 static inline gboolean
is_today(GDateTime * now,GDateTime * dt)256 is_today (GDateTime *now,
257           GDateTime *dt)
258 {
259   return g_date_time_get_year (dt) == g_date_time_get_year (now) &&
260          g_date_time_get_month (dt) == g_date_time_get_month (now) &&
261          g_date_time_get_day_of_month (dt) == g_date_time_get_day_of_month (now);
262 }
263 
264 static GList*
get_tasks_for_today(guint * n_events)265 get_tasks_for_today (guint *n_events)
266 {
267   g_autoptr (GDateTime) now;
268   GListModel *lists;
269   GtdManager *manager;
270   GList *result;
271   guint n_tasks;
272   guint i;
273 
274   now = g_date_time_new_now_local ();
275   result = NULL;
276   n_tasks = 0;
277   manager = gtd_manager_get_default ();
278   lists = gtd_manager_get_task_lists_model (manager);
279 
280   for (i = 0; i < g_list_model_get_n_items (lists); i++)
281     {
282       g_autoptr (GListModel) list = NULL;
283       guint j;
284 
285       list = g_list_model_get_item (lists, i);
286 
287       for (j = 0; j < g_list_model_get_n_items (list); j++)
288         {
289           GDateTime *due_date;
290           GtdTask *task;
291 
292           task = g_list_model_get_item (list, j);
293 
294           due_date = gtd_task_get_due_date (task);
295 
296           if (!due_date || !is_today (now, due_date) || gtd_task_get_complete (task))
297             continue;
298 
299           n_tasks += 1;
300           result = g_list_prepend (result, task);
301         }
302     }
303 
304   if (n_events)
305     *n_events = n_tasks;
306 
307   return result;
308 }
309 
310 static void
send_notification(GtdPluginBackground * self)311 send_notification (GtdPluginBackground *self)
312 {
313   GNotification *notification;
314   GApplication *app;
315   GtdWindow *window;
316   guint n_tasks;
317   GList *tasks;
318   gchar *title;
319   gchar *body;
320 
321   window = GTD_WINDOW (get_window ());
322 
323   /*
324    * If the user already focused To Do's window, we don't have to
325    * notify about the number of tasks.
326    */
327   if (gtk_window_is_active (GTK_WINDOW (window)))
328     return;
329 
330   /* The user don't want to be bothered with notifications */
331   if (!g_settings_get_boolean (self->settings, "show-notifications"))
332     return;
333 
334   app = g_application_get_default ();
335   tasks = get_tasks_for_today (&n_tasks);
336 
337   /* If n_tasks == 0, tasks == NULL, thus we don't need to free it */
338   if (n_tasks == 0)
339     return;
340 
341   title = g_strdup_printf (g_dngettext (GETTEXT_PACKAGE,
342                                         "You have %d task for today",
343                                         "You have %d tasks for today",
344                                         n_tasks),
345                            n_tasks);
346 
347   body = format_notification_body (tasks, n_tasks);
348 
349   /* Build up the notification */
350   notification = g_notification_new (title);
351   g_notification_set_body (notification, body);
352   g_notification_set_default_action (notification, "app.activate");
353 
354   g_application_send_notification (app, AUTOSTART_NOTIFICATION_ID, notification);
355 
356   g_clear_pointer (&tasks, g_list_free);
357   g_clear_object (&notification);
358 }
359 
360 /*
361  * Callbacks
362  */
363 static void
on_startup_changed(GSettings * settings,const gchar * key,GtdPluginBackground * self)364 on_startup_changed (GSettings           *settings,
365                     const gchar         *key,
366                     GtdPluginBackground *self)
367 {
368   self->autostart = g_settings_get_boolean (settings, key);
369 
370   start_update (self);
371 }
372 
373 static gboolean
on_startup_timeout_cb(GtdPluginBackground * self)374 on_startup_timeout_cb (GtdPluginBackground *self)
375 {
376   send_notification (self);
377 
378   self->startup_notification_timeout_id = 0;
379 
380   g_signal_handlers_disconnect_by_func (gtd_manager_get_default (),
381                                         on_tasklist_notified,
382                                         self);
383 
384   return G_SOURCE_REMOVE;
385 }
386 
387 static void
on_tasklist_notified(GtdPluginBackground * self)388 on_tasklist_notified (GtdPluginBackground *self)
389 {
390   /* Remove previously set timeout */
391   if (self->startup_notification_timeout_id > 0)
392     {
393       g_source_remove (self->startup_notification_timeout_id);
394       self->startup_notification_timeout_id = 0;
395     }
396 
397   self->startup_notification_timeout_id = g_timeout_add_seconds (AUTOSTART_NOTIFICATION_TIMEOUT,
398                                                                  (GSourceFunc) on_startup_timeout_cb,
399                                                                  self);
400 }
401 
402 static void
watch_manager_for_new_lists(GtdPluginBackground * self)403 watch_manager_for_new_lists (GtdPluginBackground *self)
404 {
405   GtdManager *manager = gtd_manager_get_default ();
406 
407   g_signal_connect_swapped (manager,
408                             "list-added",
409                             G_CALLBACK (on_tasklist_notified),
410                             self);
411 
412   g_signal_connect_swapped (manager,
413                             "list-changed",
414                             G_CALLBACK (on_tasklist_notified),
415                             self);
416 
417   g_signal_connect_swapped (manager,
418                             "list-removed",
419                             G_CALLBACK (on_tasklist_notified),
420                             self);
421 
422   g_signal_connect_swapped (gtd_manager_get_clock (manager),
423                             "day-changed",
424                             G_CALLBACK (send_notification),
425                             self);
426 }
427 
428 /*
429  * GtdActivatable interface implementation
430  */
431 static void
gtd_plugin_background_activate(GtdActivatable * activatable)432 gtd_plugin_background_activate (GtdActivatable *activatable)
433 {
434   GtdPluginBackground *self;
435   GtkWindow *window;
436 
437   self = GTD_PLUGIN_BACKGROUND (activatable);
438   window = get_window ();
439 
440   self->enabled = TRUE;
441 
442   gtk_window_set_hide_on_close (window, TRUE);
443 
444   on_startup_changed (self->settings, "run-on-startup", self);
445   g_signal_connect (self->settings,
446                     "changed::run-on-startup",
447                     G_CALLBACK (on_startup_changed),
448                     self);
449 
450   /* Start watching the manager to notify the user about today's tasks */
451   watch_manager_for_new_lists (self);
452 }
453 
454 static void
gtd_plugin_background_deactivate(GtdActivatable * activatable)455 gtd_plugin_background_deactivate (GtdActivatable *activatable)
456 {
457   GtdPluginBackground *self;
458   GtdManager *manager;
459   GtkWindow *window;
460 
461   self = GTD_PLUGIN_BACKGROUND (activatable);
462   manager = gtd_manager_get_default ();
463   window = get_window ();
464 
465   self->enabled = FALSE;
466   self->autostart = FALSE;
467 
468   gtk_window_set_hide_on_close (window, FALSE);
469 
470   g_signal_handlers_disconnect_by_func (self->settings,
471                                         on_startup_changed,
472                                         self);
473 
474   g_signal_handlers_disconnect_by_func (manager,
475                                         on_tasklist_notified,
476                                         self);
477 
478   g_signal_handlers_disconnect_by_func (gtd_manager_get_clock (manager),
479                                         send_notification,
480                                         self);
481 
482   /* Deactivate the timeout */
483   if (self->startup_notification_timeout_id > 0)
484     {
485       g_source_remove (self->startup_notification_timeout_id);
486       self->startup_notification_timeout_id = 0;
487     }
488 
489   /* Deactivate auto startup */
490   start_update (self);
491 }
492 
493 static GtkWidget*
gtd_plugin_background_get_preferences_panel(GtdActivatable * activatable)494 gtd_plugin_background_get_preferences_panel (GtdActivatable *activatable)
495 {
496   GtdPluginBackground *self = GTD_PLUGIN_BACKGROUND (activatable);
497 
498   return self->preferences_panel;
499 }
500 
501 static void
gtd_activatable_iface_init(GtdActivatableInterface * iface)502 gtd_activatable_iface_init (GtdActivatableInterface *iface)
503 {
504   iface->activate = gtd_plugin_background_activate;
505   iface->deactivate = gtd_plugin_background_deactivate;
506   iface->get_preferences_panel = gtd_plugin_background_get_preferences_panel;
507 }
508 
509 static void
gtd_plugin_background_finalize(GObject * object)510 gtd_plugin_background_finalize (GObject *object)
511 {
512   GtdPluginBackground *self = (GtdPluginBackground *)object;
513 
514   g_clear_object (&self->settings);
515   g_clear_object (&self->portal);
516 
517   G_OBJECT_CLASS (gtd_plugin_background_parent_class)->finalize (object);
518 }
519 
520 static void
gtd_plugin_background_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)521 gtd_plugin_background_get_property (GObject    *object,
522                                     guint       prop_id,
523                                     GValue     *value,
524                                     GParamSpec *pspec)
525 {
526   GtdPluginBackground *self = GTD_PLUGIN_BACKGROUND (object);
527 
528   switch (prop_id)
529     {
530     case PROP_PREFERENCES_PANEL:
531       g_value_set_object (value, self->preferences_panel);
532       break;
533 
534     default:
535       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
536     }
537 }
538 
539 static void
gtd_plugin_background_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)540 gtd_plugin_background_set_property (GObject      *object,
541                                        guint         prop_id,
542                                        const GValue *value,
543                                        GParamSpec   *pspec)
544 {
545   G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
546 }
547 
548 static void
gtd_plugin_background_class_init(GtdPluginBackgroundClass * klass)549 gtd_plugin_background_class_init (GtdPluginBackgroundClass *klass)
550 {
551   GObjectClass *object_class = G_OBJECT_CLASS (klass);
552 
553   object_class->finalize = gtd_plugin_background_finalize;
554   object_class->get_property = gtd_plugin_background_get_property;
555   object_class->set_property = gtd_plugin_background_set_property;
556 
557   g_object_class_override_property (object_class,
558                                     PROP_PREFERENCES_PANEL,
559                                     "preferences-panel");
560 }
561 
562 static void
gtd_plugin_background_init(GtdPluginBackground * self)563 gtd_plugin_background_init (GtdPluginBackground *self)
564 {
565   GtkBuilder *builder;
566 
567   /* Load the settings */
568   self->settings = g_settings_new ("org.gnome.todo.plugins.background");
569 
570   /* And the preferences panel */
571   builder = gtk_builder_new_from_resource ("/org/gnome/todo/plugins/background/ui/preferences.ui");
572 
573   self->preferences_panel = GTK_WIDGET (gtk_builder_get_object (builder, "main_box"));
574   self->portal = xdp_portal_new ();
575 
576   g_settings_bind (self->settings,
577                    "run-on-startup",
578                    gtk_builder_get_object (builder, "startup_switch"),
579                    "active",
580                    G_SETTINGS_BIND_DEFAULT);
581 
582   g_settings_bind (self->settings,
583                    "show-notifications",
584                    gtk_builder_get_object (builder, "notifications_switch"),
585                    "active",
586                    G_SETTINGS_BIND_DEFAULT);
587 }
588 
589 static void
gtd_plugin_background_class_finalize(GtdPluginBackgroundClass * klass)590 gtd_plugin_background_class_finalize (GtdPluginBackgroundClass *klass)
591 {
592 }
593 
594 G_MODULE_EXPORT void
gtd_plugin_background_register_types(PeasObjectModule * module)595 gtd_plugin_background_register_types (PeasObjectModule *module)
596 {
597   gtd_plugin_background_register_type (G_TYPE_MODULE (module));
598 
599   peas_object_module_register_extension_type (module,
600                                               GTD_TYPE_ACTIVATABLE,
601                                               GTD_TYPE_PLUGIN_BACKGROUND);
602 }
603