diff options
author | nyquist@chromium.org <nyquist@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-02-22 02:03:54 +0000 |
---|---|---|
committer | nyquist@chromium.org <nyquist@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-02-22 02:03:54 +0000 |
commit | e7c4dc79da6f08f11811b258dee190fda23fb2a4 (patch) | |
tree | c4c67446bb87affe91256705bc7d1559724e4c34 | |
parent | c68c8a5791da6c91ee15ba55668ffca18a0c4367 (diff) | |
download | chromium_src-e7c4dc79da6f08f11811b258dee190fda23fb2a4.zip chromium_src-e7c4dc79da6f08f11811b258dee190fda23fb2a4.tar.gz chromium_src-e7c4dc79da6f08f11811b258dee190fda23fb2a4.tar.bz2 |
[sync] Upstream the code that triggers the InvalidationController
The InvalidationController needs to be started and stopped based on when
the main activity is resumed and paused. This upstreams the listener
part of that.
It also adds a test for the new functionality and a mock implementation
of the SyncContentResolverDelegate.
BUG=159203
Review URL: https://chromiumcodereview.appspot.com/12310008
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@183975 0039d316-1c4b-4281-b951-d872f2087c98
8 files changed, 305 insertions, 12 deletions
diff --git a/sync/android/DEPS b/sync/android/DEPS index 338ad7b..d83a77e 100644 --- a/sync/android/DEPS +++ b/sync/android/DEPS @@ -1,4 +1,5 @@ include_rules = [ "+third_party/cacheinvalidation", # For imports in sync/notifier. "+net", # For imports in sync/signin. + "+sync/test/android", # For sync test tools ] diff --git a/sync/android/java/src/org/chromium/sync/notifier/InvalidationController.java b/sync/android/java/src/org/chromium/sync/notifier/InvalidationController.java index 9b91c98..cd98293 100644 --- a/sync/android/java/src/org/chromium/sync/notifier/InvalidationController.java +++ b/sync/android/java/src/org/chromium/sync/notifier/InvalidationController.java @@ -16,6 +16,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; +import org.chromium.base.ActivityStatus; import org.chromium.sync.internal_api.pub.base.ModelType; import java.util.HashSet; @@ -27,7 +28,7 @@ import javax.annotation.Nullable; * Controller used to send start, stop, and registration-change commands to the invalidation * client library used by Sync. */ -public class InvalidationController { +public class InvalidationController implements ActivityStatus.StateListener { /** * Constants and utility methods to create the intents used to communicate between the * controller and the invalidation client library. @@ -104,6 +105,10 @@ public class InvalidationController { */ private static final String TAG = InvalidationController.class.getSimpleName(); + private static final Object LOCK = new Object(); + + private static InvalidationController sInstance; + private final Context mContext; /** @@ -163,10 +168,30 @@ public class InvalidationController { } /** - * Returns a new instance that will use {@code context} to issue intents. + * Returns the instance that will use {@code context} to issue intents. + * + * Calling this method will create the instance if it does not yet exist. */ + public static InvalidationController get(Context context) { + synchronized (LOCK) { + if (sInstance == null) { + sInstance = new InvalidationController(context); + } + return sInstance; + } + } + + /** + * Returns the singleton instance that will use {@code context} to issue intents. + * + * This method is only kept until the downstream callers of this method have been changed to use + * {@link InvalidationController#get(android.content.Context)}. + * + * TODO(nyquist) Remove this method. + */ + @Deprecated public static InvalidationController newInstance(Context context) { - return new InvalidationController(context); + return get(context); } /** @@ -174,7 +199,8 @@ public class InvalidationController { */ @VisibleForTesting InvalidationController(Context context) { - this.mContext = Preconditions.checkNotNull(context.getApplicationContext()); + mContext = Preconditions.checkNotNull(context.getApplicationContext()); + ActivityStatus.registerStateListener(this); } /** @@ -218,4 +244,15 @@ public class InvalidationController { ModelTypeResolver getModelTypeResolver() { return new ModelTypeResolverImpl(); } + + @Override + public void onActivityStateChange(int newState) { + if (SyncStatusHelper.get(mContext).isSyncEnabled()) { + if (newState == ActivityStatus.PAUSED) { + stop(); + } else if (newState == ActivityStatus.RESUMED) { + start(); + } + } + } } diff --git a/sync/android/java/src/org/chromium/sync/notifier/InvalidationService.java b/sync/android/java/src/org/chromium/sync/notifier/InvalidationService.java index b867e80..5786e6f 100644 --- a/sync/android/java/src/org/chromium/sync/notifier/InvalidationService.java +++ b/sync/android/java/src/org/chromium/sync/notifier/InvalidationService.java @@ -388,7 +388,7 @@ public class InvalidationService extends AndroidListener { bundle.putString("payload", (payload == null) ? "" : payload); } Account account = SyncStatusHelper.get(this).getSignedInUser(); - String contractAuthority = InvalidationController.newInstance(this).getContractAuthority(); + String contractAuthority = InvalidationController.get(this).getContractAuthority(); requestSyncFromContentResolver(bundle, account, contractAuthority); } diff --git a/sync/android/java/src/org/chromium/sync/notifier/SyncStatusHelper.java b/sync/android/java/src/org/chromium/sync/notifier/SyncStatusHelper.java index 4fc63581..4b42eb8 100644 --- a/sync/android/java/src/org/chromium/sync/notifier/SyncStatusHelper.java +++ b/sync/android/java/src/org/chromium/sync/notifier/SyncStatusHelper.java @@ -131,7 +131,7 @@ public class SyncStatusHelper { public boolean isSyncEnabled(Account account) { StrictMode.ThreadPolicy oldPolicy = temporarilyAllowDiskWritesAndDiskReads(); String contractAuthority = - InvalidationController.newInstance(mApplicationContext).getContractAuthority(); + InvalidationController.get(mApplicationContext).getContractAuthority(); boolean enabled = account != null && mSyncContentResolverWrapper.getMasterSyncAutomatically() && mSyncContentResolverWrapper.getSyncAutomatically(account, contractAuthority); @@ -163,7 +163,7 @@ public class SyncStatusHelper { public boolean isSyncEnabledForChrome(Account account) { StrictMode.ThreadPolicy oldPolicy = temporarilyAllowDiskWritesAndDiskReads(); String contractAuthority = - InvalidationController.newInstance(mApplicationContext).getContractAuthority(); + InvalidationController.get(mApplicationContext).getContractAuthority(); boolean enabled = account != null && mSyncContentResolverWrapper.getSyncAutomatically(account, contractAuthority); StrictMode.setThreadPolicy(oldPolicy); @@ -191,7 +191,7 @@ public class SyncStatusHelper { StrictMode.ThreadPolicy oldPolicy = temporarilyAllowDiskWritesAndDiskReads(); makeSyncable(account); String contractAuthority = - InvalidationController.newInstance(mApplicationContext).getContractAuthority(); + InvalidationController.get(mApplicationContext).getContractAuthority(); if (!mSyncContentResolverWrapper.getSyncAutomatically(account, contractAuthority)) { mSyncContentResolverWrapper.setSyncAutomatically(account, contractAuthority, true); } @@ -206,13 +206,14 @@ public class SyncStatusHelper { public void disableAndroidSync(Account account) { StrictMode.ThreadPolicy oldPolicy = temporarilyAllowDiskWritesAndDiskReads(); String contractAuthority = - InvalidationController.newInstance(mApplicationContext).getContractAuthority(); + InvalidationController.get(mApplicationContext).getContractAuthority(); if (mSyncContentResolverWrapper.getSyncAutomatically(account, contractAuthority)) { mSyncContentResolverWrapper.setSyncAutomatically(account, contractAuthority, false); } StrictMode.setThreadPolicy(oldPolicy); } + // TODO(nyquist) Move all these methods about signed in user to GoogleServicesManager. public Account getSignedInUser() { String syncAccountName = getSignedInAccountName(); if (syncAccountName == null) { @@ -252,7 +253,7 @@ public class SyncStatusHelper { */ private void makeSyncable(Account account) { String contractAuthority = - InvalidationController.newInstance(mApplicationContext).getContractAuthority(); + InvalidationController.get(mApplicationContext).getContractAuthority(); if (hasFinishedFirstSync(account)) { mSyncContentResolverWrapper.setIsSyncable(account, contractAuthority, 1); } @@ -274,7 +275,7 @@ public class SyncStatusHelper { */ boolean hasFinishedFirstSync(Account account) { String contractAuthority = - InvalidationController.newInstance(mApplicationContext).getContractAuthority(); + InvalidationController.get(mApplicationContext).getContractAuthority(); return mSyncContentResolverWrapper.getIsSyncable(account, contractAuthority) <= 0; } diff --git a/sync/android/javatests/src/org/chromium/sync/notifier/InvalidationControllerTest.java b/sync/android/javatests/src/org/chromium/sync/notifier/InvalidationControllerTest.java index b7f96e3..6006ef3 100644 --- a/sync/android/javatests/src/org/chromium/sync/notifier/InvalidationControllerTest.java +++ b/sync/android/javatests/src/org/chromium/sync/notifier/InvalidationControllerTest.java @@ -5,6 +5,7 @@ package org.chromium.sync.notifier; import android.accounts.Account; +import android.app.Activity; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -15,11 +16,13 @@ import android.test.suitebuilder.annotation.SmallTest; import com.google.common.collect.Lists; import com.google.common.collect.Sets; +import org.chromium.base.ActivityStatus; import org.chromium.base.test.util.AdvancedMockContext; import org.chromium.base.test.util.Feature; import org.chromium.sync.internal_api.pub.base.ModelType; import org.chromium.sync.notifier.InvalidationController.IntentProtocol; import org.chromium.sync.signin.AccountManagerHelper; +import org.chromium.sync.test.util.MockSyncContentResolverDelegate; import java.util.ArrayList; import java.util.HashSet; @@ -38,7 +41,7 @@ public class InvalidationControllerTest extends InstrumentationTestCase { @Override protected void setUp() throws Exception { mContext = new IntentSavingContext(getInstrumentation().getTargetContext()); - mController = InvalidationController.newInstance(mContext); + mController = InvalidationController.get(mContext); } @SmallTest @@ -65,6 +68,87 @@ public class InvalidationControllerTest extends InstrumentationTestCase { @SmallTest @Feature({"Sync"}) + public void testResumingMainActivity() throws Exception { + // Resuming main activity should trigger a start if sync is enabled. + setupSync(true); + mController.onActivityStateChange(ActivityStatus.RESUMED); + assertEquals(1, mContext.getNumStartedIntents()); + Intent intent = mContext.getStartedIntent(0); + validateIntentComponent(intent); + assertNull(intent.getExtras()); + } + + @SmallTest + @Feature({"Sync"}) + public void testResumingMainActivityWithSyncDisabled() throws Exception { + // Resuming main activity should NOT trigger a start if sync is disabled. + setupSync(false); + mController.onActivityStateChange(ActivityStatus.RESUMED); + assertEquals(0, mContext.getNumStartedIntents()); + } + + @SmallTest + @Feature({"Sync"}) + public void testPausingMainActivity() throws Exception { + // Resuming main activity should trigger a stop if sync is enabled. + setupSync(true); + mController.onActivityStateChange(ActivityStatus.PAUSED); + assertEquals(1, mContext.getNumStartedIntents()); + Intent intent = mContext.getStartedIntent(0); + validateIntentComponent(intent); + assertEquals(1, intent.getExtras().size()); + assertTrue(intent.hasExtra(IntentProtocol.EXTRA_STOP)); + assertTrue(intent.getBooleanExtra(IntentProtocol.EXTRA_STOP, false)); + } + + @SmallTest + @Feature({"Sync"}) + public void testPausingMainActivityWithSyncDisabled() throws Exception { + // Resuming main activity should NOT trigger a stop if sync is disabled. + setupSync(false); + mController.onActivityStateChange(ActivityStatus.PAUSED); + assertEquals(0, mContext.getNumStartedIntents()); + } + + private void setupSync(boolean syncEnabled) { + MockSyncContentResolverDelegate contentResolver = new MockSyncContentResolverDelegate(); + // Android master sync can safely always be on. + contentResolver.setMasterSyncAutomatically(true); + // We don't want to use the system content resolver, so we override it. + SyncStatusHelper.overrideSyncStatusHelperForTests(mContext, contentResolver); + Account account = AccountManagerHelper.createAccountFromName("test@gmail.com"); + SyncStatusHelper syncStatusHelper = SyncStatusHelper.get(mContext); + syncStatusHelper.setSignedInAccountName(account.name); + if (syncEnabled) { + syncStatusHelper.enableAndroidSync(account); + } else { + syncStatusHelper.disableAndroidSync(account); + } + } + + @SmallTest + @Feature({"Sync"}) + public void testEnsureConstructorRegistersListener() throws Exception { + final AtomicBoolean listenerCallbackCalled = new AtomicBoolean(); + + // Create instance. + new InvalidationController(mContext) { + @Override + public void onActivityStateChange(int newState) { + listenerCallbackCalled.set(true); + } + }; + + // Ensure initial state is correct. + assertFalse(listenerCallbackCalled.get()); + + // Ensure we get a callback, which means we have registered for them. + ActivityStatus.onStateChange(new Activity(), ActivityStatus.RESUMED); + assertTrue(listenerCallbackCalled.get()); + } + + @SmallTest + @Feature({"Sync"}) public void testRegisterForSpecificTypes() { final String controllerFlag = "resolveModelTypes"; final ModelTypeResolver resolver = new ModelTypeResolver() { diff --git a/sync/sync_tests.gypi b/sync/sync_tests.gypi index 8b399da..059c930 100644 --- a/sync/sync_tests.gypi +++ b/sync/sync_tests.gypi @@ -572,10 +572,23 @@ }, 'dependencies': [ 'sync_java', + 'sync_java_test_support', '../base/base.gyp:base_java_test_support', ], 'includes': [ '../build/java.gypi' ], }, + { + 'target_name': 'sync_java_test_support', + 'type': 'none', + 'variables': { + 'package_name': 'sync_java_test_support', + 'java_in_dir': '../sync/test/android/javatests', + }, + 'dependencies': [ + 'sync_java', + ], + 'includes': [ '../build/java.gypi' ], + }, ], }], # Special target to wrap a gtest_target_type==shared_library diff --git a/sync/test/android/OWNERS b/sync/test/android/OWNERS new file mode 100644 index 0000000..72643ec --- /dev/null +++ b/sync/test/android/OWNERS @@ -0,0 +1,3 @@ +nileshagrawal@chromium.org +nyquist@chromium.org +yfriedman@chromium.org diff --git a/sync/test/android/javatests/src/org/chromium/sync/test/util/MockSyncContentResolverDelegate.java b/sync/test/android/javatests/src/org/chromium/sync/test/util/MockSyncContentResolverDelegate.java new file mode 100644 index 0000000..f253d8c --- /dev/null +++ b/sync/test/android/javatests/src/org/chromium/sync/test/util/MockSyncContentResolverDelegate.java @@ -0,0 +1,154 @@ +// Copyright (c) 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.sync.test.util; + + +import android.accounts.Account; +import android.content.ContentResolver; +import android.content.SyncStatusObserver; +import android.os.AsyncTask; + +import org.chromium.sync.notifier.SyncContentResolverDelegate; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + + +/** + * Mock implementation of the SyncContentResolverWrapper. + * + * This implementation only supports status change listeners for the type + * SYNC_OBSERVER_TYPE_SETTINGS. + */ +public class MockSyncContentResolverDelegate implements SyncContentResolverDelegate { + + private final Map<String, Boolean> mSyncAutomaticallyMap; + + private final Set<AsyncSyncStatusObserver> mObservers; + + private boolean mMasterSyncAutomatically; + + public MockSyncContentResolverDelegate() { + mSyncAutomaticallyMap = new HashMap<String, Boolean>(); + mObservers = new HashSet<AsyncSyncStatusObserver>(); + } + + @Override + public Object addStatusChangeListener(int mask, SyncStatusObserver callback) { + if (mask != ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS) { + throw new IllegalArgumentException("This implementation only supports " + + "ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS as the mask"); + } + AsyncSyncStatusObserver asyncSyncStatusObserver = new AsyncSyncStatusObserver(callback); + synchronized (mObservers) { + mObservers.add(asyncSyncStatusObserver); + } + return asyncSyncStatusObserver; + } + + @Override + public void removeStatusChangeListener(Object handle) { + synchronized (mObservers) { + mObservers.remove(handle); + } + } + + @Override + public void setMasterSyncAutomatically(boolean sync) { + mMasterSyncAutomatically = sync; + notifyObservers(); + } + + @Override + public boolean getMasterSyncAutomatically() { + return mMasterSyncAutomatically; + } + + @Override + public boolean getSyncAutomatically(Account account, String authority) { + String key = createKey(account, authority); + synchronized (mSyncAutomaticallyMap) { + return mSyncAutomaticallyMap.containsKey(key) && mSyncAutomaticallyMap.get(key); + } + } + + @Override + public void setSyncAutomatically(Account account, String authority, boolean sync) { + String key = createKey(account, authority); + synchronized (mSyncAutomaticallyMap) { + if (!mSyncAutomaticallyMap.containsKey(key)) { + throw new IllegalArgumentException("Account " + account + + " is not syncable for authority " + authority + + ". Can not set sync state to " + sync); + } + mSyncAutomaticallyMap.put(key, sync); + } + notifyObservers(); + } + + @Override + public void setIsSyncable(Account account, String authority, int syncable) { + synchronized (mSyncAutomaticallyMap) { + switch (syncable) { + case 0: + mSyncAutomaticallyMap.remove(createKey(account, authority)); + break; + case 1: + mSyncAutomaticallyMap.put(createKey(account, authority), false); + break; + default: + throw new IllegalArgumentException("Unable to understand syncable argument: " + + syncable); + } + } + notifyObservers(); + } + + @Override + public int getIsSyncable(Account account, String authority) { + synchronized (mSyncAutomaticallyMap) { + final Boolean isSyncable = mSyncAutomaticallyMap.get(createKey(account, authority)); + if (isSyncable == null) { + return -1; + } + return isSyncable ? 1 : 0; + } + } + + private static String createKey(Account account, String authority) { + return account.name + "@@@" + account.type + "@@@" + authority; + } + + private void notifyObservers() { + synchronized (mObservers) { + for (AsyncSyncStatusObserver observer : mObservers) { + observer.notifyObserverAsync(); + } + } + } + + private static class AsyncSyncStatusObserver { + + private final SyncStatusObserver mSyncStatusObserver; + + private AsyncSyncStatusObserver(SyncStatusObserver syncStatusObserver) { + mSyncStatusObserver = syncStatusObserver; + } + + private void notifyObserverAsync() { + new AsyncTask<Void, Void, Void>() { + + @Override + protected Void doInBackground(Void... params) { + mSyncStatusObserver.onStatusChanged( + ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS); + return null; + } + }.execute(); + } + } +} |