/* * Copyright © 2012 Canonical Ltd. * By Siegfried-A. Gevatter * Copyright © 2013 Seif Lotfy * Copyright © 2013 Rico Tzschichholz * * Based upon a C implementation (© 2010-2012 Canonical Ltd) by: * Mikkel Kamstrup Erlandsen * Michal Hruby * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 2.1 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . * */ /** * Zeitgeist is an activity-logging framework to enable the desktop of * the future. * * Its main component is the Zeitgeist engine, a D-Bus service that logs * any events other applications send to it. An event may be anything like: * * - The user opened/created/modified/closed a file, or visited a website. * - The user received an e-mail, a phone call or an IM notification. * - Someone modified a remote (eg. Google Drive) document owned by the user. * * This information is then made available to other Zeitgeist-enabled * applications over a powerful querying and monitoring API, and can be used * and analyzed to create intelligent or adaptive interfaces. * * Zeitgeist also comes with a blacklist extension to make sure the user * always stays in control of what information is logged. */ namespace Zeitgeist { /** * Primary access point for talking to the Zeitgeist daemon * * {@link Log} encapsulates the low level access to the Zeitgeist daemon. * You can use it to manage the log by inserting and deleting entries as well * as do queries on the logged data. * * It's important to realize that the #ZeitgeistLog class does not expose * any API that does synchronous communications with the message bus - * everything is asynchronous. To ease development some of the methods have * variants that are "fire and forget" ignoring the normal return value, so * that callbacks does not have to be set up. */ public class Log : QueuedProxyWrapper { class DbWorker { private unowned ThreadFunc func; public DbWorker (ThreadFunc func) { this.func = func; } public void run () { this.func (); } } private static Log default_instance; private RemoteLog proxy; private Variant? engine_version; private HashTable monitors; private DbReader dbreader; private ThreadPool threads; private bool allow_direct_read; public Log () { monitors = new HashTable (direct_hash, direct_equal); MainLoop mainloop = new MainLoop (MainContext.get_thread_default ()); allow_direct_read = Utils.log_may_read_directly (); Bus.get_proxy.begin (BusType.SESSION, Utils.ENGINE_DBUS_NAME, Utils.ENGINE_DBUS_PATH, 0, null, (obj, res) => { try { proxy = Bus.get_proxy.end (res); proxy_acquired (proxy); } catch (IOError err) { critical ("Unable to connect to Zeitgeist: %s", err.message); proxy_unavailable (err); } finally { mainloop.quit(); } }); mainloop.run(); } /** * Get a unique instance of #ZeitgeistLog, that you can share in your * application without caring about memory management. * * See zeitgeist_log_new() for more information. * * @return ZeitgeistLog. */ public static Log get_default () { if (default_instance == null) default_instance = new Log (); return default_instance; } protected override void on_connection_established () { // Reinstate all active monitors foreach (unowned Monitor monitor in monitors.get_keys ()) { reinstall_monitor.begin (monitor); } // Update our cached version property engine_version = proxy.version; warn_if_fail (engine_version.get_type_string () == "(iii)"); try { threads = new ThreadPool.with_owned_data ((worker) => { worker.run (); }, (int) get_num_processors (), true); } catch (ThreadError err) { warning ("%s", err.message); threads = null; } if (allow_direct_read && threads != null && proxy.datapath != ":memory:" && FileUtils.test (proxy.datapath, GLib.FileTest.EXISTS)) { Utils.set_database_file_path (proxy.datapath); try { dbreader = new DbReader (); } catch (EngineError err){ warning ("%s", err.message); dbreader = null; } } else { dbreader = null; } } protected override void on_connection_lost () { // Reset the monitor's registration id (0 = not registered) foreach (unowned Monitor monitor in monitors.get_keys ()) { monitors.replace (monitor, 0); } dbreader = null; } /** * Asynchronously send a set of events to the Zeitgeist daemon, requesting they * be inserted into the log. * * @param event A {@link Event} * @param cancellable a {@link GLib.Cancellable} to cancel the operation or %NULL */ public async Array insert_event (Event event, Cancellable? cancellable=null) throws Error { var events = new GenericArray (); events.add (event); return yield insert_events (events, cancellable); } /** * Asynchronously send a set of events to the Zeitgeist daemon, requesting they * be inserted into the log. * * @param events An {@link GLib.GenericArray} of {@link Event} * @param cancellable a {@link GLib.Cancellable} to cancel the operation or %NULL */ public async Array insert_events (GenericArray events, Cancellable? cancellable=null) throws Error { var events_cp = new GenericArray (); for (int i = 0; i < events.length; i++) events_cp.add (events.get (i)); yield wait_for_proxy (); uint32[] ids = yield proxy.insert_events (Events.to_variant (events_cp), cancellable); var result = new Array (); // Ideally we'd just place "(owned) ids" into the GArray, but .data isn't // in the Vala bindings... for (int i = 0; i < ids.length; ++i) result.append_val (ids[i]); return result; } /** * Asynchronously send a set of events to the Zeitgeist daemon, requesting they * be inserted into the log. * This method is "fire and forget" and the caller will never know * whether the events was successfully inserted or not. * * This method is exactly equivalent to calling zeitgeist_log_insert_event() * with NULL set as @cancellable, @callback, and @user_data. * * @param event A {@link Event} */ public void insert_event_no_reply (Event event) throws Error { insert_event.begin (event); } /** * Asynchronously send a set of events to the Zeitgeist daemon, requesting they * be inserted into the log. * This method is "fire and forget" and the caller will never know * whether the events was successfully inserted or not. * * This method is exactly equivalent to calling zeitgeist_log_insert_event() * with NULL set as @cancellable, @callback, and @user_data. * * @param events An {@link GLib.GenericArray} of {@link Event} */ public void insert_events_no_reply (GenericArray events) throws Error { insert_events.begin (events); } /** * Send a query matching a collection of {@link Event} templates to the {@link Log}. * The query will match if an event matches any of the templates. If an event * template has more than one {@link Subject} the query will match if any one * of the {@link Subject}s templates match. * * The query will be done via an asynchronous DBus call and this method will * return immediately. The return value will be passed to callback as a list * of {@link Event}s. This list must be the sole argument for the callback. * * If you need to do a query yielding a large (or unpredictable) result set * and you only want to show some of the results at the same time (eg., by * paging them), consider using {@link find_event_ids}. * * In order to use this method there needs to be a mainloop runnning. * Both Qt and GLib mainloops are supported. * * @param time_range {@link TimeRange} A time range in which the events should be considered in * @param storage_state {@link StorageState} storage state * @param event_templates An {@link GLib.GenericArray} of {@link Event} * @param num_events int represteing the number of events that should be returned * @param result_type {@link ResultType} how the events should be grouped and sorted * @param cancellable a {@link GLib.Cancellable} to cancel the operation or %NULL */ public async ResultSet find_events ( TimeRange time_range, GenericArray event_templates, StorageState storage_state, uint32 num_events, ResultType result_type, Cancellable? cancellable=null) throws Error { var event_templates_cp = new GenericArray (); for (int i = 0; i < event_templates.length; i++) event_templates_cp.add (event_templates.get (i)); if (dbreader != null) { SimpleResultSet result_set = null; EngineError error = null; ThreadFunc run = () => { try { var result = dbreader.find_events (time_range, event_templates_cp, storage_state, num_events, result_type); result_set = new SimpleResultSet (result); } catch (EngineError err) { error = err; } finally { Idle.add (find_events.callback); } return null; }; threads.add (new DbWorker (run)); yield; if (error != null) throw error; return result_set; } yield wait_for_proxy (); var result = yield proxy.find_events (time_range.to_variant (), Events.to_variant (event_templates_cp), storage_state, num_events, result_type, cancellable); return new SimpleResultSet (Events.from_variant (result)); } /** * Send a query matching a collection of {@link Event} templates to the {@link Log}. * The query will match if an event matches any of the templates. If an event * template has more than one {@link Subject} the query will match if any one * of the {@link Subject}s templates match. * * The query will be done via an asynchronous DBus call and this method will * return immediately. The return value will be passed to callback as a list * of intergers represrting {@link Event} id's. * This list must be the sole argument for the callback. * * In order to use this method there needs to be a mainloop runnning. * Both Qt and GLib mainloops are supported. * * @param time_range {@link TimeRange} A time range in which the events should be considered in * @param storage_state {@link StorageState} storage state * @param event_templates An {@link GLib.GenericArray} of {@link Event} * @param num_events int represteing the number of events that should be returned * @param result_type {@link ResultType} how the events should be grouped and sorted * @param cancellable a {@link GLib.Cancellable} to cancel the operation or %NULL */ public async uint32[] find_event_ids ( TimeRange time_range, GenericArray event_templates, StorageState storage_state, uint32 num_events, ResultType result_type, Cancellable? cancellable=null) throws Error { var event_templates_cp = new GenericArray (); for (int i = 0; i < event_templates.length; i++) event_templates_cp.add(event_templates.get (i)); if (dbreader != null) { uint32[] ids = null; EngineError error = null; ThreadFunc run = () => { try { ids = dbreader.find_event_ids (time_range, event_templates_cp, storage_state, num_events, result_type); } catch (EngineError err) { error = err; } finally { Idle.add (find_event_ids.callback); } return null; }; threads.add (new DbWorker (run)); yield; if (error != null) throw error; return ids; } yield wait_for_proxy (); return yield proxy.find_event_ids (time_range.to_variant (), Events.to_variant (event_templates_cp), storage_state, num_events, result_type, cancellable); } /** * Look up a collection of {@link Event} in the {@link Log} given a collection * of event ids. This is useful for looking up the event data for events found * with the find_event_ids_* family of functions. * * Each {@link Event} which is not found in the {@link Log} is represented by * NULL in the resulting collection. The query will be done via an asynchronous * DBus call and this method will return immediately. The returned events will * be passed to callback as a list of {@link Event}s, which must be the only * argument of the function. * * In order to use this method there needs to be a mainloop runnning. * * @param event_ids a {@link GLib.Array} of {@link Event} ids * @param cancellable a {@link GLib.Cancellable} to cancel the operation or %NULL */ public async ResultSet get_events ( Array event_ids, Cancellable? cancellable=null) throws Error { uint32[] simple_event_ids = new uint32[event_ids.length]; for (int i = 0; i < event_ids.length; i++) simple_event_ids[i] = event_ids.index (i); if (dbreader != null) { SimpleResultSet result_set = null; EngineError error = null; ThreadFunc run = () => { try { var result = dbreader.get_events (simple_event_ids); result_set = new SimpleResultSet (result); } catch (EngineError err) { error = err; } finally { Idle.add (get_events.callback); } return null; }; threads.add (new DbWorker (run)); yield; if (error != null) throw error; return result_set; } yield wait_for_proxy (); var result = yield proxy.get_events (simple_event_ids, cancellable); return new SimpleResultSet (Events.from_variant (result)); } /** * Get a list of URIs of subjects which frequently occur together with events * matching event_templates. Possibly restricting to time_range or to URIs * that occur as subject of events matching result_event_templates. * * @param time_range {@link TimeRange} A time range in which the events should be considered in * @param storage_state {@link StorageState} storage state * @param event_templates An {@link GLib.GenericArray} of {@link Event} describing the events to relate to * @param result_event_templates An {@link GLib.GenericArray} of {@link Event} desrcibing the result to be returned * @param num_events int represteing the number of events that should be returned * @param result_type {@link ResultType} how the events should be grouped and sorted * @param cancellable a {@link GLib.Cancellable} to cancel the operation or %NULL */ public async string[] find_related_uris ( TimeRange time_range, GenericArray event_templates, GenericArray result_event_templates, StorageState storage_state, uint32 num_events, RelevantResultType result_type, Cancellable? cancellable=null) throws Error { var events_cp = new GenericArray (); for (int i = 0; i < event_templates.length; i++) events_cp.add (event_templates.get (i)); var results_cp = new GenericArray (); for (int i = 0; i < result_event_templates.length; i++) results_cp.add (result_event_templates.get (i)); if (dbreader != null) { string[] uris = null; EngineError error = null; ThreadFunc run = () => { try { uris = dbreader.find_related_uris (time_range, events_cp, results_cp, storage_state, num_events, result_type); } catch (EngineError err) { error = err; } finally { Idle.add (find_related_uris.callback); } return null; }; threads.add (new DbWorker (run)); yield; if (error != null) throw error; return uris; } yield wait_for_proxy (); return yield proxy.find_related_uris (time_range.to_variant (), Events.to_variant (events_cp), Events.to_variant (results_cp), storage_state, num_events, result_type, cancellable); } /** * Delete a collection of events from the zeitgeist log given their event ids. * * The deletion will be done asynchronously, and this method returns immediately. * * @param event_ids Array * @param cancellable a {@link GLib.Cancellable} to cancel the operation or %NULL */ public async TimeRange delete_events (Array event_ids, Cancellable? cancellable=null) throws Error { uint32[] _ids = new uint32 [event_ids.length]; for (int i=0; i {}); // Save the monitor's registration id (0 = not registered) monitors.insert(monitor, 0); if (is_connected) reinstall_monitor.begin (monitor); } private async void reinstall_monitor (Monitor monitor) requires (is_connected) { if (monitors.lookup (monitor) == 0) { DBusConnection conn = ((DBusProxy) proxy).get_connection (); try { uint registration_id = conn.register_object ( monitor.get_path (), monitor); monitors.replace (monitor, registration_id); } catch (GLib.IOError err) { warning ("Error installing monitor: %s", err.message); return; } } proxy.install_monitor.begin ( monitor.get_path (), monitor.time_range.to_variant (), Events.to_variant (monitor.get_templates ())); } /** * Remove a monitor from Zeitgeist engine that calls back when events matching event_templates are logged. * * @param monitor A {@link Monitor} to report back inserts and deletes */ public void remove_monitor (owned Monitor monitor) throws Error { proxy.remove_monitor.begin (monitor.get_path (), null, (obj, res) => { try { ((RemoteLog) obj).remove_monitor.end (res); } catch (Error err) { warning ("Failed to remove monitor from Zeitgeist. Retracting" + "%s from the bus nonetheless: %s", monitor.get_path (), err.message); } }); uint registration_id = monitors.lookup (monitor); if (registration_id != 0) { var connection = ((DBusProxy) proxy).get_connection (); connection.unregister_object (registration_id); } monitors.remove (monitor); } /** * Gets version of currently running Zeitgeist daemon. * * This method will return the version of Zeitgeist daemon this instance is * connected to. If you call this method right after zeitgeist_log_new(), * only zeros will be returned, a valid version number will only be returned * once this instance successfully connected to the Zeitgeist daemon - ie. * the value of the "is-connected" property must be TRUE (you can connect * to the "notify::is-connected" signal otherwise). * * @param major Location for the major version * @param minor Location for the minor version * @param micro Location for the micro version */ public void get_version (out int major, out int minor, out int micro) { major = minor = micro = 0; if (engine_version != null) engine_version.get ("(iii)", &major, &minor, µ); } /** * Gets extensions of the running Zeitgeist daemon. * * @return array of extenstions names strings */ public string[] get_extensions () { return proxy.extensions; } /** * Gets datapath of the running Zeitgeist daemon. * * @return string datapath */ public string datapath () { return proxy.datapath; } } }