blob: 5309344082f19e2039eabf3525150ed36bb04c79 [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.signin.services;
import android.accounts.Account;
import android.content.Context;
import androidx.annotation.VisibleForTesting;
import com.google.android.gms.auth.AccountChangeEvent;
import com.google.android.gms.auth.GoogleAuthException;
import com.google.android.gms.auth.GoogleAuthUtil;
import org.chromium.base.ApplicationState;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.TraceEvent;
import org.chromium.base.task.AsyncTask;
import org.chromium.components.signin.AccountManagerFacadeProvider;
import org.chromium.components.signin.AccountTrackerService;
import org.chromium.components.signin.AccountUtils;
import org.chromium.components.signin.base.CoreAccountInfo;
import org.chromium.components.signin.identitymanager.ConsentLevel;
import org.chromium.components.signin.metrics.SigninAccessPoint;
import org.chromium.components.signin.metrics.SignoutReason;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* A helper for tasks like re-signin.
*
* This should be merged into SigninManager when it is upstreamed.
*/
public class SigninHelper implements ApplicationStatus.ApplicationStateListener {
private static final String TAG = "SigninHelper";
/**
* Retrieve more detailed information from account changed intents.
*/
public interface AccountChangeEventChecker {
List<String> getAccountChangeEvents(Context context, int index, String accountName);
}
/**
* Uses GoogleAuthUtil.getAccountChangeEvents to detect if account
* renaming has occurred.
*/
public static final class SystemAccountChangeEventChecker
implements SigninHelper.AccountChangeEventChecker {
@Override
public List<String> getAccountChangeEvents(Context context, int index, String accountName) {
try {
List<AccountChangeEvent> list =
GoogleAuthUtil.getAccountChangeEvents(context, index, accountName);
List<String> result = new ArrayList<>(list.size());
for (AccountChangeEvent e : list) {
if (e.getChangeType() == GoogleAuthUtil.CHANGE_TYPE_ACCOUNT_RENAMED_TO) {
result.add(e.getChangeData());
} else {
result.add(null);
}
}
return result;
} catch (IOException | GoogleAuthException e) {
Log.w(TAG, "Failed to get change events", e);
}
return new ArrayList<>(0);
}
}
private final SigninManager mSigninManager;
private final AccountTrackerService mAccountTrackerService;
private final SigninPreferencesManager mPrefsManager;
/**
* Please use SigninHelperProvider to get SigninHelper instance instead of creating it
* manually.
*/
public SigninHelper(SigninManager signinManager, AccountTrackerService accountTrackerService,
SigninPreferencesManager signinPreferencesManager) {
mSigninManager = signinManager;
mAccountTrackerService = accountTrackerService;
mPrefsManager = signinPreferencesManager;
ApplicationStatus.registerApplicationStateListener(this);
}
public void validateAccountSettings(boolean accountsChanged) {
// validateAccountsInternal accesses account list (to check whether account exists), so
// postpone the call until account list cache in AccountManagerFacade is ready.
AccountManagerFacadeProvider.getInstance().runAfterCacheIsPopulated(
() -> validateAccountsInternal(accountsChanged));
}
private void validateAccountsInternal(boolean accountsChanged) {
// Ensure System accounts have been seeded.
mAccountTrackerService.checkAndSeedSystemAccounts();
if (!accountsChanged) {
mAccountTrackerService.validateSystemAccounts();
}
if (mSigninManager.isOperationInProgress()) {
// Wait for ongoing sign-in/sign-out operation to finish before validating accounts.
mSigninManager.runAfterOperationInProgress(
() -> validateAccountsInternal(accountsChanged));
return;
}
final CoreAccountInfo syncAccount =
mSigninManager.getIdentityManager().getPrimaryAccountInfo(ConsentLevel.SYNC);
if (syncAccount == null) {
return;
}
String renamedAccount = mPrefsManager.getNewSignedInAccountName();
if (accountsChanged && renamedAccount != null) {
handleAccountRename(syncAccount.getEmail(), renamedAccount);
return;
}
final List<Account> accounts =
AccountManagerFacadeProvider.getInstance().tryGetGoogleAccounts();
// Always check for account deleted.
if (AccountUtils.findAccountByName(accounts, syncAccount.getEmail()) == null) {
// It is possible that Chrome got to this point without account
// rename notification. Let us signout before doing a rename.
AsyncTask<Void> task = new AsyncTask<Void>() {
@Override
protected Void doInBackground() {
updateAccountRenameData(
new SystemAccountChangeEventChecker(), syncAccount.getEmail());
return null;
}
@Override
protected void onPostExecute(Void result) {
String renamedAccount = mPrefsManager.getNewSignedInAccountName();
if (renamedAccount != null || mSigninManager.isOperationInProgress()) {
// Found account rename event or there's a sign-in/sign-out operation in
// progress. Restart validation process.
validateAccountsInternal(true);
return;
}
mSigninManager.signOut(SignoutReason.ACCOUNT_REMOVED_FROM_DEVICE);
}
};
task.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
return;
}
if (accountsChanged) {
// Account details have changed so inform the token service that credentials
// should now be available.
mSigninManager.reloadAllAccountsFromSystem();
}
}
/**
* Deal with account rename. The current approach is to sign out and then sign back in.
* In the (near) future, we should just be clearing all the cached email address here
* and have the UI re-fetch the emailing address based on the ID.
*/
private void handleAccountRename(final String oldName, final String newName) {
Log.i(TAG, "handleAccountRename from: " + oldName + " to " + newName);
// TODO(acleung): I think most of the operations need to run on the main
// thread. May be we should have a progress Dialog?
// TODO(acleung): Deal with passphrase or just prompt user to re-enter it?
// Perform a sign-out with a callback to sign-in again.
mSigninManager.signOut(SignoutReason.USER_CLICKED_SIGNOUT_SETTINGS, () -> {
// Clear the shared perf only after signOut is successful.
// If Chrome dies, we can try it again on next run.
// Otherwise, if re-sign-in fails, we'll just leave chrome
// signed-out.
mPrefsManager.clearNewSignedInAccountName();
performResignin(newName);
}, false);
}
private void performResignin(String newName) {
// This is the correct account now.
final Account account = AccountUtils.createAccountFromName(newName);
mSigninManager.signinAndEnableSync(
SigninAccessPoint.ACCOUNT_RENAMED, account, new SigninManager.SignInCallback() {
@Override
public void onSignInComplete() {
validateAccountsInternal(true);
}
@Override
public void onSignInAborted() {}
});
}
@VisibleForTesting
public static void updateAccountRenameData(
AccountChangeEventChecker checker, String currentName) {
// Skip the search if there is no signed in account.
if (currentName == null) return;
String newName = currentName;
SigninPreferencesManager prefsManager = SigninPreferencesManager.getInstance();
int eventIndex = prefsManager.getLastAccountChangedEventIndex();
int newIndex = eventIndex;
try {
outerLoop:
while (true) {
final List<Account> accounts =
AccountManagerFacadeProvider.getInstance().tryGetGoogleAccounts();
List<String> nameChanges = checker.getAccountChangeEvents(
ContextUtils.getApplicationContext(), newIndex, newName);
for (String name : nameChanges) {
if (name != null) {
// We have found a rename event of the current account.
// We need to check if that account is further renamed.
newName = name;
if (AccountUtils.findAccountByName(accounts, newName) == null) {
newIndex = 0; // Start from the beginning of the new account.
continue outerLoop;
}
break;
}
}
// If there is no rename event pending. Update the last read index to avoid
// re-reading them in the future.
newIndex = nameChanges.size();
break;
}
} catch (Exception e) {
Log.w(TAG, "Error while looking for rename events.", e);
}
if (!currentName.equals(newName)) {
prefsManager.setNewSignedInAccountName(newName);
}
if (newIndex != eventIndex) {
prefsManager.setLastAccountChangedEventIndex(newIndex);
}
}
/**
* Called once during initialization and then again for every start (warm-start).
* Responsible for checking if configuration has changed since Chrome was last launched
* and updates state accordingly.
*/
public void onMainActivityStart() {
try (TraceEvent ignored = TraceEvent.scoped("SigninHelper.onMainActivityStart")) {
boolean accountsChanged =
SigninPreferencesManager.getInstance().checkAndClearAccountsChangedPref();
validateAccountSettings(accountsChanged);
}
}
@Override
public void onApplicationStateChange(int newState) {
if (newState == ApplicationState.HAS_RUNNING_ACTIVITIES) {
onMainActivityStart();
}
}
}