// 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.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.ChromeApplication;
import org.chromium.chrome.browser.childaccounts.ChildAccountService;
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.signin.AccountManagementFragment;
import org.chromium.chrome.browser.signin.SigninManager;
import org.chromium.chrome.browser.sync.ui.PassphraseActivity;
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";
private static SyncController sInstance;
private static boolean sInitialized = false;
private final Context mContext;
private final ChromeSigninController mChromeSigninController;
private final ProfileSyncService mProfileSyncService;
private final SyncNotificationController mSyncNotificationController;
private SyncController(Context context) {
mContext = context;
mChromeSigninController = ChromeSigninController.get(mContext);
AndroidSyncSettings.registerObserver(context, this);
mProfileSyncService = ProfileSyncService.get();
mProfileSyncService.addSyncStateChangedListener(this);
mProfileSyncService.setMasterSyncEnabledProvider(
new ProfileSyncService.MasterSyncEnabledProvider() {
public boolean isMasterSyncEnabled() {
return AndroidSyncSettings.isMasterSyncEnabled(mContext);
}
});
setSessionsId();
// Create the SyncNotificationController.
mSyncNotificationController = new SyncNotificationController(
mContext, PassphraseActivity.class, AccountManagementFragment.class);
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 =
((ChromeApplication) context.getApplicationContext()).createGmsCoreSyncListener();
if (gmsCoreSyncListener != null) {
mProfileSyncService.addSyncStateChangedListener(gmsCoreSyncListener);
}
SigninManager.get(mContext).addSignInStateObserver(new SigninManager.SignInStateObserver() {
@Override
public void onSignedIn() {
mProfileSyncService.requestStart();
}
@Override
public void onSignedOut() {}
});
}
/**
* Retrieve the singleton instance of this class.
*
* @param context the current context.
* @return the singleton instance.
*/
@Nullable
public static SyncController get(Context context) {
ThreadUtils.assertOnUiThread();
if (!sInitialized) {
if (ProfileSyncService.get() != null) {
sInstance = new SyncController(context.getApplicationContext());
}
sInitialized = true;
}
return sInstance;
}
/**
* Updates sync to reflect the state of the Android sync settings.
*/
private void updateSyncStateFromAndroid() {
boolean isSyncEnabled = AndroidSyncSettings.isSyncEnabled(mContext);
if (isSyncEnabled == mProfileSyncService.isSyncRequested()) return;
if (isSyncEnabled) {
mProfileSyncService.requestStart();
} else {
if (ChildAccountService.isChildAccount()) {
// 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(mContext);
} else {
if (AndroidSyncSettings.isMasterSyncEnabled(mContext)) {
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(mContext);
if (mProfileSyncService.isSyncRequested()) {
if (!invalidationController.isStarted()) {
invalidationController.ensureStartedAndUpdateRegisteredTypes();
}
if (!AndroidSyncSettings.isSyncEnabled(mContext)) {
assert AndroidSyncSettings.isMasterSyncEnabled(mContext);
AndroidSyncSettings.enableChromeSync(mContext);
}
} else {
if (invalidationController.isStarted()) {
invalidationController.stop();
}
if (AndroidSyncSettings.isSyncEnabled(mContext)) {
// 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(mContext);
}
}
}
/**
* 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.isBackendInitialized()
&& 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);
}
}