DO NOT MERGE Migrate AccountsListFragment to Preference Fragment

- AccountsListFragment is now AccountSettingsFragment and uses the
PreferenceController data model
- Added tests for AccountsListFragment and new
AccountListPreferenceController

Test: Tested on device and feature parity is the same as before
Test: Existing and New Robolectric tests pass
Fixes: 117335023
Change-Id: I898f85a3ca32a42fe3506cab44f017761fd977cd
diff --git a/src/com/android/car/settings/accounts/AccountListPreferenceController.java b/src/com/android/car/settings/accounts/AccountListPreferenceController.java
new file mode 100644
index 0000000..1a04b4b
--- /dev/null
+++ b/src/com/android/car/settings/accounts/AccountListPreferenceController.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.car.settings.accounts;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.car.userlib.CarUserManagerHelper;
+import android.content.Context;
+import android.content.pm.UserInfo;
+import android.graphics.drawable.Drawable;
+import android.os.UserHandle;
+
+import androidx.collection.ArrayMap;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.OnLifecycleEvent;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceCategory;
+import androidx.preference.PreferenceScreen;
+
+import com.android.car.settings.R;
+import com.android.car.settings.common.FragmentController;
+import com.android.car.settings.common.NoSetupPreferenceController;
+import com.android.settingslib.accounts.AuthenticatorHelper;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Controller for listing accounts.
+ *
+ * <p>Largely derived from {@link com.android.settings.accounts.AccountPreferenceController}
+ */
+public class AccountListPreferenceController extends NoSetupPreferenceController implements
+        AuthenticatorHelper.OnAccountsUpdateListener,
+        CarUserManagerHelper.OnUsersUpdateListener, LifecycleObserver {
+    private static final String NO_ACCOUNT_PREF_KEY = "no_accounts_added";
+
+    private final UserInfo mUserInfo;
+    private final CarUserManagerHelper mCarUserManagerHelper;
+    private final ArrayMap<String, Preference> mPreferences = new ArrayMap<>();
+    private AuthenticatorHelper mAuthenticatorHelper;
+    private PreferenceCategory mAccountsCategory;
+    private String[] mAuthorities;
+
+    public AccountListPreferenceController(Context context, String preferenceKey,
+            FragmentController fragmentController) {
+        super(context, preferenceKey, fragmentController);
+        mCarUserManagerHelper = new CarUserManagerHelper(context);
+        mUserInfo = mCarUserManagerHelper.getCurrentProcessUserInfo();
+        mAuthenticatorHelper = new AuthenticatorHelper(context,
+                mUserInfo.getUserHandle(), /* listener= */ this);
+    }
+
+    /** Sets the account authorities that are available. */
+    public void setAuthorities(String[] authorities) {
+        mAuthorities = authorities;
+    }
+
+    @Override
+    public void displayPreference(PreferenceScreen screen) {
+        super.displayPreference(screen);
+
+        // Add preferences for each account if the controller should be available
+        if (isAvailable()) {
+            mAccountsCategory = (PreferenceCategory) screen.findPreference(getPreferenceKey());
+            forceUpdateAccountsCategory();
+        }
+    }
+
+    @Override
+    public boolean handlePreferenceTreeClick(Preference preference) {
+        // Show the account's details when an account is clicked on.
+        if (preference instanceof AccountPreference) {
+            getFragmentController().launchFragment(AccountDetailsFragment.newInstance(
+                    ((AccountPreference) preference).getAccount(), mUserInfo));
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public int getAvailabilityStatus() {
+        return mCarUserManagerHelper.canCurrentProcessModifyAccounts() ? AVAILABLE
+                : DISABLED_FOR_USER;
+    }
+
+    /**
+     * Registers the account update and user update callbacks.
+     */
+    @OnLifecycleEvent(Lifecycle.Event.ON_START)
+    public void onStart() {
+        mAuthenticatorHelper.listenToAccountUpdates();
+        mCarUserManagerHelper.registerOnUsersUpdateListener(this);
+    }
+
+    /**
+     * Unregisters the account update and user update callbacks.
+     */
+    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
+    public void onStop() {
+        mAuthenticatorHelper.stopListeningToAccountUpdates();
+        mCarUserManagerHelper.unregisterOnUsersUpdateListener(this);
+    }
+
+    @Override
+    public void onAccountsUpdate(UserHandle userHandle) {
+        if (userHandle.equals(mUserInfo.getUserHandle())) {
+            forceUpdateAccountsCategory();
+        }
+    }
+
+    @Override
+    public void onUsersUpdate() {
+        forceUpdateAccountsCategory();
+    }
+
+    /** Forces a refresh of the account preferences. */
+    private void forceUpdateAccountsCategory() {
+        // Set the category title and include the user's name
+        mAccountsCategory.setTitle(mContext.getString(R.string.account_list_title, mUserInfo.name));
+
+        // Recreate the authentication helper to refresh the list of enabled accounts
+        mAuthenticatorHelper = new AuthenticatorHelper(mContext, mUserInfo.getUserHandle(), this);
+
+        Map<String, Preference> preferencesToRemove = new ArrayMap<>(mPreferences);
+        List<? extends Preference> preferences = getAccountPreferences(preferencesToRemove);
+        // Add all preferences that aren't already shown. Manually set the order so that existing
+        // preferences are reordered correctly.
+        for (int i = 0; i < preferences.size(); i++) {
+            Preference preference = preferences.get(i);
+            String key = preference.getKey();
+            if (!mPreferences.containsKey(key)) {
+                preference.setOrder(i);
+                mAccountsCategory.addPreference(preference);
+                mPreferences.put(key, preference);
+            } else {
+                mPreferences.get(key).setOrder(i);
+            }
+        }
+
+        // Remove all preferences that should no longer be shown
+        for (String key : preferencesToRemove.keySet()) {
+            mAccountsCategory.removePreference(mPreferences.get(key));
+            mPreferences.remove(key);
+        }
+    }
+
+    /**
+     * Returns a list of preferences corresponding to the accounts for the current user.
+     *
+     * <p> Derived from
+     * {@link com.android.settings.accounts.AccountPreferenceController#getAccountTypePreferences}
+     *
+     * @param preferencesToRemove the current preferences shown; only preferences to be removed will
+     *                            remain after method execution
+     */
+    private List<? extends Preference> getAccountPreferences(
+            Map<String, Preference> preferencesToRemove) {
+        String[] accountTypes = mAuthenticatorHelper.getEnabledAccountTypes();
+        ArrayList<AccountPreference> accountPreferences =
+                new ArrayList<>(accountTypes.length);
+
+        for (int i = 0; i < accountTypes.length; i++) {
+            String accountType = accountTypes[i];
+            // Skip showing any account that does not have any of the requested authorities
+            if (!accountTypeHasAnyRequestedAuthorities(accountType)) {
+                continue;
+            }
+            CharSequence label = mAuthenticatorHelper.getLabelForType(mContext, accountType);
+            if (label == null) {
+                continue;
+            }
+
+            Account[] accounts = AccountManager.get(mContext)
+                    .getAccountsByTypeAsUser(accountType, mUserInfo.getUserHandle());
+            Drawable icon = mAuthenticatorHelper.getDrawableForType(mContext, accountType);
+
+            // Add a preference row for each individual account
+            for (Account account : accounts) {
+                AccountPreference preference =
+                        (AccountPreference) preferencesToRemove.remove(
+                                AccountPreference.buildKey(account));
+                if (preference != null) {
+                    accountPreferences.add(preference);
+                    continue;
+                }
+                accountPreferences.add(new AccountPreference(
+                        mContext, account, label, icon));
+            }
+            mAuthenticatorHelper.preloadDrawableForType(mContext, accountType);
+        }
+
+        // If there are no accounts, return the "no account added" preference.
+        if (accountPreferences.isEmpty()) {
+            preferencesToRemove.remove(NO_ACCOUNT_PREF_KEY);
+            return Arrays.asList(createNoAccountsAddedPreference());
+        }
+
+        Collections.sort(accountPreferences, Comparator.comparing(
+                (AccountPreference a) -> a.getSummary().toString())
+                .thenComparing((AccountPreference a) -> a.getTitle().toString()));
+
+        return accountPreferences;
+    }
+
+    private Preference createNoAccountsAddedPreference() {
+        Preference emptyPreference = new Preference(mContext);
+        emptyPreference.setTitle(R.string.no_accounts_added);
+        emptyPreference.setKey(NO_ACCOUNT_PREF_KEY);
+        emptyPreference.setSelectable(false);
+
+        return emptyPreference;
+    }
+
+    /**
+     * Returns whether the account type has any of the authorities requested by the caller.
+     *
+     * <p> Derived from {@link AccountPreferenceController#accountTypeHasAnyRequestedAuthorities}
+     */
+    private boolean accountTypeHasAnyRequestedAuthorities(String accountType) {
+        if (mAuthorities == null || mAuthorities.length == 0) {
+            // No authorities required
+            return true;
+        }
+        ArrayList<String> authoritiesForType =
+                mAuthenticatorHelper.getAuthoritiesForAccountType(accountType);
+        if (authoritiesForType == null) {
+            return false;
+        }
+        for (int j = 0; j < mAuthorities.length; j++) {
+            if (authoritiesForType.contains(mAuthorities[j])) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static class AccountPreference extends Preference {
+        /** Account that this Preference represents. */
+        private final Account mAccount;
+
+        private AccountPreference(Context context, Account account, CharSequence label,
+                Drawable icon) {
+            super(context);
+            mAccount = account;
+
+            setKey(buildKey(account));
+            setTitle(account.name);
+            setSummary(label);
+            setIcon(icon);
+        }
+
+        /**
+         * Build a unique preference key based on the account.
+         */
+        public static String buildKey(Account account) {
+            return String.valueOf(account.hashCode());
+        }
+
+        public Account getAccount() {
+            return mAccount;
+        }
+    }
+}