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.settings.accounts;
18 
19 import static android.app.Activity.RESULT_CANCELED;
20 import static android.app.Activity.RESULT_OK;
21 import static android.content.Intent.EXTRA_USER;
22 
23 import android.accounts.AccountManager;
24 import android.accounts.AuthenticatorDescription;
25 import android.app.Activity;
26 import android.content.ContentResolver;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.SyncAdapterType;
30 import android.content.pm.PackageManager;
31 import android.content.res.Resources;
32 import android.graphics.drawable.Drawable;
33 import android.os.UserHandle;
34 import android.util.Log;
35 
36 import androidx.annotation.VisibleForTesting;
37 import androidx.preference.Preference;
38 import androidx.preference.PreferenceScreen;
39 
40 import com.android.settings.core.BasePreferenceController;
41 import com.android.settingslib.RestrictedLockUtils;
42 import com.android.settingslib.RestrictedLockUtilsInternal;
43 
44 import com.google.android.collect.Maps;
45 
46 import java.util.ArrayList;
47 import java.util.Collections;
48 import java.util.HashMap;
49 import java.util.HashSet;
50 import java.util.List;
51 import java.util.Map;
52 import java.util.Set;
53 import java.util.StringJoiner;
54 
55 /**
56  * An extra {@link UserHandle} can be specified in the intent as {@link EXTRA_USER}, if the user for
57  * which the action needs to be performed is different to the one the Settings App will run in.
58  */
59 public class ChooseAccountPreferenceController extends BasePreferenceController {
60 
61     private static final String TAG = "ChooseAccountPrefCtrler";
62 
63     private final List<ProviderEntry> mProviderList;
64     private final Map<String, AuthenticatorDescription> mTypeToAuthDescription;
65 
66     private String[] mAuthorities;
67     private Set<String> mAccountTypesFilter;
68     private AuthenticatorDescription[] mAuthDescs;
69     private Map<String, List<String>> mAccountTypeToAuthorities;
70     // The UserHandle of the user we are choosing an account for
71     private UserHandle mUserHandle;
72     private Activity mActivity;
73     private PreferenceScreen mScreen;
74 
ChooseAccountPreferenceController(Context context, String preferenceKey)75     public ChooseAccountPreferenceController(Context context, String preferenceKey) {
76         super(context, preferenceKey);
77 
78         mProviderList = new ArrayList<>();
79         mTypeToAuthDescription = new HashMap<>();
80     }
81 
initialize(String[] authorities, String[] accountTypesFilter, UserHandle userHandle, Activity activity)82     public void initialize(String[] authorities, String[] accountTypesFilter, UserHandle userHandle,
83             Activity activity) {
84         mActivity = activity;
85         mAuthorities = authorities;
86         mUserHandle = userHandle;
87 
88         if (accountTypesFilter != null) {
89             mAccountTypesFilter = new HashSet<>();
90             for (String accountType : accountTypesFilter) {
91                 mAccountTypesFilter.add(accountType);
92             }
93         }
94     }
95 
96     @Override
getAvailabilityStatus()97     public int getAvailabilityStatus() {
98         return AVAILABLE;
99     }
100 
101     @Override
displayPreference(PreferenceScreen screen)102     public void displayPreference(PreferenceScreen screen) {
103         super.displayPreference(screen);
104         mScreen = screen;
105         updateAuthDescriptions();
106     }
107 
108     @Override
handlePreferenceTreeClick(Preference preference)109     public boolean handlePreferenceTreeClick(Preference preference) {
110         if (!(preference instanceof ProviderPreference)) {
111             return false;
112         }
113 
114         ProviderPreference pref = (ProviderPreference) preference;
115         if (Log.isLoggable(TAG, Log.VERBOSE)) {
116             Log.v(TAG, "Attempting to add account of type " + pref.getAccountType());
117         }
118         finishWithAccountType(pref.getAccountType());
119         return true;
120     }
121 
122     /**
123      * Updates provider icons. Subclasses should call this in onCreate()
124      * and update any UI that depends on AuthenticatorDescriptions in onAuthDescriptionsUpdated().
125      */
updateAuthDescriptions()126     private void updateAuthDescriptions() {
127         mAuthDescs = AccountManager.get(mContext).getAuthenticatorTypesAsUser(
128                 mUserHandle.getIdentifier());
129         for (int i = 0; i < mAuthDescs.length; i++) {
130             mTypeToAuthDescription.put(mAuthDescs[i].type, mAuthDescs[i]);
131         }
132         onAuthDescriptionsUpdated();
133     }
134 
onAuthDescriptionsUpdated()135     private void onAuthDescriptionsUpdated() {
136         // Create list of providers to show on preference screen
137         for (int i = 0; i < mAuthDescs.length; i++) {
138             final String accountType = mAuthDescs[i].type;
139             final CharSequence providerName = getLabelForType(accountType);
140 
141             // Skip preferences for authorities not specified. If no authorities specified,
142             // then include them all.
143             final List<String> accountAuths = getAuthoritiesForAccountType(accountType);
144             boolean addAccountPref = true;
145             if (mAuthorities != null && mAuthorities.length > 0 && accountAuths != null) {
146                 addAccountPref = false;
147                 for (int k = 0; k < mAuthorities.length; k++) {
148                     if (accountAuths.contains(mAuthorities[k])) {
149                         addAccountPref = true;
150                         break;
151                     }
152                 }
153             }
154             if (addAccountPref && mAccountTypesFilter != null
155                     && !mAccountTypesFilter.contains(accountType)) {
156                 addAccountPref = false;
157             }
158             if (addAccountPref) {
159                 mProviderList.add(
160                         new ProviderEntry(providerName, accountType));
161             } else {
162                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
163                     Log.v(TAG, "Skipped pref " + providerName + ": has no authority we need");
164                 }
165             }
166         }
167         final Context context = mScreen.getContext();
168         if (mProviderList.size() == 1) {
169             // There's only one provider that matches. If it is disabled by admin show the
170             // support dialog otherwise run it.
171             final RestrictedLockUtils.EnforcedAdmin admin =
172                     RestrictedLockUtilsInternal.checkIfAccountManagementDisabled(
173                             context, mProviderList.get(0).getType(), mUserHandle.getIdentifier());
174             if (admin != null) {
175                 mActivity.setResult(RESULT_CANCELED,
176                         RestrictedLockUtils.getShowAdminSupportDetailsIntent(
177                                 context, admin));
178                 mActivity.finish();
179             } else {
180                 finishWithAccountType(mProviderList.get(0).getType());
181             }
182         } else if (mProviderList.size() > 0) {
183             Collections.sort(mProviderList);
184             for (ProviderEntry pref : mProviderList) {
185                 final Drawable drawable = getDrawableForType(pref.getType());
186                 final ProviderPreference p = new ProviderPreference(context,
187                         pref.getType(), drawable, pref.getName());
188                 p.setKey(pref.getType().toString());
189                 p.checkAccountManagementAndSetDisabled(mUserHandle.getIdentifier());
190                 mScreen.addPreference(p);
191             }
192         } else {
193             if (mAuthorities != null && Log.isLoggable(TAG, Log.VERBOSE)) {
194                 final StringBuilder auths = new StringBuilder();
195                 for (String a : mAuthorities) {
196                     auths.append(a);
197                     auths.append(' ');
198                 }
199                 Log.v(TAG, "No providers found for authorities: " + auths);
200             }
201             if (mAccountTypesFilter != null) {
202                 final StringJoiner types = new StringJoiner(", ", "", "");
203                 mAccountTypesFilter.forEach(types::add);
204                 Log.w(TAG, "No providers found for account types: " + types);
205             }
206             mActivity.setResult(RESULT_CANCELED);
207             // Do not finish activity to avoid the caller getting the existing account list because
208             // the prompt respond reveals that the input account does not exist.
209         }
210     }
211 
getAuthoritiesForAccountType(String type)212     private List<String> getAuthoritiesForAccountType(String type) {
213         if (mAccountTypeToAuthorities == null) {
214             mAccountTypeToAuthorities = Maps.newHashMap();
215             final SyncAdapterType[] syncAdapters = ContentResolver.getSyncAdapterTypesAsUser(
216                     mUserHandle.getIdentifier());
217             for (int i = 0, n = syncAdapters.length; i < n; i++) {
218                 final SyncAdapterType sa = syncAdapters[i];
219                 List<String> authorities = mAccountTypeToAuthorities.get(sa.accountType);
220                 if (authorities == null) {
221                     authorities = new ArrayList<>();
222                     mAccountTypeToAuthorities.put(sa.accountType, authorities);
223                 }
224                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
225                     Log.v(TAG, "added authority " + sa.authority + " to accountType "
226                             + sa.accountType);
227                 }
228                 authorities.add(sa.authority);
229             }
230         }
231         return mAccountTypeToAuthorities.get(type);
232     }
233 
234     /**
235      * Gets an icon associated with a particular account type. If none found, return null.
236      *
237      * @param accountType the type of account
238      * @return a drawable for the icon or a default icon returned by
239      * {@link PackageManager#getDefaultActivityIcon} if one cannot be found.
240      */
241     @VisibleForTesting
getDrawableForType(final String accountType)242     Drawable getDrawableForType(final String accountType) {
243         Drawable icon = null;
244         if (mTypeToAuthDescription.containsKey(accountType)) {
245             try {
246                 final AuthenticatorDescription desc = mTypeToAuthDescription.get(accountType);
247                 final Context authContext = mActivity
248                         .createPackageContextAsUser(desc.packageName, 0, mUserHandle);
249                 icon = mContext.getPackageManager().getUserBadgedIcon(
250                         authContext.getDrawable(desc.iconId), mUserHandle);
251             } catch (PackageManager.NameNotFoundException e) {
252                 Log.w(TAG, "No icon name for account type " + accountType);
253             } catch (Resources.NotFoundException e) {
254                 Log.w(TAG, "No icon resource for account type " + accountType);
255             }
256         }
257         if (icon != null) {
258             return icon;
259         } else {
260             return mContext.getPackageManager().getDefaultActivityIcon();
261         }
262     }
263 
264     /**
265      * Gets the label associated with a particular account type. If none found, return null.
266      *
267      * @param accountType the type of account
268      * @return a CharSequence for the label or null if one cannot be found.
269      */
270     @VisibleForTesting
getLabelForType(final String accountType)271     CharSequence getLabelForType(final String accountType) {
272         CharSequence label = null;
273         if (mTypeToAuthDescription.containsKey(accountType)) {
274             try {
275                 final AuthenticatorDescription desc = mTypeToAuthDescription.get(accountType);
276                 final Context authContext = mActivity
277                         .createPackageContextAsUser(desc.packageName, 0, mUserHandle);
278                 label = authContext.getResources().getText(desc.labelId);
279             } catch (PackageManager.NameNotFoundException e) {
280                 Log.w(TAG, "No label name for account type " + accountType);
281             } catch (Resources.NotFoundException e) {
282                 Log.w(TAG, "No label resource for account type " + accountType);
283             }
284         }
285         return label;
286     }
287 
finishWithAccountType(String accountType)288     private void finishWithAccountType(String accountType) {
289         Intent intent = new Intent();
290         intent.putExtra(AddAccountSettings.EXTRA_SELECTED_ACCOUNT, accountType);
291         intent.putExtra(EXTRA_USER, mUserHandle);
292         mActivity.setResult(RESULT_OK, intent);
293         mActivity.finish();
294     }
295 }
296