--- a/mobile/android/base/android-services.mozbuild
+++ b/mobile/android/base/android-services.mozbuild
@@ -827,16 +827,17 @@ sync_java_files = [TOPSRCDIR + '/mobile/
'fxa/authenticator/AccountPickler.java',
'fxa/authenticator/AndroidFxAccount.java',
'fxa/authenticator/FxAccountAuthenticator.java',
'fxa/authenticator/FxAccountAuthenticatorService.java',
'fxa/authenticator/FxAccountLoginDelegate.java',
'fxa/authenticator/FxAccountLoginException.java',
'fxa/authenticator/FxADefaultLoginStateMachineDelegate.java',
'fxa/devices/FxAccountDevice.java',
+ 'fxa/devices/FxAccountDeviceListUpdater.java',
'fxa/devices/FxAccountDeviceRegistrator.java',
'fxa/FirefoxAccounts.java',
'fxa/FxAccountConstants.java',
'fxa/FxAccountPushHandler.java',
'fxa/login/BaseRequestDelegate.java',
'fxa/login/Cohabiting.java',
'fxa/login/Doghouse.java',
'fxa/login/Engaged.java',
--- 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
@@ -800,49 +800,57 @@ public class AndroidFxAccount {
intent.putExtra(FxAccountProfileService.KEY_AUTH_TOKEN, authToken);
intent.putExtra(FxAccountProfileService.KEY_PROFILE_SERVER_URI, getProfileServerURI());
intent.putExtra(FxAccountProfileService.KEY_RESULT_RECEIVER, new ProfileResultReceiver(new Handler()));
context.startService(intent);
}
});
}
- @SuppressWarnings("unchecked")
- private <T extends Number> T getUserDataNumber(String key, T defaultValue) {
+ private long getUserDataLong(String key, long defaultValue) {
final String numStr = accountManager.getUserData(account, key);
if (TextUtils.isEmpty(numStr)) {
return defaultValue;
}
try {
- return (T) NumberFormat.getInstance().parse(numStr);
- } catch (ParseException e) {
- Logger.warn(LOG_TAG, "Couldn't parse " + key + "; defaulting to 0L.", e);
+ return Long.parseLong(key);
+ } catch (NumberFormatException e) {
+ Logger.warn(LOG_TAG, "Couldn't parse " + key + "; defaulting to " + defaultValue, e);
return defaultValue;
}
}
@Nullable
public synchronized String getDeviceId() {
return accountManager.getUserData(account, ACCOUNT_KEY_DEVICE_ID);
}
public synchronized int getDeviceRegistrationVersion() {
- return getUserDataNumber(ACCOUNT_KEY_DEVICE_REGISTRATION_VERSION, 0);
+ final String numStr = accountManager.getUserData(account, ACCOUNT_KEY_DEVICE_REGISTRATION_VERSION);
+ if (TextUtils.isEmpty(numStr)) {
+ return 0;
+ }
+ try {
+ return Integer.parseInt(numStr);
+ } catch (NumberFormatException e) {
+ Logger.warn(LOG_TAG, "Couldn't parse ACCOUNT_KEY_DEVICE_REGISTRATION_VERSION; defaulting to 0", e);
+ return 0;
+ }
}
public synchronized long getDeviceRegistrationTimestamp() {
- return getUserDataNumber(ACCOUNT_KEY_DEVICE_REGISTRATION_TIMESTAMP, 0L);
+ return getUserDataLong(ACCOUNT_KEY_DEVICE_REGISTRATION_TIMESTAMP, 0L);
}
public synchronized long getDevicePushRegistrationError() {
- return getUserDataNumber(ACCOUNT_KEY_DEVICE_PUSH_REGISTRATION_ERROR, 0L);
+ return getUserDataLong(ACCOUNT_KEY_DEVICE_PUSH_REGISTRATION_ERROR, 0L);
}
public synchronized long getDevicePushRegistrationErrorTime() {
- return getUserDataNumber(ACCOUNT_KEY_DEVICE_PUSH_REGISTRATION_ERROR_TIME, 0L);
+ return getUserDataLong(ACCOUNT_KEY_DEVICE_PUSH_REGISTRATION_ERROR_TIME, 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,
--- 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
@@ -3,52 +3,58 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.fxa.devices;
import org.mozilla.gecko.sync.ExtendedJSONObject;
public class FxAccountDevice {
- public static final String JSON_KEY_NAME = "name";
- public static final String JSON_KEY_ID = "id";
- public static final String JSON_KEY_TYPE = "type";
- public static final String JSON_KEY_ISCURRENTDEVICE = "isCurrentDevice";
- public static final String JSON_KEY_PUSH_CALLBACK = "pushCallback";
- public static final String JSON_KEY_PUSH_PUBLICKEY = "pushPublicKey";
- public static final String JSON_KEY_PUSH_AUTHKEY = "pushAuthKey";
+ 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";
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 FxAccountDevice(String name, String id, String type, Boolean isCurrentDevice,
- String pushCallback, String pushPublicKey, String pushAuthKey) {
+ Long lastAccessTime, String pushCallback, String pushPublicKey,
+ String pushAuthKey) {
this.name = name;
this.id = id;
this.type = type;
this.isCurrentDevice = isCurrentDevice;
+ this.lastAccessTime = lastAccessTime;
this.pushCallback = pushCallback;
this.pushPublicKey = pushPublicKey;
this.pushAuthKey = pushAuthKey;
}
public static FxAccountDevice fromJson(ExtendedJSONObject json) {
- String name = json.getString(JSON_KEY_NAME);
- String id = json.getString(JSON_KEY_ID);
- String type = json.getString(JSON_KEY_TYPE);
- Boolean isCurrentDevice = json.getBoolean(JSON_KEY_ISCURRENTDEVICE);
- String pushCallback = json.getString(JSON_KEY_PUSH_CALLBACK);
- String pushPublicKey = json.getString(JSON_KEY_PUSH_PUBLICKEY);
- String pushAuthKey = json.getString(JSON_KEY_PUSH_AUTHKEY);
- return new FxAccountDevice(name, id, type, isCurrentDevice, pushCallback, pushPublicKey, pushAuthKey);
+ 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 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);
+ return new FxAccountDevice(name, id, type, isCurrentDevice, lastAccessTime, pushCallback,
+ pushPublicKey, pushAuthKey);
}
public ExtendedJSONObject toJson() {
final ExtendedJSONObject body = new ExtendedJSONObject();
if (this.name != null) {
body.put(JSON_KEY_NAME, this.name);
}
if (this.id != null) {
@@ -68,47 +74,42 @@ public class FxAccountDevice {
}
return body;
}
public static class Builder {
private String id;
private String name;
private String type;
- private Boolean isCurrentDevice;
private String pushCallback;
private String pushPublicKey;
private String pushAuthKey;
public void id(String id) {
this.id = id;
}
public void name(String name) {
this.name = name;
}
public void type(String type) {
this.type = type;
}
- public void isCurrentDevice() {
- this.isCurrentDevice = Boolean.TRUE;
- }
-
public void pushCallback(String pushCallback) {
this.pushCallback = pushCallback;
}
public void pushPublicKey(String pushPublicKey) {
this.pushPublicKey = pushPublicKey;
}
public void pushAuthKey(String pushAuthKey) {
this.pushAuthKey = pushAuthKey;
}
public FxAccountDevice build() {
- return new FxAccountDevice(this.name, this.id, this.type, this.isCurrentDevice,
+ return new FxAccountDevice(this.name, this.id, this.type, null, null,
this.pushCallback, this.pushPublicKey, this.pushAuthKey);
}
}
}
new file mode 100644
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/devices/FxAccountDeviceListUpdater.java
@@ -0,0 +1,111 @@
+/* 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.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 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;
+
+ public FxAccountDeviceListUpdater(final AndroidFxAccount fxAccount, final ContentResolver cr) {
+ this.fxAccount = fxAccount;
+ this.contentResolver = cr;
+ }
+
+ @Override
+ public void handleSuccess(final FxAccountDevice[] result) {
+ final Uri uri = RemoteDevices.CONTENT_URI
+ .buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_PROFILE, fxAccount.getProfile())
+ .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];
+ 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);
+ 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) {
+ Log.e(LOG_TAG, "Error persisting the new remote device list.", e);
+ }
+ }
+
+ @Override
+ public void handleError(Exception e) {
+ onError(e);
+ }
+
+ @Override
+ public void handleFailure(FxAccountClientException.FxAccountClientRemoteException e) {
+ onError(e);
+ }
+
+ private void onError(final Exception e) {
+ Log.e(LOG_TAG, "Error while getting the FxA device list.", e);
+ }
+
+ @VisibleForTesting
+ FxAccountClient getSynchronousFxaClient() {
+ return new FxAccountClient20(fxAccount.getAccountServerURI(),
+ // Current thread executor :)
+ new Executor() {
+ @Override
+ public void execute(@NonNull Runnable runnable) {
+ runnable.run();
+ }
+ }
+ );
+ }
+
+ public void update() {
+ Log.i(LOG_TAG, "Beginning FxA device list update.");
+ final byte[] sessionToken;
+ try {
+ sessionToken = fxAccount.getState().getSessionToken();
+ } 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);
+ }
+}
--- 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
@@ -373,18 +373,19 @@ public class FxAccountDeviceRegistrator
continue;
}
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,
- device.isCurrentDevice, device.pushCallback,
- device.pushPublicKey, device.pushAuthKey);
+ null, null,
+ device.pushCallback, device.pushPublicKey,
+ device.pushAuthKey);
doFxaRegistration(context, fxAccount, updatedDevice, false);
return;
}
onError();
}
});
}
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/receivers/FxAccountDeletedService.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/receivers/FxAccountDeletedService.java
@@ -1,25 +1,28 @@
/* 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.receivers;
import android.app.IntentService;
+import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
+import android.net.Uri;
import android.text.TextUtils;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountClient20;
import org.mozilla.gecko.background.fxa.FxAccountClientException;
import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClient;
import org.mozilla.gecko.background.fxa.oauth.FxAccountAbstractClientException.FxAccountAbstractClientRemoteException;
import org.mozilla.gecko.background.fxa.oauth.FxAccountOAuthClient10;
+import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.sync.FxAccountNotificationManager;
import org.mozilla.gecko.fxa.sync.FxAccountSyncAdapter;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.repositories.android.ClientsDatabase;
import org.mozilla.gecko.sync.repositories.android.FennecTabsRepository;
@@ -70,16 +73,18 @@ public class FxAccountDeletedService ext
final String accountName = intent.getStringExtra(
FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_KEY);
if (accountName == null) {
Logger.warn(LOG_TAG, "Intent malformed: no account name given. Not cleaning up after " +
"deleted Account.");
return;
}
+ clearRemoteDevicesList(intent, context);
+
// Delete current device the from FxA devices list.
deleteFxADevice(intent);
// Fire up gecko and unsubscribe push
final Intent geckoIntent = new Intent();
geckoIntent.setAction("create-services");
geckoIntent.setClassName(context, "org.mozilla.gecko.GeckoService");
geckoIntent.putExtra("category", "android-push-service");
@@ -154,16 +159,27 @@ public class FxAccountDeletedService ext
Logger.error(LOG_TAG, "Exception during cached OAuth token deletion; ignoring.", e);
}
}
} else {
Logger.error(LOG_TAG, "Cached OAuth server URI is null or cached OAuth tokens are null; ignoring.");
}
}
+ private void clearRemoteDevicesList(Intent intent, Context context) {
+ final Uri remoteDevicesUriWithProfile = BrowserContract.RemoteDevices.CONTENT_URI
+ .buildUpon()
+ .appendQueryParameter(BrowserContract.PARAM_PROFILE,
+ intent.getStringExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_PROFILE))
+ .build();
+ ContentResolver cr = context.getContentResolver();
+
+ cr.delete(remoteDevicesUriWithProfile, null, null);
+ }
+
// Remove our current device from the FxA device list.
private void deleteFxADevice(Intent intent) {
final byte[] sessionToken = intent.getByteArrayExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_SESSION_TOKEN);
if (sessionToken == null) {
Logger.warn(LOG_TAG, "Empty session token, skipping FxA device destruction.");
return;
}
final String deviceId = intent.getStringExtra(FxAccountConstants.ACCOUNT_DELETED_INTENT_ACCOUNT_DEVICE_ID);
--- 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
@@ -1,32 +1,34 @@
/* 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.sync;
import android.accounts.Account;
import android.content.AbstractThreadedSyncAdapter;
+import android.content.ContentProvider;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SyncResult;
import android.os.Bundle;
import android.os.SystemClock;
import android.text.TextUtils;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.common.telemetry.TelemetryWrapper;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.background.fxa.SkewHandler;
import org.mozilla.gecko.browserid.JSONWebTokenUtils;
import org.mozilla.gecko.fxa.FirefoxAccounts;
import org.mozilla.gecko.fxa.FxAccountConstants;
+import org.mozilla.gecko.fxa.devices.FxAccountDeviceListUpdater;
import org.mozilla.gecko.fxa.devices.FxAccountDeviceRegistrator;
import org.mozilla.gecko.fxa.authenticator.AccountPickler;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.authenticator.FxADefaultLoginStateMachineDelegate;
import org.mozilla.gecko.fxa.authenticator.FxAccountAuthenticator;
import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine;
import org.mozilla.gecko.fxa.login.Married;
import org.mozilla.gecko.fxa.login.State;
@@ -413,16 +415,25 @@ public class FxAccountSyncAdapter extend
// 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.shouldRenewRegistration(fxAccount)) {
FxAccountDeviceRegistrator.renewRegistration(context);
}
}
+ 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();
+ }
+
/**
* 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.
*/
@Override
@@ -540,17 +551,17 @@ public class FxAccountSyncAdapter extend
final FxAccountLoginStateMachine stateMachine = new FxAccountLoginStateMachine();
stateMachine.advance(state, StateLabel.Married, new FxADefaultLoginStateMachineDelegate(context, fxAccount) {
@Override
public void handleNotMarried(State notMarried) {
Logger.info(LOG_TAG, "handleNotMarried: in " + notMarried.getStateLabel());
schedulePolicy.onHandleFinal(notMarried.getNeededAction());
syncDelegate.handleCannotSync(notMarried);
if (notMarried.getStateLabel() == StateLabel.Engaged) {
- maybeRegisterDevice(context, fxAccount);
+ onSessionTokenStateReached(context, fxAccount);
}
}
private boolean shouldRequestToken(final BackoffHandler tokenBackoffHandler, final Bundle extras) {
return shouldPerformSync(tokenBackoffHandler, "token", extras);
}
@Override
@@ -583,25 +594,25 @@ public class FxAccountSyncAdapter extend
// in the Married state, so instead we simply do this here, once.
final BackoffHandler tokenBackoffHandler = new PrefsBackoffHandler(sharedPrefs, "token");
if (!shouldRequestToken(tokenBackoffHandler, extras)) {
Logger.info(LOG_TAG, "Not syncing (token server).");
syncDelegate.postponeSync(tokenBackoffHandler.delayMilliseconds());
return;
}
+ onSessionTokenStateReached(context, fxAccount);
+
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);
- maybeRegisterDevice(context, fxAccount);
-
// 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;
}
}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/devices/TestFxAccountDeviceListUpdater.java
@@ -0,0 +1,188 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.fxa.devices;
+
+import android.content.ContentProviderClient;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.RemoteException;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mozilla.gecko.background.db.DelegatingTestContentProvider;
+import org.mozilla.gecko.background.fxa.FxAccountClient;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.db.BrowserProvider;
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+import org.mozilla.gecko.fxa.login.State;
+import org.robolectric.shadows.ShadowContentResolver;
+
+import java.util.List;
+
+import static java.util.Objects.deepEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@RunWith(TestRunner.class)
+public class TestFxAccountDeviceListUpdater {
+
+ @Before
+ public void init() {
+ // Process Mockito annotations
+ MockitoAnnotations.initMocks(this);
+ fxaDevicesUpdater = spy(new FxAccountDeviceListUpdater(fxAccount, contentResolver));
+ }
+
+ FxAccountDeviceListUpdater fxaDevicesUpdater;
+
+ @Mock
+ AndroidFxAccount fxAccount;
+
+ @Mock
+ State state;
+
+ @Mock
+ ContentResolver contentResolver;
+
+ @Mock
+ FxAccountClient fxaClient;
+
+ @Test
+ public void testUpdate() throws Throwable {
+ byte[] token = "usertoken".getBytes();
+
+ when(fxAccount.getState()).thenReturn(state);
+ when(state.getSessionToken()).thenReturn(token);
+ doReturn(fxaClient).when(fxaDevicesUpdater).getSynchronousFxaClient();
+
+ 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");
+ FxAccountDevice device2 = new FxAccountDevice("Desktop PC", "deviceid2", "desktop", true, System.currentTimeMillis(),
+ "https://localhost/push/callback2", "abc123", "321cba");
+ result[0] = device1;
+ result[1] = device2;
+
+ when(fxAccount.getProfile()).thenReturn("default");
+
+ long timeBeforeCall = System.currentTimeMillis();
+ fxaDevicesUpdater.handleSuccess(result);
+
+ ArgumentCaptor<Bundle> captor = ArgumentCaptor.forClass(Bundle.class);
+ verify(contentResolver).call(any(Uri.class), eq(BrowserContract.METHOD_REPLACE_REMOTE_CLIENTS), anyString(), captor.capture());
+ List<Bundle> allArgs = captor.getAllValues();
+ assertTrue(allArgs.size() == 1);
+ ContentValues[] allValues = (ContentValues[]) allArgs.get(0).getParcelableArray(BrowserContract.METHOD_PARAM_DATA);
+
+ ContentValues firstDevice = allValues[0];
+ checkInsertDeviceContentValues(device1, firstDevice, timeBeforeCall);
+ ContentValues secondDevice = allValues[1];
+ checkInsertDeviceContentValues(device2, secondDevice, timeBeforeCall);
+ }
+
+ private void checkInsertDeviceContentValues(FxAccountDevice device, ContentValues firstDevice, long timeBeforeCall) {
+ assertEquals(firstDevice.getAsString(BrowserContract.RemoteDevices.GUID), device.id);
+ assertEquals(firstDevice.getAsString(BrowserContract.RemoteDevices.TYPE), device.type);
+ assertEquals(firstDevice.getAsString(BrowserContract.RemoteDevices.NAME), device.name);
+ assertEquals(firstDevice.getAsBoolean(BrowserContract.RemoteDevices.IS_CURRENT_DEVICE), device.isCurrentDevice);
+ deepEquals(firstDevice.getAsString(BrowserContract.RemoteDevices.LAST_ACCESS_TIME), device.lastAccessTime);
+ assertTrue(firstDevice.getAsLong(BrowserContract.RemoteDevices.DATE_CREATED) < timeBeforeCall + 10000); // Give 10 secs of leeway
+ assertTrue(firstDevice.getAsLong(BrowserContract.RemoteDevices.DATE_MODIFIED) < timeBeforeCall + 10000);
+ assertEquals(firstDevice.getAsString(BrowserContract.RemoteDevices.NAME), device.name);
+ }
+
+ @Test
+ public void testBrowserProvider() {
+ Uri uri = testUri(BrowserContract.RemoteDevices.CONTENT_URI);
+
+ BrowserProvider provider = new BrowserProvider();
+ Cursor c = null;
+ try {
+ provider.onCreate();
+ ShadowContentResolver.registerProvider(BrowserContract.AUTHORITY, new DelegatingTestContentProvider(provider));
+
+ final ShadowContentResolver cr = new ShadowContentResolver();
+ ContentProviderClient remoteDevicesClient = cr.acquireContentProviderClient(BrowserContract.RemoteDevices.CONTENT_URI);
+
+ // First let's insert a client for initial state.
+
+ Bundle bundle = new Bundle();
+ ContentValues device1 = createMockRemoteClientValues("device1");
+ bundle.putParcelableArray(BrowserContract.METHOD_PARAM_DATA, new ContentValues[] { device1 });
+
+ remoteDevicesClient.call(BrowserContract.METHOD_REPLACE_REMOTE_CLIENTS, uri.toString(), bundle);
+
+ c = remoteDevicesClient.query(uri, null, null, null, "name ASC");
+ assertEquals(c.getCount(), 1);
+ c.moveToFirst();
+ int nameCol = c.getColumnIndexOrThrow("name");
+ assertEquals(c.getString(nameCol), "device1");
+ c.close();
+
+ // Then we replace our remote clients list with a new one.
+
+ bundle = new Bundle();
+ ContentValues device2 = createMockRemoteClientValues("device2");
+ ContentValues device3 = createMockRemoteClientValues("device3");
+ bundle.putParcelableArray(BrowserContract.METHOD_PARAM_DATA, new ContentValues[] { device2, device3 });
+
+ remoteDevicesClient.call(BrowserContract.METHOD_REPLACE_REMOTE_CLIENTS, uri.toString(), bundle);
+
+ c = remoteDevicesClient.query(uri, null, null, null, "name ASC");
+ assertEquals(c.getCount(), 2);
+ c.moveToFirst();
+ nameCol = c.getColumnIndexOrThrow("name");
+ assertEquals(c.getString(nameCol), "device2");
+ c.moveToNext();
+ assertEquals(c.getString(nameCol), "device3");
+ c.close();
+ } catch (RemoteException e) {
+ fail(e.getMessage());
+ } finally {
+ if (c != null && !c.isClosed()) {
+ c.close();
+ }
+ provider.shutdown();
+ }
+ }
+
+ private Uri testUri(Uri baseUri) {
+ return baseUri.buildUpon().appendQueryParameter(BrowserContract.PARAM_IS_TEST, "1").build();
+ }
+
+ private ContentValues createMockRemoteClientValues(String name) {
+ final long now = System.currentTimeMillis();
+ ContentValues cli = new ContentValues();
+ cli.put(BrowserContract.RemoteDevices.GUID, "R" + Math.floor(Math.random() * 10));
+ cli.put(BrowserContract.RemoteDevices.NAME, name);
+ cli.put(BrowserContract.RemoteDevices.TYPE, "mobile");
+ cli.put(BrowserContract.RemoteDevices.IS_CURRENT_DEVICE, false);
+ cli.put(BrowserContract.RemoteDevices.LAST_ACCESS_TIME, System.currentTimeMillis());
+ cli.put(BrowserContract.RemoteDevices.DATE_CREATED, now);
+ cli.put(BrowserContract.RemoteDevices.DATE_MODIFIED, now);
+ return cli;
+ }
+}
--- 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
@@ -174,27 +174,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);
+ FxAccountDevice device = new FxAccountDevice(deviceToRegister.name, deviceId, deviceToRegister.type, null, null, null, null, null);
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.pushCallback, existingDevice.pushPublicKey,existingDevice.pushAuthKey);
+ FxAccountDevice device = new FxAccountDevice(deviceName, existingDevice.id, existingDevice.type, existingDevice.isCurrentDevice,
+ existingDevice.lastAccessTime, existingDevice.pushCallback, existingDevice.pushPublicKey,existingDevice.pushAuthKey);
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);