diff options
author | nyquist@chromium.org <nyquist@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-09-04 19:01:37 +0000 |
---|---|---|
committer | nyquist@chromium.org <nyquist@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-09-04 19:01:37 +0000 |
commit | ed4d7052c09336e1df60a0b21f3cf6e3c57327e4 (patch) | |
tree | 7865e5da1526017e9b39d029e815b0afcdb5c0fa | |
parent | 7b0dc98917f4113d85af354d10fb64c7efaaf4b1 (diff) | |
download | chromium_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
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); + } + } +} |