blob: 21aef45434541b4650858816087c70ccd3f34e74 [file] [log] [blame]
// Copyright 2015 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.annotation.SuppressLint;
import android.content.ContentResolver;
import android.content.SyncStatusObserver;
import android.os.Bundle;
import androidx.annotation.MainThread;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.ContextUtils;
import org.chromium.base.ObserverList;
import org.chromium.base.ThreadUtils;
import org.chromium.base.task.PostTask;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.signin.IdentityServicesProvider;
import org.chromium.components.signin.AccountManagerFacadeProvider;
import org.chromium.components.signin.base.CoreAccountInfo;
import org.chromium.components.signin.identitymanager.ConsentLevel;
import org.chromium.components.signin.identitymanager.IdentityManager;
import org.chromium.content_public.browser.UiThreadTaskTraits;
/**
* WARNING: Chrome will be decoupled from Android auto-sync (crbug.com/1105795).
* Some documentation in this class may be outdated or may not be coherent when
* DecoupleSyncFromAndroidMasterSync is enabled.
*
* A helper class to handle the current status of sync for Chrome in Android settings.
* It also provides an observer to be used whenever Android sync settings change.
*
* {@link #updateAccount(Account)} should be invoked whenever sync account is changed.
*/
@MainThread
public class AndroidSyncSettings {
@SuppressLint("StaticFieldLeak")
private static AndroidSyncSettings sInstance;
// Cached value of the static |getContractAuthority()|.
private final String mContractAuthority;
private final SyncContentResolverDelegate mSyncContentResolverDelegate;
private Account mAccount;
private boolean mIsSyncable;
private boolean mChromeSyncEnabled;
private boolean mMasterSyncEnabled;
private boolean mShouldDecoupleSyncFromMasterSync;
private final ObserverList<AndroidSyncSettingsObserver> mObservers = new ObserverList<>();
/**
* Provides notifications when Android sync settings have changed.
*/
public interface AndroidSyncSettingsObserver {
void androidSyncSettingsChanged();
}
/**
Singleton instance getter. Will initialize the singleton if it hasn't been initialized before.
*/
@MainThread
public static AndroidSyncSettings get() {
ThreadUtils.assertOnUiThread();
if (sInstance == null) {
sInstance = new AndroidSyncSettings(getSyncAccount());
}
return sInstance;
}
/**
* Overrides AndroidSyncSettings instance for tests.
*/
@MainThread
@VisibleForTesting
public static void overrideForTests(AndroidSyncSettings instance) {
ThreadUtils.assertOnUiThread();
sInstance = instance;
}
// 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.
/**
* WARNING: Consider using |overrideForTests()| to inject a mock instead.
* @param account The sync account if sync is enabled, null otherwise.
*/
@VisibleForTesting
@Deprecated
public AndroidSyncSettings(@Nullable Account account) {
ThreadUtils.assertOnUiThread();
mContractAuthority = getContractAuthority();
mSyncContentResolverDelegate = SyncContentResolverDelegate.get();
mAccount = account;
updateCachedSettings();
updateSyncability();
ProfileSyncService syncService = ProfileSyncService.get();
if (syncService != null
&& ChromeFeatureList.isEnabled(
ChromeFeatureList.DECOUPLE_SYNC_FROM_ANDROID_MASTER_SYNC)) {
// Read initial persisted value.
mShouldDecoupleSyncFromMasterSync = syncService.getDecoupledFromAndroidMasterSync();
}
SyncStatusObserver androidOsListener = new SyncStatusObserver() {
@Override
public void onStatusChanged(int which) {
if (which != ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS) return;
// This is called by Android on a background thread, but AndroidSyncSettings
// methods should be called from the UI thread, so post a task.
PostTask.postTask(UiThreadTaskTraits.DEFAULT, () -> {
if (updateCachedSettings()) {
notifyObservers();
}
});
}
};
mSyncContentResolverDelegate.addStatusChangeListener(
ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, androidOsListener);
}
/**
* Checks whether sync is currently enabled from Chrome for the currently signed in account.
*
* It checks both the master sync for the device, and Chrome sync setting for the given account.
* If no user is currently signed in it returns false.
*
* @return true if sync is on, false otherwise
*/
public boolean isSyncEnabled() {
ThreadUtils.assertOnUiThread();
return mChromeSyncEnabled && doesMasterSyncSettingAllowChromeSync();
}
/**
* Checks whether sync is currently enabled for Chrome for a given account.
*
* It checks only Chrome sync setting for the given account,
* and ignores the master sync setting.
*
* @return true if sync is on, false otherwise
*/
public boolean isChromeSyncEnabled() {
ThreadUtils.assertOnUiThread();
return mChromeSyncEnabled;
}
/**
* Checks whether the master sync flag for Android allows syncing Chrome
* data.
*/
public boolean doesMasterSyncSettingAllowChromeSync() {
ThreadUtils.assertOnUiThread();
return mMasterSyncEnabled || mShouldDecoupleSyncFromMasterSync;
}
/**
* Enables Chrome sync for |mAccount| if it's non-null.
*/
public void enableChromeSync() {
ThreadUtils.assertOnUiThread();
setChromeSyncEnabled(true);
}
/**
* Disables Chrome sync for |mAccount| if it's non-null.
*/
public void disableChromeSync() {
ThreadUtils.assertOnUiThread();
setChromeSyncEnabled(false);
}
/**
* Must be called with the new account on sign-in and with null on sign-out.
*/
public void updateAccount(Account account) {
ThreadUtils.assertOnUiThread();
mAccount = account;
updateSyncability();
if (updateCachedSettings()) {
notifyObservers();
}
}
/**
* Returns the contract authority used by Chrome when talking to auto-sync.
* Exposed only to tests, so they can fake user interaction with the
* auto-sync UI.
*/
@VisibleForTesting
public static String getContractAuthority() {
return ContextUtils.getApplicationContext().getPackageName();
}
/**
* Add a new AndroidSyncSettingsObserver.
*/
public void registerObserver(AndroidSyncSettingsObserver observer) {
ThreadUtils.assertOnUiThread();
mObservers.addObserver(observer);
}
/**
* Remove an AndroidSyncSettingsObserver that was previously added.
*/
public void unregisterObserver(AndroidSyncSettingsObserver observer) {
ThreadUtils.assertOnUiThread();
mObservers.removeObserver(observer);
}
private void setChromeSyncEnabled(boolean value) {
updateSyncability();
if (value == mChromeSyncEnabled || mAccount == null) return;
mChromeSyncEnabled = value;
mSyncContentResolverDelegate.setSyncAutomatically(mAccount, mContractAuthority, value);
notifyObservers();
}
/**
* Updates whether Chrome is registered with the Android Auto-Sync Manager.
*
* This is what causes the "Chrome" option to appear in Settings -> Accounts -> Sync .
*/
private void updateSyncability() {
boolean shouldBeSyncable = mAccount != null
&& !ChromeFeatureList.isEnabled(
ChromeFeatureList.DECOUPLE_SYNC_FROM_ANDROID_MASTER_SYNC);
if (mIsSyncable == shouldBeSyncable) return;
mIsSyncable = shouldBeSyncable;
// Make account syncable if there is one.
if (shouldBeSyncable) {
mSyncContentResolverDelegate.setIsSyncable(mAccount, mContractAuthority, 1);
// This reduces unnecessary resource usage. See http://crbug.com/480688 for details.
mSyncContentResolverDelegate.removePeriodicSync(
mAccount, mContractAuthority, Bundle.EMPTY);
} else if (mAccount != null) {
mSyncContentResolverDelegate.setIsSyncable(mAccount, mContractAuthority, 0);
}
// Disable the syncability of Chrome for all other accounts.
AccountManagerFacadeProvider.getInstance().tryGetGoogleAccounts(accounts -> {
for (Account account : accounts) {
if (account.equals(mAccount)) continue;
if (mSyncContentResolverDelegate.getIsSyncable(account, mContractAuthority) <= 0) {
continue;
}
mSyncContentResolverDelegate.setIsSyncable(account, mContractAuthority, 0);
}
});
}
/**
* Update the three cached settings from the content resolver and the
* master sync decoupling setting.
*
* @return Whether either chromeSyncEnabled or masterSyncEnabled changed.
*/
private boolean updateCachedSettings() {
boolean oldChromeSyncEnabled = mChromeSyncEnabled;
boolean oldMasterSyncEnabled = mMasterSyncEnabled;
if (mAccount != null) {
mIsSyncable =
mSyncContentResolverDelegate.getIsSyncable(mAccount, mContractAuthority) > 0;
mChromeSyncEnabled =
mSyncContentResolverDelegate.getSyncAutomatically(mAccount, mContractAuthority);
} else {
mIsSyncable = false;
mChromeSyncEnabled = false;
}
mMasterSyncEnabled = mSyncContentResolverDelegate.getMasterSyncAutomatically();
if (mAccount != null && ProfileSyncService.get() != null
&& ChromeFeatureList.isEnabled(
ChromeFeatureList.DECOUPLE_SYNC_FROM_ANDROID_MASTER_SYNC)
&& mMasterSyncEnabled && !mShouldDecoupleSyncFromMasterSync) {
// Re-enabling master sync at least once should cause Sync to no longer care whether
// the former is enabled or not. This fact should be persisted via ProfileSyncService
// so it's known on the next startup.
mShouldDecoupleSyncFromMasterSync = true;
ProfileSyncService.get().setDecoupledFromAndroidMasterSync();
}
return oldChromeSyncEnabled != mChromeSyncEnabled
|| oldMasterSyncEnabled != mMasterSyncEnabled;
}
private void notifyObservers() {
for (AndroidSyncSettingsObserver observer : mObservers) {
observer.androidSyncSettingsChanged();
}
}
/**
* Returns the sync account in the last used regular profile.
*/
private static @Nullable Account getSyncAccount() {
IdentityManager identityManager = IdentityServicesProvider.get().getIdentityManager(
Profile.getLastUsedRegularProfile());
return CoreAccountInfo.getAndroidAccountFrom(
identityManager.getPrimaryAccountInfo(ConsentLevel.SYNC));
}
}