diff --git 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 index e04da4456b6e..f7cb9bb37895 100644 --- 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 @@ -15,7 +15,8 @@ public class FxAccountDevice { 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; @@ -25,10 +26,11 @@ public class FxAccountDevice { 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; @@ -37,6 +39,7 @@ public class FxAccountDevice { this.pushCallback = pushCallback; this.pushPublicKey = pushPublicKey; this.pushAuthKey = pushAuthKey; + this.pushEndpointExpired = pushEndpointExpired; } public static FxAccountDevice fromJson(ExtendedJSONObject json) { @@ -44,12 +47,24 @@ public class FxAccountDevice { 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() { @@ -109,7 +124,7 @@ public class FxAccountDevice { 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); } } } diff --git 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 index 1a48943c5bf4..e293b0168c76 100644 --- 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 @@ -6,6 +6,8 @@ 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; @@ -19,7 +21,12 @@ 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 { @@ -27,6 +34,10 @@ public class FxAccountDeviceListUpdater implements FxAccountClient20.RequestDele 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; @@ -46,6 +57,9 @@ public class FxAccountDeviceListUpdater implements FxAccountClient20.RequestDele 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); @@ -53,9 +67,7 @@ public class FxAccountDeviceListUpdater implements FxAccountClient20.RequestDele 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); @@ -108,4 +120,53 @@ public class FxAccountDeviceListUpdater implements FxAccountClient20.RequestDele 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 (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | IllegalAccessException 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); + } } diff --git 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 index d511ce1330f7..bbd54ad9185b 100644 --- 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 @@ -380,7 +380,7 @@ public class FxAccountDeviceRegistrator implements BundleEventListener { 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; } diff --git 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 index 312a1e3654d1..aab808652038 100644 --- 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 @@ -484,7 +484,7 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter { 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); } /** diff --git 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 index adfb276903d0..872b0107b7ae 100644 --- 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 @@ -81,9 +81,9 @@ public class TestFxAccountDeviceListUpdater { 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; diff --git 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 index a84ff974ab3b..00f0cc548a7c 100644 --- 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 @@ -180,7 +180,7 @@ public class MockFxAccountClient implements FxAccountClient { 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); @@ -190,7 +190,7 @@ public class MockFxAccountClient implements FxAccountClient { 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");