1 /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
2 
3 #include "config.h"
4 
5 #include "shell-app-system.h"
6 #include "shell-app-usage.h"
7 #include <string.h>
8 
9 #include <gio/gio.h>
10 #include <glib/gi18n.h>
11 
12 #include "shell-app-cache-private.h"
13 #include "shell-app-private.h"
14 #include "shell-window-tracker-private.h"
15 #include "shell-app-system-private.h"
16 #include "shell-global.h"
17 #include "shell-util.h"
18 #include "st.h"
19 
20 /* Rescan for at most RESCAN_TIMEOUT_MS * MAX_RESCAN_RETRIES. That
21  * should be plenty of time for even a slow spinning drive to update
22  * the icon cache.
23  */
24 #define RESCAN_TIMEOUT_MS 2500
25 #define MAX_RESCAN_RETRIES 6
26 
27 /* Vendor prefixes are something that can be preprended to a .desktop
28  * file name.  Undo this.
29  */
30 static const char*const vendor_prefixes[] = { "gnome-",
31                                               "fedora-",
32                                               "mozilla-",
33                                               "debian-",
34                                               NULL };
35 
36 enum {
37    PROP_0,
38 
39 };
40 
41 enum {
42   APP_STATE_CHANGED,
43   INSTALLED_CHANGED,
44   LAST_SIGNAL
45 };
46 
47 static guint signals[LAST_SIGNAL] = { 0 };
48 
49 typedef struct _ShellAppSystemPrivate ShellAppSystemPrivate;
50 
51 struct _ShellAppSystem
52 {
53   GObject parent;
54 
55   ShellAppSystemPrivate *priv;
56 };
57 
58 struct _ShellAppSystemPrivate {
59   GHashTable *running_apps;
60   GHashTable *id_to_app;
61   GHashTable *startup_wm_class_to_id;
62   GList *installed_apps;
63 
64   guint rescan_icons_timeout_id;
65   guint n_rescan_retries;
66 };
67 
68 static void shell_app_system_finalize (GObject *object);
69 
70 G_DEFINE_TYPE_WITH_PRIVATE (ShellAppSystem, shell_app_system, G_TYPE_OBJECT);
71 
shell_app_system_class_init(ShellAppSystemClass * klass)72 static void shell_app_system_class_init(ShellAppSystemClass *klass)
73 {
74   GObjectClass *gobject_class = (GObjectClass *)klass;
75 
76   gobject_class->finalize = shell_app_system_finalize;
77 
78   signals[APP_STATE_CHANGED] = g_signal_new ("app-state-changed",
79                                              SHELL_TYPE_APP_SYSTEM,
80                                              G_SIGNAL_RUN_LAST,
81                                              0,
82                                              NULL, NULL, NULL,
83                                              G_TYPE_NONE, 1,
84                                              SHELL_TYPE_APP);
85   signals[INSTALLED_CHANGED] =
86     g_signal_new ("installed-changed",
87 		  SHELL_TYPE_APP_SYSTEM,
88 		  G_SIGNAL_RUN_LAST,
89                   0,
90                   NULL, NULL, NULL,
91 		  G_TYPE_NONE, 0);
92 }
93 
94 static void
scan_startup_wm_class_to_id(ShellAppSystem * self)95 scan_startup_wm_class_to_id (ShellAppSystem *self)
96 {
97   ShellAppSystemPrivate *priv = self->priv;
98   const GList *l;
99   GList *all;
100 
101   g_hash_table_remove_all (priv->startup_wm_class_to_id);
102 
103   all = shell_app_cache_get_all (shell_app_cache_get_default ());
104 
105   for (l = all; l != NULL; l = l->next)
106     {
107       GAppInfo *info = l->data;
108       const char *startup_wm_class, *id, *old_id;
109 
110       id = g_app_info_get_id (info);
111       startup_wm_class = g_desktop_app_info_get_startup_wm_class (G_DESKTOP_APP_INFO (info));
112 
113       if (startup_wm_class == NULL)
114         continue;
115 
116       /* In case multiple .desktop files set the same StartupWMClass, prefer
117        * the one where ID and StartupWMClass match */
118       old_id = g_hash_table_lookup (priv->startup_wm_class_to_id, startup_wm_class);
119       if (old_id == NULL || strcmp (id, startup_wm_class) == 0)
120         g_hash_table_insert (priv->startup_wm_class_to_id,
121                              g_strdup (startup_wm_class), g_strdup (id));
122     }
123 }
124 
125 static gboolean
app_is_stale(ShellApp * app)126 app_is_stale (ShellApp *app)
127 {
128   GDesktopAppInfo *info, *old;
129   GAppInfo *old_info, *new_info;
130   gboolean is_unchanged;
131 
132   if (shell_app_is_window_backed (app))
133     return FALSE;
134 
135   info = shell_app_cache_get_info (shell_app_cache_get_default (),
136                                    shell_app_get_id (app));
137   if (!info)
138     return TRUE;
139 
140   old = shell_app_get_app_info (app);
141   old_info = G_APP_INFO (old);
142   new_info = G_APP_INFO (info);
143 
144   is_unchanged =
145     g_app_info_should_show (old_info) == g_app_info_should_show (new_info) &&
146     strcmp (g_desktop_app_info_get_filename (old),
147             g_desktop_app_info_get_filename (info)) == 0 &&
148     g_strcmp0 (g_app_info_get_executable (old_info),
149                g_app_info_get_executable (new_info)) == 0 &&
150     g_strcmp0 (g_app_info_get_commandline (old_info),
151                g_app_info_get_commandline (new_info)) == 0 &&
152     strcmp (g_app_info_get_name (old_info),
153             g_app_info_get_name (new_info)) == 0 &&
154     g_strcmp0 (g_app_info_get_description (old_info),
155                g_app_info_get_description (new_info)) == 0 &&
156     strcmp (g_app_info_get_display_name (old_info),
157             g_app_info_get_display_name (new_info)) == 0 &&
158     g_icon_equal (g_app_info_get_icon (old_info),
159                   g_app_info_get_icon (new_info));
160 
161   return !is_unchanged;
162 }
163 
164 static gboolean
stale_app_remove_func(gpointer key,gpointer value,gpointer user_data)165 stale_app_remove_func (gpointer key,
166                        gpointer value,
167                        gpointer user_data)
168 {
169   return app_is_stale (value);
170 }
171 
172 static gboolean
rescan_icon_theme_cb(gpointer user_data)173 rescan_icon_theme_cb (gpointer user_data)
174 {
175   ShellAppSystemPrivate *priv;
176   ShellAppSystem *self;
177   StTextureCache *texture_cache;
178   gboolean rescanned;
179 
180   self = (ShellAppSystem *) user_data;
181   priv = self->priv;
182 
183   texture_cache = st_texture_cache_get_default ();
184   rescanned = st_texture_cache_rescan_icon_theme (texture_cache);
185 
186   priv->n_rescan_retries++;
187 
188   if (rescanned || priv->n_rescan_retries >= MAX_RESCAN_RETRIES)
189     {
190       priv->n_rescan_retries = 0;
191       priv->rescan_icons_timeout_id = 0;
192       return G_SOURCE_REMOVE;
193     }
194 
195   return G_SOURCE_CONTINUE;
196 }
197 
198 static void
rescan_icon_theme(ShellAppSystem * self)199 rescan_icon_theme (ShellAppSystem *self)
200 {
201   ShellAppSystemPrivate *priv = self->priv;
202 
203   priv->n_rescan_retries = 0;
204 
205   if (priv->rescan_icons_timeout_id > 0)
206     return;
207 
208   priv->rescan_icons_timeout_id = g_timeout_add (RESCAN_TIMEOUT_MS,
209                                                  rescan_icon_theme_cb,
210                                                  self);
211 }
212 
213 static void
installed_changed(ShellAppCache * cache,ShellAppSystem * self)214 installed_changed (ShellAppCache  *cache,
215                    ShellAppSystem *self)
216 {
217   rescan_icon_theme (self);
218   scan_startup_wm_class_to_id (self);
219 
220   g_hash_table_foreach_remove (self->priv->id_to_app, stale_app_remove_func, NULL);
221 
222   g_signal_emit (self, signals[INSTALLED_CHANGED], 0, NULL);
223 }
224 
225 static void
shell_app_system_init(ShellAppSystem * self)226 shell_app_system_init (ShellAppSystem *self)
227 {
228   ShellAppSystemPrivate *priv;
229   ShellAppCache *cache;
230 
231   self->priv = priv = shell_app_system_get_instance_private (self);
232 
233   priv->running_apps = g_hash_table_new_full (NULL, NULL, (GDestroyNotify) g_object_unref, NULL);
234   priv->id_to_app = g_hash_table_new_full (g_str_hash, g_str_equal,
235                                            NULL,
236                                            (GDestroyNotify)g_object_unref);
237 
238   priv->startup_wm_class_to_id = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
239 
240   cache = shell_app_cache_get_default ();
241   g_signal_connect (cache, "changed", G_CALLBACK (installed_changed), self);
242   installed_changed (cache, self);
243 }
244 
245 static void
shell_app_system_finalize(GObject * object)246 shell_app_system_finalize (GObject *object)
247 {
248   ShellAppSystem *self = SHELL_APP_SYSTEM (object);
249   ShellAppSystemPrivate *priv = self->priv;
250 
251   g_hash_table_destroy (priv->running_apps);
252   g_hash_table_destroy (priv->id_to_app);
253   g_hash_table_destroy (priv->startup_wm_class_to_id);
254   g_list_free_full (priv->installed_apps, g_object_unref);
255   g_clear_handle_id (&priv->rescan_icons_timeout_id, g_source_remove);
256 
257   G_OBJECT_CLASS (shell_app_system_parent_class)->finalize (object);
258 }
259 
260 /**
261  * shell_app_system_get_default:
262  *
263  * Return Value: (transfer none): The global #ShellAppSystem singleton
264  */
265 ShellAppSystem *
shell_app_system_get_default(void)266 shell_app_system_get_default (void)
267 {
268   static ShellAppSystem *instance = NULL;
269 
270   if (instance == NULL)
271     instance = g_object_new (SHELL_TYPE_APP_SYSTEM, NULL);
272 
273   return instance;
274 }
275 
276 /**
277  * shell_app_system_lookup_app:
278  *
279  * Find a #ShellApp corresponding to an id.
280  *
281  * Return value: (transfer none): The #ShellApp for id, or %NULL if none
282  */
283 ShellApp *
shell_app_system_lookup_app(ShellAppSystem * self,const char * id)284 shell_app_system_lookup_app (ShellAppSystem   *self,
285                              const char       *id)
286 {
287   ShellAppSystemPrivate *priv = self->priv;
288   ShellApp *app;
289   GDesktopAppInfo *info;
290 
291   app = g_hash_table_lookup (priv->id_to_app, id);
292   if (app)
293     return app;
294 
295   info = shell_app_cache_get_info (shell_app_cache_get_default (), id);
296   if (!info)
297     return NULL;
298 
299   app = _shell_app_new (info);
300   g_hash_table_insert (priv->id_to_app, (char *) shell_app_get_id (app), app);
301   return app;
302 }
303 
304 /**
305  * shell_app_system_lookup_heuristic_basename:
306  * @system: a #ShellAppSystem
307  * @id: Probable application identifier
308  *
309  * Find a valid application corresponding to a given
310  * heuristically determined application identifier
311  * string, or %NULL if none.
312  *
313  * Returns: (transfer none): A #ShellApp for @name
314  */
315 ShellApp *
shell_app_system_lookup_heuristic_basename(ShellAppSystem * system,const char * name)316 shell_app_system_lookup_heuristic_basename (ShellAppSystem *system,
317                                             const char     *name)
318 {
319   ShellApp *result;
320   const char *const *prefix;
321 
322   result = shell_app_system_lookup_app (system, name);
323   if (result != NULL)
324     return result;
325 
326   for (prefix = vendor_prefixes; *prefix != NULL; prefix++)
327     {
328       char *tmpid = g_strconcat (*prefix, name, NULL);
329       result = shell_app_system_lookup_app (system, tmpid);
330       g_free (tmpid);
331       if (result != NULL)
332         return result;
333     }
334 
335   return NULL;
336 }
337 
338 /**
339  * shell_app_system_lookup_desktop_wmclass:
340  * @system: a #ShellAppSystem
341  * @wmclass: (nullable): A WM_CLASS value
342  *
343  * Find a valid application whose .desktop file, without the extension
344  * and properly canonicalized, matches @wmclass.
345  *
346  * Returns: (transfer none): A #ShellApp for @wmclass
347  */
348 ShellApp *
shell_app_system_lookup_desktop_wmclass(ShellAppSystem * system,const char * wmclass)349 shell_app_system_lookup_desktop_wmclass (ShellAppSystem *system,
350                                          const char     *wmclass)
351 {
352   char *canonicalized;
353   char *desktop_file;
354   ShellApp *app;
355 
356   if (wmclass == NULL)
357     return NULL;
358 
359   /* First try without changing the case (this handles
360      org.example.Foo.Bar.desktop applications)
361 
362      Note that is slightly wrong in that Gtk+ would set
363      the WM_CLASS to Org.example.Foo.Bar, but it also
364      sets the instance part to org.example.Foo.Bar, so we're ok
365   */
366   desktop_file = g_strconcat (wmclass, ".desktop", NULL);
367   app = shell_app_system_lookup_heuristic_basename (system, desktop_file);
368   g_free (desktop_file);
369 
370   if (app)
371     return app;
372 
373   canonicalized = g_ascii_strdown (wmclass, -1);
374 
375   /* This handles "Fedora Eclipse", probably others.
376    * Note g_strdelimit is modify-in-place. */
377   g_strdelimit (canonicalized, " ", '-');
378 
379   desktop_file = g_strconcat (canonicalized, ".desktop", NULL);
380 
381   app = shell_app_system_lookup_heuristic_basename (system, desktop_file);
382 
383   g_free (canonicalized);
384   g_free (desktop_file);
385 
386   return app;
387 }
388 
389 /**
390  * shell_app_system_lookup_startup_wmclass:
391  * @system: a #ShellAppSystem
392  * @wmclass: (nullable): A WM_CLASS value
393  *
394  * Find a valid application whose .desktop file contains a
395  * StartupWMClass entry matching @wmclass.
396  *
397  * Returns: (transfer none): A #ShellApp for @wmclass
398  */
399 ShellApp *
shell_app_system_lookup_startup_wmclass(ShellAppSystem * system,const char * wmclass)400 shell_app_system_lookup_startup_wmclass (ShellAppSystem *system,
401                                          const char     *wmclass)
402 {
403   const char *id;
404 
405   if (wmclass == NULL)
406     return NULL;
407 
408   id = g_hash_table_lookup (system->priv->startup_wm_class_to_id, wmclass);
409   if (id == NULL)
410     return NULL;
411 
412   return shell_app_system_lookup_app (system, id);
413 }
414 
415 void
_shell_app_system_notify_app_state_changed(ShellAppSystem * self,ShellApp * app)416 _shell_app_system_notify_app_state_changed (ShellAppSystem *self,
417                                             ShellApp       *app)
418 {
419   ShellAppState state = shell_app_get_state (app);
420 
421   switch (state)
422     {
423     case SHELL_APP_STATE_RUNNING:
424       g_hash_table_insert (self->priv->running_apps, g_object_ref (app), NULL);
425       break;
426     case SHELL_APP_STATE_STARTING:
427       break;
428     case SHELL_APP_STATE_STOPPED:
429       g_hash_table_remove (self->priv->running_apps, app);
430       break;
431     default:
432       g_warn_if_reached();
433       break;
434     }
435   g_signal_emit (self, signals[APP_STATE_CHANGED], 0, app);
436 }
437 
438 /**
439  * shell_app_system_get_running:
440  * @self: A #ShellAppSystem
441  *
442  * Returns the set of applications which currently have at least one
443  * open window.  The returned list will be sorted by shell_app_compare().
444  *
445  * Returns: (element-type ShellApp) (transfer container): Active applications
446  */
447 GSList *
shell_app_system_get_running(ShellAppSystem * self)448 shell_app_system_get_running (ShellAppSystem *self)
449 {
450   gpointer key, value;
451   GSList *ret;
452   GHashTableIter iter;
453 
454   g_hash_table_iter_init (&iter, self->priv->running_apps);
455 
456   ret = NULL;
457   while (g_hash_table_iter_next (&iter, &key, &value))
458     {
459       ShellApp *app = key;
460 
461       ret = g_slist_prepend (ret, app);
462     }
463 
464   ret = g_slist_sort (ret, (GCompareFunc)shell_app_compare);
465 
466   return ret;
467 }
468 
469 /**
470  * shell_app_system_search:
471  * @search_string: the search string to use
472  *
473  * Wrapper around g_desktop_app_info_search() that replaces results that
474  * don't validate as UTF-8 with the empty string.
475  *
476  * Returns: (array zero-terminated=1) (element-type GStrv) (transfer full): a
477  *   list of strvs.  Free each item with g_strfreev() and free the outer
478  *   list with g_free().
479  */
480 char ***
shell_app_system_search(const char * search_string)481 shell_app_system_search (const char *search_string)
482 {
483   char ***results = g_desktop_app_info_search (search_string);
484   char ***groups, **ids;
485 
486   for (groups = results; *groups; groups++)
487     for (ids = *groups; *ids; ids++)
488       if (!g_utf8_validate (*ids, -1, NULL))
489         **ids = '\0';
490 
491   return results;
492 }
493 
494 /**
495  * shell_app_system_get_installed:
496  * @self: the #ShellAppSystem
497  *
498  * Returns all installed apps, as a list of #GAppInfo
499  *
500  * Returns: (transfer none) (element-type GAppInfo): a list of #GAppInfo
501  *   describing all known applications. This memory is owned by the
502  *   #ShellAppSystem and should not be freed.
503  **/
504 GList *
shell_app_system_get_installed(ShellAppSystem * self)505 shell_app_system_get_installed (ShellAppSystem *self)
506 {
507   return shell_app_cache_get_all (shell_app_cache_get_default ());
508 }
509