// 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;
import android.accounts.Account;
import android.content.Context;
import android.os.AsyncTask;
import org.chromium.base.Callback;
import org.chromium.base.Log;
import org.chromium.base.ObserverList;
import org.chromium.base.ThreadUtils;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.annotations.JNINamespace;
import org.chromium.components.signin.AccountManagerHelper;
/**
* Android wrapper of AccountTrackerService which provides access from the java layer.
* It offers the capability of fetching and seeding system accounts into AccountTrackerService in C++
* layer, and notifies observers when it is complete.
*/
@JNINamespace("signin::android")
public class AccountTrackerService {
private static final String TAG = "AccountService";
private static AccountTrackerService sAccountTrackerService;
private SystemAccountsSeedingStatus mSystemAccountsSeedingStatus;
private boolean mSystemAccountsChanged;
private boolean mSyncForceRefreshedForTest;
private final Context mContext;
private enum SystemAccountsSeedingStatus {
SEEDING_NOT_STARTED,
SEEDING_IN_PROGRESS,
SEEDING_DONE,
SEEDING_VALIDATING
}
/**
* Classes that want to listen for system accounts fetching and seeding should implement
* this interface and register with {@link #addSystemAccountsSeededListener}.
*/
public interface OnSystemAccountsSeededListener {
// Called at the end of seedSystemAccounts().
void onSystemAccountsSeedingComplete();
// Called in invalidateAccountSeedStatus() indicating that accounts have changed.
void onSystemAccountsChanged();
}
private final ObserverList<OnSystemAccountsSeededListener> mSystemAccountsSeedingObservers =
new ObserverList<>();
public static AccountTrackerService get(Context context) {
ThreadUtils.assertOnUiThread();
if (sAccountTrackerService == null) {
sAccountTrackerService = new AccountTrackerService(context);
}
return sAccountTrackerService;
}
private AccountTrackerService(Context context) {
mContext = context;
mSystemAccountsSeedingStatus = SystemAccountsSeedingStatus.SEEDING_NOT_STARTED;
mSystemAccountsChanged = false;
}
/**
* Checks whether the account id <-> email mapping has been seeded into C++ layer.
* If not, it automatically starts fetching the mapping and seeds it.
* @return Whether the accounts have been seeded already.
*/
public boolean checkAndSeedSystemAccounts() {
ThreadUtils.assertOnUiThread();
if (mSystemAccountsSeedingStatus == SystemAccountsSeedingStatus.SEEDING_DONE
&& !mSystemAccountsChanged) {
return true;
}
if ((mSystemAccountsSeedingStatus == SystemAccountsSeedingStatus.SEEDING_NOT_STARTED
|| mSystemAccountsChanged)
&& mSystemAccountsSeedingStatus
!= SystemAccountsSeedingStatus.SEEDING_IN_PROGRESS) {
seedSystemAccounts();
}
return false;
}
/**
* Register an |observer| to observe system accounts seeding status.
*/
public void addSystemAccountsSeededListener(OnSystemAccountsSeededListener observer) {
ThreadUtils.assertOnUiThread();
mSystemAccountsSeedingObservers.addObserver(observer);
if (mSystemAccountsSeedingStatus == SystemAccountsSeedingStatus.SEEDING_DONE) {
observer.onSystemAccountsSeedingComplete();
}
}
/**
* Remove an |observer| from the list of observers.
*/
public void removeSystemAccountsSeededListener(OnSystemAccountsSeededListener observer) {
ThreadUtils.assertOnUiThread();
mSystemAccountsSeedingObservers.removeObserver(observer);
}
private void seedSystemAccounts() {
ThreadUtils.assertOnUiThread();
mSystemAccountsChanged = false;
mSyncForceRefreshedForTest = false;
final AccountIdProvider accountIdProvider = AccountIdProvider.getInstance();
if (accountIdProvider.canBeUsed(mContext)) {
mSystemAccountsSeedingStatus = SystemAccountsSeedingStatus.SEEDING_IN_PROGRESS;
} else {
mSystemAccountsSeedingStatus = SystemAccountsSeedingStatus.SEEDING_NOT_STARTED;
return;
}
AccountManagerHelper.get(mContext).getGoogleAccounts(new Callback<Account[]>() {
@Override
public void onResult(final Account[] accounts) {
new AsyncTask<Void, Void, String[][]>() {
@Override
public String[][] doInBackground(Void... params) {
Log.d(TAG, "Getting id/email mapping");
String[][] accountIdNameMap = new String[2][accounts.length];
for (int i = 0; i < accounts.length; ++i) {
accountIdNameMap[0][i] =
accountIdProvider.getAccountId(mContext, accounts[i].name);
accountIdNameMap[1][i] = accounts[i].name;
}
return accountIdNameMap;
}
@Override
public void onPostExecute(String[][] accountIdNameMap) {
if (mSyncForceRefreshedForTest) return;
if (mSystemAccountsChanged) {
seedSystemAccounts();
return;
}
if (areAccountIdsValid(accountIdNameMap[0])) {
nativeSeedAccountsInfo(accountIdNameMap[0], accountIdNameMap[1]);
mSystemAccountsSeedingStatus = SystemAccountsSeedingStatus.SEEDING_DONE;
notifyObserversOnSeedingComplete();
} else {
Log.w(TAG, "Invalid mapping of id/email");
seedSystemAccounts();
}
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
});
}
private boolean areAccountIdsValid(String[] accountIds) {
for (int i = 0; i < accountIds.length; ++i) {
if (accountIds[i] == null) return false;
}
return true;
}
private void notifyObserversOnSeedingComplete() {
for (OnSystemAccountsSeededListener observer : mSystemAccountsSeedingObservers) {
observer.onSystemAccountsSeedingComplete();
}
}
/**
* Seed system accounts into AccountTrackerService synchronously for test purpose.
*/
@VisibleForTesting
public void syncForceRefreshForTest(String[] accountIds, String[] accountNames) {
ThreadUtils.assertOnUiThread();
mSystemAccountsSeedingStatus = SystemAccountsSeedingStatus.SEEDING_IN_PROGRESS;
mSyncForceRefreshedForTest = true;
nativeSeedAccountsInfo(accountIds, accountNames);
mSystemAccountsSeedingStatus = SystemAccountsSeedingStatus.SEEDING_DONE;
}
/**
* Notifies the AccountTrackerService about changed system accounts. without actually triggering
* @param reSeedAccounts Whether to also start seeding the new account information immediately.
*/
public void invalidateAccountSeedStatus(boolean reSeedAccounts) {
ThreadUtils.assertOnUiThread();
mSystemAccountsChanged = true;
notifyObserversOnAccountsChange();
if (reSeedAccounts) checkAndSeedSystemAccounts();
}
/**
* Verifies whether seeded accounts in AccountTrackerService are up-to-date with the accounts in
* Android. It sets seeding status to SEEDING_VALIDATING temporarily to block services depending
* on it and sets it back to SEEDING_DONE after passing the verification. This function is
* created because accounts changed notification from Android to Chrome has latency.
*/
public void validateSystemAccounts() {
ThreadUtils.assertOnUiThread();
if (!checkAndSeedSystemAccounts()) {
// Do nothing if seeding is not done.
return;
}
mSystemAccountsSeedingStatus = SystemAccountsSeedingStatus.SEEDING_VALIDATING;
AccountManagerHelper.get(mContext).getGoogleAccounts(new Callback<Account[]>() {
@Override
public void onResult(final Account[] accounts) {
if (mSystemAccountsChanged
|| mSystemAccountsSeedingStatus
!= SystemAccountsSeedingStatus.SEEDING_VALIDATING) {
return;
}
String[] accountNames = new String[accounts.length];
for (int i = 0; i < accounts.length; ++i) {
accountNames[i] = accounts[i].name;
}
if (nativeAreAccountsSeeded(accountNames)) {
mSystemAccountsSeedingStatus = SystemAccountsSeedingStatus.SEEDING_DONE;
notifyObserversOnSeedingComplete();
}
}
});
}
private void notifyObserversOnAccountsChange() {
for (OnSystemAccountsSeededListener observer : mSystemAccountsSeedingObservers) {
observer.onSystemAccountsChanged();
}
}
private static native void nativeSeedAccountsInfo(String[] gaiaIds, String[] accountNames);
private static native boolean nativeAreAccountsSeeded(String[] accountNames);
}