blob: 9e28c6f17fda8b3361303dce1608318b8757916b [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.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import org.chromium.base.ActivityState;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.ApplicationStatus.ActivityStateListener;
import org.chromium.base.Log;
import org.chromium.base.ThreadUtils;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.chrome.browser.AppHooks;
import org.chromium.chrome.browser.identity.UniqueIdentificationGenerator;
import org.chromium.chrome.browser.identity.UniqueIdentificationGeneratorFactory;
import org.chromium.chrome.browser.invalidation.InvalidationController;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.signin.SigninManager;
import org.chromium.components.signin.ChromeSigninController;
import org.chromium.components.sync.AndroidSyncSettings;
import org.chromium.components.sync.ModelType;
import org.chromium.components.sync.PassphraseType;
import org.chromium.components.sync.StopSource;
import javax.annotation.Nullable;
/**
* SyncController handles the coordination of sync state between the invalidation controller,
* the Android sync settings, and the native sync code.
*
* It also handles initialization of some pieces of sync state on startup.
*
* Sync state can be changed from four places:
*
* - The Chrome UI, which will call SyncController directly.
* - Native sync, which can disable it via a dashboard stop and clear.
* - Android's Chrome sync setting.
* - Android's master sync setting.
*
* SyncController implements listeners for the last three cases. When master sync is disabled, we
* are careful to not change the Android Chrome sync setting so we know whether to turn sync back
* on when it is re-enabled.
*/
public class SyncController implements ProfileSyncService.SyncStateChangedListener,
AndroidSyncSettings.AndroidSyncSettingsObserver {
private static final String TAG = "SyncController";
/**
* An identifier for the generator in UniqueIdentificationGeneratorFactory to be used to
* generate the sync sessions ID. The generator is registered in the Application's onCreate
* method.
*/
public static final String GENERATOR_ID = "SYNC";
@VisibleForTesting
public static final String SESSION_TAG_PREFIX = "session_sync";
@SuppressLint("StaticFieldLeak")
private static SyncController sInstance;
private static boolean sInitialized;
private final ChromeSigninController mChromeSigninController;
private final ProfileSyncService mProfileSyncService;
private final SyncNotificationController mSyncNotificationController;
private SyncController() {
mChromeSigninController = ChromeSigninController.get();
AndroidSyncSettings.registerObserver(this);
mProfileSyncService = ProfileSyncService.get();
mProfileSyncService.addSyncStateChangedListener(this);
mProfileSyncService.setMasterSyncEnabledProvider(
new ProfileSyncService.MasterSyncEnabledProvider() {
@Override
public boolean isMasterSyncEnabled() {
return AndroidSyncSettings.isMasterSyncEnabled();
}
});
setSessionsId();
// Create the SyncNotificationController.
mSyncNotificationController = new SyncNotificationController();
mProfileSyncService.addSyncStateChangedListener(mSyncNotificationController);
updateSyncStateFromAndroid();
// When the application gets paused, tell sync to flush the directory to disk.
ApplicationStatus.registerStateListenerForAllActivities(new ActivityStateListener() {
@Override
public void onActivityStateChange(Activity activity, int newState) {
if (newState == ActivityState.PAUSED) {
mProfileSyncService.flushDirectory();
}
}
});
GmsCoreSyncListener gmsCoreSyncListener = AppHooks.get().createGmsCoreSyncListener();
if (gmsCoreSyncListener != null) {
mProfileSyncService.addSyncStateChangedListener(gmsCoreSyncListener);
}
SigninManager.get().addSignInStateObserver(new SigninManager.SignInStateObserver() {
@Override
public void onSignedIn() {
mProfileSyncService.requestStart();
}
@Override
public void onSignedOut() {}
});
}
/**
* Retrieve the singleton instance of this class.
*
* @return the singleton instance.
*/
@Nullable
public static SyncController get() {
ThreadUtils.assertOnUiThread();
if (!sInitialized) {
if (ProfileSyncService.get() != null) {
sInstance = new SyncController();
}
sInitialized = true;
}
return sInstance;
}
/**
* Retrieve the singleton instance of this class.
* @deprecated Use get with no arguments instead.
* @return the singleton instance.
*/
@Nullable
public static SyncController get(Context context) {
return get();
}
/**
* Updates sync to reflect the state of the Android sync settings.
*/
private void updateSyncStateFromAndroid() {
// Note: |isChromeSyncEnabled| maps to SyncRequested, and
// |isMasterSyncEnabled| maps to *both* SyncRequested and
// SyncAllowedByPlatform.
// TODO(crbug.com/867901): Don't mix these two concepts.
mProfileSyncService.setSyncAllowedByPlatform(AndroidSyncSettings.isMasterSyncEnabled());
boolean isSyncEnabled = AndroidSyncSettings.isSyncEnabled();
if (isSyncEnabled == mProfileSyncService.isSyncRequested()) return;
if (isSyncEnabled) {
mProfileSyncService.requestStart();
} else {
if (Profile.getLastUsedProfile().isChild()) {
// For child accounts, Sync needs to stay enabled, so we reenable it in settings.
// TODO(bauerb): Remove the dependency on child account code and instead go through
// prefs (here and in the Sync customization UI).
AndroidSyncSettings.enableChromeSync();
} else {
if (AndroidSyncSettings.isMasterSyncEnabled()) {
RecordHistogram.recordEnumeratedHistogram("Sync.StopSource",
StopSource.ANDROID_CHROME_SYNC, StopSource.STOP_SOURCE_LIMIT);
} else {
RecordHistogram.recordEnumeratedHistogram("Sync.StopSource",
StopSource.ANDROID_MASTER_SYNC, StopSource.STOP_SOURCE_LIMIT);
}
mProfileSyncService.requestStop();
}
}
}
/**
* From {@link ProfileSyncService.SyncStateChangedListener}.
*
* Changes the invalidation controller and Android sync setting state to match
* the new native sync state.
*/
@Override
public void syncStateChanged() {
ThreadUtils.assertOnUiThread();
InvalidationController invalidationController = InvalidationController.get();
if (mProfileSyncService.isSyncRequested()) {
if (!invalidationController.isStarted()) {
invalidationController.ensureStartedAndUpdateRegisteredTypes();
}
if (!AndroidSyncSettings.isSyncEnabled()) {
assert AndroidSyncSettings.isMasterSyncEnabled();
AndroidSyncSettings.enableChromeSync();
}
} else {
if (invalidationController.isStarted()) {
invalidationController.stop();
}
if (AndroidSyncSettings.isSyncEnabled()) {
// Both Android's master and Chrome sync setting are enabled, so we want to disable
// the Chrome sync setting to match isSyncRequested. We have to be careful not to
// disable it when isSyncRequested becomes false due to master sync being disabled
// so that sync will turn back on if master sync is re-enabled.
AndroidSyncSettings.disableChromeSync();
}
}
}
/**
* From {@link AndroidSyncSettings.AndroidSyncSettingsObserver}.
*/
@Override
public void androidSyncSettingsChanged() {
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
updateSyncStateFromAndroid();
}
});
}
/**
* @return Whether sync is enabled to sync urls or open tabs with a non custom passphrase.
*/
public boolean isSyncingUrlsWithKeystorePassphrase() {
return mProfileSyncService.isEngineInitialized()
&& mProfileSyncService.getPreferredDataTypes().contains(ModelType.TYPED_URLS)
&& mProfileSyncService.getPassphraseType().equals(
PassphraseType.KEYSTORE_PASSPHRASE);
}
/**
* Returns the SyncNotificationController.
*/
public SyncNotificationController getSyncNotificationController() {
return mSyncNotificationController;
}
/**
* Set the sessions ID using the generator that was registered for GENERATOR_ID.
*/
private void setSessionsId() {
UniqueIdentificationGenerator generator =
UniqueIdentificationGeneratorFactory.getInstance(GENERATOR_ID);
String uniqueTag = generator.getUniqueId(null);
if (uniqueTag.isEmpty()) {
Log.e(TAG, "Unable to get unique tag for sync. "
+ "This may lead to unexpected tab sync behavior.");
return;
}
mProfileSyncService.setSessionsId(SESSION_TAG_PREFIX + uniqueTag);
}
}