/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ #include #include #include #include #include #include #include #include #include #include #include "shell-app-monitor.h" #include "shell-app-system.h" #include "shell-global.h" #include "display.h" #include "window.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 */ #define APP_MONITOR_GCONF_DIR SHELL_GCONF_DIR"/app_monitor" #define ENABLE_MONITORING_KEY APP_MONITOR_GCONF_DIR"/enable_monitoring" /* 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[] = { {"mozilla-firefox", ".* - Mozilla Firefox", NULL}, \ {"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; GConfClient *gconf_client; glong activity_time; gulong last_idle; guint poll_id; guint save_apps_id; guint gconf_notify; gboolean currently_idle; gboolean enable_monitoring; /* */ GHashTable *running_appids; /* */ GHashTable *window_to_app; 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 void update_enable_monitoring (ShellAppMonitor *monitor); static void on_enable_monitoring_key_changed (GConfClient *client, guint connexion_id, GConfEntry *entry, gpointer 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 char * get_wmclass_for_window (MetaWindow *window) { static gboolean patterns_initialized = FALSE; const char *wm_class; char *title; int i; wm_class = meta_window_get_wm_class (window); g_object_get (window, "title", &title, NULL); if (!patterns_initialized) /* Generate match patterns once for all */ { patterns_initialized = TRUE; for (i = 0; title_patterns[i].app_id; i++) { title_patterns[i].regex = g_regex_new (title_patterns[i].pattern, 0, 0, NULL); } } /* 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 */ wm_class = title_patterns[i].app_id; break; } } } g_free (title); return g_strdup (wm_class); } /** * get_cleaned_wmclass_for_window: * * A "cleaned" wmclass is the WM_CLASS property of a window, * after some transformations to turn it into a form * somewhat more resilient to changes, such as lowercasing. */ static char * get_cleaned_wmclass_for_window (MetaWindow *window) { char *wmclass; char *cleaned_wmclass; if (meta_window_get_window_type (window) != META_WINDOW_NORMAL) return NULL; wmclass = get_wmclass_for_window (window); if (!wmclass) return NULL; cleaned_wmclass = g_utf8_strdown (wmclass, -1); g_free (wmclass); /* This handles "Fedora Eclipse", probably others */ g_strdelimit (cleaned_wmclass, " ", '-'); wmclass = g_strdup (cleaned_wmclass); g_free (cleaned_wmclass); return wmclass; } /** * get_app_for_window: * * Returns the application associated with a window, or %NULL if * we're unable to determine one. */ static ShellAppInfo * get_app_for_window (MetaWindow *window) { char *wmclass; char *with_desktop; ShellAppInfo *result; ShellAppSystem *appsys; wmclass = get_cleaned_wmclass_for_window (window); if (!wmclass) return NULL; with_desktop = g_strjoin (NULL, wmclass, ".desktop", NULL); g_free (wmclass); appsys = shell_app_system_get_default (); result = shell_app_system_lookup_heuristic_basename (appsys, with_desktop); g_free (with_desktop); return result; } static void track_window (ShellAppMonitor *self, MetaWindow *window) { ShellAppInfo *app; guint window_count; const char *appid; app = get_app_for_window (window); if (!app) return; /* Steal the ref */ g_hash_table_insert (self->window_to_app, window, app); appid = shell_app_info_get_id (app); window_count = GPOINTER_TO_UINT (g_hash_table_lookup (self->running_appids, appid)); window_count += 1; g_hash_table_insert (self->running_appids, g_strdup (appid), GUINT_TO_POINTER (window_count)); if (window_count == 1) g_signal_emit (self, signals[CHANGED], 0); } static void shell_app_monitor_on_window_added (MetaWorkspace *workspace, MetaWindow *window, gpointer user_data) { ShellAppMonitor *self = SHELL_APP_MONITOR (user_data); track_window (self, window); } static void shell_app_monitor_on_window_removed (MetaWorkspace *workspace, MetaWindow *window, gpointer user_data) { ShellAppMonitor *self = SHELL_APP_MONITOR (user_data); ShellAppInfo *app; const char *appid; guint window_count; app = g_hash_table_lookup (self->window_to_app, window); if (!app) return; appid = shell_app_info_get_id (app); window_count = GPOINTER_TO_UINT (g_hash_table_lookup (self->running_appids, appid)); window_count -= 1; if (window_count == 0) { g_hash_table_remove (self->running_appids, appid); g_signal_emit (self, signals[CHANGED], 0); } else { g_hash_table_insert (self->running_appids, g_strdup (appid), GUINT_TO_POINTER (window_count)); } g_hash_table_remove (self->window_to_app, window); } static void load_initial_windows (ShellAppMonitor *monitor) { GList *workspaces, *iter; MetaScreen *screen = shell_global_get_screen (shell_global_get ()); workspaces = meta_screen_get_workspaces (screen); for (iter = workspaces; iter; iter = iter->next) { MetaWorkspace *workspace = iter->data; GList *windows = meta_workspace_list_windows (workspace); GList *window_iter; for (window_iter = windows; window_iter; window_iter = window_iter->next) track_window (monitor, (MetaWindow*)window_iter->data); g_list_free (windows); } } /** * shell_app_monitor_get_windows_for_app: * @self: * @appid: Find windows for this id * * Returns: (transfer container) (element-type MetaWindow): List of #MetaWindow corresponding to appid */ GSList * shell_app_monitor_get_windows_for_app (ShellAppMonitor *self, const char *appid) { GHashTableIter iter; gpointer key, value; GSList *ret = NULL; g_hash_table_iter_init (&iter, self->window_to_app); while (g_hash_table_iter_next (&iter, &key, &value)) { MetaWindow *window = key; ShellAppInfo *app = value; const char *id = shell_app_info_get_id (app); if (strcmp (id, appid) != 0) continue; ret = g_slist_prepend (ret, window); } return ret; } static void shell_app_monitor_on_n_workspaces_changed (MetaScreen *screen, GParamSpec *pspec, gpointer user_data) { ShellAppMonitor *self = SHELL_APP_MONITOR (user_data); GList *workspaces, *iter; workspaces = meta_screen_get_workspaces (screen); for (iter = workspaces; iter; iter = iter->next) { MetaWorkspace *workspace = iter->data; /* This pair of disconnect/connect is idempotent if we were * already connected, while ensuring we get connected for * new workspaces. */ g_signal_handlers_disconnect_by_func (workspace, shell_app_monitor_on_window_added, self); g_signal_handlers_disconnect_by_func (workspace, shell_app_monitor_on_window_removed, self); g_signal_connect (workspace, "window-added", G_CALLBACK (shell_app_monitor_on_window_added), self); g_signal_connect (workspace, "window-removed", G_CALLBACK (shell_app_monitor_on_window_removed), self); } } static void init_window_monitoring (ShellAppMonitor *self) { MetaScreen *screen = shell_global_get_screen (shell_global_get ()); g_signal_connect (screen, "notify::n-workspaces", G_CALLBACK (shell_app_monitor_on_n_workspaces_changed), self); shell_app_monitor_on_n_workspaces_changed (screen, NULL, self); } static void shell_app_monitor_init (ShellAppMonitor *self) { int event_base, error_base; GdkDisplay *display; Display *xdisplay; char *path; char *shell_config_dir; /* 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; self->enable_monitoring = 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); self->running_appids = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); self->window_to_app = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, (GDestroyNotify) shell_app_info_unref); load_initial_windows (self); init_window_monitoring (self); 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); self->gconf_client = gconf_client_get_default (); gconf_client_add_dir (self->gconf_client, APP_MONITOR_GCONF_DIR, GCONF_CLIENT_PRELOAD_NONE, NULL); self->gconf_notify = gconf_client_notify_add (self->gconf_client, ENABLE_MONITORING_KEY, on_enable_monitoring_key_changed, self, NULL, NULL); update_enable_monitoring (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); gconf_client_notify_remove (self->gconf_client, self->gconf_notify); g_object_unref (self->gconf_client); 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_most_used_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_most_used_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; } /** * shell_app_monitor_get_window_app * @monitor: An app monitor instance * @metawin: A #MetaWindow * * Returns: Desktop file id associated with window */ ShellAppInfo * shell_app_monitor_get_window_app (ShellAppMonitor *monitor, MetaWindow *metawin) { ShellAppInfo *info = g_hash_table_lookup (monitor->window_to_app, metawin); if (info) shell_app_info_ref (info); return info; } /** * shell_app_monitor_get_running_app_ids: * * @monitor: An app monitor instance * * Returns: (element-type utf8) (transfer container): List of application desktop * identifiers */ GList * shell_app_monitor_get_running_app_ids (ShellAppMonitor *monitor) { return g_hash_table_get_keys (monitor->running_appids); } void update_app_info (ShellAppMonitor *monitor) { char *wm_class; ShellGlobal *global; GHashTable *app_active_times = NULL; /* last active time for an application */ MetaScreen *screen; MetaDisplay *display; MetaWindow *active; int activity; guint32 timestamp; global = shell_global_get (); g_object_get (global, "screen", &screen, NULL); display = meta_screen_get_display (screen); g_object_unref (screen); active = meta_display_get_focus_window (display); if (active == NULL) return; wm_class = get_cleaned_wmclass_for_window (active); 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; gpointer key; gpointer value; 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, &key, &value) && value) { activity = GPOINTER_TO_INT (key); popularity = value; 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 = NULL; 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_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); popularity = NULL; } activity = atoi (line); /* FIXME: do something if conversion fails! */ /* like: errno = NULL; ... if (errno) { g_free (line); goto out; } */ } else { g_free (line); 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); } } /* Enable or disable the timers, depending on the value of ENABLE_MONITORING_KEY * and taking care of the previous state. If monitoring is disabled, we still * report apps usage based on (possibly) saved data, but don't collect data. */ static void update_enable_monitoring (ShellAppMonitor *monitor) { GConfValue *value; gboolean enable; value = gconf_client_get (monitor->gconf_client, ENABLE_MONITORING_KEY, NULL); if (value) { enable = gconf_value_get_bool (value); gconf_value_free (value); } else /* Schema is not present, set default value by hand to avoid getting FALSE */ enable = TRUE; /* Be sure not to start the timers if they were already set */ if (enable && !monitor->enable_monitoring) { /* If no stats are available so far, set burst mode on */ if (g_hash_table_size (monitor->popularities)) monitor->upload_apps_burst_count = 0; else monitor->upload_apps_burst_count = SAVE_APPS_BURST_LENGTH / SAVE_APPS_BURST_TIMEOUT; monitor->poll_id = g_timeout_add_seconds (5, poll_for_idleness, monitor); if (monitor->upload_apps_burst_count > 0) monitor->save_apps_id = g_timeout_add_seconds (SAVE_APPS_BURST_TIMEOUT, on_save_apps_timeout, monitor); else monitor->save_apps_id = g_timeout_add_seconds (SAVE_APPS_TIMEOUT, on_save_apps_timeout, monitor); } /* ...and don't try to stop them if they were not running */ else if (!enable && monitor->enable_monitoring) { g_source_remove (monitor->poll_id); g_source_remove (monitor->save_apps_id); } monitor->enable_monitoring = enable; } /* Called when the ENABLE_MONITORING_KEY boolean has changed */ static void on_enable_monitoring_key_changed (GConfClient *client, guint connexion_id, GConfEntry *entry, gpointer monitor) { update_enable_monitoring ((ShellAppMonitor *) monitor); } /** * shell_app_monitor_get_default: * * Return Value: (transfer none): The global #ShellAppMonitor instance */ ShellAppMonitor * shell_app_monitor_get_default () { static ShellAppMonitor *instance; if (instance == NULL) instance = g_object_new (SHELL_TYPE_APP_MONITOR, NULL); return instance; }