Commit 191c6b86 authored by Victor Hugo Vianna Silva's avatar Victor Hugo Vianna Silva Committed by Commit Bot

Prevent Android from causing AndroidSyncSettings use on background thread

AndroidSyncSettings registers a listener that is called by the Android
system on a background thread. Before this CL, the listener directly
modified the state of the class when notified. Because
AndroidSyncSettings is also used from another thread (UI), this required
it to be implemented in a thread-safe manner.

In this CL, we modify the listener to rather post the update to the UI
thread, one step towards having this class be used from a single thread.
As a bonus, objects that in turn observe AndroidSyncSettings will now
only be notified on the UI thread, which solves the linked bug. Cleaning
up the observers so they don't post to the UI thread themselves is left
for the next CL.

We also take the time to brush up MockSyncContentResolverDelegate, namely
simplify its notification logic and use a global lock for the class.

Bug: 1028568
Change-Id: Iac820125bc37796eade94b503b8e0983b171108f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2415150Reviewed-by: default avatarFriedrich [CET] <fhorschig@chromium.org>
Reviewed-by: default avatarMarc Treib <treib@chromium.org>
Commit-Queue: Victor Vianna <victorvianna@google.com>
Cr-Commit-Position: refs/heads/master@{#808289}
parent cec8b8de
...@@ -18,6 +18,7 @@ import org.chromium.base.ContextUtils; ...@@ -18,6 +18,7 @@ import org.chromium.base.ContextUtils;
import org.chromium.base.ObserverList; import org.chromium.base.ObserverList;
import org.chromium.base.StrictModeContext; import org.chromium.base.StrictModeContext;
import org.chromium.base.ThreadUtils; import org.chromium.base.ThreadUtils;
import org.chromium.base.task.PostTask;
import org.chromium.chrome.browser.flags.ChromeFeatureList; import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.profiles.Profile; import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.signin.IdentityServicesProvider; import org.chromium.chrome.browser.signin.IdentityServicesProvider;
...@@ -27,6 +28,7 @@ import org.chromium.components.signin.identitymanager.ConsentLevel; ...@@ -27,6 +28,7 @@ import org.chromium.components.signin.identitymanager.ConsentLevel;
import org.chromium.components.signin.identitymanager.IdentityManager; import org.chromium.components.signin.identitymanager.IdentityManager;
import org.chromium.components.sync.SyncContentResolverDelegate; import org.chromium.components.sync.SyncContentResolverDelegate;
import org.chromium.components.sync.SystemSyncContentResolverDelegate; import org.chromium.components.sync.SystemSyncContentResolverDelegate;
import org.chromium.content_public.browser.UiThreadTaskTraits;
/** /**
* WARNING: Chrome will be decoupled from Android auto-sync (crbug.com/1105795). * WARNING: Chrome will be decoupled from Android auto-sync (crbug.com/1105795).
...@@ -102,6 +104,13 @@ public class AndroidSyncSettings { ...@@ -102,6 +104,13 @@ public class AndroidSyncSettings {
* @param callback Callback that will be called after updating account is finished. * @param callback Callback that will be called after updating account is finished.
* @param account The sync account if sync is enabled, null otherwise. * @param account The sync account if sync is enabled, null otherwise.
*/ */
// TODO(crbug.com/1125622): Once this class is used only on the UI thread, |callback|
// can be removed (syncability update will be synchronous). Same goes for the corresponding
// parameter in |updateAccount()|.
// TODO(crbug.com/1125622): Exposing these testing constructors that don't register the
// singleton instance can be dangerous when there's code that explicitly calls |get()|
// (in that case, a new object would be returned, not the one constructed by the test).
// Consider exposing them as static methods that also register a singleton instance.
@VisibleForTesting @VisibleForTesting
public AndroidSyncSettings(SyncContentResolverDelegate syncContentResolverDelegate, public AndroidSyncSettings(SyncContentResolverDelegate syncContentResolverDelegate,
@Nullable Runnable callback, @Nullable Account account) { @Nullable Runnable callback, @Nullable Account account) {
...@@ -112,19 +121,21 @@ public class AndroidSyncSettings { ...@@ -112,19 +121,21 @@ public class AndroidSyncSettings {
updateCachedSettings(); updateCachedSettings();
updateSyncability(callback); updateSyncability(callback);
mSyncContentResolverDelegate.addStatusChangeListener( SyncStatusObserver androidOsListener = new SyncStatusObserver() {
ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, new SyncStatusObserver() { @Override
@Override public void onStatusChanged(int which) {
public void onStatusChanged(int which) { if (which != ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS) return;
if (which == ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS) { // This is called by Android on a background thread, but AndroidSyncSettings
// Sync settings have changed; update our cached values. // methods should be called from the UI thread, so post a task.
if (updateCachedSettings()) { PostTask.postTask(UiThreadTaskTraits.DEFAULT, () -> {
// If something actually changed, tell our observers. if (updateCachedSettings()) {
notifyObservers(); notifyObservers();
}
}
} }
}); });
}
};
mSyncContentResolverDelegate.addStatusChangeListener(
ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, androidOsListener);
} }
/** /**
......
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
package org.chromium.chrome.browser.password_manager.settings; package org.chromium.chrome.browser.password_manager.settings;
import static org.mockito.Mockito.when;
import android.accounts.Account; import android.accounts.Account;
import androidx.test.filters.SmallTest; import androidx.test.filters.SmallTest;
...@@ -14,6 +16,9 @@ import org.junit.Rule; ...@@ -14,6 +16,9 @@ import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.RuleChain; import org.junit.rules.RuleChain;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.chromium.base.test.BaseJUnit4ClassRunner; import org.chromium.base.test.BaseJUnit4ClassRunner;
import org.chromium.base.test.util.Feature; import org.chromium.base.test.util.Feature;
...@@ -23,7 +28,6 @@ import org.chromium.chrome.browser.sync.AndroidSyncSettings; ...@@ -23,7 +28,6 @@ import org.chromium.chrome.browser.sync.AndroidSyncSettings;
import org.chromium.chrome.browser.sync.ProfileSyncService; import org.chromium.chrome.browser.sync.ProfileSyncService;
import org.chromium.chrome.test.ChromeBrowserTestRule; import org.chromium.chrome.test.ChromeBrowserTestRule;
import org.chromium.components.browser_ui.settings.ChromeBasePreference; import org.chromium.components.browser_ui.settings.ChromeBasePreference;
import org.chromium.components.sync.test.util.MockSyncContentResolverDelegate;
import org.chromium.content_public.browser.test.util.TestThreadUtils; import org.chromium.content_public.browser.test.util.TestThreadUtils;
/** /**
...@@ -44,24 +48,26 @@ public class PasswordViewingTypeTest { ...@@ -44,24 +48,26 @@ public class PasswordViewingTypeTest {
public final RuleChain mRuleChain = public final RuleChain mRuleChain =
RuleChain.outerRule(mChromeBrowserTestRule).around(mSettingsActivityTestRule); RuleChain.outerRule(mChromeBrowserTestRule).around(mSettingsActivityTestRule);
@Rule
public final MockitoRule mMockitoRule = MockitoJUnit.rule();
private ChromeBasePreference mPasswordsPref; private ChromeBasePreference mPasswordsPref;
private MockSyncContentResolverDelegate mSyncContentResolverDelegate;
private String mAuthority; private String mAuthority;
private Account mAccount; private Account mAccount;
@Mock
private AndroidSyncSettings mAndroidSyncSettings;
@Before @Before
public void setUp() { public void setUp() {
mAccount = mChromeBrowserTestRule.addAccount("account@example.com"); mAccount = mChromeBrowserTestRule.addAccount("account@example.com");
mSyncContentResolverDelegate = new MockSyncContentResolverDelegate();
mSettingsActivityTestRule.startSettingsActivity(); mSettingsActivityTestRule.startSettingsActivity();
MainSettings mainSettings = mSettingsActivityTestRule.getFragment(); MainSettings mainSettings = mSettingsActivityTestRule.getFragment();
mPasswordsPref = mPasswordsPref =
(ChromeBasePreference) mainSettings.findPreference(MainSettings.PREF_PASSWORDS); (ChromeBasePreference) mainSettings.findPreference(MainSettings.PREF_PASSWORDS);
TestThreadUtils.runOnUiThreadBlocking(() -> { TestThreadUtils.runOnUiThreadBlocking(() -> {
AndroidSyncSettings.overrideForTests( AndroidSyncSettings.overrideForTests(mAndroidSyncSettings);
new AndroidSyncSettings(mSyncContentResolverDelegate)); setSyncability(false);
mAuthority = AndroidSyncSettings.getContractAuthority(); mAuthority = AndroidSyncSettings.getContractAuthority();
AndroidSyncSettings.get().updateAccount(mAccount);
}); });
} }
...@@ -87,19 +93,12 @@ public class PasswordViewingTypeTest { ...@@ -87,19 +93,12 @@ public class PasswordViewingTypeTest {
/** /**
* Turn syncability on/off. * Turn syncability on/off.
*/ */
private void setSyncability(boolean syncState) throws InterruptedException { private void setSyncability(boolean syncState) {
// Turn on syncability TestThreadUtils.runOnUiThreadBlocking(() -> {
mSyncContentResolverDelegate.setMasterSyncAutomatically(syncState); when(mAndroidSyncSettings.isSyncEnabled()).thenReturn(syncState);
mSyncContentResolverDelegate.waitForLastNotificationCompleted(); when(mAndroidSyncSettings.isChromeSyncEnabled()).thenReturn(syncState);
when(mAndroidSyncSettings.doesMasterSyncSettingAllowChromeSync()).thenReturn(true);
// First sync });
mSyncContentResolverDelegate.setIsSyncable(mAccount, mAuthority, (syncState) ? 1 : 0);
mSyncContentResolverDelegate.waitForLastNotificationCompleted();
if (syncState) {
mSyncContentResolverDelegate.setSyncAutomatically(mAccount, mAuthority, syncState);
mSyncContentResolverDelegate.waitForLastNotificationCompleted();
}
} }
/** /**
...@@ -109,7 +108,7 @@ public class PasswordViewingTypeTest { ...@@ -109,7 +108,7 @@ public class PasswordViewingTypeTest {
@Test @Test
@SmallTest @SmallTest
@Feature({"Sync"}) @Feature({"Sync"})
public void testUserRedirectSyncSettings() throws InterruptedException { public void testUserRedirectSyncSettings() {
setSyncability(true); setSyncability(true);
overrideProfileSyncService(false); overrideProfileSyncService(false);
TestThreadUtils.runOnUiThreadBlocking(() -> { TestThreadUtils.runOnUiThreadBlocking(() -> {
...@@ -127,7 +126,7 @@ public class PasswordViewingTypeTest { ...@@ -127,7 +126,7 @@ public class PasswordViewingTypeTest {
*/ */
@Test @Test
@SmallTest @SmallTest
public void testSyncingNativePasswordView() throws InterruptedException { public void testSyncingNativePasswordView() {
setSyncability(true); setSyncability(true);
overrideProfileSyncService(true); overrideProfileSyncService(true);
Assert.assertEquals( Assert.assertEquals(
...@@ -140,7 +139,7 @@ public class PasswordViewingTypeTest { ...@@ -140,7 +139,7 @@ public class PasswordViewingTypeTest {
*/ */
@Test @Test
@SmallTest @SmallTest
public void testNonSyncingNativePasswordView() throws InterruptedException { public void testNonSyncingNativePasswordView() {
setSyncability(false); setSyncability(false);
Assert.assertEquals( Assert.assertEquals(
PasswordSettings.class.getCanonicalName(), mPasswordsPref.getFragment()); PasswordSettings.class.getCanonicalName(), mPasswordsPref.getFragment());
......
...@@ -9,6 +9,9 @@ import androidx.annotation.VisibleForTesting; ...@@ -9,6 +9,9 @@ import androidx.annotation.VisibleForTesting;
import org.chromium.base.annotations.CalledByNative; import org.chromium.base.annotations.CalledByNative;
import org.chromium.components.sync.SyncContentResolverDelegate; import org.chromium.components.sync.SyncContentResolverDelegate;
import org.chromium.components.sync.test.util.MockSyncContentResolverDelegate; import org.chromium.components.sync.test.util.MockSyncContentResolverDelegate;
import org.chromium.content_public.browser.test.util.TestThreadUtils;
import java.util.concurrent.Callable;
/** /**
* A utility class for mocking AndroidSyncSettings. * A utility class for mocking AndroidSyncSettings.
...@@ -25,7 +28,6 @@ public class AndroidSyncSettingsTestUtils { ...@@ -25,7 +28,6 @@ public class AndroidSyncSettingsTestUtils {
setUpAndroidSyncSettingsForTesting(new MockSyncContentResolverDelegate()); setUpAndroidSyncSettingsForTesting(new MockSyncContentResolverDelegate());
} }
@VisibleForTesting
public static void setUpAndroidSyncSettingsForTesting(SyncContentResolverDelegate delegate) { public static void setUpAndroidSyncSettingsForTesting(SyncContentResolverDelegate delegate) {
delegate.setMasterSyncAutomatically(true); delegate.setMasterSyncAutomatically(true);
// Explicitly pass null account to AndroidSyncSettings ctor. Normally, AndroidSyncSettings // Explicitly pass null account to AndroidSyncSettings ctor. Normally, AndroidSyncSettings
...@@ -33,4 +35,31 @@ public class AndroidSyncSettingsTestUtils { ...@@ -33,4 +35,31 @@ public class AndroidSyncSettingsTestUtils {
// before profiles are initialized (when IdentityManager doesn't exist yet). // before profiles are initialized (when IdentityManager doesn't exist yet).
AndroidSyncSettings.overrideForTests(new AndroidSyncSettings(delegate, null, null)); AndroidSyncSettings.overrideForTests(new AndroidSyncSettings(delegate, null, null));
} }
public static boolean getIsSyncEnabledOnUiThread() {
return TestThreadUtils.runOnUiThreadBlockingNoException(new Callable<Boolean>() {
@Override
public Boolean call() {
return AndroidSyncSettings.get().isSyncEnabled();
}
});
}
public static boolean getIsChromeSyncEnabledOnUiThread() {
return TestThreadUtils.runOnUiThreadBlockingNoException(new Callable<Boolean>() {
@Override
public Boolean call() {
return AndroidSyncSettings.get().isChromeSyncEnabled();
}
});
}
public static boolean getDoesMasterSyncAllowSyncOnUiThread() {
return TestThreadUtils.runOnUiThreadBlockingNoException(new Callable<Boolean>() {
@Override
public Boolean call() {
return AndroidSyncSettings.get().doesMasterSyncSettingAllowChromeSync();
}
});
}
} }
...@@ -27,6 +27,7 @@ import org.chromium.chrome.test.ChromeJUnit4ClassRunner; ...@@ -27,6 +27,7 @@ import org.chromium.chrome.test.ChromeJUnit4ClassRunner;
import org.chromium.chrome.test.util.browser.Features; import org.chromium.chrome.test.util.browser.Features;
import org.chromium.chrome.test.util.browser.signin.MockChangeEventChecker; import org.chromium.chrome.test.util.browser.signin.MockChangeEventChecker;
import org.chromium.chrome.test.util.browser.sync.SyncTestUtil; import org.chromium.chrome.test.util.browser.sync.SyncTestUtil;
import org.chromium.components.sync.test.util.MockSyncContentResolverDelegate;
import org.chromium.content_public.browser.test.util.Criteria; import org.chromium.content_public.browser.test.util.Criteria;
import org.chromium.content_public.browser.test.util.CriteriaHelper; import org.chromium.content_public.browser.test.util.CriteriaHelper;
import org.chromium.content_public.browser.test.util.TestThreadUtils; import org.chromium.content_public.browser.test.util.TestThreadUtils;
...@@ -107,10 +108,16 @@ public class SyncTest { ...@@ -107,10 +108,16 @@ public class SyncTest {
SigninHelper.updateAccountRenameData(eventChecker, oldAccount.name); SigninHelper.updateAccountRenameData(eventChecker, oldAccount.name);
// Tell the fake content resolver that a rename had happen and copy over the sync // Tell the fake content resolver that a rename had happen and copy over the sync
// settings. This would normally be done by the // settings.
// SystemSyncTestRule.getSyncContentResolver(). MockSyncContentResolverDelegate contentResolver =
mSyncTestRule.getSyncContentResolver().renameAccounts( mSyncTestRule.getSyncContentResolver();
oldAccount, newAccount, AndroidSyncSettings.getContractAuthority()); String authority = AndroidSyncSettings.getContractAuthority();
int oldIsSyncable = contentResolver.getIsSyncable(oldAccount, authority);
contentResolver.setIsSyncable(newAccount, authority, oldIsSyncable);
if (oldIsSyncable > 0) {
contentResolver.setSyncAutomatically(newAccount, authority,
contentResolver.getSyncAutomatically(oldAccount, authority));
}
// Starts the rename process. Normally, this is triggered by the broadcast // Starts the rename process. Normally, this is triggered by the broadcast
// listener as well. // listener as well.
...@@ -223,8 +230,6 @@ public class SyncTest { ...@@ -223,8 +230,6 @@ public class SyncTest {
// Disabling master sync first. Sync should be off. // Disabling master sync first. Sync should be off.
mSyncTestRule.getSyncContentResolver().setMasterSyncAutomatically(false); mSyncTestRule.getSyncContentResolver().setMasterSyncAutomatically(false);
// TODO(crbug.com/921025): Master sync shouldn't influence isSyncRequested, so at this point
// isSyncRequested should be true; only canSyncFeatureStart should be false.
Assert.assertFalse(SyncTestUtil.isSyncRequested()); Assert.assertFalse(SyncTestUtil.isSyncRequested());
Assert.assertFalse(SyncTestUtil.canSyncFeatureStart()); Assert.assertFalse(SyncTestUtil.canSyncFeatureStart());
...@@ -235,9 +240,6 @@ public class SyncTest { ...@@ -235,9 +240,6 @@ public class SyncTest {
// Re-enabling Chrome sync should not turn sync back on. // Re-enabling Chrome sync should not turn sync back on.
mSyncTestRule.getSyncContentResolver().setSyncAutomatically(account, authority, true); mSyncTestRule.getSyncContentResolver().setSyncAutomatically(account, authority, true);
// TODO(crbug.com/921025): Master sync (which is still disabled) shouldn't influence
// isSyncRequested, so at this point isSyncRequested should be true (but
// canSyncFeatureStart should remain false).
Assert.assertFalse(SyncTestUtil.isSyncRequested()); Assert.assertFalse(SyncTestUtil.isSyncRequested());
Assert.assertFalse(SyncTestUtil.canSyncFeatureStart()); Assert.assertFalse(SyncTestUtil.canSyncFeatureStart());
......
...@@ -11,18 +11,12 @@ import android.os.Bundle; ...@@ -11,18 +11,12 @@ import android.os.Bundle;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import org.junit.Assert;
import org.chromium.base.ThreadUtils;
import org.chromium.base.task.AsyncTask;
import org.chromium.components.sync.SyncContentResolverDelegate; import org.chromium.components.sync.SyncContentResolverDelegate;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
/** /**
* Fake implementation of the {@link SyncContentResolverDelegate}. * Fake implementation of the {@link SyncContentResolverDelegate}.
...@@ -35,65 +29,65 @@ import java.util.concurrent.TimeUnit; ...@@ -35,65 +29,65 @@ import java.util.concurrent.TimeUnit;
public class MockSyncContentResolverDelegate implements SyncContentResolverDelegate { public class MockSyncContentResolverDelegate implements SyncContentResolverDelegate {
private final Set<String> mSyncAutomaticallySet; private final Set<String> mSyncAutomaticallySet;
private final Map<String, Boolean> mIsSyncableMap; private final Map<String, Boolean> mIsSyncableMap;
private final Object mSyncableMapLock = new Object(); private final Set<SyncStatusObserver> mObservers;
private final Set<AsyncSyncStatusObserver> mObservers;
private boolean mMasterSyncAutomatically; private boolean mMasterSyncAutomatically;
private boolean mDisableObserverNotifications;
private Semaphore mPendingObserverCount; private Object mLock = new Object();
public MockSyncContentResolverDelegate() { public MockSyncContentResolverDelegate() {
mSyncAutomaticallySet = new HashSet<String>(); mSyncAutomaticallySet = new HashSet<String>();
mIsSyncableMap = new HashMap<String, Boolean>(); mIsSyncableMap = new HashMap<String, Boolean>();
mObservers = new HashSet<AsyncSyncStatusObserver>(); mObservers = new HashSet<SyncStatusObserver>();
} }
@Override @Override
public Object addStatusChangeListener(int mask, SyncStatusObserver callback) { public Object addStatusChangeListener(int mask, SyncStatusObserver observer) {
if (mask != ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS) { if (mask != ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS) {
throw new IllegalArgumentException("This implementation only supports " throw new IllegalArgumentException("This implementation only supports "
+ "ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS as the mask"); + "ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS as the mask");
} }
AsyncSyncStatusObserver asyncSyncStatusObserver = new AsyncSyncStatusObserver(callback); synchronized (mLock) {
synchronized (mObservers) { mObservers.add(observer);
mObservers.add(asyncSyncStatusObserver);
} }
return asyncSyncStatusObserver; return observer;
} }
@Override @Override
public void removeStatusChangeListener(Object handle) { public void removeStatusChangeListener(Object handle) {
synchronized (mObservers) { synchronized (mLock) {
mObservers.remove(handle); mObservers.remove(handle);
} }
} }
// Once this returns, observers are guaranteed to have been synchronously notified.
@Override @Override
@VisibleForTesting @VisibleForTesting
public void setMasterSyncAutomatically(boolean sync) { public void setMasterSyncAutomatically(boolean sync) {
if (mMasterSyncAutomatically == sync) return; synchronized (mLock) {
if (mMasterSyncAutomatically == sync) return;
mMasterSyncAutomatically = sync; mMasterSyncAutomatically = sync;
}
notifyObservers(); notifyObservers();
} }
@Override @Override
public boolean getMasterSyncAutomatically() { public boolean getMasterSyncAutomatically() {
return mMasterSyncAutomatically; synchronized (mLock) {
return mMasterSyncAutomatically;
}
} }
@Override @Override
public boolean getSyncAutomatically(Account account, String authority) { public boolean getSyncAutomatically(Account account, String authority) {
synchronized (mSyncableMapLock) { synchronized (mLock) {
return mSyncAutomaticallySet.contains(createKey(account, authority)); return mSyncAutomaticallySet.contains(createKey(account, authority));
} }
} }
// Once this returns, observers are guaranteed to have been synchronously notified.
@Override @Override
public void setSyncAutomatically(Account account, String authority, boolean sync) { public void setSyncAutomatically(Account account, String authority, boolean sync) {
synchronized (mSyncableMapLock) { synchronized (mLock) {
String key = createKey(account, authority); String key = createKey(account, authority);
if (sync) { if (sync) {
mSyncAutomaticallySet.add(key); mSyncAutomaticallySet.add(key);
...@@ -104,9 +98,10 @@ public class MockSyncContentResolverDelegate implements SyncContentResolverDeleg ...@@ -104,9 +98,10 @@ public class MockSyncContentResolverDelegate implements SyncContentResolverDeleg
notifyObservers(); notifyObservers();
} }
// Once this returns, observers are guaranteed to have been synchronously notified.
@Override @Override
public void setIsSyncable(Account account, String authority, int syncable) { public void setIsSyncable(Account account, String authority, int syncable) {
synchronized (mSyncableMapLock) { synchronized (mLock) {
String key = createKey(account, authority); String key = createKey(account, authority);
if (syncable > 0) { if (syncable > 0) {
mIsSyncableMap.put(key, true); mIsSyncableMap.put(key, true);
...@@ -121,7 +116,7 @@ public class MockSyncContentResolverDelegate implements SyncContentResolverDeleg ...@@ -121,7 +116,7 @@ public class MockSyncContentResolverDelegate implements SyncContentResolverDeleg
@Override @Override
public int getIsSyncable(Account account, String authority) { public int getIsSyncable(Account account, String authority) {
synchronized (mSyncableMapLock) { synchronized (mLock) {
String key = createKey(account, authority); String key = createKey(account, authority);
if (mIsSyncableMap.containsKey(key)) { if (mIsSyncableMap.containsKey(key)) {
return mIsSyncableMap.get(key) ? 1 : 0; return mIsSyncableMap.get(key) ? 1 : 0;
...@@ -138,71 +133,11 @@ public class MockSyncContentResolverDelegate implements SyncContentResolverDeleg ...@@ -138,71 +133,11 @@ public class MockSyncContentResolverDelegate implements SyncContentResolverDeleg
} }
private void notifyObservers() { private void notifyObservers() {
if (mDisableObserverNotifications) return; synchronized (mLock) {
synchronized (mObservers) { for (SyncStatusObserver observer : mObservers) {
mPendingObserverCount = new Semaphore(1 - mObservers.size()); observer.onStatusChanged(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS);
for (AsyncSyncStatusObserver observer : mObservers) {
observer.notifyObserverAsync(mPendingObserverCount);
} }
} }
} }
/**
* Blocks until the last notification has been issued to all registered observers.
* Note that if an observer is removed while a notification is being handled this can
* fail to return correctly.
*
* @throws InterruptedException
*/
@VisibleForTesting
public void waitForLastNotificationCompleted() throws InterruptedException {
Assert.assertTrue("Timed out waiting for notifications to complete.",
mPendingObserverCount.tryAcquire(5, TimeUnit.SECONDS));
}
public void disableObserverNotifications() {
mDisableObserverNotifications = true;
}
/**
* Simulate an account rename, which copies settings to the new account.
*/
public void renameAccounts(Account oldAccount, Account newAccount, String authority) {
int oldIsSyncable = getIsSyncable(oldAccount, authority);
setIsSyncable(newAccount, authority, oldIsSyncable);
if (oldIsSyncable == 1) {
setSyncAutomatically(
newAccount, authority, getSyncAutomatically(oldAccount, authority));
}
}
private static class AsyncSyncStatusObserver {
private final SyncStatusObserver mSyncStatusObserver;
private AsyncSyncStatusObserver(SyncStatusObserver syncStatusObserver) {
mSyncStatusObserver = syncStatusObserver;
}
private void notifyObserverAsync(final Semaphore pendingObserverCount) {
if (ThreadUtils.runningOnUiThread()) {
new AsyncTask<Void>() {
@Override
protected Void doInBackground() {
mSyncStatusObserver.onStatusChanged(
ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS);
return null;
}
@Override
protected void onPostExecute(Void result) {
pendingObserverCount.release();
}
}
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} else {
mSyncStatusObserver.onStatusChanged(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS);
pendingObserverCount.release();
}
}
}
} }
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment