1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.car.settings.accounts;
18 
19 import android.accounts.AccountManager;
20 import android.accounts.AuthenticatorDescription;
21 import android.car.drivingstate.CarUxRestrictions;
22 import android.car.userlib.CarUserManagerHelper;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.graphics.drawable.Drawable;
26 import android.os.Handler;
27 import android.os.UserHandle;
28 
29 import androidx.annotation.Nullable;
30 import androidx.annotation.VisibleForTesting;
31 import androidx.collection.ArrayMap;
32 import androidx.preference.PreferenceGroup;
33 
34 import com.android.car.settings.common.FragmentController;
35 import com.android.car.settings.common.PreferenceController;
36 import com.android.car.ui.preference.CarUiPreference;
37 import com.android.settingslib.accounts.AuthenticatorHelper;
38 
39 import java.util.ArrayList;
40 import java.util.Collections;
41 import java.util.Comparator;
42 import java.util.HashSet;
43 import java.util.List;
44 import java.util.Set;
45 
46 /**
47  * Controller for showing the user the list of accounts they can add.
48  *
49  * <p>Largely derived from {@link com.android.settings.accounts.ChooseAccountActivity}
50  */
51 public class ChooseAccountPreferenceController extends
52         PreferenceController<PreferenceGroup> implements
53         AuthenticatorHelper.OnAccountsUpdateListener {
54     private static final int ADD_ACCOUNT_REQUEST_CODE = 1;
55 
56     private final UserHandle mUserHandle;
57     private AuthenticatorHelper mAuthenticatorHelper;
58     private List<String> mAuthorities;
59     private Set<String> mAccountTypesFilter;
60     private Set<String> mAccountTypesExclusionFilter;
61     private ArrayMap<String, AuthenticatorDescriptionPreference> mPreferences = new ArrayMap<>();
62     private boolean mIsStarted = false;
63     private boolean mHasPendingBack = false;
64 
ChooseAccountPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions)65     public ChooseAccountPreferenceController(Context context, String preferenceKey,
66             FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
67         super(context, preferenceKey, fragmentController, uxRestrictions);
68         mUserHandle = new CarUserManagerHelper(context).getCurrentProcessUserInfo().getUserHandle();
69 
70         HashSet<String> accountTypeExclusionFilter = new HashSet<>();
71 
72         // Hardcoding bluetooth account type
73         accountTypeExclusionFilter.add("com.android.bluetooth.pbapsink");
74         setAccountTypesExclusionFilter(accountTypeExclusionFilter);
75     }
76 
77     /** Sets the authorities that the user has. */
setAuthorities(List<String> authorities)78     public void setAuthorities(List<String> authorities) {
79         mAuthorities = authorities;
80     }
81 
82     /** Sets the filter for accounts that should be shown. */
setAccountTypesFilter(Set<String> accountTypesFilter)83     public void setAccountTypesFilter(Set<String> accountTypesFilter) {
84         mAccountTypesFilter = accountTypesFilter;
85     }
86 
87     /** Sets the filter for accounts that should NOT be shown. */
setAccountTypesExclusionFilter(Set<String> accountTypesExclusionFilterFilter)88     protected void setAccountTypesExclusionFilter(Set<String> accountTypesExclusionFilterFilter) {
89         mAccountTypesExclusionFilter = accountTypesExclusionFilterFilter;
90     }
91 
92     @Override
getPreferenceType()93     protected Class<PreferenceGroup> getPreferenceType() {
94         return PreferenceGroup.class;
95     }
96 
97     @Override
updateState(PreferenceGroup preferenceGroup)98     protected void updateState(PreferenceGroup preferenceGroup) {
99         forceUpdateAccountsCategory();
100     }
101 
102     /** Initializes the authenticator helper. */
103     @Override
onCreateInternal()104     protected void onCreateInternal() {
105         mAuthenticatorHelper = new AuthenticatorHelper(getContext(), mUserHandle, this);
106     }
107 
108     /**
109      * Registers the account update callback.
110      */
111     @Override
onStartInternal()112     protected void onStartInternal() {
113         mAuthenticatorHelper.listenToAccountUpdates();
114         mIsStarted = true;
115 
116         if (mHasPendingBack) {
117             mHasPendingBack = false;
118 
119             // Post the fragment navigation because FragmentManager may still be executing
120             // transactions during onStart.
121             new Handler().post(() -> getFragmentController().goBack());
122         }
123     }
124 
125     /**
126      * Unregisters the account update callback.
127      */
128     @Override
onStopInternal()129     protected void onStopInternal() {
130         mAuthenticatorHelper.stopListeningToAccountUpdates();
131         mIsStarted = false;
132     }
133 
134     @Override
onAccountsUpdate(UserHandle userHandle)135     public void onAccountsUpdate(UserHandle userHandle) {
136         // Only force a refresh if accounts have changed for the current user.
137         if (userHandle.equals(mUserHandle)) {
138             forceUpdateAccountsCategory();
139         }
140     }
141 
142     /** Forces a refresh of the authenticator description preferences. */
forceUpdateAccountsCategory()143     private void forceUpdateAccountsCategory() {
144         Set<String> preferencesToRemove = new HashSet<>(mPreferences.keySet());
145         List<AuthenticatorDescriptionPreference> preferences =
146                 getAuthenticatorDescriptionPreferences(preferencesToRemove);
147         // Add all preferences that aren't already shown
148         for (int i = 0; i < preferences.size(); i++) {
149             AuthenticatorDescriptionPreference preference = preferences.get(i);
150             preference.setOrder(i);
151             String key = preference.getKey();
152             getPreference().addPreference(preference);
153             mPreferences.put(key, preference);
154         }
155 
156         // Remove all preferences that should no longer be shown
157         for (String key : preferencesToRemove) {
158             getPreference().removePreference(mPreferences.get(key));
159             mPreferences.remove(key);
160         }
161     }
162 
163     /**
164      * Returns a list of preferences corresponding to the account types the user can add.
165      *
166      * <p> Derived from
167      * {@link com.android.settings.accounts.ChooseAccountActivity#onAuthDescriptionsUpdated}
168      *
169      * @param preferencesToRemove the current preferences shown; will contain the preferences that
170      *                            need to be removed from the screen after method execution
171      */
getAuthenticatorDescriptionPreferences( Set<String> preferencesToRemove)172     private List<AuthenticatorDescriptionPreference> getAuthenticatorDescriptionPreferences(
173             Set<String> preferencesToRemove) {
174         AuthenticatorDescription[] authenticatorDescriptions = AccountManager.get(
175                 getContext()).getAuthenticatorTypesAsUser(
176                 mUserHandle.getIdentifier());
177 
178         ArrayList<AuthenticatorDescriptionPreference> authenticatorDescriptionPreferences =
179                 new ArrayList<>();
180         // Create list of account providers to show on page.
181         for (AuthenticatorDescription authenticatorDescription : authenticatorDescriptions) {
182             String accountType = authenticatorDescription.type;
183             CharSequence label = mAuthenticatorHelper.getLabelForType(getContext(), accountType);
184             Drawable icon = mAuthenticatorHelper.getDrawableForType(getContext(), accountType);
185 
186             List<String> accountAuthorities =
187                     mAuthenticatorHelper.getAuthoritiesForAccountType(accountType);
188 
189             // If there are specific authorities required, we need to check whether they are
190             // included in the account type.
191             boolean authorized =
192                     (mAuthorities == null || mAuthorities.isEmpty() || accountAuthorities == null
193                             || !Collections.disjoint(accountAuthorities, mAuthorities));
194 
195             // If there is an account type filter, make sure this account type is included.
196             authorized = authorized && (mAccountTypesFilter == null
197                     || mAccountTypesFilter.contains(accountType));
198 
199             // Check if the account type is in the exclusion list.
200             authorized = authorized && (mAccountTypesExclusionFilter == null
201                     || !mAccountTypesExclusionFilter.contains(accountType));
202 
203             // If authorized, add a preference for the provider to the list and remove it from
204             // preferencesToRemove.
205             if (authorized) {
206                 AuthenticatorDescriptionPreference preference = mPreferences.getOrDefault(
207                         accountType,
208                         new AuthenticatorDescriptionPreference(getContext(), accountType, label,
209                                 icon));
210                 preference.setOnPreferenceClickListener(
211                         pref -> onAddAccount(preference.getAccountType()));
212                 authenticatorDescriptionPreferences.add(preference);
213                 preferencesToRemove.remove(accountType);
214             }
215         }
216 
217         Collections.sort(authenticatorDescriptionPreferences, Comparator.comparing(
218                 (AuthenticatorDescriptionPreference a) -> a.getTitle().toString()));
219 
220         return authenticatorDescriptionPreferences;
221     }
222 
223     /** Starts the {@link AddAccountActivity} to add an account for the given account type. */
onAddAccount(String accountType)224     private boolean onAddAccount(String accountType) {
225         Intent intent = new Intent(getContext(), AddAccountActivity.class);
226         intent.putExtra(AddAccountActivity.EXTRA_SELECTED_ACCOUNT, accountType);
227         getFragmentController().startActivityForResult(intent, ADD_ACCOUNT_REQUEST_CODE,
228                 this::onAccountAdded);
229         return true;
230     }
231 
onAccountAdded(int requestCode, int resultCode, @Nullable Intent data)232     private void onAccountAdded(int requestCode, int resultCode, @Nullable Intent data) {
233         if (requestCode == ADD_ACCOUNT_REQUEST_CODE) {
234             if (mIsStarted) {
235                 getFragmentController().goBack();
236             } else {
237                 mHasPendingBack = true;
238             }
239         }
240     }
241 
242     /** Used for testing to trigger an account update. */
243     @VisibleForTesting
getAuthenticatorHelper()244     AuthenticatorHelper getAuthenticatorHelper() {
245         return mAuthenticatorHelper;
246     }
247 
248     /** Handles adding accounts. */
249     interface AddAccountListener {
250         /** Handles adding an account. */
addAccount(String accountType)251         void addAccount(String accountType);
252     }
253 
254     private static class AuthenticatorDescriptionPreference extends CarUiPreference {
255         private final String mType;
256 
AuthenticatorDescriptionPreference(Context context, String accountType, CharSequence label, Drawable icon)257         AuthenticatorDescriptionPreference(Context context, String accountType, CharSequence label,
258                 Drawable icon) {
259             super(context);
260             mType = accountType;
261 
262             setKey(accountType);
263             setTitle(label);
264             setIcon(icon);
265             setShowChevron(false);
266         }
267 
getAccountType()268         private String getAccountType() {
269             return mType;
270         }
271     }
272 }
273