// Copyright 2016 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.signin;
import android.annotation.SuppressLint;
import android.app.DialogFragment;
import android.app.Fragment;
import android.app.FragmentManager;
import android.content.Context;
import android.support.annotation.IntDef;
import android.text.TextUtils;
import org.chromium.base.Callback;
import org.chromium.base.Promise;
import org.chromium.chrome.browser.signin.ConfirmImportSyncDataDialog.ImportSyncType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* This class takes care of the various dialogs that must be shown when the user changes the
* account they are syncing to (either directly, or by signing in to a new account). Most of the
* complexity is due to many of the decisions getting answered through callbacks.
*
* This class progresses along the following state machine:
*
* E----\ G--\
* ^ | ^ |
* | | | v
* A->B->C->D-+->F->H
* | |
* \-------/
*
* Where:
* A - Start
* B - Decision: progress to C if the user signed in previously to a different account, F otherwise.
* C - Decision: progress to E if we are switching from a managed account, D otherwise.
* D - Action: show Import Data Dialog.
* E - Action: show Switching from Managed Account Dialog.
* F - Decision: progress to G if we are switching to a managed account, H otherwise.
* G - Action: show Switching to Managed Account Dialog.
* H - End: perform {@link ConfirmImportSyncDataDialog.Listener#onConfirm} with the result of the
* Import Data Dialog, if displayed or true if switching from a managed account.
*
* At any dialog, the user can cancel the dialog and end the whole process (resulting in
* {@link ConfirmImportSyncDataDialog.Listener#onCancel}).
*/
public class ConfirmSyncDataStateMachine
implements ConfirmImportSyncDataDialog.Listener, ConfirmManagedSyncDataDialog.Listener {
@IntDef({
BEFORE_OLD_ACCOUNT_DIALOG, BEFORE_NEW_ACCOUNT_DIALOG,
AFTER_NEW_ACCOUNT_DIALOG, DONE
})
@Retention(RetentionPolicy.SOURCE)
private @interface State {}
private static final int BEFORE_OLD_ACCOUNT_DIALOG = 0; // Start of state B.
private static final int BEFORE_NEW_ACCOUNT_DIALOG = 1; // Start of state F.
private static final int AFTER_NEW_ACCOUNT_DIALOG = 2; // Start of state H.
private static final int DONE = 4;
private boolean mWipeData;
@State private int mState = BEFORE_OLD_ACCOUNT_DIALOG;
private final ConfirmImportSyncDataDialog.Listener mCallback;
private final String mOldAccountName;
private final String mNewAccountName;
private final boolean mCurrentlyManaged;
private final Promise<Boolean> mNewAccountManaged = new Promise<>();
private final FragmentManager mFragmentManager;
private final Context mContext;
private final ImportSyncType mImportSyncType;
/**
* Run this state machine, displaying the appropriate dialogs.
* @param callback One of the two functions of the {@link ConfirmImportSyncDataDialog.Listener}
* are guaranteed to be called.
*/
public static void run(String oldAccountName, String newAccountName,
ImportSyncType importSyncType, FragmentManager fragmentManager, Context context,
ConfirmImportSyncDataDialog.Listener callback) {
// Includes implicit not-null assertion.
assert !newAccountName.equals("") : "New account name must be provided.";
ConfirmSyncDataStateMachine stateMachine = new ConfirmSyncDataStateMachine(oldAccountName,
newAccountName, importSyncType, fragmentManager, context, callback);
stateMachine.progress();
}
/**
* If any of the dialogs used by this state machine are shown, cancel them. If this state
* machine is running and a dialog is being shown, the given
* {@link ConfirmImportSyncDataDialog.Listener#onCancel())} is called.
*/
public static void cancelAllDialogs(FragmentManager fragmentManager) {
cancelDialog(fragmentManager,
ConfirmImportSyncDataDialog.CONFIRM_IMPORT_SYNC_DATA_DIALOG_TAG);
cancelDialog(fragmentManager,
ConfirmManagedSyncDataDialog.CONFIRM_IMPORT_SYNC_DATA_DIALOG_TAG);
}
private static void cancelDialog(FragmentManager fragmentManager, String tag) {
Fragment fragment = fragmentManager.findFragmentByTag(tag);
if (fragment == null) return;
DialogFragment dialogFragment = (DialogFragment) fragment;
if (dialogFragment.getDialog() == null) return;
dialogFragment.getDialog().cancel();
}
private ConfirmSyncDataStateMachine(String oldAccountName, String newAccountName,
ImportSyncType importSyncType, FragmentManager fragmentManager, Context context,
ConfirmImportSyncDataDialog.Listener callback) {
mOldAccountName = oldAccountName;
mNewAccountName = newAccountName;
mImportSyncType = importSyncType;
mFragmentManager = fragmentManager;
mContext = context;
mCallback = callback;
mCurrentlyManaged = SigninManager.get(context).getManagementDomain() != null;
// This check isn't needed right now, but can take a few seconds, so we kick it off early.
SigninManager.isUserManaged(mNewAccountName, mNewAccountManaged.fulfillmentCallback());
}
/**
* This will progress the state machine, by moving the state along and then by either calling
* itself directly or creating a dialog. If the dialog is dismissed or answered negatively the
* entire flow is over, if it is answered positively one of the onConfirm functions is called
* and this function is called again.
*/
// TODO(crbug.com/635567): Fix this properly.
@SuppressLint("SwitchIntDef")
private void progress() {
switch (mState) {
case BEFORE_OLD_ACCOUNT_DIALOG:
mState = BEFORE_NEW_ACCOUNT_DIALOG;
if (TextUtils.isEmpty(mOldAccountName) || mNewAccountName.equals(mOldAccountName)) {
// If there is no old account or the user is just logging back into whatever
// they were previously logged in as, progress past the old account checks.
progress();
} else if (mCurrentlyManaged
&& mImportSyncType == ImportSyncType.SWITCHING_SYNC_ACCOUNTS) {
// We only care about the user's previous account being managed if they are
// switching accounts.
mWipeData = true;
// This will call back into onConfirm() on success.
ConfirmManagedSyncDataDialog.showSwitchFromManagedAccountDialog(this,
mFragmentManager, mContext.getResources(),
SigninManager.extractDomainName(mOldAccountName),
mOldAccountName, mNewAccountName);
} else {
// This will call back into onConfirm(boolean wipeData) on success.
ConfirmImportSyncDataDialog.showNewInstance(mOldAccountName, mNewAccountName,
mImportSyncType, mFragmentManager, this);
}
break;
case BEFORE_NEW_ACCOUNT_DIALOG:
mState = AFTER_NEW_ACCOUNT_DIALOG;
mNewAccountManaged.then(new Callback<Boolean>() {
@Override
public void onResult(Boolean newAccountManaged) {
if (newAccountManaged) {
// Show 'logging into managed account' dialog
// This will call back into onConfirm on success.
ConfirmManagedSyncDataDialog.showSignInToManagedAccountDialog(
ConfirmSyncDataStateMachine.this,
mFragmentManager, mContext.getResources(),
SigninManager.extractDomainName(mNewAccountName));
} else {
progress();
}
}
});
break;
case AFTER_NEW_ACCOUNT_DIALOG:
mState = DONE;
mCallback.onConfirm(mWipeData);
break;
default:
assert false : "Invalid state: " + mState;
}
}
// ConfirmImportSyncDataDialog.Listener implementation.
@Override
public void onConfirm(boolean wipeData) {
mWipeData = wipeData;
progress();
}
// ConfirmManagedSyncDataDialog.Listener implementation.
@Override
public void onConfirm() {
progress();
}
// ConfirmImportSyncDataDialog.Listener & ConfirmManagedSyncDataDialog.Listener implementation.
@Override
public void onCancel() {
mState = DONE;
mCallback.onCancel();
}
}