From dd3f4bde18b75cfa68b7e9241017af9d5f8e9a24 Mon Sep 17 00:00:00 2001 From: Edouard Oger Date: Wed, 19 Apr 2017 17:45:49 -0400 Subject: [PATCH] Bug 1351805 part 3 - Refresh the remote devices list on Married/Engaged states. r=Grisha MozReview-Commit-ID: 1Ktbtlzc1fI --HG-- extra : rebase_source : 2d52926ee1ba8511a32b5a9cfdc13cd04ef4bbb8 --- mobile/android/base/android-services.mozbuild | 1 + .../fxa/authenticator/AndroidFxAccount.java | 26 ++- .../gecko/fxa/devices/FxAccountDevice.java | 45 +++-- .../devices/FxAccountDeviceListUpdater.java | 111 +++++++++++ .../devices/FxAccountDeviceRegistrator.java | 5 +- .../receivers/FxAccountDeletedService.java | 16 ++ .../gecko/fxa/sync/FxAccountSyncAdapter.java | 17 +- .../TestFxAccountDeviceListUpdater.java | 188 ++++++++++++++++++ .../gecko/fxa/login/MockFxAccountClient.java | 6 +- 9 files changed, 376 insertions(+), 39 deletions(-) create mode 100644 mobile/android/services/src/main/java/org/mozilla/gecko/fxa/devices/FxAccountDeviceListUpdater.java create mode 100644 mobile/android/tests/background/junit4/src/org/mozilla/gecko/fxa/devices/TestFxAccountDeviceListUpdater.java diff --git a/mobile/android/base/android-services.mozbuild b/mobile/android/base/android-services.mozbuild index adf93a62b66e..d81e1b9aaae1 100644 --- a/mobile/android/base/android-services.mozbuild +++ b/mobile/android/base/android-services.mozbuild @@ -832,6 +832,7 @@ sync_java_files = [TOPSRCDIR + '/mobile/android/services/src/main/java/org/mozil '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', diff --git 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 index 9eb4b8538e4d..203259a4067a 100644 --- 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 @@ -805,16 +805,15 @@ public class AndroidFxAccount { }); } - @SuppressWarnings("unchecked") - private 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; } } @@ -825,19 +824,28 @@ public class AndroidFxAccount { } 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) { 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 91a2ecbd5f55..e04da4456b6e 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 @@ -8,42 +8,48 @@ 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() { @@ -73,7 +79,6 @@ public class FxAccountDevice { private String id; private String name; private String type; - private Boolean isCurrentDevice; private String pushCallback; private String pushPublicKey; private String pushAuthKey; @@ -90,10 +95,6 @@ public class FxAccountDevice { this.type = type; } - public void isCurrentDevice() { - this.isCurrentDevice = Boolean.TRUE; - } - public void pushCallback(String pushCallback) { this.pushCallback = pushCallback; } @@ -107,7 +108,7 @@ public class FxAccountDevice { } 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); } } 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 new file mode 100644 index 000000000000..1a48943c5bf4 --- /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 { + 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); + } +} 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 2ec3420e25db..d511ce1330f7 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 @@ -378,8 +378,9 @@ public class FxAccountDeviceRegistrator implements BundleEventListener { 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; } diff --git 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 index e2a58eb1413f..e6a08f887002 100644 --- 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 @@ -5,8 +5,10 @@ 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; @@ -15,6 +17,7 @@ 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; @@ -75,6 +78,8 @@ public class FxAccountDeletedService extends IntentService { return; } + clearRemoteDevicesList(intent, context); + // Delete current device the from FxA devices list. deleteFxADevice(intent); @@ -159,6 +164,17 @@ public class FxAccountDeletedService extends IntentService { } } + 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); 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 b967e5c149f8..340def5d97fe 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 @@ -6,6 +6,7 @@ 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; @@ -22,6 +23,7 @@ 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; @@ -418,6 +420,15 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter { } } + 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. @@ -545,7 +556,7 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter { schedulePolicy.onHandleFinal(notMarried.getNeededAction()); syncDelegate.handleCannotSync(notMarried); if (notMarried.getStateLabel() == StateLabel.Engaged) { - maybeRegisterDevice(context, fxAccount); + onSessionTokenStateReached(context, fxAccount); } } @@ -588,6 +599,8 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter { return; } + onSessionTokenStateReached(context, fxAccount); + final SessionCallback sessionCallback = new SessionCallback(syncDelegate, schedulePolicy); final KeyBundle syncKeyBundle = married.getSyncKeyBundle(); final String clientState = married.getClientState(); @@ -595,8 +608,6 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter { 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(); 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 new file mode 100644 index 000000000000..1e122ef18556 --- /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 captor = ArgumentCaptor.forClass(Bundle.class); + verify(contentResolver).call(any(Uri.class), eq(BrowserContract.METHOD_REPLACE_REMOTE_CLIENTS), anyString(), captor.capture()); + List 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; + } +} 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 d10c3c932ddd..806568dc672f 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 @@ -179,7 +179,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); + 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); @@ -188,8 +188,8 @@ public class MockFxAccountClient implements FxAccountClient { 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");