Bug 1131421 - Part 1: initial stub reading list service and SyncAdapter. r=nalexander
authorRichard Newman <rnewman@mozilla.com>
Mon, 09 Feb 2015 21:08:05 -0800
changeset 242049 1d374c27ade1c35a045613f86990b7cbad02cee7
parent 242048 d86ac92e8a4174568c67f4c32015452b42cae311
child 242050 b8fa35d8bc9acc394ffae15575af75400dbf18f4
push id634
push usermozilla@noorenberghe.ca
push dateTue, 10 Feb 2015 22:34:30 +0000
reviewersnalexander
bugs1131421
milestone38.0a1
Bug 1131421 - Part 1: initial stub reading list service and SyncAdapter. r=nalexander
mobile/android/base/android-services.mozbuild
mobile/android/base/fxa/authenticator/AndroidFxAccount.java
mobile/android/base/fxa/sync/FxAccountSyncAdapter.java
mobile/android/base/fxa/sync/FxAccountSyncDelegate.java
mobile/android/base/reading/ReadingListSyncAdapter.java
mobile/android/base/reading/ReadingListSyncService.java
mobile/android/base/resources/xml/readinglist_syncadapter.xml
mobile/android/services/manifests/FxAccountAndroidManifest_services.xml.in
--- a/mobile/android/base/android-services.mozbuild
+++ b/mobile/android/base/android-services.mozbuild
@@ -877,16 +877,17 @@ sync_java_files = [
     'fxa/login/TokensAndKeysState.java',
     'fxa/receivers/FxAccountDeletedReceiver.java',
     'fxa/receivers/FxAccountDeletedService.java',
     'fxa/receivers/FxAccountUpgradeReceiver.java',
     'fxa/sync/FxAccountGlobalSession.java',
     'fxa/sync/FxAccountNotificationManager.java',
     'fxa/sync/FxAccountSchedulePolicy.java',
     'fxa/sync/FxAccountSyncAdapter.java',
+    'fxa/sync/FxAccountSyncDelegate.java',
     'fxa/sync/FxAccountSyncService.java',
     'fxa/sync/FxAccountSyncStatusHelper.java',
     'fxa/sync/SchedulePolicy.java',
     'fxa/tasks/FxAccountCodeResender.java',
     'fxa/tasks/FxAccountCreateAccountTask.java',
     'fxa/tasks/FxAccountSetupTask.java',
     'fxa/tasks/FxAccountSignInTask.java',
     'fxa/tasks/FxAccountUnlockCodeResender.java',
@@ -1144,8 +1145,12 @@ sync_java_files = [
     'sync/UnexpectedJSONException.java',
     'sync/UnknownSynchronizerConfigurationVersionException.java',
     'sync/Utils.java',
     'tokenserver/TokenServerClient.java',
     'tokenserver/TokenServerClientDelegate.java',
     'tokenserver/TokenServerException.java',
     'tokenserver/TokenServerToken.java',
 ]
+reading_list_service_java_files = [
+    'reading/ReadingListSyncAdapter.java',
+    'reading/ReadingListSyncService.java',
+]
--- a/mobile/android/base/fxa/authenticator/AndroidFxAccount.java
+++ b/mobile/android/base/fxa/authenticator/AndroidFxAccount.java
@@ -40,17 +40,18 @@ import android.os.Bundle;
  * <p>
  * Account user data is accessible only to the Android App(s) that own the
  * Account type. Account user data is not removed when the App's private data is
  * cleared.
  */
 public class AndroidFxAccount {
   protected static final String LOG_TAG = AndroidFxAccount.class.getSimpleName();
 
-  public static final int CURRENT_PREFS_VERSION = 1;
+  public static final int CURRENT_SYNC_PREFS_VERSION = 1;
+  public static final int CURRENT_RL_PREFS_VERSION = 1;
 
   // When updating the account, do not forget to update AccountPickler.
   public static final int CURRENT_ACCOUNT_VERSION = 3;
   public static final String ACCOUNT_KEY_ACCOUNT_VERSION = "version";
   public static final String ACCOUNT_KEY_PROFILE = "profile";
   public static final String ACCOUNT_KEY_IDP_SERVER = "idpServerURI";
 
   // The audience should always be a prefix of the token server URI.
@@ -236,53 +237,66 @@ public class AndroidFxAccount {
   public String getAudience() {
     return accountManager.getUserData(account, ACCOUNT_KEY_AUDIENCE);
   }
 
   public String getTokenServerURI() {
     return accountManager.getUserData(account, ACCOUNT_KEY_TOKEN_SERVER);
   }
 
-  /**
-   * This needs to return a string because of the tortured prefs access in GlobalSession.
-   */
-  public String getSyncPrefsPath() throws GeneralSecurityException, UnsupportedEncodingException {
+  private String constructPrefsPath(String product, long version, String extra) throws GeneralSecurityException, UnsupportedEncodingException {
     String profile = getProfile();
     String username = account.name;
 
     if (profile == null) {
       throw new IllegalStateException("Missing profile. Cannot fetch prefs.");
     }
 
     if (username == null) {
       throw new IllegalStateException("Missing username. Cannot fetch prefs.");
     }
 
-    final String tokenServerURI = getTokenServerURI();
-    if (tokenServerURI == null) {
-      throw new IllegalStateException("No token server URI. Cannot fetch prefs.");
-    }
-
     final String fxaServerURI = getAccountServerURI();
     if (fxaServerURI == null) {
       throw new IllegalStateException("No account server URI. Cannot fetch prefs.");
     }
 
-    final String product = GlobalConstants.BROWSER_INTENT_PACKAGE + ".fxa";
-    final long version = CURRENT_PREFS_VERSION;
+    // This is unique for each syncing 'view' of the account.
+    final String serverURLThing = fxaServerURI + "!" + extra;
+    return Utils.getPrefsPath(product, username, serverURLThing, profile, version);
+  }
 
-    // This is unique for each syncing 'view' of the account.
-    final String serverURLThing = fxaServerURI + "!" + tokenServerURI;
-    return Utils.getPrefsPath(product, username, serverURLThing, profile, version);
+  /**
+   * This needs to return a string because of the tortured prefs access in GlobalSession.
+   */
+  public String getSyncPrefsPath() throws GeneralSecurityException, UnsupportedEncodingException {
+    final String tokenServerURI = getTokenServerURI();
+    if (tokenServerURI == null) {
+      throw new IllegalStateException("No token server URI. Cannot fetch prefs.");
+    }
+
+    final String product = GlobalConstants.BROWSER_INTENT_PACKAGE + ".fxa";
+    final long version = CURRENT_SYNC_PREFS_VERSION;
+    return constructPrefsPath(product, version, tokenServerURI);
+  }
+
+  public String getReadingListPrefsPath() throws GeneralSecurityException, UnsupportedEncodingException {
+    final String product = GlobalConstants.BROWSER_INTENT_PACKAGE + ".reading";
+    final long version = CURRENT_RL_PREFS_VERSION;
+    return constructPrefsPath(product, version, "");
   }
 
   public SharedPreferences getSyncPrefs() throws UnsupportedEncodingException, GeneralSecurityException {
     return context.getSharedPreferences(getSyncPrefsPath(), Utils.SHARED_PREFERENCES_MODE);
   }
 
+  public SharedPreferences getReadingListPrefs() throws UnsupportedEncodingException, GeneralSecurityException {
+    return context.getSharedPreferences(getReadingListPrefsPath(), Utils.SHARED_PREFERENCES_MODE);
+  }
+
   /**
    * Extract a JSON dictionary of the string values associated to this account.
    * <p>
    * <b>For debugging use only!</b> The contents of this JSON object completely
    * determine the user's Firefox Account status and yield access to whatever
    * user data the device has access to.
    *
    * @return JSON-object of Strings.
--- a/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java
+++ b/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java
@@ -81,129 +81,55 @@ public class FxAccountSyncAdapter extend
   protected final FxAccountNotificationManager notificationManager;
 
   public FxAccountSyncAdapter(Context context, boolean autoInitialize) {
     super(context, autoInitialize);
     this.executor = Executors.newSingleThreadExecutor();
     this.notificationManager = new FxAccountNotificationManager(NOTIFICATION_ID);
   }
 
-  protected static class SyncDelegate {
-    protected final CountDownLatch latch;
-    protected final SyncResult syncResult;
-    protected final AndroidFxAccount fxAccount;
+  protected static class SyncDelegate extends FxAccountSyncDelegate {
+    @Override
+    public void handleSuccess() {
+      Logger.info(LOG_TAG, "Sync succeeded.");
+      super.handleSuccess();
+    }
+
+    @Override
+    public void handleError(Exception e) {
+      Logger.error(LOG_TAG, "Got exception syncing.", e);
+      super.handleError(e);
+    }
+
+    @Override
+    public void handleCannotSync(State finalState) {
+      Logger.warn(LOG_TAG, "Cannot sync from state: " + finalState.getStateLabel());
+      super.handleCannotSync(finalState);
+    }
+
+    @Override
+    public void postponeSync(long millis) {
+      if (millis <= 0) {
+        Logger.debug(LOG_TAG, "Asked to postpone sync, but zero delay.");
+      }
+      super.postponeSync(millis);
+    }
+
+    @Override
+    public void rejectSync() {
+      super.rejectSync();
+    }
+
     protected final Collection<String> stageNamesToSync;
 
     public SyncDelegate(CountDownLatch latch, SyncResult syncResult, AndroidFxAccount fxAccount, Collection<String> stageNamesToSync) {
-      if (latch == null) {
-        throw new IllegalArgumentException("latch must not be null");
-      }
-      if (syncResult == null) {
-        throw new IllegalArgumentException("syncResult must not be null");
-      }
-      if (fxAccount == null) {
-        throw new IllegalArgumentException("fxAccount must not be null");
-      }
-      this.latch = latch;
-      this.syncResult = syncResult;
-      this.fxAccount = fxAccount;
+      super(latch, syncResult, fxAccount);
       this.stageNamesToSync = Collections.unmodifiableCollection(stageNamesToSync);
     }
 
-    /**
-     * No error!  Say that we made progress.
-     */
-    protected void setSyncResultSuccess() {
-      syncResult.stats.numUpdates += 1;
-    }
-
-    /**
-     * Soft error. Say that we made progress, so that Android will sync us again
-     * after exponential backoff.
-     */
-    protected void setSyncResultSoftError() {
-      syncResult.stats.numUpdates += 1;
-      syncResult.stats.numIoExceptions += 1;
-    }
-
-    /**
-     * Hard error. We don't want Android to sync us again, even if we make
-     * progress, until the user intervenes.
-     */
-    protected void setSyncResultHardError() {
-      syncResult.stats.numAuthExceptions += 1;
-    }
-
-    public void handleSuccess() {
-      Logger.info(LOG_TAG, "Sync succeeded.");
-      setSyncResultSuccess();
-      latch.countDown();
-    }
-
-    public void handleError(Exception e) {
-      Logger.error(LOG_TAG, "Got exception syncing.", e);
-      setSyncResultSoftError();
-      // This is awful, but we need to propagate bad assertions back up the
-      // chain somehow, and this will do for now.
-      if (e instanceof TokenServerException) {
-        // We should only get here *after* we're locked into the married state.
-        State state = fxAccount.getState();
-        if (state.getStateLabel() == StateLabel.Married) {
-          Married married = (Married) state;
-          fxAccount.setState(married.makeCohabitingState());
-        }
-      }
-      latch.countDown();
-    }
-
-    /**
-     * When the login machine terminates, we might not be in the
-     * <code>Married</code> state, and therefore we can't sync. This method
-     * messages as much to the user.
-     * <p>
-     * To avoid stopping us syncing altogether, we set a soft error rather than
-     * a hard error. In future, we would like to set a hard error if we are in,
-     * for example, the <code>Separated</code> state, and then have some user
-     * initiated activity mark the Android account as ready to sync again. This
-     * is tricky, though, so we play it safe for now.
-     *
-     * @param finalState
-     *          that login machine ended in.
-     */
-    public void handleCannotSync(State finalState) {
-      Logger.warn(LOG_TAG, "Cannot sync from state: " + finalState.getStateLabel());
-      setSyncResultSoftError();
-      latch.countDown();
-    }
-
-    public void postponeSync(long millis) {
-      if (millis <= 0) {
-        Logger.debug(LOG_TAG, "Asked to postpone sync, but zero delay. Short-circuiting.");
-      } else {
-        // delayUntil is broken: https://code.google.com/p/android/issues/detail?id=65669
-        // So we don't bother doing this. Instead, we rely on the periodic sync
-        // we schedule, and the backoff handler for the rest.
-        /*
-        Logger.warn(LOG_TAG, "Postponing sync by " + millis + "ms.");
-        syncResult.delayUntil = millis / 1000;
-         */
-      }
-      setSyncResultSoftError();
-      latch.countDown();
-    }
-
-    /**
-     * Simply don't sync, without setting any error flags.
-     * This is the appropriate behavior when a routine backoff has not yet
-     * been met.
-     */
-    public void rejectSync() {
-      latch.countDown();
-    }
-
     public Collection<String> getStageNamesToSync() {
       return this.stageNamesToSync;
     }
   }
 
   protected static class SessionCallback implements BaseGlobalSessionCallback {
     protected final SyncDelegate syncDelegate;
     protected final SchedulePolicy schedulePolicy;
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/fxa/sync/FxAccountSyncDelegate.java
@@ -0,0 +1,122 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.sync;
+
+import java.util.concurrent.CountDownLatch;
+
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+import org.mozilla.gecko.fxa.login.Married;
+import org.mozilla.gecko.fxa.login.State;
+import org.mozilla.gecko.fxa.login.State.StateLabel;
+import org.mozilla.gecko.tokenserver.TokenServerException;
+
+import android.content.SyncResult;
+
+public class FxAccountSyncDelegate {
+  protected final CountDownLatch latch;
+  protected final SyncResult syncResult;
+  protected final AndroidFxAccount fxAccount;
+
+  public FxAccountSyncDelegate(CountDownLatch latch, SyncResult syncResult, AndroidFxAccount fxAccount) {
+    if (latch == null) {
+      throw new IllegalArgumentException("latch must not be null");
+    }
+    if (syncResult == null) {
+      throw new IllegalArgumentException("syncResult must not be null");
+    }
+    if (fxAccount == null) {
+      throw new IllegalArgumentException("fxAccount must not be null");
+    }
+    this.latch = latch;
+    this.syncResult = syncResult;
+    this.fxAccount = fxAccount;
+  }
+
+  /**
+   * No error!  Say that we made progress.
+   */
+  protected void setSyncResultSuccess() {
+    syncResult.stats.numUpdates += 1;
+  }
+
+  /**
+   * Soft error. Say that we made progress, so that Android will sync us again
+   * after exponential backoff.
+   */
+  protected void setSyncResultSoftError() {
+    syncResult.stats.numUpdates += 1;
+    syncResult.stats.numIoExceptions += 1;
+  }
+
+  /**
+   * Hard error. We don't want Android to sync us again, even if we make
+   * progress, until the user intervenes.
+   */
+  protected void setSyncResultHardError() {
+    syncResult.stats.numAuthExceptions += 1;
+  }
+
+  public void handleSuccess() {
+    setSyncResultSuccess();
+    latch.countDown();
+  }
+
+  public void handleError(Exception e) {
+    setSyncResultSoftError();
+    // This is awful, but we need to propagate bad assertions back up the
+    // chain somehow, and this will do for now.
+    if (e instanceof TokenServerException) {
+      // We should only get here *after* we're locked into the married state.
+      State state = fxAccount.getState();
+      if (state.getStateLabel() == StateLabel.Married) {
+        Married married = (Married) state;
+        fxAccount.setState(married.makeCohabitingState());
+      }
+    }
+    latch.countDown();
+  }
+
+  /**
+   * When the login machine terminates, we might not be in the
+   * <code>Married</code> state, and therefore we can't sync. This method
+   * messages as much to the user.
+   * <p>
+   * To avoid stopping us syncing altogether, we set a soft error rather than
+   * a hard error. In future, we would like to set a hard error if we are in,
+   * for example, the <code>Separated</code> state, and then have some user
+   * initiated activity mark the Android account as ready to sync again. This
+   * is tricky, though, so we play it safe for now.
+   *
+   * @param finalState
+   *          that login machine ended in.
+   */
+  public void handleCannotSync(State finalState) {
+    setSyncResultSoftError();
+    latch.countDown();
+  }
+
+  public void postponeSync(long millis) {
+    if (millis > 0) {
+      // delayUntil is broken: https://code.google.com/p/android/issues/detail?id=65669
+      // So we don't bother doing this. Instead, we rely on the periodic sync
+      // we schedule, and the backoff handler for the rest.
+      /*
+      Logger.warn(LOG_TAG, "Postponing sync by " + millis + "ms.");
+      syncResult.delayUntil = millis / 1000;
+       */
+    }
+    setSyncResultSoftError();
+    latch.countDown();
+  }
+
+  /**
+   * Simply don't sync, without setting any error flags.
+   * This is the appropriate behavior when a routine backoff has not yet
+   * been met.
+   */
+  public void rejectSync() {
+    latch.countDown();
+  }
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/reading/ReadingListSyncAdapter.java
@@ -0,0 +1,26 @@
+/* -*- Mode: Java; c-basic-offset: 2; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.reading;
+
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+
+import android.accounts.Account;
+import android.content.AbstractThreadedSyncAdapter;
+import android.content.ContentProviderClient;
+import android.content.Context;
+import android.content.SyncResult;
+import android.os.Bundle;
+
+public class ReadingListSyncAdapter extends AbstractThreadedSyncAdapter {
+    public ReadingListSyncAdapter(Context context, boolean autoInitialize) {
+      super(context, autoInitialize);
+    }
+
+    @Override
+    public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
+      final AndroidFxAccount fxAccount = new AndroidFxAccount(getContext(), account);
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/reading/ReadingListSyncService.java
@@ -0,0 +1,28 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.reading;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+public class ReadingListSyncService extends Service {
+  private static final Object syncAdapterLock = new Object();
+  private static ReadingListSyncAdapter syncAdapter;
+
+  @Override
+  public void onCreate() {
+    synchronized (syncAdapterLock) {
+      if (syncAdapter == null) {
+        syncAdapter = new ReadingListSyncAdapter(getApplicationContext(), true);
+      }
+    }
+  }
+
+  @Override
+  public IBinder onBind(Intent intent) {
+    return syncAdapter.getSyncAdapterBinder();
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/xml/readinglist_syncadapter.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
+    android:accountType="@string/moz_android_shared_fxaccount_type"
+    android:contentAuthority="@string/content_authority_db_readinglist"
+    android:isAlwaysSyncable="true"
+    android:supportsUploading="true"
+    android:userVisible="true"
+/>
--- a/mobile/android/services/manifests/FxAccountAndroidManifest_services.xml.in
+++ b/mobile/android/services/manifests/FxAccountAndroidManifest_services.xml.in
@@ -4,23 +4,41 @@
             <intent-filter >
                 <action android:name="android.accounts.AccountAuthenticator" />
             </intent-filter>
 
             <meta-data
                 android:name="android.accounts.AccountAuthenticator"
                 android:resource="@xml/fxaccount_authenticator" />
         </service>
+
+        <service
+            android:exported="false"
+            android:name="org.mozilla.gecko.fxa.receivers.FxAccountDeletedService" >
+        </service>
+
+        <!-- Firefox Sync. -->
         <service
             android:exported="false"
             android:name="org.mozilla.gecko.fxa.sync.FxAccountSyncService" >
             <intent-filter >
                 <action android:name="android.content.SyncAdapter" />
             </intent-filter>
 
             <meta-data
                 android:name="android.content.SyncAdapter"
                 android:resource="@xml/fxaccount_syncadapter" />
         </service>
+
+        <!-- Reading List. -->
+#ifdef MOZ_ANDROID_READING_LIST_SERVICE
         <service
             android:exported="false"
-            android:name="org.mozilla.gecko.fxa.receivers.FxAccountDeletedService" >
+            android:name="org.mozilla.gecko.reading.ReadingListSyncService" >
+            <intent-filter >
+                <action android:name="android.content.SyncAdapter" />
+            </intent-filter>
+
+            <meta-data
+                android:name="android.content.SyncAdapter"
+                android:resource="@xml/readinglist_syncadapter" />
         </service>
+#endif