/* * Copyright (C) 2010-2013 Collabora Ltd. * * This library 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 library 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this library. If not, see . * * Authors: * Arun Raghavan * Jeremy Whiting * Simon McVittie * Gustavo Padovan * Matthieu Bouron * Philip Withnall * * Based on kf-persona-store.vala by: * Travis Reitter * Philip Withnall */ using GLib; using Gee; using Folks; using Folks.Backends.BlueZ; using org.bluez; /* FIXME: Once we depend on gettext 0.18.3, translatable strings can once more * be split over multiple lines without breaking the .po file. */ /** * A persona store which is associated with a single BlueZ PBAP server (i.e. * one {@link PersonaStore} per device). It will create a {@link Persona} for * each contact on the device. * * Since large contact lists can take a long time to download in full (on the * order of 1s per 10 contacts), contacts are downloaded in two phases: * # Phase 1 downloads all non-PHOTO data. This is very fast (on the order of * 1s per 400 contacts) * # Phase 2 downloads all PHOTO data for those contacts. This is slow, but * happens later, in the background. * * Subsequent download attempts happen on an exponentially increasing interval, * up to a limit (once this limit is reached, updates occur on a regular * interval; the linear region). Download attempts repeat indefinitely unless a * certain number of consecutive attempts end in failure. See the documentation * for {@link _schedule_update_contacts} for details. * * @since 0.9.6 */ public class Folks.Backends.BlueZ.PersonaStore : Folks.PersonaStore { private HashMap _personas; private Map _personas_ro; private bool _is_prepared = false; private bool _prepare_pending = false; private bool _is_quiescent = false; private static string[] _always_writeable_properties = {}; private org.bluez.obex.Client _obex_client; private string _object_path; private Device _device; private string _display_name; /* Non-null iff an _update_contacts() call is in progress. */ private Cancellable? _update_contacts_cancellable = null; /* Non-0 iff an _update_contacts() call is scheduled. */ private uint _update_contacts_id = 0; private bool _photos_up_to_date = true; /* Counter of the number of _update_contacts() calls which have been * scheduled. */ private uint _update_contacts_n = 0; /* Number of consecutive failures in _update_contacts(). */ private uint _update_contacts_failures = 0; /* Parameters for calculating the timeout for repeated _update_contacts() * calls. See the documentation for _schedule_update_contacts() for more. */ private const uint _TIMEOUT_MIN = 4 /* seconds */; private const uint _TIMEOUT_BASE = 2 /* seconds */; private const uint _TIMEOUT_MAX = 5 * 60 /* minutes */; /* Number of consecutive failures in _update_contacts() before we give up * completely and stop trying to update from the phone. */ private const uint _MAX_CONSECUTIVE_FAILURES = 3; /** * {@inheritDoc} * * @since 0.9.6 */ public override string type_id { get { return BACKEND_NAME; } } /** * Whether this PersonaStore can add {@link Folks.Persona}s. * * See {@link Folks.PersonaStore.can_add_personas}. * * @since 0.9.6 */ public override MaybeBool can_add_personas { get { return MaybeBool.FALSE; } } /** * Whether this PersonaStore can set the alias of {@link Folks.Persona}s. * * See {@link Folks.PersonaStore.can_alias_personas}. * * @since 0.9.6 */ public override MaybeBool can_alias_personas { get { return MaybeBool.FALSE; } } /** * Whether this PersonaStore can set the groups of {@link Folks.Persona}s. * * See {@link Folks.PersonaStore.can_group_personas}. * * @since 0.9.6 */ public override MaybeBool can_group_personas { get { return MaybeBool.FALSE; } } /** * Whether this PersonaStore can remove {@link Folks.Persona}s. * * See {@link Folks.PersonaStore.can_remove_personas}. * * @since 0.9.6 */ public override MaybeBool can_remove_personas { get { return MaybeBool.FALSE; } } /** * Whether this PersonaStore has been prepared. * * See {@link Folks.PersonaStore.is_prepared}. * * @since 0.9.6 */ public override bool is_prepared { get { return this._is_prepared; } } /** * Whether this PersonaStore has reached a quiescent state. * * See {@link Folks.PersonaStore.is_quiescent}. * * @since 0.9.6 */ public override bool is_quiescent { get { return this._is_quiescent; } } /** * {@inheritDoc} * * @since 0.9.6 */ public override string[] always_writeable_properties { get { return BlueZ.PersonaStore._always_writeable_properties; } } /** * {@inheritDoc} * * @since 0.9.6 */ public override Map personas { get { return this._personas_ro; } } /** * {@inheritDoc} * * @since 0.9.6 */ public new string display_name { /* FIXME: Folks.display_name should be abstract, and this should be * override. */ get { return this._display_name; } construct { this._display_name = value; } } /** * Path of the D-Bus object backing this {@link PersonaStore}. * * This is the path of the BlueZ device object on D-Bus which provides the * contacts in this store. * * @since 0.9.6 */ public string object_path { get { return this._object_path; } construct { this._object_path = value; } } /** * Create a new PersonaStore. * * Create a new persona store to expose the {@link Persona}s provided by the * device with the given Bluetooth address. * * @param device the D-Bus object for the Bluetooth device. * @param object_path the D-Bus path of the object for the Bluetooth device * @param obex_client the D-Bus obex client object. * * @since 0.9.6 */ public PersonaStore (Device device, string object_path, org.bluez.obex.Client obex_client) { Object (id: device.address, object_path: object_path, display_name: device.alias); this._device = device; this._obex_client = obex_client; this.set_is_trusted (this._device.trusted); } construct { this._personas = new HashMap (); this._personas_ro = this._personas.read_only_view; } /** * Load contacts from a file and update the persona store. * * Load contacts from a file identified by its {@link File} and update * the persona store accordingly. Contacts are stored in the file as a * sequence of vCards, separated by blank lines. * * If a contact already exists in the store, its properties will be updated * from the vCard; otherwise it will be added as a new contact to the store. * Contacts which are in the store and not in the vCard will be removed from * the store. * * If this throws an error, it guarantees to leave the store’s internal state * unchanged, but may change the state of {@link Persona}s in the store. * * @param file the file where the contacts are stored * @throws IOError if there was an error communicating with D-Bus * @throws Error if the given file couldn’t be read * * @since 0.9.6 */ private async void _update_contacts_from_file (File file) throws IOError { var added_personas = new HashSet (); var removed_personas = new HashSet (); var photos_up_to_date = true; debug ("Parsing contacts from file ‘%s’.", file.get_path ()); /* Start with all personas being marked as removed, and then eliminate the * ones which are found in the vCard. */ removed_personas.add_all (this._personas.values); try { var dis = new DataInputStream (file.read ()); uint i = 0; string? line = null; StringBuilder vcard = new StringBuilder (); var vcard_without_photo = new StringBuilder (); /* For each vCard in the file create or update a Persona. */ while ((line = yield dis.read_line_async ()) != null) { /* Ignore blank lines between vCards. */ if (vcard.len == 0 && line.strip () == "") continue; vcard.append (line); vcard.append_c ('\n'); if (!line.has_prefix ("PHOTO:") && !line.has_prefix ("PHOTO;")) { vcard_without_photo.append (line); vcard_without_photo.append_c ('\n'); } if (line.strip () == "END:VCARD") { var card = new E.VCard.from_string (vcard.str); /* The first vCard is always the user themselves. */ var is_user = (i == 0); /* Construct the card’s IID. */ var iid_is_checksum = false; string iid; /* This prefers the ‘UID’ attribute from the vCard, if it’s * available. However, it is not a required attribute, so many * phones do not implement it; in those cases, fall back to a * checksum of the vCard data itself. This means that whenever * a contact’s properties change in the vCard its IID will * change and hence the persona will be removed and re-added, * but without stable UIDs this is unavoidable. * * Note that the checksum is always calculated from the vCard * data *without* the photo. This hopefully ensures that IIDs * from queries which do and do not include photos will * match. */ var attribute = card.get_attribute ("UID"); if (attribute != null) { /* Try the UID attribute. */ iid = attribute.get_value_decoded ().str; } else { /* Fallback. */ iid = Checksum.compute_for_string (ChecksumType.SHA1, vcard_without_photo.str); iid_is_checksum = true; } /* Create or update the persona. */ var persona = this._personas.get (iid); if (persona == null) { persona = new Persona (vcard.str, card, this, is_user, iid); photos_up_to_date = false; } else { /* If the IID is a checksum and we found the persona in * the store, that means their properties haven’t * changed, so as an optimisation, don’t bother updating * the Persona from the vCard in that case. */ if (iid_is_checksum == false || vcard_without_photo.len != vcard.len) { /* Note: This updates persona’s state, which could be * left updated if we later throw an error. */ if (persona.update_from_vcard (card) == true) photos_up_to_date = false; } } if (removed_personas.remove (persona) == false) added_personas.add (persona); i++; vcard.erase (); vcard_without_photo.erase (); } } } catch (GLib.Error e1) { /* I/O error reading the file. */ throw new IOError.FAILED ( /* Translators: the parameter is an error message. */ _("Error reading the transferred address book file: %s"), e1.message); } /* Now that all the I/O is done and no more errors can be thrown, update * the store’s internal state. */ debug ("Finished parsing personas; now updating store state with %u " + "added personas and %u removed personas.", added_personas.size, removed_personas.size); foreach (var p in added_personas) this._personas.set (p.iid, p); foreach (var p in removed_personas) this._personas.unset (p.iid); this._photos_up_to_date = photos_up_to_date; if (added_personas.is_empty == false || removed_personas.is_empty == false) { this._emit_personas_changed (added_personas, removed_personas); } } /** * Set the persona store's alias. * * This will be called in response to a property change sent to the Backend. * * @param alias the device’s new alias * * @since 0.9.6 */ internal void set_alias (string alias) { debug ("Device ‘%s’ (%s) changed alias to ‘%s’.", this._display_name, this._device.address, alias); this._display_name = alias; this.notify_property ("display-name"); } /** * Set the persona store's trust level. * * This will be called in response to a property change sent to the Backend. * * Default to partial trust. BlueZ persona UIDs are built from a SHA1 * of the contact’s vCard, which we believe can’t be maliciously edited * to corrupt linking. * * The trust for each device is manually set by the user in the BlueZ * interface on the computer. * * @param trusted ``true`` if the user trusts the device, ``false`` otherwise * * @since 0.9.6 */ internal void set_is_trusted (bool trusted) { debug ("Device ‘%s’ (%s) marked as %s.", this._device.alias, this._device.address, trusted ? "trusted" : "untrusted"); this.trust_level = trusted ? PersonaStoreTrust.FULL : PersonaStoreTrust.PARTIAL; } /** * Set the persona store's connection state. * * This will be called in response to a property change sent to the Backend. * * If this throws an error, it guarantees to leave the store’s internal state * unchanged. * * @param connected ``true`` if the device is now connected, ``false`` * otherwise * * @throws IOError if the operation was cancelled * (see {@link _update_contacts}) * @throws PersonaStoreError if the contacts couldn’t be updated * (see {@link _update_contacts}) * * @since 0.9.6 */ internal async void set_connection_state (bool connected) throws IOError, PersonaStoreError { if (connected == true) { debug ("Device ‘%s’ (%s) is connected.", this._device.alias, this._device.address); yield this._update_contacts (false); } else { debug ("Device ‘%s’ (%s) is disconnected.", this._device.alias, this._device.address); /* Cancel any ongoing or scheduled transfers. */ this.cancel_updates (); } } /** * Create a new obex session for this Persona store. * * Create a new obex session for this Persona store if no previous session * already exists. * * @param obex_pbap return location for an OBEX PBAP proxy object * @returns the path of the OBEX session D-Bus object * @throws IOError if it can't connect to D-Bus * @throws DBusError if it can't create a new OBEX session * * @since 0.9.6 */ private async dynamic ObjectPath _new_obex_session ( out org.bluez.obex.PhonebookAccess obex_pbap) throws DBusError, IOError { debug ("Creating a new OBEX session."); var args = new HashTable (null, null); args["Target"] = "PBAP"; var session_path = yield this._obex_client.create_session (this.id, args); debug (" Got OBEX session path: %s", session_path); obex_pbap = yield Bus.get_proxy (BusType.SESSION, "org.bluez.obex", session_path); debug (" Got OBEX PBAP proxy: %p", obex_pbap); return session_path; } /** * Remove the specified OBEX session from this persona store. * * Remove the specified OBEX session for this persona store and discard its * transfer. * * @param session_path the path of the OBEX session D-Bus object to remove * * @since 0.9.6 */ private async void _remove_obex_session (dynamic ObjectPath session_path) { try { yield this._obex_client.remove_session (session_path); } catch (IOError ie) { /* Ignore errors from closing or cancelling, or if the session has * disappeared already. */ if (ie is IOError.CLOSED || ie is IOError.CANCELLED) return; if (ie is IOError.DBUS_ERROR && ie.message.has_prefix ("GDBus.Error:org.freedesktop.DBus." + "Python.dbus.exceptions.DBusException: " + "('org.freedesktop.DBus.Mock.NameError'")) { /* Only used in unit tests. */ return; } warning ("Couldn’t remove OBEX session ‘%s’: %s", session_path, ie.message); } catch (DBusError de) { warning ("Couldn’t remove OBEX session ‘%s’: %s", session_path, de.message); } } /** * Watch an OBEX transfer identified by its D-Bus path. * * This only returns once the transfer is complete (or has failed) and the * transfer object has been destroyed. * * If this throws an error, it guarantees to leave the store’s internal state * unchanged. * * @param path the D-Bus transfer object path to watch. * @param cancellable an optional {@link Cancellable} object to cancel the * transfer * * @throws IOError if the operation was cancelled, or if another failure * occurred (unavoidable; valac generates invalid C if we try to handle * IOError internally here) * @throws PersonaStoreError if the transfer failed * * @since 0.9.6 */ private async void _perform_obex_transfer (string path, Cancellable? cancellable = null) throws IOError, PersonaStoreError { org.bluez.obex.Transfer? transfer = null; try { /* Bail early if the transfer's already been cancelled. */ if (cancellable != null) cancellable.set_error_if_cancelled (); /* Get an OBEX proxy for the transfer object. */ transfer = yield Bus.get_proxy (BusType.SESSION, "org.bluez.obex", path); var transfer_proxy = (DBusProxy) transfer; var has_yielded = false; string? transfer_status = null; ulong signal_id; ulong cancellable_id = 0; /* Find the initial status, if it’s already been set. Otherwise it’ll * be null. */ transfer_status = transfer.status; /* Set up the cancellable. */ if (cancellable != null) { cancellable_id = cancellable.connect (() => { transfer_status = "error"; if (has_yielded == true) this._perform_obex_transfer.callback (); }); } /* There is no need to add a timeout here, as BlueZ already has one * implemented for if transactions take too long. */ signal_id = transfer_proxy.g_properties_changed.connect ( (changed, invalidated) => { var property = changed.lookup_value ("Status", VariantType.STRING); if (property == null) return; var status = property.get_string (); transfer_status = status; if (status == "complete" || status == "error") { /* Finished. Return to the yield. */ if (has_yielded == true) this._perform_obex_transfer.callback (); } else if (status == "queued" || status == "active") { /* Do nothing. */ } else { warning ("Unknown OBEX transfer status ‘%s’.", status); } }); /* Yield until the above signal handler is called with a ‘success’ or * ‘error’ status. */ if (transfer_status != "complete" && transfer_status != "error") { has_yielded = true; yield; } transfer_proxy.disconnect (signal_id); if (cancellable_id != 0) cancellable.disconnect (cancellable_id); /* Process the results: either success or error. */ if (transfer_status == "complete") { string? filename = transfer.filename; if (filename == null) { /* The Filename property is optional, so bail if it’s not * available for whatever reason. */ throw new PersonaStoreError.STORE_OFFLINE ( /* Translators: the first parameter is the name of the * failed transfer, and the second is a Bluetooth device * alias. */ _("Error during transfer of the address book ‘%s’ from " + "Bluetooth device ‘%s’."), transfer.name, this._display_name); } var file = File.new_for_path ((!) filename); debug ("vCard’s filename for device ‘%s’ (%s): %s", this._display_name, this.id, (!) filename); yield this._update_contacts_from_file (file); } else if (transfer_status == "error") { /* On cancellation, throw an IOError instead of a * PersonaStoreError. */ if (cancellable != null) cancellable.set_error_if_cancelled (); throw new PersonaStoreError.STORE_OFFLINE ( /* Translators: the first parameter is the name of the failed * transfer, and the second is a Bluetooth device alias. */ _("Error during transfer of the address book ‘%s’ from Bluetooth device ‘%s’."), transfer.name, this._display_name); } else { assert_not_reached (); } } finally { /* Reset the OBEX transfer and clear out the temporary file. Do this * without yielding because BlueZ should choose a different filename * next time (using mkstemp() or similar). */ if (transfer != null && transfer.filename != null) { var file = File.new_for_path (transfer.filename); file.delete_async.begin (GLib.Priority.DEFAULT, null, (o, r) => { try { file.delete_async.end (r); } catch (GLib.Error e1) { /* Ignore. */ } }); } } } /** * Update contacts from this persona store. * * Update contacts from this persona store by initiating a new OBEX * transfer, unless one is already in progress. If a transfer is already in * progress, leave it running and return immediately. * * If this throws an error, it guarantees to leave the store’s internal state * unchanged, apart from scheduling a new update operation to happen in the * future. This will always happen, regardless of success or failure. * * @param download_photos whether to download photos * @throws IOError if the operation was cancelled * @throws PersonaStoreError if the contacts couldn’t be downloaded from the * device * * @since 0.9.6 */ private async void _update_contacts (bool download_photos) throws IOError, PersonaStoreError { dynamic ObjectPath? session_path = null; org.bluez.obex.PhonebookAccess? obex_pbap = null; var success = true; try { if (this._update_contacts_cancellable != null) { /* There’s an ongoing _update_contacts() call. Since downloading * the address book takes a long time (tens of seconds), we don’t * want to cancel the ongoing operation. Just return * immediately. */ debug ("Not updating contacts due to ongoing update operation."); return; } Internal.profiling_start ("updating BlueZ.PersonaStore (ID: %s) " + "contacts", this.id); debug ("Updating contacts."); string path; HashTable props; this._update_contacts_cancellable = new Cancellable (); /* Set up an OBEX session. */ try { session_path = yield this._new_obex_session (out obex_pbap); } catch (GLib.Error e1) { if (e1 is IOError.DBUS_ERROR && e1.message.has_suffix ("OBEX Connect failed with 0x43")) { /* This error is sent when the user denies the computer access * to the phone’s address book over Bluetooth, after accepting * the pairing request. */ throw new PersonaStoreError.PERMISSION_DENIED ( _("Permission to access the address book on Bluetooth device ‘%s’ was denied by the user."), this._device.alias); } throw new PersonaStoreError.STORE_OFFLINE ( /* Translators: the first parameter is a Bluetooth device * alias, and the second is an error message. */ _("An OBEX address book transfer from device ‘%s’ could not be started: %s"), this._device.alias, e1.message); } try { /* Select the phonebook object we want to download ie: * PB: phonebook for the saved contacts */ obex_pbap.select ("int", "PB"); /* Initiate a phone book transfer from the PSE server using a * plain string vCard format, transferring to a temporary file. */ var phonebook_filter = new HashTable (null , null); phonebook_filter.insert ("Format", "Vcard30"); if (download_photos == true) { /* Download everything including the photo. */ phonebook_filter.insert ("Fields", new Variant.strv ({ "UID", "N", "FN", "NICKNAME", "TEL", "URL", "EMAIL", "PHOTO" })); } else { /* Download everything except the photo. */ phonebook_filter.insert ("Fields", new Variant.strv ({ "UID", "N", "FN", "NICKNAME", "TEL", "URL", "EMAIL" })); } obex_pbap.pull_all ("", phonebook_filter, out path, out props); } catch (GLib.Error e2) { throw new PersonaStoreError.STORE_OFFLINE ( /* Translators: the first parameter is a Bluetooth device * alias, and the second is an error message. */ _("The OBEX address book transfer from device ‘%s’ failed: %s"), this._device.alias, e2.message); } try { yield this._perform_obex_transfer (path, this._update_contacts_cancellable); } catch (IOError e3) { if (e3 is IOError.CANCELLED) throw e3; throw new PersonaStoreError.STORE_OFFLINE ( /* Translators: the first parameter is a Bluetooth device * alias, and the second is an error message. */ _("Error during transfer of the address book from Bluetooth device ‘%s’: %s"), this._display_name, e3.message); } } catch (IOError e4) { /* Used below. */ success = false; throw e4; } catch (PersonaStoreError e5) { /* Used below. */ success = false; throw e5; } finally { /* Tear down again. */ if (session_path != null) this._remove_obex_session.begin (session_path); obex_pbap = null; this._update_contacts_cancellable = null; /* Track the number of consecutive failures. */ if (success == true) this._update_contacts_failures = 0; else this._update_contacts_failures++; /* Schedule the next update. See the documentation for * _schedule_update_contacts() for details. */ var new_download_photos = success == true && this._photos_up_to_date == false; this._schedule_update_contacts (new_download_photos); Internal.profiling_end ("updating BlueZ.PersonaStore (ID: %s) " + "contacts", this.id); } } /** * Schedule the next call to {@link _update_contacts}. * * This calculates a suitable timeout value and schedules the next timeout * for updating the contacts. * * The update scheme is as follows: * 1. Download the contacts (without photos) as soon as connected to the * phone. * 2. Schedule a second download attempt for a few seconds after the first * one completes. If the first one completes successfully, this second * download will include photos; otherwise, it won’t. * 3. Schedule subsequent download attempts for exponentially increasing * timeouts, up to a maximum timeout (at which point the timeouts enter a * linear region and repeat indefinitely). Subsequent download attempts * will include photos only if they have not been successfully downloaded * already, or if the previous download attempt caused other property * changes in a persona (indicating that the address book has been edited * on the phone). * 4. If updates fail a certain number of consecutive times, give up * completely and leave the persona store in a prepared but empty * quiescent state. Update attempts will only restart if the phone is then * disconnected and reconnected. * * The rationale for this design is to: * A. Allow for the user accidentally denying the first connection request on * the phone, or not noticing it and it timing out. Attempting a second * download after a timeout gives them an opportunity to fix the problem. * B. If the user explicitly denies the connection request on the phone, the * phone should remember this and automatically deny all future connection * attempts until the consecutive failure limit is reached. The user * shouldn’t be pestered to accept again. * C. Watch for changes in the user’s address book and update the persona * store accordingly. Unfortunately this has to be done by polling, since * neither PBAP not OBEX itself support push notifications. * * @param download_photos whether to download photos * * @since 0.9.7 */ private void _schedule_update_contacts (bool download_photos) { /* Bail if a call is already scheduled. */ if (this._update_contacts_id != 0) return; /* If there have been too many consecutive failures in _update_contacts(), * give up. */ if (this._update_contacts_failures >= PersonaStore._MAX_CONSECUTIVE_FAILURES) return; /* Calculate the timeout (in milliseconds). If no divisor is applied, the * timeout should always be a whole number of seconds. */ var timeout = uint.min (PersonaStore._TIMEOUT_MIN + (uint) Math.pow (PersonaStore._TIMEOUT_BASE, this._update_contacts_n), PersonaStore._TIMEOUT_MAX); this._update_contacts_n++; timeout *= 1000; /* convert from seconds to milliseconds */ /* Allow the timeout to be tweaked for testing. */ var divisor_str = Environment.get_variable ("FOLKS_BLUEZ_TIMEOUT_DIVISOR"); if (divisor_str != null) { uint64 divisor; if (uint64.try_parse (divisor_str, out divisor) == true) timeout /= (uint) divisor; } /* Schedule the update. */ SourceFunc fn = () => { debug ("Scheduled update firing for BlueZ store ‘%s’.", this.id); /* Acknowledge the source has fired. */ this._update_contacts_id = 0; this._update_contacts.begin (download_photos, (o, r) => { try { this._update_contacts.end (r); } catch (GLib.Error e4) { /* Ignore cancellation. */ if (e4 is IOError.CANCELLED) return; /* Don't warn about offline stores. */ if (e4 is PersonaStoreError.STORE_OFFLINE) { debug ("Not updating persona store from BlueZ due to " + "store being offline: %s", e4.message); } else { warning ("Error updating persona store from BlueZ: %s", e4.message); } } }); return false; }; if (timeout % 1000 == 0) { this._update_contacts_id = Timeout.add_seconds (timeout / 1000, (owned) fn); } else { this._update_contacts_id = Timeout.add (timeout, (owned) fn); } } /** * Cancel ongoing and scheduled updates from the device. * * This doesn't remove the store, but does cancel all ongoing updates and * future scheduled updates, in preparation for removing the store. This is * necessary to avoid the store maintaining a reference to itself (through the * closure for the next scheduled update) and thus never being finalised. * * @since 0.9.7 */ internal void cancel_updates () { if (this._update_contacts_cancellable != null) this._update_contacts_cancellable.cancel (); if (this._update_contacts_id != 0) { Source.remove (this._update_contacts_id); this._update_contacts_id = 0; } } /** * {@inheritDoc} * * @since 0.9.6 */ public override async void prepare () throws PersonaStoreError { Internal.profiling_start ("preparing BlueZ.PersonaStore (ID: %s)", this.id); if (this._is_prepared || this._prepare_pending) { return; } try { this._prepare_pending = true; /* Start downloading the contacts, regardless of the phone’s * connection state. If the phone is disconnected, the download should * force it to be connected. */ try { yield this._update_contacts (false); } catch (IOError e1) { /* If this happens, the update operation was cancelled, which * means the phone spontaneously disconnected during the transfer. * Act as if the store has gone offline and mark preparation as * complete. */ throw new PersonaStoreError.STORE_OFFLINE ( _("Bluetooth device ‘%s’ disappeared during address book transfer."), this._device.alias); } finally { /* Done or failed. We always mark the persona store as prepared * and quiescent because of the limited data available to us from * BlueZ: we only have the Paired and Connected properties. * So a phone can be paired with the laptop, but its Bluetooth * can be turned off; or a phone can be paired with the laptop and * its Bluetooth turned on but no connection is active. In the * former case, we don't want to connect to the device (because * that will just fail). In the latter case, we do, because we * want to download the address book. However, BlueZ exposes no * information allowing differentiation of the two cases, so we * must always create a persona store for a paired device, and * must always try and connect. In order to prevent paired but * disconnected phones from causing quiescence to never be reached * (which may be a common occurrence), we always mark the stores * as prepared and quiescent. * * FIXME: Note that this will fit in well with caching, if that is * ever implemented in the BlueZ backend. Paired but disconnected * phones (with their Bluetooth off) can still have persona stores * on the laptop, and those persona stores can be populated by * cached personas until the phone is reconnected. */ this._is_prepared = true; this.notify_property ("is-prepared"); this._is_quiescent = true; this.notify_property ("is-quiescent"); } } finally { this._prepare_pending = false; } Internal.profiling_end ("preparing BlueZ.PersonaStore (ID: %s)", this.id); } /** * Remove a {@link Persona} from the PersonaStore. * * See {@link Folks.PersonaStore.remove_persona}. * * @param persona the {@link Persona} to remove * @throws Folks.PersonaStoreError.READ_ONLY every time since the * BlueZ backend is read-only. * * @since 0.9.6 */ public override async void remove_persona (Folks.Persona persona) throws Folks.PersonaStoreError { throw new PersonaStoreError.READ_ONLY ( "Personas cannot be removed from this store."); } /** * Add a new {@link Persona} to the PersonaStore. * * See {@link Folks.PersonaStore.add_persona_from_details}. * * @param details a map of keys to values giving the persona’s initial details * @throws Folks.PersonaStoreError.READ_ONLY every time since the * BlueZ backend is read-only. * * @since 0.9.6 */ public override async Folks.Persona? add_persona_from_details ( HashTable details) throws Folks.PersonaStoreError { throw new PersonaStoreError.READ_ONLY ( "Personas cannot be added to this store."); } }