summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authornyquist@chromium.org <nyquist@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-09-04 19:01:37 +0000
committernyquist@chromium.org <nyquist@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2013-09-04 19:01:37 +0000
commited4d7052c09336e1df60a0b21f3cf6e3c57327e4 (patch)
tree7865e5da1526017e9b39d029e815b0afcdb5c0fa
parent7b0dc98917f4113d85af354d10fb64c7efaaf4b1 (diff)
downloadchromium_src-ed4d7052c09336e1df60a0b21f3cf6e3c57327e4.zip
chromium_src-ed4d7052c09336e1df60a0b21f3cf6e3c57327e4.tar.gz
chromium_src-ed4d7052c09336e1df60a0b21f3cf6e3c57327e4.tar.bz2
Add more control over sync for Chromium testshell.
* Adds more functionality to the Chromium testshell SyncController. * Enables GCM for sync. * Upstream sync test framework and chrome://sync tests. * Adds auto-login to Chromium testshell. BUG=272584 Review URL: https://chromiumcodereview.appspot.com/22914014 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@221234 0039d316-1c4b-4281-b951-d872f2087c98
-rwxr-xr-xchrome/android/host_driven_tests/SyncTest.py55
-rw-r--r--chrome/android/java/src/org/chromium/chrome/browser/identity/UuidBasedUniqueIdentificationGenerator.java1
-rw-r--r--chrome/android/java/src/org/chromium/chrome/browser/sync/ProfileSyncService.java11
-rw-r--r--chrome/android/javatests/src/org/chromium/chrome/browser/sync/SyncTest.java188
-rw-r--r--chrome/android/testshell/java/AndroidManifest.xml94
-rw-r--r--chrome/android/testshell/java/src/org/chromium/chrome/testshell/ChromiumTestShellActivity.java9
-rw-r--r--chrome/android/testshell/java/src/org/chromium/chrome/testshell/sync/SyncController.java107
-rw-r--r--chrome/browser/sync/profile_sync_service_android.cc9
-rw-r--r--chrome/browser/sync/profile_sync_service_android.h4
-rw-r--r--chrome/chrome_tests.gypi2
-rw-r--r--chrome/test/android/DEPS5
-rw-r--r--chrome/test/android/javatests/src/org/chromium/chrome/test/util/browser/sync/SyncTestUtil.java345
12 files changed, 806 insertions, 24 deletions
diff --git a/chrome/android/host_driven_tests/SyncTest.py b/chrome/android/host_driven_tests/SyncTest.py
new file mode 100755
index 0000000..a270cbb
--- /dev/null
+++ b/chrome/android/host_driven_tests/SyncTest.py
@@ -0,0 +1,55 @@
+#!/usr/bin/python
+# Copyright 2013 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Host-driven Java tests which exercise sync functionality."""
+
+from pylib import constants
+from pylib.host_driven import test_case
+from pylib.host_driven import test_server
+from pylib.host_driven import tests_annotations
+
+
+class SyncTest(test_case.HostDrivenTestCase):
+ """Host-driven Java tests which exercise sync functionality."""
+
+ def __init__(self, *args, **kwargs):
+ super(SyncTest, self).__init__(*args, **kwargs)
+ self.test_server = None
+ self.additional_flags = []
+
+ def SetUp(self, device, shard_index, push_deps, cleanup_test_files):
+ super(SyncTest, self).SetUp(device, shard_index, push_deps,
+ cleanup_test_files)
+ self.test_server = test_server.TestServer(
+ shard_index,
+ constants.TEST_SYNC_SERVER_PORT,
+ test_server.TEST_SYNC_SERVER_PATH)
+ # These ought not to change in the middle of a test for obvious reasons.
+ self.additional_flags = [
+ '--sync-url=http://%s:%d/chromiumsync' %
+ (self.test_server.host, self.test_server.port)]
+ self.ports_to_forward = [self.test_server.port]
+
+ def TearDown(self):
+ self.test_server.TearDown()
+ super(SyncTest, self).TearDown()
+
+ def _RunSyncTests(self, test_names):
+ full_names = []
+ for test_name in test_names:
+ full_names.append('SyncTest.' + test_name)
+ return self._RunJavaTestFilters(full_names, self.additional_flags)
+
+ @tests_annotations.Feature(['Sync'])
+ @tests_annotations.EnormousTest
+ def testGetAboutSyncInfoYieldsValidData(self):
+ java_tests = ['testGetAboutSyncInfoYieldsValidData']
+ return self._RunSyncTests(java_tests)
+
+ @tests_annotations.Feature(['Sync'])
+ @tests_annotations.EnormousTest
+ def testAboutSyncPageDisplaysCurrentSyncStatus(self):
+ java_tests = ['testAboutSyncPageDisplaysCurrentSyncStatus']
+ return self._RunSyncTests(java_tests)
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/identity/UuidBasedUniqueIdentificationGenerator.java b/chrome/android/java/src/org/chromium/chrome/browser/identity/UuidBasedUniqueIdentificationGenerator.java
index 268813b..0bea1ab 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/identity/UuidBasedUniqueIdentificationGenerator.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/identity/UuidBasedUniqueIdentificationGenerator.java
@@ -18,6 +18,7 @@ import javax.annotation.Nullable;
* Generates unique IDs that are {@link UUID} strings.
*/
public class UuidBasedUniqueIdentificationGenerator implements UniqueIdentificationGenerator {
+ public static final String GENERATOR_ID = "UUID";
private final Context mContext;
private final String mPreferenceKey;
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/sync/ProfileSyncService.java b/chrome/android/java/src/org/chromium/chrome/browser/sync/ProfileSyncService.java
index f202ef7..4e846c0 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/sync/ProfileSyncService.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/sync/ProfileSyncService.java
@@ -489,6 +489,16 @@ public class ProfileSyncService {
nativeDisableSync(mNativeProfileSyncServiceAndroid);
}
+ /**
+ * Returns the time when the last sync cycle was completed.
+ *
+ * @return The difference measured in microseconds, between last sync cycle completion time
+ * and 1 January 1970 00:00:00 UTC.
+ */
+ public long getLastSyncedTimeForTest() {
+ return nativeGetLastSyncedTimeForTest(mNativeProfileSyncServiceAndroid);
+ }
+
// Native methods
private native void nativeNudgeSyncer(
int nativeProfileSyncServiceAndroid, String objectId, long version, String payload);
@@ -539,4 +549,5 @@ public class ProfileSyncService {
private native boolean nativeHasKeepEverythingSynced(int nativeProfileSyncServiceAndroid);
private native boolean nativeHasUnrecoverableError(int nativeProfileSyncServiceAndroid);
private native String nativeGetAboutInfoForTest(int nativeProfileSyncServiceAndroid);
+ private native long nativeGetLastSyncedTimeForTest(int nativeProfileSyncServiceAndroid);
}
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/sync/SyncTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/sync/SyncTest.java
new file mode 100644
index 0000000..8645404
--- /dev/null
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/sync/SyncTest.java
@@ -0,0 +1,188 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.sync;
+
+import android.accounts.Account;
+import android.app.Activity;
+import android.content.Context;
+import android.util.Log;
+
+import org.chromium.base.ThreadUtils;
+import org.chromium.base.test.util.HostDrivenTest;
+import org.chromium.chrome.browser.identity.UniqueIdentificationGenerator;
+import org.chromium.chrome.browser.identity.UniqueIdentificationGeneratorFactory;
+import org.chromium.chrome.browser.identity.UuidBasedUniqueIdentificationGenerator;
+import org.chromium.chrome.test.util.browser.sync.SyncTestUtil;
+import org.chromium.chrome.testshell.ChromiumTestShellActivity;
+import org.chromium.chrome.testshell.ChromiumTestShellTestBase;
+import org.chromium.chrome.testshell.sync.SyncController;
+import org.chromium.content.browser.BrowserStartupController;
+import org.chromium.content.browser.ContentView;
+import org.chromium.content.browser.ContentViewCore;
+import org.chromium.content.browser.test.util.Criteria;
+import org.chromium.content.browser.test.util.CriteriaHelper;
+import org.chromium.content.browser.test.util.JavaScriptUtils;
+import org.chromium.content.browser.test.util.TestCallbackHelperContainer;
+import org.chromium.content.common.CommandLine;
+import org.chromium.sync.notifier.SyncStatusHelper;
+import org.chromium.sync.signin.AccountManagerHelper;
+import org.chromium.sync.signin.ChromeSigninController;
+import org.chromium.sync.test.util.MockAccountManager;
+import org.chromium.sync.test.util.MockSyncContentResolverDelegate;
+
+import java.lang.Override;
+import java.lang.Runnable;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Test suite for Sync.
+ */
+public class SyncTest extends ChromiumTestShellTestBase {
+ private static final String TAG = "SyncTest";
+
+ private static final String FOREIGN_SESSION_TEST_MACHINE_ID =
+ "DeleteForeignSessionTest_Machine_1";
+
+ private SyncTestUtil.SyncTestContext mContext;
+ private MockAccountManager mAccountManager;
+ private SyncController mSyncController;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ clearAppData();
+
+ // Mock out the account manager on the device.
+ mContext = new SyncTestUtil.SyncTestContext(getInstrumentation().getTargetContext());
+ mAccountManager = new MockAccountManager(mContext, getInstrumentation().getContext());
+ AccountManagerHelper.overrideAccountManagerHelperForTests(mContext, mAccountManager);
+ MockSyncContentResolverDelegate syncContentResolverDelegate =
+ new MockSyncContentResolverDelegate();
+ syncContentResolverDelegate.setMasterSyncAutomatically(true);
+ SyncStatusHelper.overrideSyncStatusHelperForTests(mContext, syncContentResolverDelegate);
+ // This call initializes the ChromeSigninController to use our test context.
+ ChromeSigninController.get(mContext);
+ startChromeBrowserProcessSync(getInstrumentation().getTargetContext());
+ ThreadUtils.runOnUiThreadBlocking(new Runnable() {
+ @Override
+ public void run() {
+ mSyncController = SyncController.get(mContext);
+ }
+ });
+ SyncTestUtil.verifySyncServerIsRunning();
+ }
+
+ private static void startChromeBrowserProcessSync(final Context targetContext) {
+ ThreadUtils.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ CommandLine.initFromFile("/data/local/tmp/chromium-testshell-command-line");
+ BrowserStartupController.get(targetContext).startBrowserProcessesSync(
+ BrowserStartupController.MAX_RENDERERS_LIMIT);
+ }
+ });
+ }
+
+ @HostDrivenTest
+ public void testGetAboutSyncInfoYieldsValidData() throws Throwable {
+ setupTestAccountAndSignInToSync(FOREIGN_SESSION_TEST_MACHINE_ID);
+
+ final SyncTestUtil.AboutSyncInfoGetter syncInfoGetter =
+ new SyncTestUtil.AboutSyncInfoGetter(getActivity());
+ runTestOnUiThread(syncInfoGetter);
+
+ boolean gotInfo = CriteriaHelper.pollForCriteria(new Criteria() {
+ @Override
+ public boolean isSatisfied() {
+ return !syncInfoGetter.getAboutInfo().isEmpty();
+ }
+ }, SyncTestUtil.UI_TIMEOUT_MS, SyncTestUtil.CHECK_INTERVAL_MS);
+
+ assertTrue("Couldn't get about info.", gotInfo);
+ }
+
+ @HostDrivenTest
+ public void testAboutSyncPageDisplaysCurrentSyncStatus() throws InterruptedException {
+ setupTestAccountAndSignInToSync(FOREIGN_SESSION_TEST_MACHINE_ID);
+
+ loadUrlWithSanitization("chrome://sync");
+ SyncTestUtil.AboutSyncInfoGetter aboutInfoGetter =
+ new SyncTestUtil.AboutSyncInfoGetter(getActivity());
+ try {
+ runTestOnUiThread(aboutInfoGetter);
+ } catch (Throwable t) {
+ Log.w(TAG,
+ "Exception while trying to fetch about sync info from ProfileSyncService.", t);
+ fail("Unable to fetch sync info from ProfileSyncService.");
+ }
+ assertFalse("About sync info should not be empty.",
+ aboutInfoGetter.getAboutInfo().isEmpty());
+ assertTrue("About sync info should have sync summary status.",
+ aboutInfoGetter.getAboutInfo().containsKey(SyncTestUtil.SYNC_SUMMARY_STATUS));
+ final String expectedSyncSummary =
+ aboutInfoGetter.getAboutInfo().get(SyncTestUtil.SYNC_SUMMARY_STATUS);
+
+ Criteria checker = new Criteria() {
+ @Override
+ public boolean isSatisfied() {
+ final ContentViewCore contentViewCore = getContentViewCore(getActivity());
+ String innerHtml = "";
+ try {
+ final TestCallbackHelperContainer.OnEvaluateJavaScriptResultHelper helper =
+ new TestCallbackHelperContainer.OnEvaluateJavaScriptResultHelper();
+ innerHtml = JavaScriptUtils.executeJavaScriptAndWaitForResult(
+ contentViewCore, helper, "document.documentElement.innerHTML");
+ } catch (InterruptedException e) {
+ Log.w(TAG, "Interrupted while polling about:sync page for sync status.", e);
+ } catch (TimeoutException e) {
+ Log.w(TAG, "Interrupted while polling about:sync page for sync status.", e);
+ }
+ return innerHtml.contains(expectedSyncSummary);
+ }
+
+ };
+ boolean hadExpectedStatus = CriteriaHelper.pollForCriteria(
+ checker, SyncTestUtil.UI_TIMEOUT_MS, SyncTestUtil.CHECK_INTERVAL_MS);
+ assertTrue("Sync status not present on about sync page: " + expectedSyncSummary,
+ hadExpectedStatus);
+ }
+
+ private void setupTestAccountAndSignInToSync(
+ final String syncClientIdentifier)
+ throws InterruptedException {
+ Account defaultTestAccount = SyncTestUtil.setupTestAccount(mAccountManager,
+ SyncTestUtil.DEFAULT_TEST_ACCOUNT, SyncTestUtil.DEFAULT_PASSWORD,
+ SyncTestUtil.CHROME_SYNC_OAUTH2_SCOPE, SyncTestUtil.LOGIN_OAUTH2_SCOPE,
+ SyncStatusHelper.AUTH_TOKEN_TYPE_SYNC);
+
+ UniqueIdentificationGeneratorFactory.registerGenerator(
+ UuidBasedUniqueIdentificationGenerator.GENERATOR_ID,
+ new UniqueIdentificationGenerator() {
+ @Override
+ public String getUniqueId(String salt) {
+ return syncClientIdentifier;
+ }
+ }, true);
+
+ SyncTestUtil.verifySyncIsSignedOut(getActivity());
+
+ final Activity activity = launchChromiumTestShellWithBlankPage();
+ ThreadUtils.runOnUiThreadBlocking(new Runnable() {
+ @Override
+ public void run() {
+ mSyncController.signIn(activity, SyncTestUtil.DEFAULT_TEST_ACCOUNT);
+ }
+ });
+
+ SyncTestUtil.verifySyncIsSignedIn(mContext, defaultTestAccount);
+ }
+
+ private static ContentViewCore getContentViewCore(ChromiumTestShellActivity activity) {
+ ContentView contentView = activity.getActiveContentView();
+ if (contentView == null) return null;
+ return contentView.getContentViewCore();
+ }
+}
diff --git a/chrome/android/testshell/java/AndroidManifest.xml b/chrome/android/testshell/java/AndroidManifest.xml
index 4d8f7a5..050e9db 100644
--- a/chrome/android/testshell/java/AndroidManifest.xml
+++ b/chrome/android/testshell/java/AndroidManifest.xml
@@ -9,8 +9,27 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.chromium.chrome.testshell">
+ <uses-sdk android:minSdkVersion="14" android:targetSdkVersion="17" />
<permission android:name="org.chromium.chrome.testshell.permission.SANDBOX"
android:protectionLevel="signature" />
+ <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+ <uses-permission android:name="android.permission.CAMERA" />
+ <uses-permission android:name="android.permission.GET_ACCOUNTS"/>
+ <uses-permission android:name="android.permission.INTERNET"/>
+ <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
+ <uses-permission android:name="android.permission.RECORD_AUDIO"/>
+ <uses-permission android:name="android.permission.VIBRATE"/>
+ <uses-permission android:name="android.permission.WAKE_LOCK"/>
+ <uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/>
+ <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
+ <uses-permission android:name="android.permission.USE_CREDENTIALS" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+ <!-- Only Chrome can receive the messages and registration result for GCM -->
+ <permission android:name="org.chromium.chrome.testshell.permission.C2D_MESSAGE"
+ android:protectionLevel="signature" />
+ <uses-permission android:name="org.chromium.chrome.testshell.permission.C2D_MESSAGE" />
+ <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<application android:name="org.chromium.chrome.testshell.ChromiumTestShellApplication"
android:label="ChromiumTestShell">
@@ -25,6 +44,13 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
+ <activity android:name="org.chromium.sync.test.util.MockGrantCredentialsPermissionActivity"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
<!-- The following service entries exist in order to allow us to
start more than one sandboxed process. -->
@@ -96,6 +122,55 @@
android:isolatedProcess="true"
android:exported="false" />
+ <!-- Receiver for GCM messages. Rebroadcasts them locally for sync. -->
+ <receiver android:exported="true"
+ android:name="com.google.ipc.invalidation.external.client.contrib.MultiplexingGcmListener$GCMReceiver"
+ android:permission="com.google.android.c2dm.permission.SEND">
+ <intent-filter>
+ <action android:name="com.google.android.c2dm.intent.RECEIVE" />
+ <action android:name="com.google.android.c2dm.intent.REGISTRATION" />
+ <category android:name="org.chromium.chrome.testshell"/>
+ </intent-filter>
+ </receiver>
+ <service android:exported="false"
+ android:name="com.google.ipc.invalidation.external.client.contrib.MultiplexingGcmListener">
+ <meta-data android:name="sender_ids"
+ android:value="cloudprint.c2dm@gmail.com,ipc.invalidation@gmail.com"/>
+ </service>
+
+ <!-- Notification service for sync. -->
+ <meta-data android:name="ipc.invalidation.ticl.listener_service_class"
+ android:value="org.chromium.sync.notifier.InvalidationService"/>
+ <service android:name="org.chromium.sync.notifier.InvalidationService"
+ android:exported="false">
+ <intent-filter>
+ <action android:name="com.google.ipc.invalidation.AUTH_TOKEN_REQUEST"/>
+ </intent-filter>
+ </service>
+ <service android:exported="false"
+ android:name="com.google.ipc.invalidation.ticl.android2.TiclService"/>
+ <service android:exported="false"
+ android:name="com.google.ipc.invalidation.ticl.android2.channel.AndroidMessageSenderService"/>
+ <receiver android:exported="false"
+ android:name="com.google.ipc.invalidation.ticl.android2.AndroidInternalScheduler$AlarmReceiver"/>
+ <receiver android:exported="false"
+ android:name="com.google.ipc.invalidation.external.client.contrib.AndroidListener$AlarmReceiver"/>
+
+ <!-- Notification service multiplexed GCM receiver -->
+ <service android:exported="false"
+ android:name="com.google.ipc.invalidation.ticl.android2.channel.AndroidMessageReceiverService"
+ android:enabled="true"/>
+ <receiver android:exported="false"
+ android:name="com.google.ipc.invalidation.ticl.android2.channel.AndroidMessageReceiverService$Receiver">
+ <intent-filter>
+ <action android:name="com.google.ipc.invalidation.gcmmplex.EVENT" />
+ </intent-filter>
+ </receiver>
+
+ <provider android:name="org.chromium.chrome.browser.ChromeBrowserProvider"
+ android:authorities="org.chromium.chrome.testshell"
+ android:exported="true" />
+
<!-- Sync adapter for browser sync. -->
<service android:exported="false"
android:name="org.chromium.chrome.testshell.sync.ChromiumTestShellSyncAdapterService">
@@ -105,24 +180,5 @@
<meta-data android:name="android.content.SyncAdapter"
android:resource="@xml/syncadapter" />
</service>
-
- <!-- Name of the class implementing the invalidation client, for sync notifications. -->
- <meta-data android:name="org.chromium.sync.notifier.IMPLEMENTING_CLASS_NAME"
- android:value="org.chromium.sync.notifier.TEST_VALUE" />
</application>
-
- <uses-sdk android:minSdkVersion="14" android:targetSdkVersion="17" />
- <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
- <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
- <uses-permission android:name="android.permission.CAMERA" />
- <uses-permission android:name="android.permission.GET_ACCOUNTS"/>
- <uses-permission android:name="android.permission.INTERNET"/>
- <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
- <uses-permission android:name="android.permission.RECORD_AUDIO"/>
- <uses-permission android:name="android.permission.VIBRATE"/>
- <uses-permission android:name="android.permission.WAKE_LOCK"/>
- <uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/>
- <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
- <uses-permission android:name="android.permission.USE_CREDENTIALS" />
- <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
</manifest>
diff --git a/chrome/android/testshell/java/src/org/chromium/chrome/testshell/ChromiumTestShellActivity.java b/chrome/android/testshell/java/src/org/chromium/chrome/testshell/ChromiumTestShellActivity.java
index fe7a904..8672ba9 100644
--- a/chrome/android/testshell/java/src/org/chromium/chrome/testshell/ChromiumTestShellActivity.java
+++ b/chrome/android/testshell/java/src/org/chromium/chrome/testshell/ChromiumTestShellActivity.java
@@ -50,6 +50,7 @@ public class ChromiumTestShellActivity extends ChromiumActivity implements MenuH
private WindowAndroid mWindow;
private TabManager mTabManager;
private DevToolsServer mDevToolsServer;
+ private SyncController mSyncController;
@Override
protected void onCreate(final Bundle savedInstanceState) {
@@ -95,6 +96,10 @@ public class ChromiumTestShellActivity extends ChromiumActivity implements MenuH
mDevToolsServer = new DevToolsServer("chromium_testshell");
mDevToolsServer.setRemoteDebuggingEnabled(true);
+ mSyncController = SyncController.get(this);
+ // In case this method is called after the first onResume(), we need to inform the
+ // SyncController that we have resumed.
+ mSyncController.onResume();
}
@Override
@@ -156,6 +161,10 @@ public class ChromiumTestShellActivity extends ChromiumActivity implements MenuH
ContentView view = getActiveContentView();
if (view != null) view.onActivityResume();
+
+ if (mSyncController != null) {
+ mSyncController.onResume();
+ }
}
@Override
diff --git a/chrome/android/testshell/java/src/org/chromium/chrome/testshell/sync/SyncController.java b/chrome/android/testshell/java/src/org/chromium/chrome/testshell/sync/SyncController.java
index 9799ee3..5db791e 100644
--- a/chrome/android/testshell/java/src/org/chromium/chrome/testshell/sync/SyncController.java
+++ b/chrome/android/testshell/java/src/org/chromium/chrome/testshell/sync/SyncController.java
@@ -8,24 +8,46 @@ import android.accounts.Account;
import android.app.Activity;
import android.app.FragmentManager;
import android.content.Context;
+import android.util.Log;
import org.chromium.base.ThreadUtils;
+import org.chromium.chrome.browser.identity.UniqueIdentificationGeneratorFactory;
import org.chromium.chrome.browser.signin.SigninManager;
+import org.chromium.chrome.browser.identity.UuidBasedUniqueIdentificationGenerator;
import org.chromium.chrome.browser.sync.ProfileSyncService;
+import org.chromium.sync.notifier.InvalidationController;
import org.chromium.sync.notifier.SyncStatusHelper;
import org.chromium.sync.signin.AccountManagerHelper;
+import org.chromium.sync.signin.ChromeSigninController;
/**
- * A helper class for signing in and out of Chromium.
+ * A helper class for managing sync state for the ChromiumTestShell.
+ *
+ * Builds on top of the ProfileSyncService (which manages Chrome's sync engine's state) and mimics
+ * the minimum additional functionality needed to fully enable sync for Chrome on Android.
*/
-public class SyncController {
+public class SyncController implements ProfileSyncService.SyncStateChangedListener,
+ SyncStatusHelper.SyncSettingsChangedObserver {
+ private static final String TAG = "SyncController";
+
+ private static final String SESSIONS_UUID_PREF_KEY = "chromium.sync.sessions.id";
private static SyncController sInstance;
private final Context mContext;
+ private final ChromeSigninController mChromeSigninController;
+ private final SyncStatusHelper mSyncStatusHelper;
+ private final ProfileSyncService mProfileSyncService;
private SyncController(Context context) {
mContext = context;
+ mChromeSigninController = ChromeSigninController.get(mContext);
+ mSyncStatusHelper = SyncStatusHelper.get(context);
+ mProfileSyncService = ProfileSyncService.get(mContext);
+ mProfileSyncService.addSyncStateChangedListener(this);
+
+ setupSessionSyncId();
+ mChromeSigninController.ensureGcmIsInitialized();
}
/**
@@ -81,13 +103,88 @@ public class SyncController {
signinManager.startSignIn(activity, account, passive, new SigninManager.Observer() {
@Override
public void onSigninComplete() {
- ProfileSyncService.get(mContext).setSetupInProgress(false);
- // The SigninManager does not control the Android sync state.
- SyncStatusHelper.get(mContext).enableAndroidSync(account);
+ SigninManager.get(mContext).logInSignedInUser();
+ mProfileSyncService.setSetupInProgress(false);
+ mProfileSyncService.syncSignIn();
+ start();
}
@Override
public void onSigninCancelled() {
+ stop();
+ }
+ });
+ }
+
+ public void onResume() {
+ refreshSyncState();
+ }
+
+ private void setupSessionSyncId() {
+ // Ensure that sync uses the correct UniqueIdentificationGenerator, but do not force the
+ // registration, in case a test case has already overridden it.
+ UuidBasedUniqueIdentificationGenerator generator =
+ new UuidBasedUniqueIdentificationGenerator(mContext, SESSIONS_UUID_PREF_KEY);
+ UniqueIdentificationGeneratorFactory.registerGenerator(
+ UuidBasedUniqueIdentificationGenerator.GENERATOR_ID, generator, false);
+ // Since we do not override the UniqueIdentificationGenerator, we get it from the factory,
+ // instead of using the instance we just created.
+ mProfileSyncService.setSessionsId(UniqueIdentificationGeneratorFactory
+ .getInstance(UuidBasedUniqueIdentificationGenerator.GENERATOR_ID));
+ }
+
+ private void refreshSyncState() {
+ if (mSyncStatusHelper.isSyncEnabled())
+ start();
+ else
+ stop();
+ }
+
+ private void start() {
+ ThreadUtils.assertOnUiThread();
+ if (mSyncStatusHelper.isMasterSyncAutomaticallyEnabled()) {
+ Log.d(TAG, "Enabling sync");
+ Account account = mChromeSigninController.getSignedInUser();
+ InvalidationController.get(mContext).start();
+ mProfileSyncService.enableSync();
+ mSyncStatusHelper.enableAndroidSync(account);
+ }
+ }
+
+ private void stop() {
+ ThreadUtils.assertOnUiThread();
+ if (mChromeSigninController.isSignedIn()) {
+ Log.d(TAG, "Disabling sync");
+ Account account = mChromeSigninController.getSignedInUser();
+ InvalidationController.get(mContext).stop();
+ mProfileSyncService.disableSync();
+ mSyncStatusHelper.disableAndroidSync(account);
+ }
+ }
+
+ /**
+ * From {@link ProfileSyncService.SyncStateChangedListener}.
+ */
+ @Override
+ public void syncStateChanged() {
+ ThreadUtils.assertOnUiThread();
+ // If sync has been disabled from the dashboard, we must disable it.
+ Account account = mChromeSigninController.getSignedInUser();
+ boolean isSyncSuppressStart = mProfileSyncService.isStartSuppressed();
+ boolean isSyncEnabled = mSyncStatusHelper.isSyncEnabled(account);
+ if (account != null && isSyncSuppressStart && isSyncEnabled)
+ stop();
+ }
+
+ /**
+ * From {@link SyncStatusHelper.SyncSettingsChangedObserver}.
+ */
+ @Override
+ public void syncSettingsChanged() {
+ ThreadUtils.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ refreshSyncState();
}
});
}
diff --git a/chrome/browser/sync/profile_sync_service_android.cc b/chrome/browser/sync/profile_sync_service_android.cc
index 8894c78..daa4e17 100644
--- a/chrome/browser/sync/profile_sync_service_android.cc
+++ b/chrome/browser/sync/profile_sync_service_android.cc
@@ -471,6 +471,15 @@ ScopedJavaLocalRef<jstring> ProfileSyncServiceAndroid::GetAboutInfoForTest(
return ConvertUTF8ToJavaString(env, about_info_json);
}
+jlong ProfileSyncServiceAndroid::GetLastSyncedTimeForTest(
+ JNIEnv* env, jobject obj) {
+ // Use profile preferences here instead of SyncPrefs to avoid an extra
+ // conversion, since SyncPrefs::GetLastSyncedTime() converts the stored value
+ // to to base::Time.
+ return static_cast<jlong>(
+ profile_->GetPrefs()->GetInt64(prefs::kSyncLastSyncedTime));
+}
+
void ProfileSyncServiceAndroid::NudgeSyncer(JNIEnv* env,
jobject obj,
jstring objectId,
diff --git a/chrome/browser/sync/profile_sync_service_android.h b/chrome/browser/sync/profile_sync_service_android.h
index a269e9d..fc4732e 100644
--- a/chrome/browser/sync/profile_sync_service_android.h
+++ b/chrome/browser/sync/profile_sync_service_android.h
@@ -193,6 +193,10 @@ class ProfileSyncServiceAndroid : public ProfileSyncServiceObserver {
// ProfileSyncServiceObserver:
virtual void OnStateChanged() OVERRIDE;
+ // Returns a timestamp for when a sync was last executed. The return value is
+ // the internal value of base::Time.
+ jlong GetLastSyncedTimeForTest(JNIEnv* env, jobject obj);
+
static ProfileSyncServiceAndroid* GetProfileSyncServiceAndroid();
// Registers the ProfileSyncServiceAndroid's native methods through JNI.
diff --git a/chrome/chrome_tests.gypi b/chrome/chrome_tests.gypi
index bedff9c..22d43ee 100644
--- a/chrome/chrome_tests.gypi
+++ b/chrome/chrome_tests.gypi
@@ -3373,6 +3373,8 @@
'dependencies': [
'chrome_java',
'../content/content.gyp:content_java_test_support',
+ '../sync/sync.gyp:sync_java',
+ '../sync/sync.gyp:sync_java_test_support',
],
'includes': [ '../build/java.gypi' ],
},
diff --git a/chrome/test/android/DEPS b/chrome/test/android/DEPS
new file mode 100644
index 0000000..e635662
--- /dev/null
+++ b/chrome/test/android/DEPS
@@ -0,0 +1,5 @@
+include_rules = [
+ # This test code needs to depend on sync related classes and test tools.
+ "+sync/android/java/src/org/chromium/sync/signin",
+ "+sync/test/android/javatests/src/org/chromium/sync/test/util",
+]
diff --git a/chrome/test/android/javatests/src/org/chromium/chrome/test/util/browser/sync/SyncTestUtil.java b/chrome/test/android/javatests/src/org/chromium/chrome/test/util/browser/sync/SyncTestUtil.java
new file mode 100644
index 0000000..e2a1c7f
--- /dev/null
+++ b/chrome/test/android/javatests/src/org/chromium/chrome/test/util/browser/sync/SyncTestUtil.java
@@ -0,0 +1,345 @@
+// Copyright 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.test.util.browser.sync;
+
+import android.accounts.Account;
+import android.content.Context;
+import android.util.Log;
+import android.util.Pair;
+
+import junit.framework.Assert;
+
+import org.chromium.base.ThreadUtils;
+import org.chromium.base.test.util.AdvancedMockContext;
+import org.chromium.chrome.browser.sync.ProfileSyncService;
+import org.chromium.chrome.test.util.TestHttpServerClient;
+import org.chromium.content.browser.test.util.Criteria;
+import org.chromium.content.browser.test.util.CriteriaHelper;
+import org.chromium.content.common.CommandLine;
+import org.chromium.sync.signin.AccountManagerHelper;
+import org.chromium.sync.signin.ChromeSigninController;
+import org.chromium.sync.test.util.AccountHolder;
+import org.chromium.sync.test.util.MockAccountManager;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+public final class SyncTestUtil {
+
+ public static final String DEFAULT_TEST_ACCOUNT = "test@gmail.com";
+ public static final String DEFAULT_PASSWORD = "myPassword";
+ public static final String CHROME_SYNC_OAUTH2_SCOPE =
+ "oauth2:https://www.googleapis.com/auth/chromesync";
+ public static final String LOGIN_OAUTH2_SCOPE =
+ "oauth2:https://www.google.com/accounts/OAuthLogin";
+ private static final String TAG = "SyncTestUtil";
+
+ public static final int UI_TIMEOUT_MS = 20000;
+ public static final int CHECK_INTERVAL_MS = 250;
+
+ private static final int SYNC_WAIT_TIMEOUT_MS = 30 * 1000;
+ private static final int SYNC_CHECK_INTERVAL_MS = 250;
+
+ public static final Pair<String, String> SYNC_SUMMARY_STATUS =
+ newPair("Summary", "Summary");
+ protected static final String UNINITIALIZED = "Uninitialized";
+ protected static final Pair<String, String> USERNAME_STAT =
+ newPair("Credentials", "Username");
+
+ // Override the default server used for profile sync.
+ // Native switch - chrome_switches::kSyncServiceURL
+ private static final String SYNC_URL = "sync-url";
+
+ private SyncTestUtil() {
+ }
+
+ /**
+ * Creates a Pair of lowercased and trimmed Strings. Makes it easier to avoid running afoul of
+ * case-sensitive comparison since getAboutInfoStats(), et al, use Pair<String, String> as map
+ * keys.
+ */
+ private static Pair<String, String> newPair(String first, String second) {
+ return Pair.create(first.toLowerCase().trim(), second.toLowerCase().trim());
+ }
+
+ /**
+ * Parses raw JSON into a map with keys Pair<String, String>. The first string in each Pair
+ * corresponds to the title under which a given stat_name/stat_value is situated, and the second
+ * contains the name of the actual stat. For example, a stat named "Syncing" which falls under
+ * "Local State" would be a Pair of newPair("Local State", "Syncing").
+ *
+ * @param rawJson the JSON to parse into a map
+ * @return a map containing a mapping of titles and stat names to stat values
+ * @throws org.json.JSONException
+ */
+ public static Map<Pair<String, String>, String> getAboutInfoStats(String rawJson)
+ throws JSONException {
+
+ // What we get back is what you'd get from chrome.sync.aboutInfo at chrome://sync. This is
+ // a JSON object, and we care about the "details" field in that object. "details" itself has
+ // objects with two fields: data and title. The data field itself contains an array of
+ // objects. These objects contains two fields: stat_name and stat_value. Ultimately these
+ // are the values displayed on the page and the values we care about in this method.
+ Map<Pair<String, String>, String> statLookup = new HashMap<Pair<String, String>, String>();
+ JSONObject aboutInfo = new JSONObject(rawJson);
+ JSONArray detailsArray = aboutInfo.getJSONArray("details");
+ for (int i = 0; i < detailsArray.length(); i++) {
+ JSONObject dataObj = detailsArray.getJSONObject(i);
+ String dataTitle = dataObj.getString("title");
+ JSONArray dataArray = dataObj.getJSONArray("data");
+ for (int j = 0; j < dataArray.length(); j++) {
+ JSONObject statObj = dataArray.getJSONObject(j);
+ String statName = statObj.getString("stat_name");
+ Pair<String, String> key = newPair(dataTitle, statName);
+ statLookup.put(key, statObj.getString("stat_value"));
+ }
+ }
+
+ return statLookup;
+ }
+
+ /**
+ * Verifies that sync is signed out and its status is "Syncing not enabled".
+ * TODO(mmontgomery): check whether or not this method is necessary. It queries
+ * syncSummaryStatus(), which is a slightly more direct route than via JSON.
+ */
+ public static void verifySyncIsSignedOut(Context context) {
+ Map<Pair<String, String>, String> expectedStats =
+ new HashMap<Pair<String, String>, String>();
+ expectedStats.put(SYNC_SUMMARY_STATUS, UNINITIALIZED);
+ expectedStats.put(USERNAME_STAT, ""); // Expect an empty username when sync is signed out.
+ Assert.assertTrue("Expected sync to be disabled.",
+ pollAboutSyncStats(context, expectedStats));
+ }
+
+ /**
+ * Polls the stats on about:sync until timeout or all expected stats match actual stats. The
+ * comparison is case insensitive. *All* stats must match those passed in via expectedStats.
+ *
+ *
+ * @param expectedStats a map of stat names to their expected values
+ * @return whether the stats matched up before the timeout
+ */
+ public static boolean pollAboutSyncStats(
+ Context context, final Map<Pair<String, String>, String> expectedStats) {
+ final AboutSyncInfoGetter aboutInfoGetter =
+ new AboutSyncInfoGetter(context);
+
+ Criteria statChecker = new Criteria() {
+ @Override
+ public boolean isSatisfied() {
+ try {
+ ThreadUtils.runOnUiThreadBlocking(aboutInfoGetter);
+ Map<Pair<String, String>, String> actualStats = aboutInfoGetter.getAboutInfo();
+ return areExpectedStatsAmongActual(expectedStats, actualStats);
+ } catch (Throwable e) {
+ Log.w(TAG, "Interrupted while attempting to fetch sync internals info.", e);
+ }
+ return false;
+ }
+ };
+
+ boolean matched = false;
+ try {
+ matched = CriteriaHelper.pollForCriteria(statChecker, UI_TIMEOUT_MS, CHECK_INTERVAL_MS);
+ } catch (InterruptedException e) {
+ Log.w(TAG, "Interrupted while polling sync internals info.", e);
+ Assert.fail("Interrupted while polling sync internals info.");
+ }
+ return matched;
+ }
+
+ /**
+ * Checks whether the expected map's keys and values are a subset of those in another map. Both
+ * keys and values are compared in a case-insensitive fashion.
+ *
+ * @param expectedStats a map which may be a subset of actualSet
+ * @param actualStats a map which may be a superset of expectedSet
+ * @return true if all key/value pairs in expectedSet are in actualSet; false otherwise
+ */
+ private static boolean areExpectedStatsAmongActual(
+ Map<Pair<String, String>, String> expectedStats,
+ Map<Pair<String, String>, String> actualStats) {
+ for (Map.Entry<Pair<String, String>, String> statEntry : expectedStats.entrySet()) {
+ // Make stuff lowercase here, at the site of comparison.
+ String expectedValue = statEntry.getValue().toLowerCase().trim();
+ String actualValue = actualStats.get(statEntry.getKey());
+ if (actualValue == null) {
+ return false;
+ }
+ actualValue = actualValue.toLowerCase().trim();
+ if (!expectedValue.contentEquals(actualValue)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Triggers a sync and waits till it is complete.
+ */
+ public static void triggerSyncAndWaitForCompletion(final Context context)
+ throws InterruptedException {
+ final long oldSyncTime = getCurrentSyncTime(context);
+ // Request sync.
+ ThreadUtils.runOnUiThreadBlocking(new Runnable() {
+ @Override
+ public void run() {
+ ProfileSyncService.get(context).requestSyncCycleForTest();
+ }
+ });
+
+ // Wait till lastSyncedTime > oldSyncTime.
+ Assert.assertTrue("Timed out waiting for syncing to complete.",
+ CriteriaHelper.pollForCriteria(new Criteria() {
+ @Override
+ public boolean isSatisfied() {
+ long currentSyncTime = 0;
+ try {
+ currentSyncTime = getCurrentSyncTime(context);
+ } catch (InterruptedException e) {
+ Log.w(TAG, "Interrupted while getting sync time.", e);
+ }
+ return currentSyncTime > oldSyncTime;
+ }
+ }, SYNC_WAIT_TIMEOUT_MS, SYNC_CHECK_INTERVAL_MS));
+ }
+
+ private static long getCurrentSyncTime(final Context context) throws InterruptedException {
+ final Semaphore s = new Semaphore(0);
+ final AtomicLong result = new AtomicLong();
+ ThreadUtils.runOnUiThreadBlocking(new Runnable() {
+ @Override
+ public void run() {
+ result.set(ProfileSyncService.get(context).getLastSyncedTimeForTest());
+ s.release();
+ }
+ });
+ Assert.assertTrue(s.tryAcquire(SYNC_WAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ return result.get();
+ }
+
+ /**
+ * Waits for a possible async initialization of the sync backend.
+ */
+ public static void ensureSyncInitialized(final Context context) throws InterruptedException {
+ Assert.assertTrue("Timed out waiting for syncing to be initialized.",
+ CriteriaHelper.pollForCriteria(new Criteria() {
+ @Override
+ public boolean isSatisfied() {
+ return ThreadUtils.runOnUiThreadBlockingNoException(
+ new Callable<Boolean>() {
+ @Override
+ public Boolean call() throws Exception {
+ return ProfileSyncService.get(context)
+ .isSyncInitialized();
+
+ }
+ });
+ }
+ }, SYNC_WAIT_TIMEOUT_MS, SYNC_CHECK_INTERVAL_MS));
+ }
+
+ /**
+ * Verifies that the sync status is "READY" and sync is signed in with the account.
+ */
+ public static void verifySyncIsSignedIn(Context context, Account account)
+ throws InterruptedException {
+ ensureSyncInitialized(context);
+ triggerSyncAndWaitForCompletion(context);
+ verifySignedInWithAccount(context, account);
+ }
+
+ /**
+ * Makes sure that sync is enabled with the correct account.
+ */
+ public static void verifySignedInWithAccount(Context context, Account account) {
+ if (account == null) return;
+
+ Assert.assertEquals(
+ account.name, ChromeSigninController.get(context).getSignedInAccountName());
+ }
+
+ /**
+ * Makes sure that the Python sync server was successfully started by checking for a well known
+ * response to a request for the server time. The command line argument for the sync server must
+ * be present in order for this check to be valid.
+ */
+ public static void verifySyncServerIsRunning() {
+ boolean hasSwitch = CommandLine.getInstance().hasSwitch(SYNC_URL);
+ Assert.assertTrue(SYNC_URL + " is a required parameter for the sync tests.", hasSwitch);
+ String syncTimeUrl = CommandLine.getInstance().getSwitchValue(SYNC_URL) + "/time";
+ TestHttpServerClient.checkServerIsUp(syncTimeUrl, "0123456789");
+ }
+
+ /**
+ * Sets up a test Google account on the device.
+ */
+ public static Account setupTestAccount(MockAccountManager accountManager, String accountName,
+ String password, String... allowedAuthTokenTypes) {
+ Account account = AccountManagerHelper.createAccountFromName(accountName);
+ AccountHolder.Builder accountHolder =
+ AccountHolder.create().account(account).password(password);
+ if (allowedAuthTokenTypes != null) {
+ // Auto-allowing provided auth token types
+ for (String authTokenType : allowedAuthTokenTypes) {
+ accountHolder.hasBeenAccepted(authTokenType, true);
+ }
+ }
+ accountManager.addAccountHolderExplicitly(accountHolder.build());
+ return account;
+ }
+
+ public static class AboutSyncInfoGetter implements Runnable {
+ private static final String TAG = "AboutSyncInfoGetter";
+ final Context mContext;
+ Map<Pair<String, String>, String> mAboutInfo;
+
+ public AboutSyncInfoGetter(Context context) {
+ mContext = context.getApplicationContext();
+ mAboutInfo = new HashMap<Pair<String, String>, String>();
+ }
+
+ @Override
+ public void run() {
+ String info = ProfileSyncService.get(mContext).getSyncInternalsInfoForTest();
+ try {
+ mAboutInfo = getAboutInfoStats(info);
+ } catch (JSONException e) {
+ Log.w(TAG, "Unable to parse JSON message: " + info, e);
+ }
+ }
+
+ public Map<Pair<String, String>, String> getAboutInfo() {
+ return mAboutInfo;
+ }
+ }
+
+ /**
+ * Helper class used to create a mock account on the device.
+ */
+ public static class SyncTestContext extends AdvancedMockContext {
+
+ public SyncTestContext(Context context) {
+ super(context);
+ }
+
+ @Override
+ public Object getSystemService(String name) {
+ if (Context.ACCOUNT_SERVICE.equals(name)) {
+ throw new UnsupportedOperationException(
+ "Sync tests should not use system Account Manager.");
+ }
+ return super.getSystemService(name);
+ }
+ }
+}