Bug 1359279 - Renew GCM token/Push registration/FxA Registration on push registration expired. r?Grisha draft
authorEdouard Oger <eoger@fastmail.com>
Wed, 23 Aug 2017 15:41:31 -0400
changeset 667944 f237560a7d7af03742cf90d3bc87213bfe5c7736
parent 667935 319a34bea9e4f3459886b5b9e835bd338320f1fd
child 732547 7c9be75c2f1664a911bf7ec28b89d5069a197807
push id80884
push userbmo:eoger@fastmail.com
push dateWed, 20 Sep 2017 22:12:46 +0000
reviewersGrisha
bugs1359279
milestone57.0a1
Bug 1359279 - Renew GCM token/Push registration/FxA Registration on push registration expired. r?Grisha MozReview-Commit-ID: HFDjBBt9CBA
mobile/android/services/src/main/java/org/mozilla/gecko/fxa/devices/FxAccountDevice.java
mobile/android/services/src/main/java/org/mozilla/gecko/fxa/devices/FxAccountDeviceListUpdater.java
mobile/android/services/src/main/java/org/mozilla/gecko/fxa/devices/FxAccountDeviceRegistrator.java
mobile/android/services/src/main/java/org/mozilla/gecko/fxa/sync/FxAccountSyncAdapter.java
mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/devices/TestFxAccountDeviceListUpdater.java
mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/login/MockFxAccountClient.java
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/devices/FxAccountDevice.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/devices/FxAccountDevice.java
@@ -10,51 +10,66 @@ public class FxAccountDevice {
 
   private static final String JSON_KEY_NAME = "name";
   private static final String JSON_KEY_ID = "id";
   private static final String JSON_KEY_TYPE = "type";
   private static final String JSON_KEY_ISCURRENTDEVICE = "isCurrentDevice";
   private static final String JSON_KEY_PUSH_CALLBACK = "pushCallback";
   private static final String JSON_KEY_PUSH_PUBLICKEY = "pushPublicKey";
   private static final String JSON_KEY_PUSH_AUTHKEY = "pushAuthKey";
-  private static final String JSON_LAST_ACCESS_TIME = "lastAccessTime";
+  private static final String JSON_KEY_LAST_ACCESS_TIME = "lastAccessTime";
+  private static final String JSON_KEY_PUSH_ENDPOINT_EXPIRED = "pushEndpointExpired";
 
   public final String id;
   public final String name;
   public final String type;
   public final Boolean isCurrentDevice;
   public final Long lastAccessTime;
   public final String pushCallback;
   public final String pushPublicKey;
   public final String pushAuthKey;
+  public final Boolean pushEndpointExpired;
 
   public FxAccountDevice(String name, String id, String type, Boolean isCurrentDevice,
                          Long lastAccessTime, String pushCallback, String pushPublicKey,
-                         String pushAuthKey) {
+                         String pushAuthKey, Boolean pushEndpointExpired) {
     this.name = name;
     this.id = id;
     this.type = type;
     this.isCurrentDevice = isCurrentDevice;
     this.lastAccessTime = lastAccessTime;
     this.pushCallback = pushCallback;
     this.pushPublicKey = pushPublicKey;
     this.pushAuthKey = pushAuthKey;
+    this.pushEndpointExpired = pushEndpointExpired;
   }
 
   public static FxAccountDevice fromJson(ExtendedJSONObject json) {
     final String name = json.getString(JSON_KEY_NAME);
     final String id = json.getString(JSON_KEY_ID);
     final String type = json.getString(JSON_KEY_TYPE);
     final Boolean isCurrentDevice = json.getBoolean(JSON_KEY_ISCURRENTDEVICE);
-    final Long lastAccessTime = json.getLong(JSON_LAST_ACCESS_TIME);
+    final Long lastAccessTime = json.getLong(JSON_KEY_LAST_ACCESS_TIME);
     final String pushCallback = json.getString(JSON_KEY_PUSH_CALLBACK);
     final String pushPublicKey = json.getString(JSON_KEY_PUSH_PUBLICKEY);
     final String pushAuthKey = json.getString(JSON_KEY_PUSH_AUTHKEY);
+    // The FxA server sends this boolean as a number (bug):
+    // https://github.com/mozilla/fxa-auth-server/pull/2122
+    // Use getBoolean directly once the fix is deployed (probably ~Oct-Nov 2017).
+    final Object pushEndpointExpiredRaw = json.get(JSON_KEY_PUSH_ENDPOINT_EXPIRED);
+    final Boolean pushEndpointExpired;
+    if (pushEndpointExpiredRaw instanceof Number) {
+      pushEndpointExpired = ((Number) pushEndpointExpiredRaw).intValue() == 1;
+    } else if (pushEndpointExpiredRaw instanceof Boolean) {
+      pushEndpointExpired = (Boolean) pushEndpointExpiredRaw;
+    } else {
+      pushEndpointExpired = false;
+    }
     return new FxAccountDevice(name, id, type, isCurrentDevice, lastAccessTime, pushCallback,
-                               pushPublicKey, pushAuthKey);
+                               pushPublicKey, pushAuthKey, pushEndpointExpired);
   }
 
   public ExtendedJSONObject toJson() {
     final ExtendedJSONObject body = new ExtendedJSONObject();
     if (this.name != null) {
       body.put(JSON_KEY_NAME, this.name);
     }
     if (this.id != null) {
@@ -104,12 +119,12 @@ public class FxAccountDevice {
     }
 
     public void pushAuthKey(String pushAuthKey) {
       this.pushAuthKey = pushAuthKey;
     }
 
     public FxAccountDevice build() {
       return new FxAccountDevice(this.name, this.id, this.type, null, null,
-                                 this.pushCallback, this.pushPublicKey, this.pushAuthKey);
+                                 this.pushCallback, this.pushPublicKey, this.pushAuthKey, null);
     }
   }
 }
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/devices/FxAccountDeviceListUpdater.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/devices/FxAccountDeviceListUpdater.java
@@ -1,37 +1,48 @@
 /* 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.devices;
 
 import android.content.ContentResolver;
 import android.content.ContentValues;
+import android.content.Context;
+import android.content.SharedPreferences;
 import android.net.Uri;
 import android.os.Bundle;
 import android.support.annotation.NonNull;
 import android.support.annotation.VisibleForTesting;
 import android.util.Log;
 
 import org.mozilla.gecko.background.fxa.FxAccountClient;
 import org.mozilla.gecko.background.fxa.FxAccountClient20;
 import org.mozilla.gecko.background.fxa.FxAccountClientException;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.db.BrowserContract.RemoteDevices;
 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
 import org.mozilla.gecko.fxa.login.State;
+import org.mozilla.gecko.util.ThreadUtils;
 
+import java.io.UnsupportedEncodingException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.security.GeneralSecurityException;
 import java.util.concurrent.Executor;
 
 public class FxAccountDeviceListUpdater implements FxAccountClient20.RequestDelegate<FxAccountDevice[]> {
     private static final String LOG_TAG = "FxADeviceListUpdater";
 
     private final AndroidFxAccount fxAccount;
     private final ContentResolver contentResolver;
+    private boolean localDevicePushEndpointExpired = false;
+
+    private final static String SYNC_PREFS_PUSH_LAST_RENEW_REGISTRATION_MS = "push.lastRenewRegistration";
+    private final static long TIME_BETWEEN_RENEW_REGISTRATION_MS = 2 * 7 * 24 * 3600 * 1000;
 
     public FxAccountDeviceListUpdater(final AndroidFxAccount fxAccount, final ContentResolver cr) {
         this.fxAccount = fxAccount;
         this.contentResolver = cr;
     }
 
     @Override
     public void handleSuccess(final FxAccountDevice[] result) {
@@ -41,26 +52,27 @@ public class FxAccountDeviceListUpdater 
                         .build();
 
         final Bundle valuesBundle = new Bundle();
         final ContentValues[] insertValues = new ContentValues[result.length];
 
         final long now = System.currentTimeMillis();
         for (int i = 0; i < result.length; i++) {
             final FxAccountDevice fxADevice = result[i];
+            if (fxADevice.isCurrentDevice && fxADevice.pushEndpointExpired) {
+                this.localDevicePushEndpointExpired = true;
+            }
             final ContentValues deviceValues = new ContentValues();
             deviceValues.put(RemoteDevices.GUID, fxADevice.id);
             deviceValues.put(RemoteDevices.TYPE, fxADevice.type);
             deviceValues.put(RemoteDevices.NAME, fxADevice.name);
             deviceValues.put(RemoteDevices.IS_CURRENT_DEVICE, fxADevice.isCurrentDevice);
             deviceValues.put(RemoteDevices.DATE_CREATED, now);
             deviceValues.put(RemoteDevices.DATE_MODIFIED, now);
-            // TODO: Remove that line once FxA sends lastAccessTime all the time.
-            final Long lastAccessTime = fxADevice.lastAccessTime != null ? fxADevice.lastAccessTime : 0;
-            deviceValues.put(RemoteDevices.LAST_ACCESS_TIME, lastAccessTime);
+            deviceValues.put(RemoteDevices.LAST_ACCESS_TIME, fxADevice.lastAccessTime);
             insertValues[i] = deviceValues;
         }
         valuesBundle.putParcelableArray(BrowserContract.METHOD_PARAM_DATA, insertValues);
         try {
             contentResolver.call(uri, BrowserContract.METHOD_REPLACE_REMOTE_CLIENTS, uri.toString(),
                                  valuesBundle);
             Log.i(LOG_TAG, "FxA Device list update done.");
         } catch (Exception e) {
@@ -103,9 +115,58 @@ public class FxAccountDeviceListUpdater 
         } catch (State.NotASessionTokenState e) {
             // This should never happen, because the caller (FxAccountSyncAdapter) verifies that
             // we are in a token state before calling this method.
             throw new IllegalStateException("Could not get a session token during Sync (?) " + e);
         }
         final FxAccountClient fxaClient = getSynchronousFxaClient();
         fxaClient.deviceList(sessionToken, this);
     }
+
+    // Updates the list of remote devices, and also renews our push registration if the list provider
+    // tells us it's expired.
+    public void updateAndMaybeRenewRegistration(final Context context) {
+        // Synchronous operation, the re-registration will happen right after the refresh if necessary.
+        this.update();
+        if (!this.localDevicePushEndpointExpired) {
+            return;
+        }
+        final SharedPreferences syncPrefs;
+        try {
+            syncPrefs = fxAccount.getSyncPrefs();
+        } catch (UnsupportedEncodingException | GeneralSecurityException e) {
+            Log.e(LOG_TAG, "Could not get sync preferences, skipping push endpoint re-registration.");
+            return;
+        }
+
+        final long lastTryMs = syncPrefs.getLong(SYNC_PREFS_PUSH_LAST_RENEW_REGISTRATION_MS, 0);
+        final long nowMs = System.currentTimeMillis();
+        if (nowMs - lastTryMs < TIME_BETWEEN_RENEW_REGISTRATION_MS) {
+            Log.w(LOG_TAG, "Last renew registration too close, skipping.");
+            return;
+        }
+
+        final SharedPreferences.Editor syncPrefsEditor = syncPrefs.edit();
+        syncPrefsEditor.putLong(SYNC_PREFS_PUSH_LAST_RENEW_REGISTRATION_MS, nowMs);
+        syncPrefsEditor.commit();
+
+        ThreadUtils.postToBackgroundThread(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    FxAccountDeviceListUpdater.this.renewPushRegistration(context);
+                } catch (Exception e) {
+                    Log.e(LOG_TAG, "Could not renew push registration, continuing anyway", e);
+                }
+                FxAccountDeviceRegistrator.renewRegistration(context);
+            }
+        });
+    }
+
+    private void renewPushRegistration(Context context) throws ClassNotFoundException, NoSuchMethodException,
+                                                InvocationTargetException, IllegalAccessException {
+        final Class<?> pushService = Class.forName("org.mozilla.gecko.push.PushService");
+        final Method getInstance = pushService.getMethod("getInstance", Context.class);
+        final Object instance = getInstance.invoke(null, context);
+        final Method onRefresh = pushService.getMethod("onRefresh");
+        onRefresh.invoke(instance);
+    }
 }
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/devices/FxAccountDeviceRegistrator.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/devices/FxAccountDeviceRegistrator.java
@@ -375,17 +375,17 @@ public class FxAccountDeviceRegistrator 
           fxAccount.setFxAUserData(fxaDevice.id, 0, 0L); // Reset device registration version/timestamp
           if (!allowRecursion) {
             Log.d(LOG_TAG, "Failure to register a device on the second try");
             break;
           }
           final FxAccountDevice updatedDevice = new FxAccountDevice(device.name, fxaDevice.id, device.type,
                                                                     null, null,
                                                                     device.pushCallback, device.pushPublicKey,
-                                                                    device.pushAuthKey);
+                                                                    device.pushAuthKey, null);
           doFxaRegistration(context, fxAccount, updatedDevice, false);
           return;
         }
         onError();
       }
     });
   }
 
--- 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
@@ -479,17 +479,17 @@ public class FxAccountSyncAdapter extend
   }
 
   private void onSessionTokenStateReached(Context context, AndroidFxAccount fxAccount) {
     // This does not block the main thread, if work has to be done it is executed in a new thread.
     maybeRegisterDevice(context, fxAccount);
 
     FxAccountDeviceListUpdater deviceListUpdater = new FxAccountDeviceListUpdater(fxAccount, context.getContentResolver());
     // Since the clients stage requires a fresh list of remote devices, we update the device list synchronously.
-    deviceListUpdater.update();
+    deviceListUpdater.updateAndMaybeRenewRegistration(context);
   }
 
   /**
    * A trivial Sync implementation that does not cache client keys,
    * certificates, or tokens.
    *
    * This should be replaced with a full {@link FxAccountAuthenticator}-based
    * token implementation.
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/devices/TestFxAccountDeviceListUpdater.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/devices/TestFxAccountDeviceListUpdater.java
@@ -76,19 +76,19 @@ public class TestFxAccountDeviceListUpda
         fxaDevicesUpdater.update();
         verify(fxaClient).deviceList(token, fxaDevicesUpdater);
     }
 
     @Test
     public void testSuccessHandler() throws Throwable {
         FxAccountDevice[] result = new FxAccountDevice[2];
         FxAccountDevice device1 = new FxAccountDevice("Current device", "deviceid1", "mobile", true, System.currentTimeMillis(),
-                "https://localhost/push/callback1", "abc123", "321cba");
+                "https://localhost/push/callback1", "abc123", "321cba", false);
         FxAccountDevice device2 = new FxAccountDevice("Desktop PC", "deviceid2", "desktop", true, System.currentTimeMillis(),
-                "https://localhost/push/callback2", "abc123", "321cba");
+                "https://localhost/push/callback2", "abc123", "321cba", false);
         result[0] = device1;
         result[1] = device2;
 
         when(fxAccount.getProfile()).thenReturn("default");
 
         long timeBeforeCall = System.currentTimeMillis();
         fxaDevicesUpdater.handleSuccess(result);
 
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/login/MockFxAccountClient.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/login/MockFxAccountClient.java
@@ -175,27 +175,27 @@ public class MockFxAccountClient impleme
     if (!user.verified) {
       handleFailure(requestDelegate, HttpStatus.SC_BAD_REQUEST, FxAccountRemoteError.ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT, "user is unverified");
       return;
     }
     try {
       String deviceId = deviceToRegister.id;
       if (TextUtils.isEmpty(deviceId)) { // Create
         deviceId = UUID.randomUUID().toString();
-        FxAccountDevice device = new FxAccountDevice(deviceToRegister.name, deviceId, deviceToRegister.type, null, null, null, null, null);
+        FxAccountDevice device = new FxAccountDevice(deviceToRegister.name, deviceId, deviceToRegister.type, null, null, null, null, null, false);
         requestDelegate.handleSuccess(device);
       } else { // Update
         FxAccountDevice existingDevice = user.devices.get(deviceId);
         if (existingDevice != null) {
           String deviceName = existingDevice.name;
           if (!TextUtils.isEmpty(deviceToRegister.name)) {
             deviceName = deviceToRegister.name;
           } // We could also update the other fields..
           FxAccountDevice device = new FxAccountDevice(deviceName, existingDevice.id, existingDevice.type, existingDevice.isCurrentDevice,
-                  existingDevice.lastAccessTime, existingDevice.pushCallback, existingDevice.pushPublicKey,existingDevice.pushAuthKey);
+                  existingDevice.lastAccessTime, existingDevice.pushCallback, existingDevice.pushPublicKey,existingDevice.pushAuthKey, false);
           requestDelegate.handleSuccess(device);
         } else { // Device unknown
           handleFailure(requestDelegate, HttpStatus.SC_BAD_REQUEST, FxAccountRemoteError.UNKNOWN_DEVICE, "device is unknown");
           return;
         }
       }
     } catch (Exception e) {
       requestDelegate.handleError(e);