[folks] bluez: Implement periodic refreshes of the persona store from the phone



commit 3439f37b9f8f00d04376195103f0242ecfd4f2e0
Author: Philip Withnall <philip withnall collabora co uk>
Date:   Mon Nov 11 09:34:39 2013 +0000

    bluez: Implement periodic refreshes of the persona store from the phone
    
    This implements two major changes:
     • Downloading contacts now happens in two phases. In the first phase, only
       textual properties are downloaded (not contact photos). This is fast,
       taking only a few seconds to download several hundred contacts over
       Bluetooth. In the second phase, contact photos are downloaded in the
       background and personas are updated to use them.
     • Contacts are periodically re-downloaded on an exponential timeout
       leading to a linear region. This keeps the persona store up-to-date with
       changes in the phone’s address book.
    
    For full details, see the documentation on
    BlueZ.PersonaStore._schedule_update_contacts().
    
    https://bugzilla.gnome.org/show_bug.cgi?id=711827

 backends/bluez/bluez-persona-store.vala |  212 +++++++++++++++++++++++++++----
 backends/bluez/bluez-persona.vala       |   14 ++-
 2 files changed, 202 insertions(+), 24 deletions(-)
---
diff --git a/backends/bluez/bluez-persona-store.vala b/backends/bluez/bluez-persona-store.vala
index b57cb6b..a846c8f 100644
--- a/backends/bluez/bluez-persona-store.vala
+++ b/backends/bluez/bluez-persona-store.vala
@@ -41,6 +41,19 @@ using org.bluez;
  * 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
@@ -60,6 +73,24 @@ public class Folks.Backends.BlueZ.PersonaStore : Folks.PersonaStore
 
   /* 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 = false;
+  /* 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}
@@ -241,6 +272,7 @@ public class Folks.Backends.BlueZ.PersonaStore : Folks.PersonaStore
     {
       var added_personas = new HashSet<Persona> ();
       var removed_personas = new HashSet<Persona> ();
+      var photos_up_to_date = this._photos_up_to_date;
 
       /* Start with all personas being marked as removed, and then eliminate the
        * ones which are found in the vCard. */
@@ -301,6 +333,7 @@ public class Folks.Backends.BlueZ.PersonaStore : Folks.PersonaStore
                     {
                       persona =
                           new Persona (vcard.str, card, this, is_user, iid);
+                      photos_up_to_date = false;
                     }
                   else
                     {
@@ -312,7 +345,8 @@ public class Folks.Backends.BlueZ.PersonaStore : Folks.PersonaStore
                         {
                           /* Note: This updates persona’s state, which could be
                            * left updated if we later throw an error. */
-                          persona.update_from_vcard (card);
+                          if (persona.update_from_vcard (card) == true)
+                              photos_up_to_date = false;
                         }
                     }
 
@@ -340,6 +374,8 @@ public class Folks.Backends.BlueZ.PersonaStore : Folks.PersonaStore
       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)
         {
@@ -416,16 +452,21 @@ public class Folks.Backends.BlueZ.PersonaStore : Folks.PersonaStore
           debug ("Device ‘%s’ (%s) is connected.", this._device.alias,
               this._device.address);
 
-          yield this._update_contacts ();
+          yield this._update_contacts (false);
         }
       else
         {
           debug ("Device ‘%s’ (%s) is disconnected.", this._device.alias,
               this._device.address);
 
-          /* Cancel any ongoing transfers. */
+          /* Cancel any ongoing or scheduled transfers. */
           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;
+            }
         }
     }
 
@@ -647,35 +688,40 @@ public class Folks.Backends.BlueZ.PersonaStore : Folks.PersonaStore
    * progress, leave it running and return immediately.
    *
    * If this throws an error, it guarantees to leave the store’s internal state
-   * unchanged.
+   * 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 () throws IOError, PersonaStoreError
+  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;
 
-      if (this._update_contacts_cancellable != null)
+      try
         {
-          /* 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;
-        }
+          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);
+          Internal.profiling_start ("updating BlueZ.PersonaStore (ID: %s) " +
+              "contacts", this.id);
 
-      debug ("Updating contacts.");
+          debug ("Updating contacts.");
 
-      try
-        {
           string path;
           HashTable<string, Variant> props;
 
@@ -717,11 +763,20 @@ public class Folks.Backends.BlueZ.PersonaStore : Folks.PersonaStore
               var phonebook_filter =
                   new HashTable<string, Variant> (null , null);
               phonebook_filter.insert ("Format", "Vcard30");
-              phonebook_filter.insert ("Fields",
-                  new Variant.strv ({
-                      "UID", "N", "FN", "NICKNAME", "TEL", "URL", "EMAIL",
-                      "PHOTO"
-                  }));
+              if (download_photos == true)
+                {
+                  /* Download only the photo (and UID, if available). */
+                  phonebook_filter.insert ("Fields",
+                      new Variant.strv ({ "UID", "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);
             }
@@ -751,6 +806,18 @@ public class Folks.Backends.BlueZ.PersonaStore : Folks.PersonaStore
                   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. */
@@ -760,12 +827,111 @@ public class Folks.Backends.BlueZ.PersonaStore : Folks.PersonaStore
 
           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 UNRELEASED
+   */
+  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. */
+      var timeout =
+          uint.min (PersonaStore._TIMEOUT_MIN +
+              (uint) Math.pow (PersonaStore._TIMEOUT_BASE,
+                      this._update_contacts_n),
+              PersonaStore._TIMEOUT_MAX);
+      this._update_contacts_n++;
+
+      /* Schedule the update. */
+      this._update_contacts_id = Timeout.add_seconds (timeout, () =>
+        {
+          /* 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;
+
+                  warning ("Error updating persona store from BlueZ: %s",
+                      e4.message);
+                }
+            });
+
+          return false;
+        });
+    }
+
+  /**
    * { inheritDoc}
    *
    * @since 0.9.6
@@ -789,7 +955,7 @@ public class Folks.Backends.BlueZ.PersonaStore : Folks.PersonaStore
            * force it to be connected. */
           try
             {
-              yield this._update_contacts ();
+              yield this._update_contacts (false);
             }
           catch (IOError e1)
             {
diff --git a/backends/bluez/bluez-persona.vala b/backends/bluez/bluez-persona.vala
index 43e0590..b93eb46 100644
--- a/backends/bluez/bluez-persona.vala
+++ b/backends/bluez/bluez-persona.vala
@@ -214,11 +214,14 @@ public class Folks.Backends.BlueZ.Persona : Folks.Persona,
    * emits property change notifications as appropriate.
    *
    * @param vcard pre-parsed vCard
+   * @return ``true`` if any properties were changed, ``false`` otherwise
    *
    * @since UNRELEASED
    */
-  internal void update_from_vcard (E.VCard card)
+  internal bool update_from_vcard (E.VCard card)
     {
+      var properties_changed = false;
+
       this.freeze_notify ();
 
       /* Phone numbers. */
@@ -239,6 +242,7 @@ public class Folks.Backends.BlueZ.Persona : Folks.Persona,
           this._phone_numbers = new_phone_numbers;
           this._phone_numbers_ro = new_phone_numbers.read_only_view;
           this.notify_property ("phone-numbers");
+          properties_changed = true;
         }
 
       /* Full name. */
@@ -252,6 +256,7 @@ public class Folks.Backends.BlueZ.Persona : Folks.Persona,
         {
           this._full_name = new_full_name;
           this.notify_property ("full-name");
+          properties_changed = true;
         }
 
       /* Nickname. */
@@ -265,6 +270,7 @@ public class Folks.Backends.BlueZ.Persona : Folks.Persona,
         {
           this._nickname = new_nickname;
           this.notify_property ("nickname");
+          properties_changed = true;
         }
 
       /* URIs. */
@@ -284,6 +290,7 @@ public class Folks.Backends.BlueZ.Persona : Folks.Persona,
           this._urls = new_uris;
           this._urls_ro = new_uris.read_only_view;
           this.notify_property ("urls");
+          properties_changed = true;
         }
 
       /* Structured name. */
@@ -321,6 +328,7 @@ public class Folks.Backends.BlueZ.Persona : Folks.Persona,
         {
           this._structured_name = new_structured_name;
           this.notify_property ("structured-name");
+          properties_changed = true;
         }
 
       /* E-mail addresses. */
@@ -341,6 +349,7 @@ public class Folks.Backends.BlueZ.Persona : Folks.Persona,
           this._email_addresses = new_email_addresses;
           this._email_addresses_ro = new_email_addresses.read_only_view;
           this.notify_property ("email-addresses");
+          properties_changed = true;
         }
 
       /* Photo. */
@@ -360,9 +369,12 @@ public class Folks.Backends.BlueZ.Persona : Folks.Persona,
         {
           this._avatar = new_avatar;
           this.notify_property ("avatar");
+          properties_changed = true;
         }
 
       this.thaw_notify ();
+
+      return properties_changed;
     }
 
   /**


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]