/*
* 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.");
}
}