| // 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(); |
| } |
| } |
| } |