author | Grigory Kruglov <gkruglov@mozilla.com> |
Wed, 08 Mar 2017 18:14:43 -0800 | |
changeset 346872 | 1b2d90a65aecd38969c6b820374fdb785ce8bec3 |
parent 346871 | c8bfaf1927b6e35e2955323c6e8845a6a5216d43 |
child 346873 | 4aa128f4b262917b813d35088ef9fd9621f5cfdf |
push id | 31480 |
push user | cbook@mozilla.com |
push date | Fri, 10 Mar 2017 10:37:06 +0000 |
treeherder | mozilla-central@e18d3dd20e8d [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | eoger, nalexander |
bugs | 1329793 |
milestone | 55.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
|
--- 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); }