[gnome-shell] application monitoring base object



commit f4f92a879cd46e468f9828e42c857794b7ebd834
Author: Milan Bouchet-Valat <nalimilan club fr>
Date:   Thu Apr 23 23:03:34 2009 +0200

    application monitoring base object
    
    The new ShellAppMonitor object is responsible for tracking active applications and store statistics.
    
    http://bugzilla.gnome.org/show_bug.cgi?id=570899
---
 configure.ac                           |    2 +-
 src/Makefile.am                        |    2 +
 src/shell-app-monitor.c                |  966 ++++++++++++++++++++++++++++++++
 src/shell-app-monitor.h                |   45 ++
 tools/build/gnome-shell-build-setup.sh |   12 +-
 5 files changed, 1020 insertions(+), 7 deletions(-)

diff --git a/configure.ac b/configure.ac
index 0c52b68..5051b9d 100644
--- a/configure.ac
+++ b/configure.ac
@@ -38,7 +38,7 @@ fi
 
 AM_CONDITIONAL(BUILD_RECORDER, $build_recorder)
 
-PKG_CHECK_MODULES(MUTTER_PLUGIN, gtk+-2.0 dbus-glib-1 metacity-plugins gjs-gi-1.0 libgnome-menu gdk-x11-2.0 clutter-x11-0.9 clutter-glx-0.9 $recorder_modules)
+PKG_CHECK_MODULES(MUTTER_PLUGIN, gtk+-2.0 dbus-glib-1 metacity-plugins gjs-gi-1.0 xscrnsaver libgnome-menu $recorder_modules gdk-x11-2.0 clutter-x11-0.9 clutter-glx-0.9)
 PKG_CHECK_MODULES(TIDY, clutter-0.9)
 PKG_CHECK_MODULES(BIG, clutter-0.9 gtk+-2.0 librsvg-2.0)
 PKG_CHECK_MODULES(GDMUSER, dbus-glib-1 gtk+-2.0)
diff --git a/src/Makefile.am b/src/Makefile.am
index 06e7d8a..7c53080 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -49,6 +49,8 @@ CLEANFILES += $(SHELL_STAMP_FILES)
 libgnome_shell_la_SOURCES =			\
 	$(shell_built_sources)			\
 	gnome-shell-plugin.c			\
+	shell-app-monitor.c			\
+	shell-app-monitor.h			\
 	shell-app-system.c			\
 	shell-app-system.h			\
 	shell-arrow.c			\
diff --git a/src/shell-app-monitor.c b/src/shell-app-monitor.c
new file mode 100644
index 0000000..5ed9b7c
--- /dev/null
+++ b/src/shell-app-monitor.c
@@ -0,0 +1,966 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+#include <string.h>
+#include <stdlib.h>
+
+#include <X11/Xlib.h>
+#include <X11/Xatom.h>
+#include <X11/extensions/scrnsaver.h>
+#include <gdk/gdk.h>
+#include <gdk/gdkx.h>
+#include <gio/gio.h>
+
+#include "shell-app-monitor.h"
+#include "shell-global.h"
+
+
+/* This file includes modified code from
+ * desktop-data-engine/engine-dbus/hippo-application-monitor.c
+ * in the functions collecting application usage data.
+ * Written by Owen Taylor, originally licensed under LGPL 2.1.
+ * Copyright Red Hat, Inc. 2006-2008
+ */
+
+/* Data is saved to file SHELL_CONFIG_DIR/DATA_FILENAME */
+#define DATA_FILENAME "applications_usage"
+
+/* How often we save internally app data, in seconds */
+#define SAVE_APPS_TIMEOUT 3600    /* One hour */
+
+/* How often we save internally app data in burst mode */
+#define SAVE_APPS_BURST_TIMEOUT 120 /* Two minutes */
+
+/* Length of the initial app burst, for each new activity */
+#define SAVE_APPS_BURST_LENGTH 3600       /* One hour */
+
+/* The ranking algorithm we use is: every time an app score reaches SCORE_MAX,
+ * divide all scores by 2. Scores are raised by 1 unit every SAVE_APPS_TIMEOUT
+ * seconds. This mechanism allows the list to update relatively fast when
+ * a new app is used intensively.
+ * To keep the list clean, and avoid being Big Brother, apps that have not been
+ * seen for a week and whose score is below SCORE_MIN are removed.
+ */
+ 
+/* With this value, an app goes from bottom to top of the 
+ * popularity list in 50 hours of use */
+#define SCORE_MAX (3600*50/SAVE_APPS_TIMEOUT)
+
+/* If an app's score in lower than this and the app has not been used in a week,
+ * remove it */
+#define SCORE_MIN 5
+
+/* Title patterns to detect apps that don't set WM class as needed.
+ * Format: pseudo/wanted WM class, title regex pattern, NULL (for GRegex) */
+static struct
+{
+  const char *app_id;
+  const char *pattern;
+  GRegex *regex;
+} title_patterns[] =  {
+    {"openoffice.org-writer", ".* - OpenOffice.org Writer$", NULL}, \
+    {"openoffice.org-calc", ".* - OpenOffice.org Calc$", NULL}, \
+    {"openoffice.org-impress", ".* - OpenOffice.org Impress$", NULL}, \
+    {"openoffice.org-draw", ".* - OpenOffice.org Draw$", NULL}, \
+    {"openoffice.org-base", ".* - OpenOffice.org Base$", NULL}, \
+    {"openoffice.org-math", ".* - OpenOffice.org Math$", NULL}, \
+    {NULL, NULL, NULL}
+};
+
+
+typedef struct AppPopularity AppPopularity;
+typedef struct ActiveAppsData ActiveAppsData;
+
+struct _ShellAppMonitor
+{
+  GObject parent;
+
+
+  GFile *configfile;
+  XScreenSaverInfo *info;
+  GdkDisplay *display;
+  glong activity_time;
+  gulong last_idle;
+  guint poll_id;
+  guint save_apps_id;
+  gboolean currently_idle;
+
+  GHashTable *apps_by_wm_class; /* Seen apps by wm_class */
+  GHashTable *popularities; /* One AppPopularity struct list per activity */
+  int upload_apps_burst_count;
+};
+
+G_DEFINE_TYPE (ShellAppMonitor, shell_app_monitor, G_TYPE_OBJECT);
+
+/* Represents an application record for a given activity */
+struct AppPopularity
+{
+  gchar *wm_class;
+  gdouble score; /* Based on the number of times we'e seen the app and normalized */
+  guint32 last_seen; /* Used to clear old apps we've only seen a few times */
+};
+
+struct ActiveAppsData
+{
+  int activity;
+  GSList *result;
+  GTime start_time;
+};
+
+enum {
+  CHANGED,
+
+  LAST_SIGNAL
+};
+
+static guint signals[LAST_SIGNAL] = { 0 };
+
+static void shell_app_monitor_finalize (GObject *object);
+
+static void get_active_apps (ShellAppMonitor *monitor,
+                             int              in_last_seconds,
+                             GSList         **wm_classes,
+                             int             *activity);
+
+static void save_active_apps (ShellAppMonitor *monitor,
+                              int              collection_period,
+                              GSList          *wm_classes,
+                              int              activity);
+
+static gboolean poll_for_idleness (void *data);
+
+static gboolean on_save_apps_timeout (gpointer data);
+
+static void save_to_file (ShellAppMonitor *monitor);
+
+static void restore_from_file (ShellAppMonitor *monitor);
+
+static glong
+get_time (void)
+{
+  GTimeVal tv;
+  g_get_current_time (&tv);
+  return tv.tv_sec;
+}
+
+static void shell_app_monitor_class_init(ShellAppMonitorClass *klass)
+{
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+  gobject_class->finalize = shell_app_monitor_finalize;
+  
+  signals[CHANGED] = g_signal_new ("changed",
+                                   SHELL_TYPE_APP_MONITOR,
+                                   G_SIGNAL_RUN_LAST,
+                                   0,
+                                   NULL, NULL,
+                                   g_cclosure_marshal_VOID__VOID,
+                                   G_TYPE_NONE, 0);
+}
+
+/* Little callback to destroy lists inside hash table */
+static void
+destroy_popularity (gpointer key,
+                    gpointer value,
+                    gpointer user_data)
+{
+  GSList *list = value;
+  GSList *l;
+  AppPopularity *app_popularity;
+  for (l = list; l; l = l->next)
+    {
+      app_popularity = (AppPopularity *) l->data;
+      g_free (app_popularity->wm_class);
+      g_free (app_popularity);
+    }
+  g_slist_free (list);
+}
+
+static void
+shell_app_monitor_init (ShellAppMonitor *self)
+{
+  int event_base, error_base;
+  GdkDisplay *display;
+  Display *xdisplay;
+  char *path;
+  char *shell_config_dir;
+  
+  /* Apps usage tracking */
+
+  /* FIXME: should we create as many monitors as there are GdkScreens? */
+  display = gdk_display_get_default();
+  xdisplay = GDK_DISPLAY_XDISPLAY (display);
+  if (!XScreenSaverQueryExtension (xdisplay, &event_base, &error_base))
+    {
+      g_warning ("Screensaver extension not found on X display, can't detect user idleness");
+    }
+  
+  self->display = g_object_ref (display);
+  self->info = XScreenSaverAllocInfo ();
+
+  self->activity_time = get_time ();
+  self->last_idle = 0;
+  self->currently_idle = FALSE;
+
+  /* No need for free functions: value is an int stored as a pointer, and keys are
+   * freed manually in finalize () since we replace elements and reuse app names */
+  self->popularities = g_hash_table_new (g_direct_hash, g_direct_equal);
+  self->apps_by_wm_class = g_hash_table_new_full (g_str_hash, g_str_equal,
+                                                  (GDestroyNotify) g_free,
+                                                  (GDestroyNotify) g_free);
+
+  g_object_get (shell_global_get(), "configdir", &shell_config_dir, NULL),
+  path = g_build_filename (shell_config_dir, DATA_FILENAME, NULL);
+  g_free (shell_config_dir);
+  self->configfile = g_file_new_for_path (path);
+  restore_from_file (self);
+  /* If no stats are available so far, set burst mode on */
+  if (g_hash_table_size(self->popularities))
+    self->upload_apps_burst_count = 0;
+  else
+    self->upload_apps_burst_count = SAVE_APPS_BURST_LENGTH / SAVE_APPS_BURST_TIMEOUT;
+
+
+  self->poll_id = g_timeout_add_seconds (5, poll_for_idleness, self);
+  self->save_apps_id =
+    g_timeout_add_seconds (SAVE_APPS_BURST_TIMEOUT, on_save_apps_timeout, self);
+}
+
+static void
+shell_app_monitor_finalize (GObject *object)
+{
+  ShellAppMonitor *self = SHELL_APP_MONITOR (object);
+  int i;
+
+  XFree (self->info);
+  g_source_remove (self->poll_id);
+  g_source_remove (self->save_apps_id);
+  g_object_unref (self->display);
+  g_hash_table_destroy (self->apps_by_wm_class);
+  g_hash_table_foreach (self->popularities, destroy_popularity, NULL);
+  g_hash_table_destroy (self->popularities);
+  for (i = 0; title_patterns[i].app_id; i++)
+    g_regex_unref (title_patterns[i].regex);
+  g_object_unref (self->configfile);
+
+  G_OBJECT_CLASS (shell_app_monitor_parent_class)->finalize(object);
+}
+
+/**
+ * shell_app_monitor_get_apps:
+ *
+ * Get a list of desktop identifiers representing the most popular applications
+ * for a given activity.
+ *
+ * @monitor: the app monitor instance to request
+ * @activity: the activity for which stats are considered
+ * @max_count: how many applications are requested. Note that the actual 
+ *     list size may be less, or NULL if not enough applications are registered.
+ *
+ * Returns: (element-type utf8) (transfer full): List of application desktop
+ *     identifiers, in low case
+ */
+GSList *
+shell_app_monitor_get_apps (ShellAppMonitor *monitor,
+                            gint             activity,
+                            gint             max_count)
+{
+  GSList *list = NULL;
+  GSList *popularity;
+  AppPopularity *app_popularity;
+  int i;
+  
+  popularity = g_hash_table_lookup (monitor->popularities,
+                                    GINT_TO_POINTER (activity));
+  
+  for (i = 0; i < max_count; i++)
+    {
+      if (!popularity)
+        break;
+      app_popularity = (AppPopularity *) (popularity->data);
+      list = g_slist_prepend (list, g_utf8_strdown (app_popularity->wm_class, -1));
+      popularity = popularity->next;
+    }
+  list = g_slist_reverse (list);
+  return list;
+}
+
+/* Find the active window in order to collect stats */
+void
+get_active_app_properties (ShellAppMonitor *monitor,
+                           char           **wm_class,
+                           char           **title)
+{
+  Display *xdisplay = GDK_DISPLAY_XDISPLAY (monitor->display);
+  int n_screens = gdk_display_get_n_screens (monitor->display);
+  Atom net_active_window_x =
+    gdk_x11_get_xatom_by_name_for_display (monitor->display,
+                                           "_NET_ACTIVE_WINDOW");
+  GdkAtom net_active_window_gdk =
+    gdk_atom_intern ("_NET_ACTIVE_WINDOW", FALSE);
+  Window active_window = None;
+  int i;
+
+  Atom type;
+  int format;
+  unsigned long n_items;
+  unsigned long bytes_after;
+  guchar *data;
+  gboolean is_desktop = FALSE;
+
+  if (wm_class)
+    *wm_class = NULL;
+  if (title)
+    *title = NULL;
+
+  /* Find the currently focused window by looking at the _NET_ACTIVE_WINDOW property
+   * on all the screens of the display.
+   */
+  for (i = 0; i < n_screens; i++)
+    {
+      GdkScreen *screen = gdk_display_get_screen (monitor->display, i);
+      GdkWindow *root = gdk_screen_get_root_window (screen);
+
+      if (!gdk_x11_screen_supports_net_wm_hint (screen, net_active_window_gdk))
+        continue;
+
+      XGetWindowProperty (xdisplay, GDK_DRAWABLE_XID (root),
+                          net_active_window_x,
+                          0, 1, False, XA_WINDOW,
+                          &type, &format, &n_items, &bytes_after, &data);
+      if (type == XA_WINDOW)
+        {
+          active_window = *(Window *) data;
+          XFree (data);
+          break;
+        }
+    }
+
+  /* Now that we have the active window, figure out the app name and WM class
+   */
+  gdk_error_trap_push ();
+
+  if (active_window && wm_class)
+    {
+      if (XGetWindowProperty (xdisplay, active_window,
+                              XA_WM_CLASS,
+                              0, G_MAXLONG, False, XA_STRING,
+                              &type, &format, &n_items, &bytes_after,
+                              &data) == Success && type == XA_STRING)
+        {
+          if (format == 8)
+            {
+              char **list;
+              int count;
+
+              count =
+                gdk_text_property_to_utf8_list_for_display (monitor->display,
+                                                            GDK_TARGET_STRING,
+                                                            8, data, n_items,
+                                                            &list);
+
+              if (count > 1)
+                {
+                  /* This is a check for Nautilus, which sets the instance to this
+                   * value for the desktop window; we do this rather than check for
+                   * the more general _NET_WM_WINDOW_TYPE_DESKTOP to avoid having
+                   * to do another XGetProperty on every iteration. We generally
+                   * don't want to count the desktop being focused as app
+                   * usage because it frequently can be a false-positive on an
+                   * empty workspace.
+                   */
+                  if (strcmp (list[0], "desktop_window") == 0)
+                    is_desktop = TRUE;
+                  else
+                    *wm_class = g_strdup (list[1]);
+                }
+
+              if (list)
+                g_strfreev (list);
+            }
+
+          XFree (data);
+        }
+    }
+
+  if (is_desktop)
+    active_window = None;
+
+  if (active_window && title)
+    {
+      Atom utf8_string =
+        gdk_x11_get_xatom_by_name_for_display (monitor->display,
+                                               "UTF8_STRING");
+
+      if (XGetWindowProperty (xdisplay, active_window,
+                              gdk_x11_get_xatom_by_name_for_display
+                              (monitor->display, "_NET_WM_NAME"), 0,
+                              G_MAXLONG, False, utf8_string, &type, &format,
+                              &n_items, &bytes_after, &data) == Success
+          && type == utf8_string)
+        {
+          if (format == 8 && g_utf8_validate ((char *) data, -1, NULL))
+            {
+              *title = g_strdup ((char *) data);
+            }
+
+          XFree (data);
+        }
+    }
+
+  if (active_window && title && *title == NULL)
+    {
+      if (XGetWindowProperty (xdisplay, active_window,
+                              XA_WM_NAME,
+                              0, G_MAXLONG, False, AnyPropertyType,
+                              &type, &format, &n_items, &bytes_after,
+                              &data) == Success && type != None)
+        {
+          if (format == 8)
+            {
+              char **list;
+              int count;
+
+              count =
+                gdk_text_property_to_utf8_list_for_display (monitor->display,
+                                                            gdk_x11_xatom_to_atom_for_display
+                                                            (monitor->display,
+                                                             type), 8, data,
+                                                            n_items, &list);
+
+              if (count > 0)
+                *title = g_strdup (list[0]);
+
+              if (list)
+                g_strfreev (list);
+            }
+
+          XFree (data);
+        }
+    }
+  gdk_error_trap_pop ();
+}
+
+void
+update_app_info (ShellAppMonitor *monitor)
+{
+  char *wm_class;
+  char *title;
+  GHashTable *app_active_times = NULL; /* GTime spent per activity */
+  static gboolean first_time = TRUE;
+  int activity;
+  guint32 timestamp;
+  int i;
+  
+  if (first_time) /* Generate match patterns once for all */
+    {
+      first_time = FALSE;
+      for (i = 0; title_patterns[i].app_id; i++)
+        {
+          title_patterns[i].regex = g_regex_new (title_patterns[i].pattern,
+                                                 0, 0, NULL);
+        }
+    }
+
+  get_active_app_properties (monitor, &wm_class, &title);
+
+  /* Match window title patterns to identifiers for non-standard apps */
+  if (title)
+    {
+      for (i = 0; title_patterns[i].app_id; i++)
+        {
+          if ( g_regex_match (title_patterns[i].regex, title, 0, NULL) )
+            {
+              /* Set a pseudo WM class, handled like true ones */
+              g_free (wm_class);              
+              wm_class = g_strdup(title_patterns[i].app_id);
+              break;
+            }
+        }
+      g_free (title);
+    }  
+  
+  if (!wm_class)
+    return;
+  
+  app_active_times = g_hash_table_lookup (monitor->apps_by_wm_class, wm_class);
+  if (!app_active_times)
+    {
+      /* Create a hash table to save times per activity
+       * Value and key are int stored as pointers, no need to free them */
+      app_active_times = g_hash_table_new (g_direct_hash, g_direct_equal);
+      g_hash_table_replace (monitor->apps_by_wm_class, g_strdup (wm_class),
+                            app_active_times);
+    }
+
+  timestamp = get_time ();
+  activity = 0;
+  g_hash_table_replace (app_active_times, GINT_TO_POINTER (activity),
+                        GINT_TO_POINTER (timestamp));
+
+  g_free (wm_class);
+}
+
+static gboolean
+poll_for_idleness (gpointer data)
+{
+  ShellAppMonitor *monitor = data;
+  int i;
+  int n_screens;
+  unsigned long idle_time;
+  gboolean was_idle;
+
+  idle_time = G_MAXINT;
+  n_screens = gdk_display_get_n_screens (monitor->display);
+  for (i = 0; i < n_screens; ++i)
+    {
+      int result = 0;
+      GdkScreen *screen;
+
+      screen = gdk_display_get_screen (monitor->display, i);
+      result = XScreenSaverQueryInfo (GDK_DISPLAY_XDISPLAY (monitor->display),
+                                      GDK_SCREEN_XSCREEN (screen)->root,
+                                      monitor->info);
+      if (result == 0)
+        {
+          g_warning ("Failed to get idle time from screensaver extension");
+          break;
+        }
+
+      /* monitor->info->idle is time in milliseconds since last user interaction event */
+      idle_time = MIN (monitor->info->idle, idle_time);
+    }
+
+  was_idle = monitor->currently_idle;
+
+  /* If the idle time has gone down, there must have been activity since we last checked */
+  if (idle_time < monitor->last_idle)
+    {
+      monitor->activity_time = get_time ();
+      monitor->currently_idle = FALSE;
+    }
+  else
+    {
+      /* If no activity, see how long ago it was and count ourselves idle 
+       * if it's been a short while. We keep this idle really short,
+       * so it can be "more aggressive" about idle detection than
+       * a screensaver would be.
+       */
+      GTime now = get_time ();
+      if (now < monitor->activity_time)
+        {
+          /* clock went backward... just "catch up" 
+           * then wait until the idle timeout expires again
+           */
+          monitor->activity_time = now;
+        }
+      else if ((now - monitor->activity_time) > 120)
+        {                       /* 120 = 2 minutes */
+          monitor->currently_idle = TRUE;
+        }
+    }
+
+  monitor->last_idle = idle_time;
+
+  if (!monitor->currently_idle)
+    {
+      update_app_info (monitor);
+    }
+
+  return TRUE;
+}
+
+/* Used to iterate over apps to create a list of those that have been active
+ * since the activity specified by app_data has been started */
+static void
+active_apps_foreach (const gpointer key,
+                     const gpointer value,
+                     gpointer       data)
+{
+  char *name = key;
+  GHashTable *app_active_times = value; /* GTime spent per activity */
+  ActiveAppsData *app_data = data;
+  GTime active_time;
+  
+  /* Only return apps that have been used in the current activity */
+  active_time = GPOINTER_TO_INT (g_hash_table_lookup
+                                 (app_active_times, GINT_TO_POINTER (app_data->activity)));
+  if (active_time > app_data->start_time)
+    app_data->result = g_slist_prepend (app_data->result, g_strdup (name));
+}
+
+/*
+ * Returns list of application names we've seen the user interacting with
+ * within the last 'in_last_seconds' seconds and in the specified activity.
+ * Free the names in the GSList with g_free(), the lists themselves with
+ * g_slist_free().
+ */
+static void
+get_active_apps (ShellAppMonitor *monitor,
+                 int              in_last_seconds,
+                 GSList         **wm_classes,
+                 int             *activity)
+{
+  ActiveAppsData app_data;
+  guint32 now;
+
+  now = get_time ();
+  app_data.activity = 0;
+  *activity = app_data.activity; /* Be sure we use the exact same timestamp everywhere */
+  app_data.start_time = now - in_last_seconds;
+
+  if (wm_classes && g_hash_table_size (monitor->apps_by_wm_class))
+    {
+      app_data.result = NULL;
+      g_hash_table_foreach (monitor->apps_by_wm_class, active_apps_foreach,
+                            &app_data);
+      *wm_classes = app_data.result;
+    }
+}
+
+static gboolean
+on_save_apps_timeout (gpointer data)
+{
+  ShellAppMonitor *monitor = (ShellAppMonitor *) data;
+  static guint32 period = SAVE_APPS_TIMEOUT;
+
+  if (monitor->upload_apps_burst_count >= 0)
+    {
+      period = SAVE_APPS_BURST_TIMEOUT;
+      monitor->upload_apps_burst_count--;
+
+      if (monitor->upload_apps_burst_count == 0)
+        {
+          g_source_remove (monitor->save_apps_id);
+          g_timeout_add_seconds (SAVE_APPS_TIMEOUT, on_save_apps_timeout, monitor);
+        }
+    }
+
+  GSList *wm_classes = NULL;
+  int activity;
+
+  get_active_apps (monitor, period, &wm_classes, &activity);
+
+  if (wm_classes)
+    {
+      save_active_apps (monitor, period, wm_classes, activity);
+      save_to_file (monitor);
+
+      if (wm_classes)
+        {
+          g_slist_foreach (wm_classes, (GFunc) g_free, NULL);
+          g_slist_free (wm_classes);
+        }
+    }
+
+  return TRUE;
+}
+
+/* Used to find an app item from its wm_class, when non empty */
+static gint
+popularity_find_app (gconstpointer list_data,
+                     gconstpointer user_data)
+{ 
+  AppPopularity *list_pop = (AppPopularity *) list_data;
+  AppPopularity *user_pop = (AppPopularity *) user_data;
+  return strcmp (list_pop->wm_class, user_pop->wm_class);
+}
+
+/* Used to sort highest scores at the top */
+static gint popularity_sort_apps (gconstpointer data1,
+                                  gconstpointer data2)
+{
+  const AppPopularity *pop1 = data1;
+  const AppPopularity *pop2 = data2;
+  
+  if (pop1->score > pop2->score)
+    return -1;
+  else if (pop1->score == pop2->score)
+    return 0;
+  else
+    return 1;
+}
+
+/* Limit the score to a certain level so that most popular apps can change */
+static void
+normalize_popularity (GSList *list)
+{
+  if (!list)
+    return;
+  
+  AppPopularity *app_popularity;
+
+  /* Highest score since list is sorted */
+  app_popularity = (AppPopularity *) (list->data);
+  /* Limiting score allows new apps to catch up (see SCORE_MAX definition) */
+  if (app_popularity->score > SCORE_MAX)
+    while (list)
+      {
+        app_popularity = (AppPopularity *) (list->data);
+        app_popularity->score /= 2;
+        list = list->next;
+      }
+}
+  
+/* Clean up apps we see rarely.
+ * The logic behind this is that if an app was seen less than SCORE_MIN times
+ * and not seen for a week, it can probably be forgotten about.
+ * This should much reduce the size of the list and avoid 'pollution'. */
+static GSList *
+clean_popularity (GSList *list)
+{
+  AppPopularity *app_popularity;
+  GDate *date;
+  guint32 date_days;
+  GSList *next, *head;
+
+  date = g_date_new ();
+  g_date_set_time_t (date, time (NULL));
+  g_date_subtract_days (date, 7);
+  date_days = g_date_get_julian (date);
+  head = list;
+  while (list)
+  {
+    next = list->next;
+    app_popularity = (AppPopularity *) (list->data);
+    if ((app_popularity->score < SCORE_MIN) &&
+          (app_popularity->last_seen < date_days))
+      head = g_slist_remove (head, list);
+    list = next;
+  }
+  g_date_free (date);
+  return head;
+}
+
+/* Save apps data internally to lists, merging if necessary */
+static void
+save_active_apps (ShellAppMonitor *monitor,
+                  int              collection_period,
+                  GSList          *wm_classes,
+                  int              activity)
+{
+  AppPopularity *app_popularity;
+  AppPopularity temp; /* We only set/use two fields here */
+  GDate *date;
+  guint32 date_days;
+  GSList *popularity;
+  GSList *item;
+  GSList *l;
+  
+  popularity = g_hash_table_lookup (monitor->popularities,
+                                    GINT_TO_POINTER (activity));
+  date = g_date_new ();
+  g_date_set_time_t (date, time (NULL));
+  date_days = g_date_get_julian (date);
+  if (!popularity) /* Just create the list using provided information */
+    {
+      for (l = wm_classes; l; l = l->next)
+        {
+          app_popularity = g_new (AppPopularity, 1);
+          app_popularity->last_seen = date_days;
+          app_popularity->score = 1;
+          /* Copy data from the old list */
+          app_popularity->wm_class = g_strdup ((gchar *) l->data);
+          popularity = g_slist_prepend (popularity, app_popularity);
+        }
+    }
+  else /* Merge with old data */
+    {
+      for (l = wm_classes; l; l = l->next)
+        {
+          temp.wm_class = (gchar *) l->data;
+          
+          item = g_slist_find_custom (popularity, &temp, popularity_find_app);
+          if (!item)
+            {
+              app_popularity = g_new (AppPopularity, 1);
+              app_popularity->score = 1;
+              /* Copy data from other lists */
+              app_popularity->wm_class = g_strdup ((gchar *) l->data);
+              popularity = g_slist_prepend (popularity, app_popularity);
+            }
+          else
+            {
+              app_popularity = (AppPopularity *) item->data;
+              app_popularity->score++;
+            }
+          app_popularity->last_seen = date_days;
+        }
+    }
+
+    /* Clean once in a while, doing so at start may no be enough if uptime is high */
+    popularity = clean_popularity (popularity);
+    /* Need to do this often since SCORE_MAX should be relatively low */
+    normalize_popularity (popularity);
+    popularity = g_slist_sort (popularity, popularity_sort_apps);
+    g_hash_table_replace (monitor->popularities, GINT_TO_POINTER (activity),
+                          popularity);
+    g_date_free (date);
+
+    g_signal_emit (monitor, signals[CHANGED], 0);
+}
+
+/* Save app data lists to file */
+static void
+save_to_file (ShellAppMonitor *monitor)
+{
+  GHashTableIter iter;
+  int activity;
+  GSList *popularity;
+  AppPopularity *app_popularity;
+  GFileOutputStream *output;
+  GDataOutputStream *data_output;
+  GError *error = NULL;
+  gchar *line;
+  gchar score_buf[G_ASCII_DTOSTR_BUF_SIZE];
+  static int last_error_code = 0;
+
+  /* Parent directory is already created by shell-global */
+  output = g_file_replace (monitor->configfile, NULL, FALSE, G_FILE_CREATE_NONE, NULL, &error);
+  if (!output)
+    {
+      if (last_error_code == error->code)
+        {
+          g_error_free (error);
+          return;
+        }
+      last_error_code = error->code;
+      g_warning ("Could not save applications usage data: %s. This warning will be printed only once.", error->message);
+      g_error_free (error);
+      return;
+    }
+  data_output = g_data_output_stream_new (G_OUTPUT_STREAM(output));
+  g_object_unref (output);
+    
+  g_hash_table_iter_init (&iter, monitor->popularities);
+  while (g_hash_table_iter_next (&iter, (gpointer *) &activity, (gpointer *) &popularity)
+         && popularity)
+    { 
+      line = g_strdup_printf ("%i\n", activity);
+      g_data_output_stream_put_string (data_output, "--\n", NULL, NULL);
+      g_data_output_stream_put_string (data_output, line, NULL, NULL);
+      g_free (line);
+
+      do
+        {
+          app_popularity = (AppPopularity *) popularity->data;
+          g_ascii_dtostr (score_buf, sizeof (score_buf), app_popularity->score);
+          line = g_strdup_printf ("%s,%s,%u\n", app_popularity->wm_class,
+                                  score_buf, app_popularity->last_seen);
+          g_data_output_stream_put_string (data_output, line, NULL, &error);
+          g_free (line);
+          if (error)
+            goto out;
+        }
+      while ( (popularity = popularity->next) );
+    }
+
+  out:
+  g_output_stream_close (G_OUTPUT_STREAM(data_output), NULL, &error);
+  g_object_unref (data_output);
+  if (error)
+    {
+      if (last_error_code == error->code)
+        {
+          g_error_free (error);
+          return;
+        }
+      last_error_code = error->code;
+      g_warning ("Could not save applications usage data: %s. This warning will be printed only once.", error->message);
+      g_error_free (error);
+    }
+}
+
+/* Load data about apps usage from file */
+static void
+restore_from_file (ShellAppMonitor *monitor)
+{
+  int activity = -1; /* Means invalid ID */
+  GSList *popularity;
+  AppPopularity *app_popularity;
+  GFileInputStream *input;
+  GDataInputStream *data_input;
+  GError *error = NULL;
+  gchar *line;
+  gchar **info;
+  
+  input = g_file_read (monitor->configfile, NULL, &error);
+  if (error)
+    {
+      if (error->code == G_IO_ERROR_NOT_FOUND)
+        g_message ("No applications usage data file found. This is normal if you start the program for the first time.");
+      else
+        g_warning ("Could not load applications usage data: %s", error->message);
+      g_error_free (error);
+      return;
+    }
+
+  data_input = g_data_input_stream_new (G_INPUT_STREAM(input));
+  g_object_unref (input);
+
+  while (TRUE)
+    {
+      line = g_data_input_stream_read_line (data_input, NULL, NULL, &error);
+      if (!line)
+        goto out;
+      if (strcmp (line, "--") == 0) /* Line starts a new activity */
+        {
+          g_free (line);
+          line = g_data_input_stream_read_line (data_input, NULL, NULL, &error);
+          if (line && (strcmp (line, "") != 0))
+            {
+              if (activity != -1) /* Save previous activity, cleaning and sorting it */
+                {
+                  popularity = clean_popularity (popularity);
+                  popularity = g_slist_sort (popularity, popularity_sort_apps);
+                  g_hash_table_replace (monitor->popularities,
+                                        GINT_TO_POINTER (activity), popularity);
+                }
+              activity = atoi (line);
+              /* FIXME: do something if conversion fails! */
+              /* like: errno = NULL; ... if (errno) { g_free (line); goto out; } */
+            }
+          else if (error)
+            {
+              g_free (line);
+              goto out;
+            }
+          else /* End of file */
+            goto out;
+        }
+      /* Line is about an app.
+       * If no activity was provided yet, just skip */
+      else if ((activity != 1) && (strcmp (line, "") != 0))
+        {
+          info = g_strsplit (line, ",", 0);
+          if (info[0] && info [1] && info[2]) /* Skip on wrong syntax */
+            {
+              app_popularity = g_new (AppPopularity, 1);
+              app_popularity->wm_class = g_strdup(info[0]);
+              app_popularity->score = g_ascii_strtod (info[1], NULL);
+              app_popularity->last_seen = (guint32) strtoul (info[2], NULL, 10);
+              popularity = g_slist_prepend (popularity, app_popularity);
+            }
+
+          g_strfreev (info);
+          g_free (line);
+        }
+      else
+        g_free (line); /* Just skip to next app */
+     }
+
+out:
+  if (activity != -1) /* Save last activity, cleaning and sorting it */
+    {
+      popularity = clean_popularity (popularity);
+      popularity = g_slist_sort  (popularity, popularity_sort_apps);
+      g_hash_table_replace (monitor->popularities, GINT_TO_POINTER (activity),
+                            popularity);
+    }
+
+    g_input_stream_close (G_INPUT_STREAM (data_input), NULL, NULL);
+    g_object_unref (data_input);
+    if (error)
+      {
+        g_warning ("Could not load applications usage data: %s", error->message);
+        g_error_free (error);
+      }
+}
diff --git a/src/shell-app-monitor.h b/src/shell-app-monitor.h
new file mode 100644
index 0000000..f873c38
--- /dev/null
+++ b/src/shell-app-monitor.h
@@ -0,0 +1,45 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+#ifndef __SHELL_APP_MONITOR_H__
+#define __SHELL_APP_MONITOR_H__
+
+#include <glib-object.h>
+#include <glib.h>
+
+/* 
+ * This object provides monitoring of system application directories (.desktop files)
+ * and activity-based statistics about applications usage 
+ */
+
+G_BEGIN_DECLS
+
+typedef struct _ShellAppMonitor ShellAppMonitor;
+typedef struct _ShellAppMonitorClass ShellAppMonitorClass;
+typedef struct _ShellAppMonitorPrivate ShellAppMonitorPrivate;
+
+#define SHELL_TYPE_APP_MONITOR              (shell_app_monitor_get_type ())
+#define SHELL_APP_MONITOR(object)           (G_TYPE_CHECK_INSTANCE_CAST ((object), SHELL_TYPE_APP_MONITOR, ShellAppMonitor))
+#define SHELL_APP_MONITOR_CLASS(klass)      (G_TYPE_CHECK_CLASS_CAST ((klass), SHELL_TYPE_APP_MONITOR, ShellAppMonitorClass))
+#define SHELL_IS_APP_MONITOR(object)        (G_TYPE_CHECK_INSTANCE_TYPE ((object), SHELL_TYPE_APP_MONITOR))
+#define SHELL_IS_APP_MONITOR_CLASS(klass)   (G_TYPE_CHECK_CLASS_TYPE ((klass), SHELL_TYPE_APP_MONITOR))
+#define SHELL_APP_MONITOR_GET_CLASS(obj)    (G_TYPE_INSTANCE_GET_CLASS ((obj), SHELL_TYPE_APP_MONITOR, ShellAppMonitorClass))
+
+struct _ShellAppMonitorClass
+{
+  GObjectClass parent_class;
+  
+  void (*apps_changed)(ShellAppMonitor *menuwrapper,
+                       gpointer         data);
+};
+
+GType shell_app_monitor_get_type (void) G_GNUC_CONST;
+
+ShellAppMonitor* shell_app_monitor_new(void);
+
+/* Get the most popular applications for a given activity */
+GSList *shell_app_monitor_get_apps (ShellAppMonitor *monitor,
+                                    int              activity,
+                                    gint             number);
+
+G_END_DECLS
+
+#endif /* __SHELL_APP_MONITOR_H__ */
diff --git a/tools/build/gnome-shell-build-setup.sh b/tools/build/gnome-shell-build-setup.sh
index 558bba3..26e1e02 100755
--- a/tools/build/gnome-shell-build-setup.sh
+++ b/tools/build/gnome-shell-build-setup.sh
@@ -32,7 +32,7 @@ fi
 # Devel packages needed by gnome-shell and its deps:
 # dbus-glib, gconf, GL, gnome-menus, gtk, libffi, libgnomeui, librsvg, libwnck,
 # python, readline, spidermonkey ({mozilla,firefox,xulrunner}-js),
-# xdamage
+# xdamage, xscrnsaver
 #
 # Non-devel packages needed by gnome-shell and its deps:
 # gdb, glxinfo, python, Xephyr, xeyes*, xlogo*, xterm*, zenity
@@ -63,7 +63,7 @@ if test x$system = xUbuntu -o x$system = xDebian ; then
     libdbus-glib-1-dev libgconf2-dev libgtk2.0-dev libffi-dev \
     libgnome-menu-dev libgnomeui-dev librsvg2-dev libwnck-dev libgl1-mesa-dev \
     mesa-common-dev python-dev libreadline5-dev xulrunner-dev \
-    xserver-xephyr \
+    xserver-xephyr libxss-dev \
     ; do
       if ! dpkg_is_installed $pkg; then
         reqd="$pkg $reqd"
@@ -84,7 +84,7 @@ if test x$system = xFedora ; then
     libtool pkgconfig \
     dbus-glib-devel GConf2-devel gnome-menus-devel gtk2-devel libffi-devel libgnomeui-devel \
     librsvg2-devel libwnck-devel mesa-libGL-devel python-devel readline-devel \
-    xulrunner-devel libXdamage-devel \
+    xulrunner-devel libXdamage-devel libXScrnSaver-devel \
     gdb glx-utils xorg-x11-apps xorg-x11-server-Xephyr xterm zenity \
     gstreamer-devel gstreamer-plugins-base gstreamer-plugins-good \
     ; do
@@ -103,8 +103,8 @@ if test x$system = xSUSE ; then
     curl \
     bison flex gnome-doc-utils-devel \
     gconf2-devel libffi-devel libgnomeui-devel librsvg-devel libwnck-devel \
-    readline-devel mozilla-xulrunner190-devel xorg-x11-devel \
-    xterm xorg-x11 xorg-x11-server-extra \
+    libXScrnSaver-devel readline-devel mozilla-xulrunner190-devel \
+    xorg-x11-devel xterm xorg-x11 xorg-x11-server-extra \
     ; do
       if ! rpm -q $pkg > /dev/null 2>&1; then
         reqd="$pkg $reqd"
@@ -124,7 +124,7 @@ if test x$system = xMandrivaLinux ; then
     bison flex gnome-common gnome-doc-utils gtk-doc intltool \
     libGConf2-devel ffi5-devel libgnomeui2-devel librsvg2-devel \
     libwnck-1-devel GL-devel readline-devel libxulrunner-devel \
-    libxdamage-devel \
+    libxdamage-devel libxscrnsaver-devel \
     mesa-demos x11-server-xephyr x11-apps xterm zenity \
     ; do
       if ! rpm -q --whatprovides $pkg > /dev/null 2>&1; then



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]