// Copyright 2013 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.accounts.Account;
import android.app.Activity;
import android.content.Context;
import android.os.Handler;
import org.chromium.base.ActivityState;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.Callback;
import org.chromium.base.Log;
import org.chromium.base.ObserverList;
import org.chromium.base.Promise;
import org.chromium.base.ThreadUtils;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.chrome.browser.externalauth.ExternalAuthUtils;
import org.chromium.chrome.browser.externalauth.UserRecoverableErrorHandler;
import org.chromium.chrome.browser.sync.SyncUserDataWiper;
import org.chromium.components.signin.AccountManagerHelper;
import org.chromium.components.signin.ChromeSigninController;
import org.chromium.components.sync.AndroidSyncSettings;
import javax.annotation.Nullable;
/**
* Android wrapper of the SigninManager which provides access from the Java layer.
* <p/>
* This class handles common paths during the sign-in and sign-out flows.
* <p/>
* Only usable from the UI thread as the native SigninManager requires its access to be in the
* UI thread.
* <p/>
* See chrome/browser/signin/signin_manager_android.h for more details.
*/
public class SigninManager implements AccountTrackerService.OnSystemAccountsSeededListener {
private static final String TAG = "SigninManager";
private static SigninManager sSigninManager;
private static int sSignInAccessPoint = SigninAccessPoint.UNKNOWN;
private final Context mContext;
private final long mNativeSigninManagerAndroid;
/** Tracks whether the First Run check has been completed.
*
* A new sign-in can not be started while this is pending, to prevent the
* pending check from eventually starting a 2nd sign-in.
*/
private boolean mFirstRunCheckIsPending = true;
private final ObserverList<SignInStateObserver> mSignInStateObservers =
new ObserverList<SignInStateObserver>();
private final ObserverList<SignInAllowedObserver> mSignInAllowedObservers =
new ObserverList<SignInAllowedObserver>();
/**
* Will be set during the sign in process, and nulled out when there is not a pending sign in.
* Needs to be null checked after ever async entry point because it can be nulled out at any time
* by system accounts changing.
*/
private SignInState mSignInState;
private Runnable mSignOutCallback;
private boolean mSigninAllowedByPolicy;
private boolean mSignOutInProgress;
/**
* A SignInStateObserver is notified when the user signs in to or out of Chrome.
*/
public interface SignInStateObserver {
/**
* Invoked when the user has signed in to Chrome.
*/
void onSignedIn();
/**
* Invoked when the user has signed out of Chrome.
*/
void onSignedOut();
}
/**
* SignInAllowedObservers will be notified once signing-in becomes allowed or disallowed.
*/
public interface SignInAllowedObserver {
/**
* Invoked once all startup checks are done and signing-in becomes allowed, or disallowed.
*/
void onSignInAllowedChanged();
}
/**
* Callbacks for the sign-in flow.
*/
public interface SignInCallback {
/**
* Invoked after sign-in is completed successfully.
*/
void onSignInComplete();
/**
* Invoked if the sign-in processes does not complete for any reason.
*/
void onSignInAborted();
}
/**
* Hooks for wiping data during sign out.
*/
public interface WipeDataHooks {
/**
* Called before data is wiped.
*/
public void preWipeData();
/**
* Called after data is wiped.
*/
public void postWipeData();
}
/**
* Contains all the state needed for signin. This forces signin flow state to be
* cleared atomically, and all final fields to be set upon initialization.
*/
private static class SignInState {
public final Account account;
public final Activity activity;
public final SignInCallback callback;
/**
* If the system accounts need to be seeded, the sign in flow will block for that to occur.
* This boolean should be set to true during that time and then reset back to false
* afterwards. This allows the manager to know if it should progress the flow when the
* account tracker broadcasts updates.
*/
public boolean blockedOnAccountSeeding = false;
/**
* @param account The account to sign in to.
* @param activity Reference to the UI to use for dialogs. Null means forced signin.
* @param callback Called when the sign-in process finishes or is cancelled. Can be null.
*/
public SignInState(
Account account, @Nullable Activity activity, @Nullable SignInCallback callback) {
this.account = account;
this.activity = activity;
this.callback = callback;
}
/**
* Returns whether this is an interactive sign-in flow.
*/
public boolean isInteractive() {
return activity != null;
}
/**
* Returns whether the sign-in flow activity was set but is no longer visible to the user.
*/
private boolean isActivityInvisible() {
return activity != null
&& (ApplicationStatus.getStateForActivity(activity) == ActivityState.STOPPED
|| ApplicationStatus.getStateForActivity(activity)
== ActivityState.DESTROYED);
}
}
/**
* A helper method for retrieving the application-wide SigninManager.
* <p/>
* Can only be accessed on the main thread.
*
* @param context the ApplicationContext is retrieved from the context used as an argument.
* @return a singleton instance of the SigninManager.
*/
public static SigninManager get(Context context) {
ThreadUtils.assertOnUiThread();
if (sSigninManager == null) {
sSigninManager = new SigninManager(context);
}
return sSigninManager;
}
private SigninManager(Context context) {
ThreadUtils.assertOnUiThread();
mContext = context.getApplicationContext();
mNativeSigninManagerAndroid = nativeInit();
mSigninAllowedByPolicy = nativeIsSigninAllowedByPolicy(mNativeSigninManagerAndroid);
AccountTrackerService.get(mContext).addSystemAccountsSeededListener(this);
}
/**
* Log the access point when the user see the view of choosing account to sign in.
* @param accessPoint the enum value of AccessPoint defined in signin_metrics.h.
*/
public static void logSigninStartAccessPoint(int accessPoint) {
RecordHistogram.recordEnumeratedHistogram(
"Signin.SigninStartedAccessPoint", accessPoint, SigninAccessPoint.MAX);
sSignInAccessPoint = accessPoint;
}
private void logSigninCompleteAccessPoint() {
RecordHistogram.recordEnumeratedHistogram(
"Signin.SigninCompletedAccessPoint", sSignInAccessPoint, SigninAccessPoint.MAX);
sSignInAccessPoint = SigninAccessPoint.UNKNOWN;
}
/**
* Notifies the SigninManager that the First Run check has completed.
*
* The user will be allowed to sign-in once this is signaled.
*/
public void onFirstRunCheckDone() {
mFirstRunCheckIsPending = false;
if (isSignInAllowed()) {
notifySignInAllowedChanged();
}
}
/**
* Returns true if signin can be started now.
*/
public boolean isSignInAllowed() {
return !mFirstRunCheckIsPending && mSignInState == null && mSigninAllowedByPolicy
&& ChromeSigninController.get(mContext).getSignedInUser() == null
&& isSigninSupported();
}
/**
* Returns true if signin is disabled by policy.
*/
public boolean isSigninDisabledByPolicy() {
return !mSigninAllowedByPolicy;
}
/**
* @return Whether true if the current user is not demo user and the user has a reasonable
* Google Play Services installed.
*/
public boolean isSigninSupported() {
return !ApiCompatibilityUtils.isDemoUser(mContext)
&& !ExternalAuthUtils.getInstance().isGooglePlayServicesMissing(mContext);
}
/**
* Registers a SignInStateObserver to be notified when the user signs in or out of Chrome.
*/
public void addSignInStateObserver(SignInStateObserver observer) {
mSignInStateObservers.addObserver(observer);
}
/**
* Unregisters a SignInStateObserver to be notified when the user signs in or out of Chrome.
*/
public void removeSignInStateObserver(SignInStateObserver observer) {
mSignInStateObservers.removeObserver(observer);
}
public void addSignInAllowedObserver(SignInAllowedObserver observer) {
mSignInAllowedObservers.addObserver(observer);
}
public void removeSignInAllowedObserver(SignInAllowedObserver observer) {
mSignInAllowedObservers.removeObserver(observer);
}
private void notifySignInAllowedChanged() {
new Handler().post(new Runnable() {
@Override
public void run() {
for (SignInAllowedObserver observer : mSignInAllowedObservers) {
observer.onSignInAllowedChanged();
}
}
});
}
/**
* Continue pending sign in after system accounts have been seeded into AccountTrackerService.
*/
@Override
public void onSystemAccountsSeedingComplete() {
if (mSignInState != null && mSignInState.blockedOnAccountSeeding) {
mSignInState.blockedOnAccountSeeding = false;
progressSignInFlowCheckPolicy();
}
}
/**
* Clear pending sign in when system accounts in AccountTrackerService were refreshed.
*/
@Override
public void onSystemAccountsChanged() {
if (mSignInState != null) {
abortSignIn();
}
}
/**
* Starts the sign-in flow, and executes the callback when finished.
*
* If an activity is provided, it is considered an "interactive" sign-in and the user can be
* prompted to confirm various aspects of sign-in using dialogs inside the activity.
* The sign-in flow goes through the following steps:
*
* - Wait for AccountTrackerService to be seeded.
* - If interactive, confirm the account change with the user.
* - Wait for policy to be checked for the account.
* - If interactive and the account is managed, warn the user.
* - If managed, wait for the policy to be fetched.
* - Complete sign-in with the native SigninManager and kick off token requests.
* - Call the callback if provided.
*
* @param account The account to sign in to.
* @param activity The activity used to launch UI prompts, or null for a forced signin.
* @param callback Optional callback for when the sign-in process is finished.
*/
public void signIn(
Account account, @Nullable Activity activity, @Nullable SignInCallback callback) {
if (account == null) {
Log.w(TAG, "Ignoring sign-in request due to null account.");
if (callback != null) callback.onSignInAborted();
return;
}
if (mSignInState != null) {
Log.w(TAG, "Ignoring sign-in request as another sign-in request is pending.");
if (callback != null) callback.onSignInAborted();
return;
}
if (mFirstRunCheckIsPending) {
Log.w(TAG, "Ignoring sign-in request until the First Run check completes.");
if (callback != null) callback.onSignInAborted();
return;
}
mSignInState = new SignInState(account, activity, callback);
notifySignInAllowedChanged();
progressSignInFlowSeedSystemAccounts();
}
/**
* Same as above but retrieves the Account object for the given accountName.
*/
public void signIn(String accountName, @Nullable final Activity activity,
@Nullable final SignInCallback callback) {
AccountManagerHelper.get(mContext).getAccountFromName(accountName, new Callback<Account>() {
@Override
public void onResult(Account account) {
signIn(account, activity, callback);
}
});
}
private void progressSignInFlowSeedSystemAccounts() {
if (AccountTrackerService.get(mContext).checkAndSeedSystemAccounts()) {
progressSignInFlowCheckPolicy();
} else if (AccountIdProvider.getInstance().canBeUsed(mContext)) {
mSignInState.blockedOnAccountSeeding = true;
} else {
Activity activity = mSignInState.activity;
UserRecoverableErrorHandler errorHandler = activity != null
? new UserRecoverableErrorHandler.ModalDialog(activity)
: new UserRecoverableErrorHandler.SystemNotification();
ExternalAuthUtils.getInstance().canUseGooglePlayServices(mContext, errorHandler);
Log.w(TAG, "Cancelling the sign-in process as Google Play services is unavailable");
abortSignIn();
}
}
/**
* Continues the signin flow by checking if there is a policy that the account is subject to.
*/
private void progressSignInFlowCheckPolicy() {
if (mSignInState == null) {
Log.w(TAG, "Ignoring sign in progress request as no pending sign in.");
return;
}
if (mSignInState.isActivityInvisible()) {
abortSignIn();
return;
}
if (!nativeShouldLoadPolicyForUser(mSignInState.account.name)) {
// Proceed with the sign-in flow without checking for policy if it can be determined
// that this account can't have management enabled based on the username.
finishSignIn();
return;
}
Log.d(TAG, "Checking if account has policy management enabled");
// This will call back to onPolicyCheckedBeforeSignIn.
nativeCheckPolicyBeforeSignIn(mNativeSigninManagerAndroid, mSignInState.account.name);
}
@CalledByNative
private void onPolicyCheckedBeforeSignIn(String managementDomain) {
assert mSignInState != null;
if (managementDomain == null) {
Log.d(TAG, "Account doesn't have policy");
finishSignIn();
return;
}
if (mSignInState.isActivityInvisible()) {
abortSignIn();
return;
}
// The user has already been notified that they are signing into a managed account.
// This will call back to onPolicyFetchedBeforeSignIn.
nativeFetchPolicyBeforeSignIn(mNativeSigninManagerAndroid);
}
@CalledByNative
private void onPolicyFetchedBeforeSignIn() {
// Policy has been fetched for the user and is being enforced; features like sync may now
// be disabled by policy, and the rest of the sign-in flow can be resumed.
finishSignIn();
}
private void finishSignIn() {
// This method should be called at most once per sign-in flow.
assert mSignInState != null;
// Tell the native side that sign-in has completed.
nativeOnSignInCompleted(mNativeSigninManagerAndroid, mSignInState.account.name);
// Cache the signed-in account name. This must be done after the native call, otherwise
// sync tries to start without being signed in natively and crashes.
ChromeSigninController.get(mContext).setSignedInAccountName(mSignInState.account.name);
AndroidSyncSettings.updateAccount(mContext, mSignInState.account);
if (mSignInState.callback != null) {
mSignInState.callback.onSignInComplete();
}
// Trigger token requests via native.
logInSignedInUser();
if (mSignInState.isInteractive()) {
// If signin was a user action, record that it succeeded.
RecordUserAction.record("Signin_Signin_Succeed");
logSigninCompleteAccessPoint();
// Log signin in reason as defined in signin_metrics.h. Right now only
// SIGNIN_PRIMARY_ACCOUNT available on Android.
RecordHistogram.recordEnumeratedHistogram("Signin.SigninReason",
SigninReason.SIGNIN_PRIMARY_ACCOUNT, SigninReason.MAX);
}
Log.d(TAG, "Signin completed.");
mSignInState = null;
notifySignInAllowedChanged();
for (SignInStateObserver observer : mSignInStateObservers) {
observer.onSignedIn();
}
}
/**
* Invokes signOut and returns a {@link Promise} that will be fulfilled on completion.
* This is equivalent to calling {@link #signOut(Runnable callback)} with a callback that
* fulfills the returned {@link Promise}.
*/
public Promise<Void> signOutPromise() {
final Promise<Void> promise = new Promise<Void>();
signOut(new Runnable(){
@Override
public void run() {
promise.fulfill(null);
}
});
return promise;
}
/**
* Invokes signOut with no callback or wipeDataHooks.
*/
public void signOut() {
signOut(null, null);
}
/**
* Invokes signOut() with no wipeDataHooks.
*/
public void signOut(Runnable callback) {
signOut(callback, null);
}
/**
* Signs out of Chrome.
* <p/>
* This method clears the signed-in username, stops sync and sends out a
* sign-out notification on the native side.
*
* @param callback Will be invoked after signout completes, if not null.
* @param wipeDataHooks Hooks to call during data wiping in case the account is managed.
*/
public void signOut(Runnable callback, WipeDataHooks wipeDataHooks) {
mSignOutInProgress = true;
mSignOutCallback = callback;
boolean wipeData = getManagementDomain() != null;
Log.d(TAG, "Signing out, wipe data? " + wipeData);
// Native signout must happen before resetting the account so data is deleted correctly.
// http://crbug.com/589028
nativeSignOut(mNativeSigninManagerAndroid);
ChromeSigninController.get(mContext).setSignedInAccountName(null);
AndroidSyncSettings.updateAccount(mContext, null);
if (wipeData) {
wipeProfileData(wipeDataHooks);
} else {
onSignOutDone();
}
AccountTrackerService.get(mContext).invalidateAccountSeedStatus(true);
}
/**
* Returns the management domain if the signed in account is managed, otherwise returns null.
*/
public String getManagementDomain() {
return nativeGetManagementDomain(mNativeSigninManagerAndroid);
}
public void logInSignedInUser() {
nativeLogInSignedInUser(mNativeSigninManagerAndroid);
}
public void clearLastSignedInUser() {
nativeClearLastSignedInUser(mNativeSigninManagerAndroid);
}
/**
* Aborts the current sign in.
*
* Package protected to allow dialog fragments to abort the signin flow.
*/
void abortSignIn() {
// Ensure this function can only run once per signin flow.
SignInState signInState = mSignInState;
assert signInState != null;
mSignInState = null;
if (signInState.callback != null) {
signInState.callback.onSignInAborted();
}
nativeAbortSignIn(mNativeSigninManagerAndroid);
Log.d(TAG, "Signin flow aborted.");
notifySignInAllowedChanged();
}
private void wipeProfileData(WipeDataHooks hooks) {
if (hooks != null) hooks.preWipeData();
// This will call back to onProfileDataWiped().
nativeWipeProfileData(mNativeSigninManagerAndroid, hooks);
}
/**
* Convenience method to return a Promise to be fulfilled when the user's sync data has been
* wiped if the parameter is true, or an already fulfilled Promise if the parameter is false.
*/
public static Promise<Void> wipeSyncUserDataIfRequired(boolean required) {
if (required) {
return SyncUserDataWiper.wipeSyncUserData();
} else {
return Promise.fulfilled(null);
}
}
@CalledByNative
private void onProfileDataWiped(WipeDataHooks hooks) {
if (hooks != null) hooks.postWipeData();
onSignOutDone();
}
@CalledByNative
private void onNativeSignOut() {
if (!mSignOutInProgress) {
signOut();
}
}
private void onSignOutDone() {
mSignOutInProgress = false;
if (mSignOutCallback != null) {
new Handler().post(mSignOutCallback);
mSignOutCallback = null;
}
for (SignInStateObserver observer : mSignInStateObservers) {
observer.onSignedOut();
}
}
/**
* @return Whether there is a signed in account on the native side.
*/
public boolean isSignedInOnNative() {
return nativeIsSignedInOnNative(mNativeSigninManagerAndroid);
}
@CalledByNative
private void onSigninAllowedByPolicyChanged(boolean newSigninAllowedByPolicy) {
mSigninAllowedByPolicy = newSigninAllowedByPolicy;
notifySignInAllowedChanged();
}
/**
* Performs an asynchronous check to see if the user is a managed user.
* @param callback A callback to be called with true if the user is a managed user and false
* otherwise.
*/
public static void isUserManaged(String email, final Callback<Boolean> callback) {
if (nativeShouldLoadPolicyForUser(email)) {
nativeIsUserManaged(email, callback);
} else {
// Although we know the result immediately, the caller may not be able to handle the
// callback being executed during this method call. So we post the callback on the
// looper.
ThreadUtils.postOnUiThread(new Runnable() {
@Override
public void run() {
callback.onResult(false);
}
});
}
}
public static String extractDomainName(String email) {
return nativeExtractDomainName(email);
}
@VisibleForTesting
public static void setInstanceForTesting(SigninManager signinManager) {
sSigninManager = signinManager;
}
// Native methods.
private static native String nativeExtractDomainName(String email);
private static native boolean nativeShouldLoadPolicyForUser(String username);
private static native void nativeIsUserManaged(String username, Callback<Boolean> callback);
private native long nativeInit();
private native boolean nativeIsSigninAllowedByPolicy(long nativeSigninManagerAndroid);
private native void nativeCheckPolicyBeforeSignIn(
long nativeSigninManagerAndroid, String username);
private native void nativeFetchPolicyBeforeSignIn(long nativeSigninManagerAndroid);
private native void nativeAbortSignIn(long nativeSigninManagerAndroid);
private native void nativeOnSignInCompleted(long nativeSigninManagerAndroid, String username);
private native void nativeSignOut(long nativeSigninManagerAndroid);
private native String nativeGetManagementDomain(long nativeSigninManagerAndroid);
private native void nativeWipeProfileData(long nativeSigninManagerAndroid, WipeDataHooks hooks);
private native void nativeClearLastSignedInUser(long nativeSigninManagerAndroid);
private native void nativeLogInSignedInUser(long nativeSigninManagerAndroid);
private native boolean nativeIsSignedInOnNative(long nativeSigninManagerAndroid);
}