diff options
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(); + } + } +} |