Bug 1329793 - Re-subscribe for a push channel periodically r=eoger,nalexander draft
authorGrigory Kruglov <gkruglov@mozilla.com>
Wed, 08 Mar 2017 18:14:43 -0800
changeset 496224 d0292acddbdd231502808469d4e5502a4ac93779
parent 494079 517c553ad64746c479456653ce11b04ab8e4977f
child 496890 c8e3cad2f9b829603cdcab7a63530bd3ffd966a8
push id48564
push usergkruglov@mozilla.com
push dateFri, 10 Mar 2017 01:16:55 +0000
reviewerseoger, nalexander
bugs1329793
milestone54.0a1
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);
   }