Bug 1329793 - Re-subscribe for a push channel periodically r=eoger,nalexander
authorGrigory Kruglov <gkruglov@mozilla.com>
Wed, 08 Mar 2017 18:14:43 -0800
changeset 346872 1b2d90a65aecd38969c6b820374fdb785ce8bec3
parent 346871 c8bfaf1927b6e35e2955323c6e8845a6a5216d43
child 346873 4aa128f4b262917b813d35088ef9fd9621f5cfdf
push id31480
push usercbook@mozilla.com
push dateFri, 10 Mar 2017 10:37:06 +0000
treeherdermozilla-central@e18d3dd20e8d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerseoger, nalexander
bugs1329793
milestone55.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1329793 - Re-subscribe for a push channel periodically r=eoger,nalexander On startup and at the beginning of a sync we check how long it has been since we've subscribed to a channel for fxa service. If it's been over 21 days, request re-subscription. MozReview-Commit-ID: GzvPecZ9hTy
mobile/android/base/java/org/mozilla/gecko/push/PushManager.java
mobile/android/base/java/org/mozilla/gecko/push/PushService.java
mobile/android/components/FxAccountsPush.js
mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountDeviceRegistrator.java
mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AndroidFxAccount.java
mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncAdapter.java
mobile/android/services/src/main/java/org/mozilla/gecko/sync/SharedPreferencesClientsDataDelegate.java
--- a/mobile/android/base/java/org/mozilla/gecko/push/PushManager.java
+++ b/mobile/android/base/java/org/mozilla/gecko/push/PushManager.java
@@ -12,17 +12,16 @@ import android.util.Log;
 import org.json.JSONObject;
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.gcm.GcmTokenClient;
 import org.mozilla.gecko.push.autopush.AutopushClientException;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import java.io.IOException;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.Iterator;
 import java.util.Map;
 
 /**
  * The push manager advances push registrations, ensuring that the upstream autopush endpoint has
  * a fresh GCM token.  It brokers channel subscription requests to the upstream and maintains
  * local state.
  * <p/>
--- a/mobile/android/base/java/org/mozilla/gecko/push/PushService.java
+++ b/mobile/android/base/java/org/mozilla/gecko/push/PushService.java
@@ -1,15 +1,17 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.push;
 
+import android.accounts.Account;
+import android.accounts.AccountManager;
 import android.content.Context;
 import android.content.Intent;
 import android.os.Bundle;
 import android.support.annotation.NonNull;
 import android.util.Log;
 
 import org.json.JSONException;
 import org.json.JSONObject;
@@ -17,17 +19,21 @@ import org.mozilla.gecko.EventDispatcher
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.GeckoService;
 import org.mozilla.gecko.GeckoThread;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.annotation.ReflectionTarget;
 import org.mozilla.gecko.db.BrowserDB;
+import org.mozilla.gecko.fxa.FxAccountConstants;
+import org.mozilla.gecko.fxa.FxAccountDeviceRegistrator;
 import org.mozilla.gecko.fxa.FxAccountPushHandler;
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+import org.mozilla.gecko.fxa.login.State;
 import org.mozilla.gecko.gcm.GcmTokenClient;
 import org.mozilla.gecko.push.autopush.AutopushClientException;
 import org.mozilla.gecko.util.BundleEventListener;
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.GeckoBundle;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import java.io.File;
@@ -97,33 +103,82 @@ public class PushService implements Bund
 
     protected final PushManager pushManager;
 
     // NB: These are not thread-safe, we're depending on these being access from the same background thread.
     private boolean isReadyPushServiceAndroidGCM = false;
     private boolean isReadyFxAccountsPush = false;
     private final List<JSONObject> pendingPushMessages;
 
+    // NB, on context use in AccountManager and AndroidFxAccount:
+    // We are not going to register any listeners, or surface any UI out of AccountManager.
+    // It should be fine to use a potentially short-lived context then, as opposed to a long-lived
+    // application context, contrary to what AndroidFxAccount docs ask for.
+    private final Context context;
+
     public PushService(Context context) {
+        this.context = context;
         pushManager = new PushManager(new PushState(context, "GeckoPushState.json"), new GcmTokenClient(context), new PushManager.PushClientFactory() {
             @Override
             public PushClient getPushClient(String autopushEndpoint, boolean debug) {
                 return new PushClient(autopushEndpoint);
             }
         });
 
         pendingPushMessages = new LinkedList<>();
     }
 
     public void onStartup() {
         Log.i(LOG_TAG, "Starting up.");
         ThreadUtils.assertOnBackgroundThread();
 
         try {
             pushManager.startup(System.currentTimeMillis());
+
+            // Determine if we need to renew our FxA Push Subscription. Unused subscriptions expire
+            // once a month, and so we do a simple check on startup to determine if it's time to get
+            // a new one. Note that this is sub-optimal, as we might have a perfectly valid (but old)
+            // subscription which we'll nevertheless unsubscribe in lieu of a new one. Improvements
+            // to this will be addressed as part of a larger Bug 1345651.
+
+            // From the Android permission docs:
+            // Prior to API 23, GET_ACCOUNTS permission was necessary to get access to information
+            // about any account. Beginning with API 23, if an app shares the signature of the
+            // authenticator that manages an account, it does not need "GET_ACCOUNTS" permission to
+            // read information about that account.
+            // We list GET_ACCOUNTS in our manifest for pre-23 devices.
+            final AccountManager accountManager = AccountManager.get(context);
+            final Account[] fxAccounts = accountManager.getAccountsByType(FxAccountConstants.ACCOUNT_TYPE);
+
+            // Nothing to renew if there isn't an account.
+            if (fxAccounts.length == 0) {
+                return;
+            }
+
+            // Defensively obtain account state. We are in a startup situation: try to not crash.
+            final AndroidFxAccount fxAccount = new AndroidFxAccount(context, fxAccounts[0]);
+            final State fxAccountState;
+            try {
+                fxAccountState = fxAccount.getState();
+            } catch (IllegalStateException e) {
+                Log.e(LOG_TAG, "Failed to obtain FxA account state while renewing registration", e);
+                return;
+            }
+
+            // This decision will be re-addressed as part of Bug 1346061.
+            if (!State.StateLabel.Married.equals(fxAccountState.getStateLabel())) {
+                Log.i(LOG_TAG, "FxA account not in Married state, not proceeding with registration renewal");
+                return;
+            }
+
+            // We'll obtain a new subscription as part of device registration.
+            if (FxAccountDeviceRegistrator.needToRenewRegistration(fxAccount.getDeviceRegistrationTimestamp())) {
+                Log.i(LOG_TAG, "FxA device needs registration renewal");
+                FxAccountDeviceRegistrator.renewRegistration(context);
+            }
         } catch (Exception e) {
             Log.e(LOG_TAG, "Got exception during startup; ignoring.", e);
             return;
         }
     }
 
     public void onRefresh() {
         Log.i(LOG_TAG, "Google Play Services requested GCM token refresh; invalidating GCM token and running startup again.");
--- a/mobile/android/components/FxAccountsPush.js
+++ b/mobile/android/components/FxAccountsPush.js
@@ -33,16 +33,19 @@ function FxAccountsPush() {
 FxAccountsPush.prototype = {
   observe: function (subject, topic, data) {
     switch (topic) {
       case "android-push-service":
         if (data === "android-fxa-subscribe") {
           this._subscribe();
         } else if (data === "android-fxa-unsubscribe") {
           this._unsubscribe();
+        } else if (data === "android-fxa-resubscribe") {
+          // If unsubscription fails, we still want to try to subscribe.
+          this._unsubscribe().then(this._subscribe, this._subscribe);
         }
         break;
       case "FxAccountsPush:ReceivedPushMessageToDecode":
         this._decodePushMessage(data);
         break;
     }
   },
 
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountDeviceRegistrator.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/FxAccountDeviceRegistrator.java
@@ -35,16 +35,23 @@ import java.util.concurrent.Executors;
 
 /* This class provides a way to register the current device against FxA
  * and also stores the registration details in the Android FxAccount.
  * This should be used in a state where we possess a sessionToken, most likely the Married state.
  */
 public class FxAccountDeviceRegistrator implements BundleEventListener {
   private static final String LOG_TAG = "FxADeviceRegistrator";
 
+  // The autopush endpoint expires stale channel subscriptions every 30 days (at a set time during
+  // the month, although we don't depend on this). To avoid the FxA service channel silently
+  // expiring from underneath us, we unsubscribe and resubscribe every 21 days.
+  // Note that this simple schedule means that we might unsubscribe perfectly valid (but old)
+  // subscriptions. This will be improved as part of Bug 1345651.
+  private static final long TIME_BETWEEN_CHANNEL_REGISTRATION_IN_MILLIS = 21 * 24 * 60 * 60 * 1000L;
+
   // The current version of the device registration, we use this to re-register
   // devices after we update what we send on device registration.
   public static final Integer DEVICE_REGISTRATION_VERSION = 2;
 
   private static FxAccountDeviceRegistrator instance;
   private final WeakReference<Context> context;
 
   private FxAccountDeviceRegistrator(Context appContext) {
@@ -55,40 +62,69 @@ public class FxAccountDeviceRegistrator 
     if (instance == null) {
       FxAccountDeviceRegistrator tempInstance = new FxAccountDeviceRegistrator(appContext);
       tempInstance.setupListeners(); // Set up listener for FxAccountPush:Subscribe:Response
       instance = tempInstance;
     }
     return instance;
   }
 
+  public static boolean needToRenewRegistration(final long timestamp) {
+    // NB: we're comparing wall clock to wall clock, at different points in time.
+    // It's possible that wall clocks have changed, and our comparison will be meaningless.
+    // However, this happens in the context of a sync, and we won't be able to sync anyways if our
+    // wall clock deviates too much from time on the server.
+    return (System.currentTimeMillis() - timestamp) > TIME_BETWEEN_CHANNEL_REGISTRATION_IN_MILLIS;
+  }
+
   public static void register(Context context) {
     Context appContext = context.getApplicationContext();
     try {
       getInstance(appContext).beginRegistration(appContext);
     } catch (Exception e) {
       Log.e(LOG_TAG, "Could not start FxA device registration", e);
     }
   }
 
+  public static void renewRegistration(Context context) {
+    Context appContext = context.getApplicationContext();
+    try {
+      getInstance(appContext).beginRegistrationRenewal(appContext);
+    } catch (Exception e) {
+      Log.e(LOG_TAG, "Could not start FxA device re-registration", e);
+    }
+  }
+
   private void beginRegistration(Context context) {
     // Fire up gecko and send event
     // We create the Intent ourselves instead of using GeckoService.getIntentToCreateServices
     // because we can't import these modules (circular dependency between browser and services)
-    final Intent geckoIntent = new Intent();
-    geckoIntent.setAction("create-services");
-    geckoIntent.setClassName(context, "org.mozilla.gecko.GeckoService");
-    geckoIntent.putExtra("category", "android-push-service");
-    geckoIntent.putExtra("data", "android-fxa-subscribe");
-    final AndroidFxAccount fxAccount = AndroidFxAccount.fromContext(context);
-    geckoIntent.putExtra("org.mozilla.gecko.intent.PROFILE_NAME", fxAccount.getProfile());
+    final Intent geckoIntent = buildCreatePushServiceIntent(context, "android-fxa-subscribe");
+    context.startService(geckoIntent);
+    // -> handleMessage()
+  }
+
+  private void beginRegistrationRenewal(Context context) {
+    // Same as registration, but unsubscribe first to get a fresh subscription.
+    final Intent geckoIntent = buildCreatePushServiceIntent(context, "android-fxa-resubscribe");
     context.startService(geckoIntent);
     // -> handleMessage()
   }
 
+  private Intent buildCreatePushServiceIntent(final Context context, final String data) {
+    final Intent intent = new Intent();
+    intent.setAction("create-services");
+    intent.setClassName(context, "org.mozilla.gecko.GeckoService");
+    intent.putExtra("category", "android-push-service");
+    intent.putExtra("data", data);
+    final AndroidFxAccount fxAccount = AndroidFxAccount.fromContext(context);
+    intent.putExtra("org.mozilla.gecko.intent.PROFILE_NAME", fxAccount.getProfile());
+    return intent;
+  }
+
   @Override
   public void handleMessage(String event, GeckoBundle message, EventCallback callback) {
     if ("FxAccountsPush:Subscribe:Response".equals(event)) {
       try {
         doFxaRegistration(message.getBundle("subscription"));
       } catch (InvalidFxAState e) {
         Log.d(LOG_TAG, "Invalid state when trying to register with FxA ", e);
       }
@@ -130,50 +166,55 @@ public class FxAccountDeviceRegistrator 
 
     ExecutorService executor = Executors.newSingleThreadExecutor(); // Not called often, it's okay to spawn another thread
     final FxAccountClient20 fxAccountClient =
             new FxAccountClient20(fxAccount.getAccountServerURI(), executor);
     fxAccountClient.registerOrUpdateDevice(sessionToken, device, new RequestDelegate<FxAccountDevice>() {
       @Override
       public void handleError(Exception e) {
         Log.e(LOG_TAG, "Error while updating a device registration: ", e);
+        fxAccount.setDeviceRegistrationTimestamp(0L);
       }
 
       @Override
       public void handleFailure(FxAccountClientRemoteException error) {
         Log.e(LOG_TAG, "Error while updating a device registration: ", error);
+
+        fxAccount.setDeviceRegistrationTimestamp(0L);
+
         if (error.httpStatusCode == 400) {
           if (error.apiErrorNumber == FxAccountRemoteError.UNKNOWN_DEVICE) {
             recoverFromUnknownDevice(fxAccount);
           } else if (error.apiErrorNumber == FxAccountRemoteError.DEVICE_SESSION_CONFLICT) {
             recoverFromDeviceSessionConflict(error, fxAccountClient, sessionToken, fxAccount, context,
                     subscription, allowRecursion);
           }
         } else
         if (error.httpStatusCode == 401
                 && error.apiErrorNumber == FxAccountRemoteError.INVALID_AUTHENTICATION_TOKEN) {
           handleTokenError(error, fxAccountClient, fxAccount);
         } else {
-          logErrorAndResetDeviceRegistrationVersion(error, fxAccount);
+          logErrorAndResetDeviceRegistrationVersionAndTimestamp(error, fxAccount);
         }
       }
 
       @Override
       public void handleSuccess(FxAccountDevice result) {
         Log.i(LOG_TAG, "Device registration complete");
         Logger.pii(LOG_TAG, "Registered device ID: " + result.id);
-        fxAccount.setFxAUserData(result.id, DEVICE_REGISTRATION_VERSION);
+        fxAccount.setFxAUserData(result.id, DEVICE_REGISTRATION_VERSION, System.currentTimeMillis());
       }
     });
   }
 
-  private static void logErrorAndResetDeviceRegistrationVersion(
+  private static void logErrorAndResetDeviceRegistrationVersionAndTimestamp(
       final FxAccountClientRemoteException error, final AndroidFxAccount fxAccount) {
     Log.e(LOG_TAG, "Device registration failed", error);
     fxAccount.resetDeviceRegistrationVersion();
+    fxAccount.setDeviceRegistrationTimestamp(0L);
   }
 
   @Nullable
   private static String getClientName(final AndroidFxAccount fxAccount, final Context context) {
     try {
       SharedPreferencesClientsDataDelegate clientsDataDelegate =
           new SharedPreferencesClientsDataDelegate(fxAccount.getSyncPrefs(), context);
       return clientsDataDelegate.getClientName();
@@ -182,17 +223,17 @@ public class FxAccountDeviceRegistrator 
       return null;
     }
   }
 
   private static void handleTokenError(final FxAccountClientRemoteException error,
                                        final FxAccountClient fxAccountClient,
                                        final AndroidFxAccount fxAccount) {
     Log.i(LOG_TAG, "Recovering from invalid token error: ", error);
-    logErrorAndResetDeviceRegistrationVersion(error, fxAccount);
+    logErrorAndResetDeviceRegistrationVersionAndTimestamp(error, fxAccount);
     fxAccountClient.accountStatus(fxAccount.getState().uid,
         new RequestDelegate<AccountStatusResponse>() {
       @Override
       public void handleError(Exception e) {
       }
 
       @Override
       public void handleFailure(FxAccountClientRemoteException e) {
@@ -228,34 +269,34 @@ public class FxAccountDeviceRegistrator 
                                                        final AndroidFxAccount fxAccount,
                                                        final Context context,
                                                        final GeckoBundle subscription,
                                                        final boolean allowRecursion) {
     Log.w(LOG_TAG, "device session conflict, attempting to ascertain the correct device id");
     fxAccountClient.deviceList(sessionToken, new RequestDelegate<FxAccountDevice[]>() {
       private void onError() {
         Log.e(LOG_TAG, "failed to recover from device-session conflict");
-        logErrorAndResetDeviceRegistrationVersion(error, fxAccount);
+        logErrorAndResetDeviceRegistrationVersionAndTimestamp(error, fxAccount);
       }
 
       @Override
       public void handleError(Exception e) {
         onError();
       }
 
       @Override
       public void handleFailure(FxAccountClientRemoteException e) {
         onError();
       }
 
       @Override
       public void handleSuccess(FxAccountDevice[] devices) {
         for (FxAccountDevice device : devices) {
           if (device.isCurrentDevice) {
-            fxAccount.setFxAUserData(device.id, 0); // Reset device registration version
+            fxAccount.setFxAUserData(device.id, 0, 0L); // Reset device registration version/timestamp
             if (!allowRecursion) {
               Log.d(LOG_TAG, "Failure to register a device on the second try");
               break;
             }
             try {
               doFxaRegistration(context, subscription, false);
               return;
             } catch (InvalidFxAState e) {
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AndroidFxAccount.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AndroidFxAccount.java
@@ -75,16 +75,17 @@ public class AndroidFxAccount {
   public static final int CURRENT_BUNDLE_VERSION = 2;
   public static final String BUNDLE_KEY_BUNDLE_VERSION = "version";
   public static final String BUNDLE_KEY_STATE_LABEL = "stateLabel";
   public static final String BUNDLE_KEY_STATE = "state";
   public static final String BUNDLE_KEY_PROFILE_JSON = "profile";
 
   public static final String ACCOUNT_KEY_DEVICE_ID = "deviceId";
   public static final String ACCOUNT_KEY_DEVICE_REGISTRATION_VERSION = "deviceRegistrationVersion";
+  private static final String ACCOUNT_KEY_DEVICE_REGISTRATION_TIMESTAMP = "deviceRegistrationTimestamp";
 
   // Account authentication token type for fetching account profile.
   public static final String PROFILE_OAUTH_TOKEN_TYPE = "oauth::profile";
 
   // Services may request OAuth tokens from the Firefox Account dynamically.
   // Each such token is prefixed with "oauth::" and a service-dependent scope.
   // Such tokens should be destroyed when the account is removed from the device.
   // This list collects all the known "oauth::" token types in order to delete them when necessary.
@@ -397,16 +398,17 @@ public class AndroidFxAccount {
     o.put("email", account.name);
     try {
       o.put("emailUTF8", Utils.byte2Hex(account.name.getBytes("UTF-8")));
     } catch (UnsupportedEncodingException e) {
       // Ignore.
     }
     o.put("fxaDeviceId", getDeviceId());
     o.put("fxaDeviceRegistrationVersion", getDeviceRegistrationVersion());
+    o.put("fxaDeviceRegistrationTimestamp", getDeviceRegistrationTimestamp());
     return o;
   }
 
   public static AndroidFxAccount addAndroidAccount(
       Context context,
       String email,
       String profile,
       String idpServerURI,
@@ -824,33 +826,57 @@ public class AndroidFxAccount {
       try {
         return Integer.parseInt(versionStr);
       } catch (NumberFormatException ex) {
         return 0;
       }
     }
   }
 
+  public synchronized long getDeviceRegistrationTimestamp() {
+    final String timestampStr = accountManager.getUserData(account, ACCOUNT_KEY_DEVICE_REGISTRATION_TIMESTAMP);
+
+    if (TextUtils.isEmpty(timestampStr)) {
+      return 0L;
+    }
+
+    // Long.valueOf might throw; while it's not expected that this might happen, let's not risk
+    // crashing here as this method will be called on startup.
+    try {
+      return Long.valueOf(timestampStr);
+    } catch (NumberFormatException e) {
+      Logger.warn(LOG_TAG, "Couldn't parse deviceRegistrationTimestamp; defaulting to 0L.", e);
+      return 0L;
+    }
+  }
+
   public synchronized void setDeviceId(String id) {
     accountManager.setUserData(account, ACCOUNT_KEY_DEVICE_ID, id);
   }
 
   public synchronized void setDeviceRegistrationVersion(int deviceRegistrationVersion) {
     accountManager.setUserData(account, ACCOUNT_KEY_DEVICE_REGISTRATION_VERSION,
         Integer.toString(deviceRegistrationVersion));
   }
 
+  public synchronized void setDeviceRegistrationTimestamp(long timestamp) {
+    accountManager.setUserData(account, ACCOUNT_KEY_DEVICE_REGISTRATION_TIMESTAMP,
+            Long.toString(timestamp));
+  }
+
   public synchronized void resetDeviceRegistrationVersion() {
     setDeviceRegistrationVersion(0);
   }
 
-  public synchronized void setFxAUserData(String id, int deviceRegistrationVersion) {
+  public synchronized void setFxAUserData(String id, int deviceRegistrationVersion, long timestamp) {
     accountManager.setUserData(account, ACCOUNT_KEY_DEVICE_ID, id);
     accountManager.setUserData(account, ACCOUNT_KEY_DEVICE_REGISTRATION_VERSION,
-        Integer.toString(deviceRegistrationVersion));
+            Integer.toString(deviceRegistrationVersion));
+    accountManager.setUserData(account, ACCOUNT_KEY_DEVICE_REGISTRATION_TIMESTAMP,
+            Long.toString(timestamp));
   }
 
   @SuppressLint("ParcelCreator") // The CREATOR field is defined in the super class.
   private class ProfileResultReceiver extends ResultReceiver {
     public ProfileResultReceiver(Handler handler) {
       super(handler);
     }
 
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncAdapter.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncAdapter.java
@@ -562,20 +562,28 @@ public class FxAccountSyncAdapter extend
 
             final SessionCallback sessionCallback = new SessionCallback(syncDelegate, schedulePolicy);
             final KeyBundle syncKeyBundle = married.getSyncKeyBundle();
             final String clientState = married.getClientState();
             syncWithAssertion(
                     assertion, tokenServerEndpointURI, tokenBackoffHandler, sharedPrefs,
                     syncKeyBundle, clientState, sessionCallback, extras, fxAccount, syncDeadline);
 
-            // Register the device if necessary (asynchronous, in another thread)
+            // Register the device if necessary (asynchronous, in another thread).
+            // As part of device registration, we obtain a PushSubscription, register our push endpoint
+            // with FxA, and update account data with fxaDeviceId, which is part of our synced
+            // clients record.
             if (fxAccount.getDeviceRegistrationVersion() != FxAccountDeviceRegistrator.DEVICE_REGISTRATION_VERSION
-                || TextUtils.isEmpty(fxAccount.getDeviceId())) {
+              || TextUtils.isEmpty(fxAccount.getDeviceId())) {
               FxAccountDeviceRegistrator.register(context);
+            // We might need to re-register periodically to ensure our FxA push subscription is valid.
+            // This involves unsubscribing, subscribing and updating remote FxA device record with
+            // new push subscription information.
+            } else if (FxAccountDeviceRegistrator.needToRenewRegistration(fxAccount.getDeviceRegistrationTimestamp())) {
+              FxAccountDeviceRegistrator.renewRegistration(context);
             }
 
             // Force fetch the profile avatar information. (asynchronous, in another thread)
             Logger.info(LOG_TAG, "Fetching profile avatar information.");
             fxAccount.fetchProfileJSON();
           } catch (Exception e) {
             syncDelegate.handleError(e);
             return;
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SharedPreferencesClientsDataDelegate.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SharedPreferencesClientsDataDelegate.java
@@ -58,16 +58,17 @@ public class SharedPreferencesClientsDat
   public synchronized void setClientName(String clientName, long now) {
     saveClientNameToSharedPreferences(clientName, now);
 
     // Update the FxA device registration
     final Account account = FirefoxAccounts.getFirefoxAccount(context);
     if (account != null) {
       final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
       fxAccount.resetDeviceRegistrationVersion();
+      fxAccount.setDeviceRegistrationTimestamp(0L);
     }
   }
 
   @Override
   public String getDefaultClientName() {
     return FxAccountUtils.defaultClientName(context);
   }