971 lines
28 KiB
C
971 lines
28 KiB
C
|
/* -*- 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 <gdk/gdk.h>
|
||
|
#include <gdk/gdkx.h>
|
||
|
#include <gio/gio.h>
|
||
|
#include <gconf/gconf.h>
|
||
|
#include <gconf/gconf-client.h>
|
||
|
#include <dbus/dbus-glib.h>
|
||
|
|
||
|
#include "shell-app-usage.h"
|
||
|
#include "shell-window-tracker.h"
|
||
|
#include "shell-global.h"
|
||
|
#include "shell-marshal.h"
|
||
|
|
||
|
#include "display.h"
|
||
|
#include "window.h"
|
||
|
#include "group.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
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* SECTION:shell-app-usage
|
||
|
* @short_description: Track application usage/state data
|
||
|
*
|
||
|
* This class maintains some usage and state statistics for
|
||
|
* applications by keeping track of the approximate time an application's
|
||
|
* windows are focused, as well as the last workspace it was seen on.
|
||
|
* This time tracking is implemented by watching for focus notifications,
|
||
|
* and computing a time delta between them. Also we watch the
|
||
|
* GNOME Session "StatusChanged" signal which by default is emitted after 5
|
||
|
* minutes to signify idle.
|
||
|
*/
|
||
|
|
||
|
#define APP_MONITOR_GCONF_DIR SHELL_GCONF_DIR"/app_monitor"
|
||
|
#define ENABLE_MONITORING_KEY APP_MONITOR_GCONF_DIR"/enable_monitoring"
|
||
|
|
||
|
#define FOCUS_TIME_MIN_SECONDS 7 /* Need 7 continuous seconds of focus */
|
||
|
|
||
|
#define USAGE_CLEAN_DAYS 7 /* If after 7 days we haven't seen an app, purge it */
|
||
|
|
||
|
/* Data is saved to file SHELL_CONFIG_DIR/DATA_FILENAME */
|
||
|
#define DATA_FILENAME "application_state"
|
||
|
|
||
|
#define IDLE_TIME_TRANSITION_SECONDS 30 /* If we transition to idle, only count
|
||
|
* this many seconds of usage */
|
||
|
|
||
|
/* 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.
|
||
|
*/
|
||
|
|
||
|
/* How often we save internally app data, in seconds */
|
||
|
#define SAVE_APPS_TIMEOUT_SECONDS 5 /* leave this low for testing, we can bump later if need be */
|
||
|
|
||
|
/* With this value, an app goes from bottom to top of the
|
||
|
* usage list in 50 hours of use */
|
||
|
#define SCORE_MAX (3600 * 50 / FOCUS_TIME_MIN_SECONDS)
|
||
|
|
||
|
/* If an app's score in lower than this and the app has not been used in a week,
|
||
|
* remove it */
|
||
|
#define SCORE_MIN (SCORE_MAX >> 3)
|
||
|
|
||
|
/* http://www.gnome.org/~mccann/gnome-session/docs/gnome-session.html#org.gnome.SessionManager.Presence */
|
||
|
#define GNOME_SESSION_STATUS_IDLE 3
|
||
|
|
||
|
typedef struct UsageData UsageData;
|
||
|
|
||
|
struct _ShellAppUsage
|
||
|
{
|
||
|
GObject parent;
|
||
|
|
||
|
GFile *configfile;
|
||
|
DBusGProxy *session_proxy;
|
||
|
GdkDisplay *display;
|
||
|
GConfClient *gconf_client;
|
||
|
gulong last_idle;
|
||
|
guint idle_focus_change_id;
|
||
|
guint save_id;
|
||
|
guint gconf_notify;
|
||
|
gboolean currently_idle;
|
||
|
gboolean enable_monitoring;
|
||
|
|
||
|
GSList *previously_running;
|
||
|
|
||
|
long watch_start_time;
|
||
|
ShellApp *watched_app;
|
||
|
|
||
|
/* <char *context, GHashTable<char *appid, UsageData *usage>> */
|
||
|
GHashTable *app_usages_for_context;
|
||
|
};
|
||
|
|
||
|
G_DEFINE_TYPE (ShellAppUsage, shell_app_usage, G_TYPE_OBJECT);
|
||
|
|
||
|
/* Represents an application record for a given context */
|
||
|
struct UsageData
|
||
|
{
|
||
|
/* Whether the application we're tracking is "transient", see
|
||
|
* shell_app_info_is_transient.
|
||
|
*/
|
||
|
gboolean transient;
|
||
|
|
||
|
gdouble score; /* Based on the number of times we'e seen the app and normalized */
|
||
|
long last_seen; /* Used to clear old apps we've only seen a few times */
|
||
|
};
|
||
|
|
||
|
static void shell_app_usage_finalize (GObject *object);
|
||
|
|
||
|
static void on_session_status_changed (DBusGProxy *proxy, guint status, ShellAppUsage *self);
|
||
|
static void on_focus_app_changed (ShellWindowTracker *tracker, GParamSpec *spec, ShellAppUsage *self);
|
||
|
static void ensure_queued_save (ShellAppUsage *self);
|
||
|
static UsageData * get_app_usage_for_context_and_id (ShellAppUsage *self,
|
||
|
const char *context,
|
||
|
const char *appid);
|
||
|
|
||
|
static gboolean idle_save_application_usage (gpointer data);
|
||
|
|
||
|
static void restore_from_file (ShellAppUsage *self);
|
||
|
|
||
|
static void update_enable_monitoring (ShellAppUsage *self);
|
||
|
|
||
|
static void on_enable_monitoring_key_changed (GConfClient *client,
|
||
|
guint connexion_id,
|
||
|
GConfEntry *entry,
|
||
|
gpointer self);
|
||
|
|
||
|
static long
|
||
|
get_time (void)
|
||
|
{
|
||
|
GTimeVal tv;
|
||
|
g_get_current_time (&tv);
|
||
|
return tv.tv_sec;
|
||
|
}
|
||
|
|
||
|
static void
|
||
|
shell_app_usage_class_init (ShellAppUsageClass *klass)
|
||
|
{
|
||
|
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
|
||
|
|
||
|
gobject_class->finalize = shell_app_usage_finalize;
|
||
|
}
|
||
|
|
||
|
static GHashTable *
|
||
|
get_usages_for_context (ShellAppUsage *self,
|
||
|
const char *context)
|
||
|
{
|
||
|
GHashTable *context_usages;
|
||
|
|
||
|
context_usages = g_hash_table_lookup (self->app_usages_for_context, context);
|
||
|
if (context_usages == NULL)
|
||
|
{
|
||
|
context_usages = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
|
||
|
g_hash_table_insert (self->app_usages_for_context, g_strdup (context),
|
||
|
context_usages);
|
||
|
}
|
||
|
return context_usages;
|
||
|
}
|
||
|
|
||
|
static UsageData *
|
||
|
get_app_usage_for_context_and_id (ShellAppUsage *self,
|
||
|
const char *context,
|
||
|
const char *appid)
|
||
|
{
|
||
|
UsageData *usage;
|
||
|
GHashTable *context_usages;
|
||
|
|
||
|
context_usages = get_usages_for_context (self, context);
|
||
|
|
||
|
usage = g_hash_table_lookup (context_usages, appid);
|
||
|
if (usage)
|
||
|
return usage;
|
||
|
|
||
|
usage = g_new0 (UsageData, 1);
|
||
|
g_hash_table_insert (context_usages, g_strdup (appid), usage);
|
||
|
|
||
|
return usage;
|
||
|
}
|
||
|
|
||
|
static UsageData *
|
||
|
get_usage_for_app (ShellAppUsage *self,
|
||
|
ShellApp *app)
|
||
|
{
|
||
|
const char *context;
|
||
|
|
||
|
context = _shell_window_tracker_get_app_context (shell_window_tracker_get_default (), app);
|
||
|
|
||
|
return get_app_usage_for_context_and_id (self, context, shell_app_get_id (app));
|
||
|
}
|
||
|
|
||
|
typedef struct {
|
||
|
gboolean in_context;
|
||
|
GHashTableIter context_iter;
|
||
|
const char *context_id;
|
||
|
GHashTableIter usage_iter;
|
||
|
} UsageIterator;
|
||
|
|
||
|
static void
|
||
|
usage_iterator_init (ShellAppUsage *self,
|
||
|
UsageIterator *iter)
|
||
|
{
|
||
|
iter->in_context = FALSE;
|
||
|
g_hash_table_iter_init (&(iter->context_iter), self->app_usages_for_context);
|
||
|
}
|
||
|
|
||
|
static gboolean
|
||
|
usage_iterator_next (ShellAppUsage *self,
|
||
|
UsageIterator *iter,
|
||
|
const char **context,
|
||
|
const char **id,
|
||
|
UsageData **usage)
|
||
|
{
|
||
|
gpointer key, value;
|
||
|
gboolean next_context;
|
||
|
|
||
|
if (!iter->in_context)
|
||
|
next_context = TRUE;
|
||
|
else if (!g_hash_table_iter_next (&(iter->usage_iter), &key, &value))
|
||
|
next_context = TRUE;
|
||
|
else
|
||
|
next_context = FALSE;
|
||
|
|
||
|
while (next_context)
|
||
|
{
|
||
|
GHashTable *app_usages;
|
||
|
|
||
|
if (!g_hash_table_iter_next (&(iter->context_iter), &key, &value))
|
||
|
return FALSE;
|
||
|
iter->in_context = TRUE;
|
||
|
iter->context_id = key;
|
||
|
app_usages = value;
|
||
|
g_hash_table_iter_init (&(iter->usage_iter), app_usages);
|
||
|
|
||
|
next_context = !g_hash_table_iter_next (&(iter->usage_iter), &key, &value);
|
||
|
}
|
||
|
|
||
|
*context = iter->context_id;
|
||
|
*id = key;
|
||
|
*usage = value;
|
||
|
|
||
|
return TRUE;
|
||
|
}
|
||
|
|
||
|
static void
|
||
|
usage_iterator_remove (ShellAppUsage *self,
|
||
|
UsageIterator *iter)
|
||
|
{
|
||
|
g_assert (iter->in_context);
|
||
|
|
||
|
g_hash_table_iter_remove (&(iter->usage_iter));
|
||
|
}
|
||
|
|
||
|
/* Limit the score to a certain level so that most used apps can change */
|
||
|
static void
|
||
|
normalize_usage (ShellAppUsage *self)
|
||
|
{
|
||
|
UsageIterator iter;
|
||
|
const char *context;
|
||
|
const char *id;
|
||
|
UsageData *usage;
|
||
|
|
||
|
usage_iterator_init (self, &iter);
|
||
|
|
||
|
while (usage_iterator_next (self, &iter, &context, &id, &usage))
|
||
|
{
|
||
|
usage->score /= 2;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static void
|
||
|
increment_usage_for_app_at_time (ShellAppUsage *self,
|
||
|
ShellApp *app,
|
||
|
long time)
|
||
|
{
|
||
|
UsageData *usage;
|
||
|
guint elapsed;
|
||
|
guint usage_count;
|
||
|
|
||
|
usage = get_usage_for_app (self, app);
|
||
|
|
||
|
usage->last_seen = time;
|
||
|
|
||
|
elapsed = time - self->watch_start_time;
|
||
|
usage_count = elapsed / FOCUS_TIME_MIN_SECONDS;
|
||
|
if (usage_count > 0)
|
||
|
{
|
||
|
usage->score += usage_count;
|
||
|
if (usage->score > SCORE_MAX)
|
||
|
normalize_usage (self);
|
||
|
ensure_queued_save (self);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static void
|
||
|
increment_usage_for_app (ShellAppUsage *self,
|
||
|
ShellApp *app)
|
||
|
{
|
||
|
long curtime = get_time ();
|
||
|
increment_usage_for_app_at_time (self, app, curtime);
|
||
|
}
|
||
|
|
||
|
static void
|
||
|
on_app_running_changed (ShellWindowTracker *tracker,
|
||
|
ShellApp *app,
|
||
|
gpointer user_data)
|
||
|
{
|
||
|
ShellAppUsage *self = SHELL_APP_USAGE (user_data);
|
||
|
UsageData *usage;
|
||
|
gboolean running;
|
||
|
|
||
|
if (shell_app_is_transient (app))
|
||
|
return;
|
||
|
|
||
|
usage = get_usage_for_app (self, app);
|
||
|
|
||
|
running = shell_app_get_n_windows (app) > 0;
|
||
|
|
||
|
usage->last_seen = get_time ();
|
||
|
}
|
||
|
|
||
|
static void
|
||
|
on_focus_app_changed (ShellWindowTracker *tracker,
|
||
|
GParamSpec *spec,
|
||
|
ShellAppUsage *self)
|
||
|
{
|
||
|
if (self->watched_app != NULL)
|
||
|
increment_usage_for_app (self, self->watched_app);
|
||
|
|
||
|
if (self->watched_app)
|
||
|
g_object_unref (self->watched_app);
|
||
|
|
||
|
g_object_get (tracker, "focus-app", &(self->watched_app), NULL);
|
||
|
self->watch_start_time = get_time ();
|
||
|
}
|
||
|
|
||
|
static void
|
||
|
on_session_status_changed (DBusGProxy *proxy,
|
||
|
guint status,
|
||
|
ShellAppUsage *self)
|
||
|
{
|
||
|
gboolean idle;
|
||
|
|
||
|
idle = (status >= GNOME_SESSION_STATUS_IDLE);
|
||
|
if (self->currently_idle == idle)
|
||
|
return;
|
||
|
|
||
|
self->currently_idle = idle;
|
||
|
if (idle)
|
||
|
{
|
||
|
long end_time;
|
||
|
|
||
|
/* The GNOME Session signal we watch is 5 minutes, but that's a long
|
||
|
* time for this purpose. Instead, just add a base 30 seconds.
|
||
|
*/
|
||
|
if (self->watched_app)
|
||
|
{
|
||
|
end_time = self->watch_start_time + IDLE_TIME_TRANSITION_SECONDS;
|
||
|
increment_usage_for_app_at_time (self, self->watched_app, end_time);
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
/* Transitioning to !idle, reset the start time */
|
||
|
self->watch_start_time = get_time ();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static void
|
||
|
shell_app_usage_init (ShellAppUsage *self)
|
||
|
{
|
||
|
char *shell_config_dir, *path;
|
||
|
DBusGConnection *session_bus;
|
||
|
ShellWindowTracker *tracker;
|
||
|
|
||
|
self->app_usages_for_context = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_hash_table_destroy);
|
||
|
|
||
|
tracker = shell_window_tracker_get_default ();
|
||
|
g_signal_connect (tracker, "notify::focus-app", G_CALLBACK (on_focus_app_changed), self);
|
||
|
g_signal_connect (tracker, "app-running-changed", G_CALLBACK (on_app_running_changed), self);
|
||
|
|
||
|
session_bus = dbus_g_bus_get (DBUS_BUS_SESSION, NULL);
|
||
|
self->session_proxy = dbus_g_proxy_new_for_name (session_bus, "org.gnome.SessionManager",
|
||
|
"/org/gnome/SessionManager/Presence",
|
||
|
"org.gnome.SessionManager");
|
||
|
dbus_g_proxy_add_signal (self->session_proxy, "StatusChanged",
|
||
|
G_TYPE_UINT, G_TYPE_INVALID, G_TYPE_INVALID);
|
||
|
dbus_g_proxy_connect_signal (self->session_proxy, "StatusChanged",
|
||
|
G_CALLBACK (on_session_status_changed), self, NULL);
|
||
|
|
||
|
self->last_idle = 0;
|
||
|
self->currently_idle = FALSE;
|
||
|
self->enable_monitoring = FALSE;
|
||
|
|
||
|
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);
|
||
|
g_free (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_usage_finalize (GObject *object)
|
||
|
{
|
||
|
ShellAppUsage *self = SHELL_APP_USAGE (object);
|
||
|
|
||
|
if (self->save_id > 0)
|
||
|
g_source_remove (self->save_id);
|
||
|
gconf_client_notify_remove (self->gconf_client, self->gconf_notify);
|
||
|
g_object_unref (self->gconf_client);
|
||
|
|
||
|
g_object_unref (self->configfile);
|
||
|
|
||
|
G_OBJECT_CLASS (shell_app_usage_parent_class)->finalize(object);
|
||
|
}
|
||
|
|
||
|
typedef struct {
|
||
|
ShellAppUsage *usage;
|
||
|
GHashTable *context_usages;
|
||
|
} SortAppsByUsageData;
|
||
|
|
||
|
static int
|
||
|
sort_apps_by_usage (gconstpointer a,
|
||
|
gconstpointer b,
|
||
|
gpointer datap)
|
||
|
{
|
||
|
SortAppsByUsageData *data = datap;
|
||
|
ShellApp *app_a, *app_b;
|
||
|
UsageData *usage_a, *usage_b;
|
||
|
|
||
|
app_a = (ShellApp*)a;
|
||
|
app_b = (ShellApp*)b;
|
||
|
|
||
|
usage_a = g_hash_table_lookup (data->context_usages, shell_app_get_id (app_a));
|
||
|
usage_b = g_hash_table_lookup (data->context_usages, shell_app_get_id (app_b));
|
||
|
|
||
|
return usage_b->score - usage_a->score;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* shell_app_usage_get_most_used:
|
||
|
* @usage: the usage instance to request
|
||
|
* @context: Activity identifier
|
||
|
* @max_count: how many applications are requested. Note that the actual
|
||
|
* list size may be less, or NULL if not enough applications are registered.
|
||
|
*
|
||
|
* Get a list of most popular applications for a given context.
|
||
|
*
|
||
|
* Returns: (element-type ShellApp) (transfer full): List of applications
|
||
|
*/
|
||
|
GSList *
|
||
|
shell_app_usage_get_most_used (ShellAppUsage *self,
|
||
|
const char *context,
|
||
|
gint max_count)
|
||
|
{
|
||
|
GSList *apps;
|
||
|
GList *appids, *iter;
|
||
|
GHashTable *usages;
|
||
|
ShellAppSystem *appsys;
|
||
|
SortAppsByUsageData data;
|
||
|
|
||
|
usages = g_hash_table_lookup (self->app_usages_for_context, context);
|
||
|
if (usages == NULL)
|
||
|
return NULL;
|
||
|
|
||
|
appsys = shell_app_system_get_default ();
|
||
|
|
||
|
appids = g_hash_table_get_keys (usages);
|
||
|
apps = NULL;
|
||
|
for (iter = appids; iter; iter = iter->next)
|
||
|
{
|
||
|
const char *appid = iter->data;
|
||
|
ShellApp *app;
|
||
|
|
||
|
app = shell_app_system_get_app (appsys, appid);
|
||
|
if (!app)
|
||
|
continue;
|
||
|
|
||
|
apps = g_slist_prepend (apps, g_object_ref (app));
|
||
|
}
|
||
|
|
||
|
g_list_free (appids);
|
||
|
|
||
|
data.usage = self;
|
||
|
data.context_usages = usages;
|
||
|
apps = g_slist_sort_with_data (apps, sort_apps_by_usage, &data);
|
||
|
|
||
|
return apps;
|
||
|
}
|
||
|
|
||
|
static void
|
||
|
ensure_queued_save (ShellAppUsage *self)
|
||
|
{
|
||
|
if (self->save_id != 0)
|
||
|
return;
|
||
|
self->save_id = g_timeout_add_seconds (SAVE_APPS_TIMEOUT_SECONDS, idle_save_application_usage, self);
|
||
|
}
|
||
|
|
||
|
/* Used to sort highest scores at the top */
|
||
|
static gint
|
||
|
usage_sort_apps (gconstpointer data1,
|
||
|
gconstpointer data2)
|
||
|
{
|
||
|
const UsageData *u1 = data1;
|
||
|
const UsageData *u2 = data2;
|
||
|
|
||
|
if (u1->score > u2->score)
|
||
|
return -1;
|
||
|
else if (u1->score == u2->score)
|
||
|
return 0;
|
||
|
else
|
||
|
return 1;
|
||
|
}
|
||
|
|
||
|
/* 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 gboolean
|
||
|
idle_clean_usage (ShellAppUsage *self)
|
||
|
{
|
||
|
UsageIterator iter;
|
||
|
const char *context;
|
||
|
const char *id;
|
||
|
UsageData *usage;
|
||
|
long current_time;
|
||
|
long week_ago;
|
||
|
|
||
|
current_time = get_time ();
|
||
|
week_ago = current_time - (7 * 24 * 60 * 60);
|
||
|
|
||
|
usage_iterator_init (self, &iter);
|
||
|
|
||
|
while (usage_iterator_next (self, &iter, &context, &id, &usage))
|
||
|
{
|
||
|
if ((usage->score < SCORE_MIN) &&
|
||
|
(usage->last_seen < week_ago))
|
||
|
usage_iterator_remove (self, &iter);
|
||
|
}
|
||
|
|
||
|
return FALSE;
|
||
|
}
|
||
|
|
||
|
static gboolean
|
||
|
write_escaped (GDataOutputStream *stream,
|
||
|
const char *str,
|
||
|
GError **error)
|
||
|
{
|
||
|
gboolean ret;
|
||
|
char *quoted = g_markup_escape_text (str, -1);
|
||
|
ret = g_data_output_stream_put_string (stream, quoted, NULL, error);
|
||
|
g_free (quoted);
|
||
|
return ret;
|
||
|
}
|
||
|
|
||
|
static gboolean
|
||
|
write_attribute_string (GDataOutputStream *stream,
|
||
|
const char *elt_name,
|
||
|
const char *str,
|
||
|
GError **error)
|
||
|
{
|
||
|
gboolean ret = FALSE;
|
||
|
char *elt;
|
||
|
|
||
|
elt = g_strdup_printf (" %s=\"", elt_name);
|
||
|
ret = g_data_output_stream_put_string (stream, elt, NULL, error);
|
||
|
g_free (elt);
|
||
|
if (!ret)
|
||
|
goto out;
|
||
|
|
||
|
ret = write_escaped (stream, str, error);
|
||
|
if (!ret)
|
||
|
goto out;
|
||
|
|
||
|
ret = g_data_output_stream_put_string (stream, "\"", NULL, error);
|
||
|
|
||
|
out:
|
||
|
return ret;
|
||
|
}
|
||
|
|
||
|
static gboolean
|
||
|
write_attribute_uint (GDataOutputStream *stream,
|
||
|
const char *elt_name,
|
||
|
guint value,
|
||
|
GError **error)
|
||
|
{
|
||
|
gboolean ret;
|
||
|
char *buf;
|
||
|
|
||
|
buf = g_strdup_printf ("%u", value);
|
||
|
ret = write_attribute_string (stream, elt_name, buf, error);
|
||
|
g_free (buf);
|
||
|
|
||
|
return ret;
|
||
|
}
|
||
|
|
||
|
static gboolean
|
||
|
write_attribute_double (GDataOutputStream *stream,
|
||
|
const char *elt_name,
|
||
|
double value,
|
||
|
GError **error)
|
||
|
{
|
||
|
gchar buf[G_ASCII_DTOSTR_BUF_SIZE];
|
||
|
gboolean ret;
|
||
|
|
||
|
g_ascii_dtostr (buf, sizeof (buf), value);
|
||
|
ret = write_attribute_string (stream, elt_name, buf, error);
|
||
|
|
||
|
return ret;
|
||
|
}
|
||
|
|
||
|
/* Save app data lists to file */
|
||
|
static gboolean
|
||
|
idle_save_application_usage (gpointer data)
|
||
|
{
|
||
|
ShellAppUsage *self = SHELL_APP_USAGE (data);
|
||
|
UsageIterator iter;
|
||
|
const char *current_context;
|
||
|
const char *context;
|
||
|
const char *id;
|
||
|
UsageData *usage;
|
||
|
GFileOutputStream *output;
|
||
|
GOutputStream *buffered_output;
|
||
|
GDataOutputStream *data_output;
|
||
|
GError *error = NULL;
|
||
|
|
||
|
self->save_id = 0;
|
||
|
|
||
|
/* Parent directory is already created by shell-global */
|
||
|
output = g_file_replace (self->configfile, NULL, FALSE, G_FILE_CREATE_NONE, NULL, &error);
|
||
|
if (!output)
|
||
|
{
|
||
|
g_debug ("Could not save applications usage data: %s", error->message);
|
||
|
g_error_free (error);
|
||
|
return FALSE;
|
||
|
}
|
||
|
buffered_output = g_buffered_output_stream_new (G_OUTPUT_STREAM (output));
|
||
|
g_object_unref (output);
|
||
|
data_output = g_data_output_stream_new (G_OUTPUT_STREAM (buffered_output));
|
||
|
g_object_unref (buffered_output);
|
||
|
|
||
|
if (!g_data_output_stream_put_string (data_output, "<?xml version=\"1.0\"?>\n<application-state>\n", NULL, &error))
|
||
|
goto out;
|
||
|
|
||
|
usage_iterator_init (self, &iter);
|
||
|
|
||
|
current_context = NULL;
|
||
|
while (usage_iterator_next (self, &iter, &context, &id, &usage))
|
||
|
{
|
||
|
ShellApp *app;
|
||
|
|
||
|
app = shell_app_system_get_app (shell_app_system_get_default(), id);
|
||
|
|
||
|
if (!app)
|
||
|
continue;
|
||
|
|
||
|
if (context != current_context)
|
||
|
{
|
||
|
if (current_context != NULL)
|
||
|
{
|
||
|
if (!g_data_output_stream_put_string (data_output, " </context>", NULL, &error))
|
||
|
goto out;
|
||
|
}
|
||
|
current_context = context;
|
||
|
if (!g_data_output_stream_put_string (data_output, " <context", NULL, &error))
|
||
|
goto out;
|
||
|
if (!write_attribute_string (data_output, "id", context, &error))
|
||
|
goto out;
|
||
|
if (!g_data_output_stream_put_string (data_output, ">\n", NULL, &error))
|
||
|
goto out;
|
||
|
}
|
||
|
if (!g_data_output_stream_put_string (data_output, " <application", NULL, &error))
|
||
|
goto out;
|
||
|
if (!write_attribute_string (data_output, "id", id, &error))
|
||
|
goto out;
|
||
|
if (!write_attribute_uint (data_output, "open-window-count", shell_app_get_n_windows (app), &error))
|
||
|
goto out;
|
||
|
|
||
|
if (!write_attribute_double (data_output, "score", usage->score, &error))
|
||
|
goto out;
|
||
|
if (!write_attribute_uint (data_output, "last-seen", usage->last_seen, &error))
|
||
|
goto out;
|
||
|
if (!g_data_output_stream_put_string (data_output, "/>\n", NULL, &error))
|
||
|
goto out;
|
||
|
}
|
||
|
if (current_context != NULL)
|
||
|
{
|
||
|
if (!g_data_output_stream_put_string (data_output, " </context>\n", NULL, &error))
|
||
|
goto out;
|
||
|
}
|
||
|
if (!g_data_output_stream_put_string (data_output, "</application-state>\n", NULL, &error))
|
||
|
goto out;
|
||
|
|
||
|
out:
|
||
|
if (!error)
|
||
|
g_output_stream_close (G_OUTPUT_STREAM(data_output), NULL, &error);
|
||
|
g_object_unref (data_output);
|
||
|
if (error)
|
||
|
{
|
||
|
g_debug ("Could not save applications usage data: %s", error->message);
|
||
|
g_error_free (error);
|
||
|
}
|
||
|
return FALSE;
|
||
|
}
|
||
|
|
||
|
typedef struct {
|
||
|
ShellAppUsage *self;
|
||
|
char *context;
|
||
|
} ParseData;
|
||
|
|
||
|
static void
|
||
|
shell_app_usage_start_element_handler (GMarkupParseContext *context,
|
||
|
const gchar *element_name,
|
||
|
const gchar **attribute_names,
|
||
|
const gchar **attribute_values,
|
||
|
gpointer user_data,
|
||
|
GError **error)
|
||
|
{
|
||
|
ParseData *data = user_data;
|
||
|
|
||
|
if (strcmp (element_name, "application-state") == 0)
|
||
|
{
|
||
|
}
|
||
|
else if (strcmp (element_name, "context") == 0)
|
||
|
{
|
||
|
char *context = NULL;
|
||
|
const char **attribute;
|
||
|
const char **value;
|
||
|
|
||
|
for (attribute = attribute_names, value = attribute_values; *attribute; attribute++, value++)
|
||
|
{
|
||
|
if (strcmp (*attribute, "id") == 0)
|
||
|
context = g_strdup (*value);
|
||
|
}
|
||
|
if (context < 0)
|
||
|
{
|
||
|
g_set_error (error,
|
||
|
G_MARKUP_ERROR,
|
||
|
G_MARKUP_ERROR_PARSE,
|
||
|
"Missing attribute id on <%s> element",
|
||
|
element_name);
|
||
|
return;
|
||
|
}
|
||
|
data->context = context;
|
||
|
}
|
||
|
else if (strcmp (element_name, "application") == 0)
|
||
|
{
|
||
|
const char **attribute;
|
||
|
const char **value;
|
||
|
UsageData *usage;
|
||
|
char *appid = NULL;
|
||
|
GHashTable *usage_table;
|
||
|
|
||
|
for (attribute = attribute_names, value = attribute_values; *attribute; attribute++, value++)
|
||
|
{
|
||
|
if (strcmp (*attribute, "id") == 0)
|
||
|
appid = g_strdup (*value);
|
||
|
}
|
||
|
|
||
|
if (!appid)
|
||
|
{
|
||
|
g_set_error (error,
|
||
|
G_MARKUP_ERROR,
|
||
|
G_MARKUP_ERROR_PARSE,
|
||
|
"Missing attribute id on <%s> element",
|
||
|
element_name);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
usage_table = get_usages_for_context (data->self, data->context);
|
||
|
|
||
|
usage = g_new0 (UsageData, 1);
|
||
|
g_hash_table_insert (usage_table, appid, usage);
|
||
|
|
||
|
for (attribute = attribute_names, value = attribute_values; *attribute; attribute++, value++)
|
||
|
{
|
||
|
if (strcmp (*attribute, "open-window-count") == 0)
|
||
|
{
|
||
|
guint count = strtoul (*value, NULL, 10);
|
||
|
if (count > 0)
|
||
|
data->self->previously_running = g_slist_prepend (data->self->previously_running,
|
||
|
usage);
|
||
|
}
|
||
|
else if (strcmp (*attribute, "score") == 0)
|
||
|
{
|
||
|
usage->score = g_ascii_strtod (*value, NULL);
|
||
|
}
|
||
|
else if (strcmp (*attribute, "last-seen") == 0)
|
||
|
{
|
||
|
usage->last_seen = (guint) g_ascii_strtoull (*value, NULL, 10);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
g_set_error (error,
|
||
|
G_MARKUP_ERROR,
|
||
|
G_MARKUP_ERROR_PARSE,
|
||
|
"Unknown element <%s>",
|
||
|
element_name);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static void
|
||
|
shell_app_usage_end_element_handler (GMarkupParseContext *context,
|
||
|
const gchar *element_name,
|
||
|
gpointer user_data,
|
||
|
GError **error)
|
||
|
{
|
||
|
ParseData *data = user_data;
|
||
|
|
||
|
if (strcmp (element_name, "context") == 0)
|
||
|
{
|
||
|
g_free (data->context);
|
||
|
data->context = NULL;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static void
|
||
|
shell_app_usage_text_handler (GMarkupParseContext *context,
|
||
|
const gchar *text,
|
||
|
gsize text_len,
|
||
|
gpointer user_data,
|
||
|
GError **error)
|
||
|
{
|
||
|
/* do nothing, very very fast */
|
||
|
}
|
||
|
|
||
|
static GMarkupParser app_state_parse_funcs =
|
||
|
{
|
||
|
shell_app_usage_start_element_handler,
|
||
|
shell_app_usage_end_element_handler,
|
||
|
shell_app_usage_text_handler,
|
||
|
NULL,
|
||
|
NULL
|
||
|
};
|
||
|
|
||
|
/* Load data about apps usage from file */
|
||
|
static void
|
||
|
restore_from_file (ShellAppUsage *self)
|
||
|
{
|
||
|
GFileInputStream *input;
|
||
|
ParseData parse_data;
|
||
|
GMarkupParseContext *parse_context;
|
||
|
GError *error = NULL;
|
||
|
char buf[1024];
|
||
|
|
||
|
input = g_file_read (self->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;
|
||
|
}
|
||
|
|
||
|
memset (&parse_data, 0, sizeof (ParseData));
|
||
|
parse_data.self = self;
|
||
|
parse_data.context = NULL;
|
||
|
parse_context = g_markup_parse_context_new (&app_state_parse_funcs, 0, &parse_data, NULL);
|
||
|
|
||
|
while (TRUE)
|
||
|
{
|
||
|
gssize count = g_input_stream_read ((GInputStream*) input, buf, sizeof(buf), NULL, &error);
|
||
|
if (count <= 0)
|
||
|
goto out;
|
||
|
if (!g_markup_parse_context_parse (parse_context, buf, count, &error))
|
||
|
goto out;
|
||
|
}
|
||
|
|
||
|
out:
|
||
|
g_free (parse_data.context);
|
||
|
g_markup_parse_context_free (parse_context);
|
||
|
g_input_stream_close ((GInputStream*)input, NULL, NULL);
|
||
|
g_object_unref (input);
|
||
|
|
||
|
idle_clean_usage (self);
|
||
|
self->previously_running = g_slist_sort (self->previously_running, usage_sort_apps);
|
||
|
|
||
|
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 selfing is disabled, we still
|
||
|
* report apps usage based on (possibly) saved data, but don't collect data.
|
||
|
*/
|
||
|
static void
|
||
|
update_enable_monitoring (ShellAppUsage *self)
|
||
|
{
|
||
|
GConfValue *value;
|
||
|
gboolean enable;
|
||
|
|
||
|
value = gconf_client_get (self->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 && !self->enable_monitoring)
|
||
|
{
|
||
|
on_focus_app_changed (shell_window_tracker_get_default (), NULL, self);
|
||
|
}
|
||
|
/* ...and don't try to stop them if they were not running */
|
||
|
else if (!enable && self->enable_monitoring)
|
||
|
{
|
||
|
if (self->watched_app)
|
||
|
g_object_unref (self->watched_app);
|
||
|
self->watched_app = NULL;
|
||
|
if (self->save_id)
|
||
|
{
|
||
|
g_source_remove (self->save_id);
|
||
|
self->save_id = 0;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
self->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 self)
|
||
|
{
|
||
|
update_enable_monitoring ((ShellAppUsage *) self);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* shell_app_usage_get_default:
|
||
|
*
|
||
|
* Return Value: (transfer none): The global #ShellAppUsage instance
|
||
|
*/
|
||
|
ShellAppUsage *
|
||
|
shell_app_usage_get_default ()
|
||
|
{
|
||
|
static ShellAppUsage *instance;
|
||
|
|
||
|
if (instance == NULL)
|
||
|
instance = g_object_new (SHELL_TYPE_APP_USAGE, NULL);
|
||
|
|
||
|
return instance;
|
||
|
}
|