1 /* bg-recent-source.c
2  *
3  * Copyright 2019 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  * SPDX-License-Identifier: GPL-3.0-or-later
19  */
20 
21 #undef G_LOG_DOMAIN
22 #define G_LOG_DOMAIN "bg-recent-source"
23 
24 #include "bg-recent-source.h"
25 #include "cc-background-item.h"
26 
27 #define ATTRIBUTES G_FILE_ATTRIBUTE_STANDARD_NAME "," \
28                    G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE "," \
29                    G_FILE_ATTRIBUTE_TIME_MODIFIED
30 
31 struct _BgRecentSource
32 {
33   BgSource      parent;
34 
35   GFile        *backgrounds_folder;
36   GFileMonitor *monitor;
37 
38   GCancellable *cancellable;
39   GHashTable   *items;
40 };
41 
42 G_DEFINE_TYPE (BgRecentSource, bg_recent_source, BG_TYPE_SOURCE)
43 
44 
45 static const gchar * const content_types[] = {
46 	"image/png",
47 	"image/jp2",
48 	"image/jpeg",
49 	"image/bmp",
50 	"image/svg+xml",
51 	"image/x-portable-anymap",
52 	NULL
53 };
54 
55 static int
sort_func(gconstpointer a,gconstpointer b,gpointer user_data)56 sort_func (gconstpointer a,
57            gconstpointer b,
58            gpointer      user_data)
59 {
60   CcBackgroundItem *item_a;
61   CcBackgroundItem *item_b;
62   guint64 modified_a;
63   guint64 modified_b;
64   int retval;
65 
66   item_a = (CcBackgroundItem *) a;
67   item_b = (CcBackgroundItem *) b;
68   modified_a = cc_background_item_get_modified (item_a);
69   modified_b = cc_background_item_get_modified (item_b);
70 
71   retval = modified_b - modified_a;
72 
73   return retval;
74 }
75 
76 static void
add_file_from_info(BgRecentSource * self,GFile * file,GFileInfo * info)77 add_file_from_info (BgRecentSource *self,
78                     GFile          *file,
79                     GFileInfo      *info)
80 {
81   g_autoptr(CcBackgroundItem) item = NULL;
82   CcBackgroundItemFlags flags = 0;
83   g_autofree gchar *source_uri = NULL;
84   g_autofree gchar *uri = NULL;
85   GListStore *store;
86   const gchar *content_type;
87   guint64 mtime;
88 
89   content_type = g_file_info_get_content_type (info);
90   mtime = g_file_info_get_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_MODIFIED);
91 
92   if (!content_type || !g_strv_contains (content_types, content_type))
93     return;
94 
95   uri = g_file_get_uri (file);
96   item = cc_background_item_new (uri);
97   flags |= CC_BACKGROUND_ITEM_HAS_SHADING | CC_BACKGROUND_ITEM_HAS_PLACEMENT;
98   g_object_set (G_OBJECT (item),
99                 "flags", flags,
100                 "shading", G_DESKTOP_BACKGROUND_SHADING_SOLID,
101                 "placement", G_DESKTOP_BACKGROUND_STYLE_ZOOM,
102                 "modified", mtime,
103                 "needs-download", FALSE,
104                 "source-url", source_uri,
105                 NULL);
106 
107   store = bg_source_get_liststore (BG_SOURCE (self));
108   g_list_store_insert_sorted (store, item, sort_func, self);
109 
110   g_hash_table_insert (self->items, g_strdup (uri), g_object_ref (item));
111 }
112 
113 static void
remove_item(BgRecentSource * self,CcBackgroundItem * item)114 remove_item (BgRecentSource   *self,
115              CcBackgroundItem *item)
116 {
117   GListStore *store;
118   const gchar *uri;
119   guint i;
120 
121   g_return_if_fail (BG_IS_RECENT_SOURCE (self));
122   g_return_if_fail (CC_IS_BACKGROUND_ITEM (item));
123 
124   uri = cc_background_item_get_uri (item);
125   store = bg_source_get_liststore (BG_SOURCE (self));
126 
127   g_debug ("Removing wallpaper %s", uri);
128 
129   for (i = 0; i < g_list_model_get_n_items (G_LIST_MODEL (store)); i++)
130     {
131       g_autoptr(CcBackgroundItem) tmp = NULL;
132 
133       tmp = g_list_model_get_item (G_LIST_MODEL (store), i);
134 
135       if (tmp == item)
136         {
137           g_list_store_remove (store, i);
138           break;
139         }
140     }
141 
142   g_hash_table_remove (self->items, cc_background_item_get_uri (item));
143 }
144 
145 static void
query_info_finished_cb(GObject * source,GAsyncResult * result,gpointer user_data)146 query_info_finished_cb (GObject      *source,
147                         GAsyncResult *result,
148                         gpointer      user_data)
149 {
150   BgRecentSource *self;
151   g_autoptr(GFileInfo) file_info = NULL;
152   g_autoptr(GError) error = NULL;
153   GFile *file = NULL;
154 
155   file = G_FILE (source);
156   file_info = g_file_query_info_finish (file, result, &error);
157   if (error)
158     {
159       if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
160         g_warning ("Could not get pictures file information: %s", error->message);
161       return;
162     }
163 
164   self = BG_RECENT_SOURCE (user_data);
165 
166   g_debug ("Adding wallpaper %s (%d)",
167            g_file_info_get_name (file_info),
168            G_IS_FILE (self->backgrounds_folder));
169 
170   add_file_from_info (self, file, file_info);
171 }
172 
173 static void
on_file_changed_cb(BgRecentSource * self,GFile * file,GFile * other_file,GFileMonitorEvent event_type)174 on_file_changed_cb (BgRecentSource    *self,
175                     GFile             *file,
176                     GFile             *other_file,
177                     GFileMonitorEvent  event_type)
178 {
179   g_autofree gchar *uri = NULL;
180 
181   switch (event_type)
182     {
183     case G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT:
184       g_file_query_info_async (file,
185                                ATTRIBUTES,
186                                G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
187                                G_PRIORITY_DEFAULT,
188                                self->cancellable,
189                                query_info_finished_cb,
190                                self);
191       break;
192 
193     case G_FILE_MONITOR_EVENT_DELETED:
194       uri = g_file_get_uri (file);
195       remove_item (self, g_hash_table_lookup (self->items, uri));
196       break;
197 
198     default:
199       return;
200     }
201 }
202 
203 static int
file_sort_func(gconstpointer a,gconstpointer b)204 file_sort_func (gconstpointer a,
205                 gconstpointer b)
206 {
207   GFileInfo *file_a = G_FILE_INFO (a);
208   GFileInfo *file_b = G_FILE_INFO (b);
209   guint64 modified_a, modified_b;
210 
211   modified_a = g_file_info_get_attribute_uint64 (file_a, G_FILE_ATTRIBUTE_TIME_MODIFIED);
212   modified_b = g_file_info_get_attribute_uint64 (file_b, G_FILE_ATTRIBUTE_TIME_MODIFIED);
213 
214   return modified_b - modified_a;
215 }
216 
217 static void
file_info_async_ready_cb(GObject * source,GAsyncResult * result,gpointer user_data)218 file_info_async_ready_cb (GObject      *source,
219                           GAsyncResult *result,
220                           gpointer      user_data)
221 {
222   BgRecentSource *self;
223   g_autolist(GFileInfo) file_infos = NULL;
224   g_autoptr(GError) error = NULL;
225   GFile *parent = NULL;
226   GList *l;
227 
228   file_infos = g_file_enumerator_next_files_finish (G_FILE_ENUMERATOR (source),
229                                                     result,
230                                                     &error);
231   if (error)
232     {
233       if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
234         g_warning ("Could not get pictures file information: %s", error->message);
235       return;
236     }
237 
238   self = BG_RECENT_SOURCE (user_data);
239   parent = g_file_enumerator_get_container (G_FILE_ENUMERATOR (source));
240 
241   file_infos = g_list_sort (file_infos, file_sort_func);
242 
243   for (l = file_infos; l; l = l->next)
244     {
245       g_autoptr(GFile) file = NULL;
246       GFileInfo *info;
247 
248       info = l->data;
249       file = g_file_get_child (parent, g_file_info_get_name (info));
250 
251       g_debug ("Found recent wallpaper %s", g_file_info_get_name (info));
252 
253       add_file_from_info (self, file, info);
254     }
255 
256   g_file_enumerator_close (G_FILE_ENUMERATOR (source), self->cancellable, &error);
257 
258   if (error)
259     g_warning ("Error closing file enumerator: %s", error->message);
260 }
261 
262 static void
enumerate_children_finished_cb(GObject * source,GAsyncResult * result,gpointer user_data)263 enumerate_children_finished_cb (GObject      *source,
264                                 GAsyncResult *result,
265                                 gpointer      user_data)
266 {
267   BgRecentSource *self;
268   g_autoptr(GFileEnumerator) enumerator = NULL;
269   g_autoptr(GError) error = NULL;
270 
271   enumerator = g_file_enumerate_children_finish (G_FILE (source), result, &error);
272 
273   if (error)
274     {
275       if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
276         g_warning ("Could not fill pictures source: %s", error->message);
277       return;
278     }
279 
280   self = BG_RECENT_SOURCE (user_data);
281   g_file_enumerator_next_files_async (enumerator,
282                                       G_MAXINT,
283                                       G_PRIORITY_DEFAULT,
284                                       self->cancellable,
285                                       file_info_async_ready_cb,
286                                       self);
287 }
288 
289 static void
load_backgrounds(BgRecentSource * self)290 load_backgrounds (BgRecentSource *self)
291 {
292   g_autofree gchar *backgrounds_path = NULL;
293   g_autoptr(GError) error = NULL;
294 
295   if (!g_file_make_directory_with_parents (self->backgrounds_folder, self->cancellable, &error) &&
296       !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS))
297     {
298       g_critical ("Failed to create local background directory: %s", error->message);
299       return;
300     }
301 
302   backgrounds_path = g_file_get_path (self->backgrounds_folder);
303   g_debug ("Enumerating wallpapers under %s", backgrounds_path);
304 
305   g_file_enumerate_children_async (self->backgrounds_folder,
306                                    ATTRIBUTES,
307                                    G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
308                                    G_PRIORITY_DEFAULT,
309                                    self->cancellable,
310                                    enumerate_children_finished_cb,
311                                    self);
312 
313   self->monitor = g_file_monitor_directory (self->backgrounds_folder,
314                                             G_FILE_MONITOR_WATCH_MOVES,
315                                             self->cancellable,
316                                             &error);
317 
318   if (!self->monitor)
319     {
320       g_critical ("Failed to monitor background directory: %s", error->message);
321       return;
322     }
323 
324   g_signal_connect_object (self->monitor, "changed", G_CALLBACK (on_file_changed_cb), self, G_CONNECT_SWAPPED);
325 }
326 
327 /* Callbacks */
328 
329 static void
on_file_copied_cb(GObject * source,GAsyncResult * result,gpointer user_data)330 on_file_copied_cb (GObject      *source,
331                    GAsyncResult *result,
332                    gpointer      user_data)
333 {
334   g_autoptr(BgRecentSource) self = BG_RECENT_SOURCE (user_data);
335   g_autofree gchar *original_file = NULL;
336   g_autoptr(GError) error = NULL;
337 
338   g_file_copy_finish (G_FILE (source), result, &error);
339 
340   if (error)
341     {
342       if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
343         g_critical ("Failed to copy file: %s", error->message);
344       return;
345     }
346 
347    original_file = g_file_get_path (G_FILE (source));
348    g_debug ("Successfully copied wallpaper: %s", original_file);
349 }
350 
351 static void
on_file_deleted_cb(GObject * source,GAsyncResult * result,gpointer user_data)352 on_file_deleted_cb (GObject      *source,
353                     GAsyncResult *result,
354                     gpointer      user_data)
355 {
356   g_autoptr(BgRecentSource) self = BG_RECENT_SOURCE (user_data);
357   g_autofree gchar *original_file = NULL;
358   g_autoptr(GError) error = NULL;
359 
360   g_file_delete_finish (G_FILE (source), result, &error);
361 
362   if (error)
363     {
364       if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
365         g_critical ("Failed to delete wallpaper: %s", error->message);
366       return;
367     }
368 
369   original_file = g_file_get_path (G_FILE (source));
370   g_debug ("Successfully deleted wallpaper: %s", original_file);
371 }
372 
373 /* GObject overrides */
374 
375 static void
bg_recent_source_finalize(GObject * object)376 bg_recent_source_finalize (GObject *object)
377 {
378   BgRecentSource *self = (BgRecentSource *)object;
379 
380   g_cancellable_cancel (self->cancellable);
381   g_clear_object (&self->cancellable);
382   g_clear_object (&self->monitor);
383 
384   G_OBJECT_CLASS (bg_recent_source_parent_class)->finalize (object);
385 }
386 
387 static void
bg_recent_source_class_init(BgRecentSourceClass * klass)388 bg_recent_source_class_init (BgRecentSourceClass *klass)
389 {
390   GObjectClass *object_class = G_OBJECT_CLASS (klass);
391 
392   object_class->finalize = bg_recent_source_finalize;
393 }
394 
395 static void
bg_recent_source_init(BgRecentSource * self)396 bg_recent_source_init (BgRecentSource *self)
397 {
398   g_autofree gchar *backgrounds_path = NULL;
399 
400   backgrounds_path = g_build_filename (g_get_user_data_dir (), "backgrounds", NULL);
401 
402   self->items = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
403   self->cancellable = g_cancellable_new ();
404   self->backgrounds_folder = g_file_new_for_path (backgrounds_path);
405 
406   load_backgrounds (self);
407 }
408 
409 BgRecentSource*
bg_recent_source_new(GtkWidget * widget)410 bg_recent_source_new (GtkWidget *widget)
411 {
412   return g_object_new (BG_TYPE_RECENT_SOURCE,
413                        "widget", widget,
414                        NULL);
415 }
416 
417 void
bg_recent_source_add_file(BgRecentSource * self,const gchar * path)418 bg_recent_source_add_file (BgRecentSource *self,
419                            const gchar    *path)
420 {
421   g_autoptr(GDateTime) now = NULL;
422   g_autofree gchar *destination_name = NULL;
423   g_autofree gchar *formatted_now = NULL;
424   g_autofree gchar *basename = NULL;
425   g_autoptr(GFile) destination = NULL;
426   g_autoptr(GFile) file = NULL;
427 
428   g_return_if_fail (BG_IS_RECENT_SOURCE (self));
429   g_return_if_fail (path && *path);
430 
431   g_debug ("Importing wallpaper %s", path);
432 
433   now = g_date_time_new_now_local ();
434   formatted_now = g_date_time_format (now, "%Y-%m-%d-%H-%M-%S");
435 
436   file = g_file_new_for_path (path);
437 
438   basename = g_file_get_basename (file);
439   destination_name = g_strdup_printf ("%s-%s", formatted_now, basename);
440   destination = g_file_get_child (self->backgrounds_folder, destination_name);
441 
442   g_file_copy_async (file,
443                      destination,
444                      G_FILE_COPY_NONE,
445                      G_PRIORITY_DEFAULT,
446                      self->cancellable,
447                      NULL, NULL,
448                      on_file_copied_cb,
449                      g_object_ref (self));
450 }
451 
452 void
bg_recent_source_remove_item(BgRecentSource * self,CcBackgroundItem * item)453 bg_recent_source_remove_item (BgRecentSource   *self,
454                               CcBackgroundItem *item)
455 {
456   g_autoptr(GFile) file = NULL;
457   const gchar *uri;
458 
459   g_return_if_fail (BG_IS_RECENT_SOURCE (self));
460   g_return_if_fail (CC_IS_BACKGROUND_ITEM (item));
461 
462   uri = cc_background_item_get_uri (item);
463   file = g_file_new_for_uri (uri);
464 
465   g_file_delete_async (file,
466                        G_PRIORITY_DEFAULT,
467                        self->cancellable,
468                        on_file_deleted_cb,
469                        g_object_ref (self));
470 }
471