1 /*
2  * Copyright (C) 2022 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.applications.credentials;
18 
19 import static androidx.lifecycle.Lifecycle.Event.ON_CREATE;
20 
21 import android.app.Activity;
22 import android.app.Dialog;
23 import android.content.ComponentName;
24 import android.content.ContentResolver;
25 import android.content.Context;
26 import android.content.DialogInterface;
27 import android.content.Intent;
28 import android.content.pm.ApplicationInfo;
29 import android.content.pm.PackageManager;
30 import android.content.pm.ServiceInfo;
31 import android.content.res.Resources;
32 import android.credentials.CredentialManager;
33 import android.credentials.CredentialProviderInfo;
34 import android.credentials.SetEnabledProvidersException;
35 import android.credentials.flags.Flags;
36 import android.database.ContentObserver;
37 import android.graphics.drawable.Drawable;
38 import android.net.Uri;
39 import android.os.Bundle;
40 import android.os.Handler;
41 import android.os.OutcomeReceiver;
42 import android.os.UserHandle;
43 import android.os.UserManager;
44 import android.provider.Settings;
45 import android.service.autofill.AutofillServiceInfo;
46 import android.text.TextUtils;
47 import android.util.Log;
48 import android.util.Pair;
49 import android.view.View;
50 import android.widget.CompoundButton;
51 
52 import androidx.annotation.NonNull;
53 import androidx.annotation.Nullable;
54 import androidx.appcompat.app.AlertDialog;
55 import androidx.core.content.ContextCompat;
56 import androidx.fragment.app.DialogFragment;
57 import androidx.fragment.app.FragmentManager;
58 import androidx.lifecycle.LifecycleObserver;
59 import androidx.lifecycle.LifecycleOwner;
60 import androidx.lifecycle.OnLifecycleEvent;
61 import androidx.preference.Preference;
62 import androidx.preference.PreferenceGroup;
63 import androidx.preference.PreferenceScreen;
64 import androidx.preference.PreferenceViewHolder;
65 
66 import com.android.internal.annotations.VisibleForTesting;
67 import com.android.internal.content.PackageMonitor;
68 import com.android.settings.R;
69 import com.android.settings.Utils;
70 import com.android.settings.core.BasePreferenceController;
71 import com.android.settings.dashboard.DashboardFragment;
72 import com.android.settingslib.RestrictedLockUtils;
73 import com.android.settingslib.RestrictedPreference;
74 import com.android.settingslib.utils.ThreadUtils;
75 
76 import java.util.ArrayList;
77 import java.util.HashMap;
78 import java.util.HashSet;
79 import java.util.List;
80 import java.util.Map;
81 import java.util.Optional;
82 import java.util.Set;
83 import java.util.concurrent.Executor;
84 
85 /** Queries available credential manager providers and adds preferences for them. */
86 public class CredentialManagerPreferenceController extends BasePreferenceController
87         implements LifecycleObserver {
88     public static final String ADD_SERVICE_DEVICE_CONFIG = "credential_manager_service_search_uri";
89 
90     private static final String TAG = "CredentialManagerPreferenceController";
91     private static final String ALTERNATE_INTENT = "android.settings.SYNC_SETTINGS";
92     private static final String PRIMARY_INTENT = "android.settings.CREDENTIAL_PROVIDER";
93     private static final int MAX_SELECTABLE_PROVIDERS = 5;
94 
95     /**
96      * In the settings logic we should hide the list of additional credman providers if there is no
97      * provider selected at the top. The current logic relies on checking whether the autofill
98      * provider is set which won't work for cred-man only providers. Therefore when a CM only
99      * provider is set we will set the autofill setting to be this placeholder.
100      */
101     public static final String AUTOFILL_CREDMAN_ONLY_PROVIDER_PLACEHOLDER = "credential-provider";
102 
103     private final PackageManager mPm;
104     private final List<CredentialProviderInfo> mServices;
105     private final Set<String> mEnabledPackageNames;
106     private final @Nullable CredentialManager mCredentialManager;
107     private final Executor mExecutor;
108     private final Map<String, CombiPreference> mPrefs = new HashMap<>(); // key is package name
109     private final List<ServiceInfo> mPendingServiceInfos = new ArrayList<>();
110     private final Handler mHandler = new Handler();
111     private final SettingContentObserver mSettingsContentObserver;
112     private final ImageUtils.IconResizer mIconResizer;
113 
114     private @Nullable FragmentManager mFragmentManager = null;
115     private @Nullable Delegate mDelegate = null;
116     private @Nullable String mFlagOverrideForTest = null;
117     private @Nullable PreferenceScreen mPreferenceScreen = null;
118     private @Nullable PreferenceGroup mPreferenceGroup = null;
119 
120     private Optional<Boolean> mSimulateHiddenForTests = Optional.empty();
121     private boolean mIsWorkProfile = false;
122     private boolean mSimulateConnectedForTests = false;
123 
CredentialManagerPreferenceController(Context context, String preferenceKey)124     public CredentialManagerPreferenceController(Context context, String preferenceKey) {
125         super(context, preferenceKey);
126         mPm = context.getPackageManager();
127         mServices = new ArrayList<>();
128         mEnabledPackageNames = new HashSet<>();
129         mExecutor = ContextCompat.getMainExecutor(mContext);
130         mCredentialManager =
131                 getCredentialManager(context, preferenceKey.equals("credentials_test"));
132         mSettingsContentObserver =
133                 new SettingContentObserver(mHandler, context.getContentResolver());
134         mSettingsContentObserver.register();
135         mSettingsPackageMonitor.register(context, context.getMainLooper(), false);
136         mIconResizer = getResizer(context);
137     }
138 
getResizer(Context context)139     private static ImageUtils.IconResizer getResizer(Context context) {
140         final Resources resources = context.getResources();
141         int size = (int) resources.getDimension(android.R.dimen.app_icon_size);
142         return new ImageUtils.IconResizer(size, size, resources.getDisplayMetrics());
143     }
144 
getCredentialManager(Context context, boolean isTest)145     private @Nullable CredentialManager getCredentialManager(Context context, boolean isTest) {
146         if (isTest) {
147             return null;
148         }
149 
150         Object service = context.getSystemService(Context.CREDENTIAL_SERVICE);
151 
152         if (service != null && CredentialManager.isServiceEnabled(context)) {
153             return (CredentialManager) service;
154         }
155 
156         return null;
157     }
158 
159     @Override
getAvailabilityStatus()160     public int getAvailabilityStatus() {
161         if (!isConnected()) {
162             return UNSUPPORTED_ON_DEVICE;
163         }
164 
165         if (!hasNonPrimaryServices()) {
166             return CONDITIONALLY_UNAVAILABLE;
167         }
168 
169         // If we are in work profile mode and there is no user then we
170         // should hide for now. We use CONDITIONALLY_UNAVAILABLE
171         // because it is possible for the user to be set later.
172         if (mIsWorkProfile) {
173             UserHandle workProfile = getWorkProfileUserHandle();
174             if (workProfile == null) {
175                 return CONDITIONALLY_UNAVAILABLE;
176             }
177         }
178 
179         return AVAILABLE;
180     }
181 
182     @VisibleForTesting
isConnected()183     public boolean isConnected() {
184         return mCredentialManager != null || mSimulateConnectedForTests;
185     }
186 
setSimulateConnectedForTests(boolean simulateConnectedForTests)187     public void setSimulateConnectedForTests(boolean simulateConnectedForTests) {
188         mSimulateConnectedForTests = simulateConnectedForTests;
189     }
190 
191     /**
192      * Initializes the controller with the parent fragment and adds the controller to observe its
193      * lifecycle. Also stores the fragment manager which is used to open dialogs.
194      *
195      * @param fragment the fragment to use as the parent
196      * @param fragmentManager the fragment manager to use
197      * @param intent the intent used to start the activity
198      * @param delegate the delegate to send results back to
199      * @param isWorkProfile whether this controller is under a work profile user
200      */
init( DashboardFragment fragment, FragmentManager fragmentManager, @Nullable Intent launchIntent, @NonNull Delegate delegate, boolean isWorkProfile)201     public void init(
202             DashboardFragment fragment,
203             FragmentManager fragmentManager,
204             @Nullable Intent launchIntent,
205             @NonNull Delegate delegate,
206             boolean isWorkProfile) {
207         fragment.getSettingsLifecycle().addObserver(this);
208         mFragmentManager = fragmentManager;
209         mIsWorkProfile = isWorkProfile;
210 
211         setDelegate(delegate);
212         verifyReceivedIntent(launchIntent);
213 
214         // Recreate the content observers because the user might have changed.
215         mSettingsContentObserver.unregister();
216         mSettingsContentObserver.register();
217 
218         // When we set the mIsWorkProfile above we should try and force a refresh
219         // so we can get the correct data.
220         delegate.forceDelegateRefresh();
221     }
222 
223     /**
224      * Parses and sets the package component name. Returns a boolean as to whether this was
225      * successful.
226      */
227     @VisibleForTesting
verifyReceivedIntent(Intent launchIntent)228     boolean verifyReceivedIntent(Intent launchIntent) {
229         if (launchIntent == null || launchIntent.getAction() == null) {
230             return false;
231         }
232 
233         final String action = launchIntent.getAction();
234         final boolean isCredProviderAction = TextUtils.equals(action, PRIMARY_INTENT);
235         final boolean isExistingAction = TextUtils.equals(action, ALTERNATE_INTENT);
236         final boolean isValid = isCredProviderAction || isExistingAction;
237 
238         if (!isValid) {
239             return false;
240         }
241 
242         // After this point we have received a set credential manager provider intent
243         // so we should return a cancelled result if the data we got is no good.
244         if (launchIntent.getData() == null) {
245             setActivityResult(Activity.RESULT_CANCELED);
246             return false;
247         }
248 
249         String packageName = launchIntent.getData().getSchemeSpecificPart();
250         if (packageName == null) {
251             setActivityResult(Activity.RESULT_CANCELED);
252             return false;
253         }
254 
255         mPendingServiceInfos.clear();
256         for (CredentialProviderInfo cpi : mServices) {
257             final ServiceInfo serviceInfo = cpi.getServiceInfo();
258             if (serviceInfo.packageName.equals(packageName)) {
259                 mPendingServiceInfos.add(serviceInfo);
260             }
261         }
262 
263         // Don't set the result as RESULT_OK here because we should wait for the user to
264         // enable the provider.
265         if (!mPendingServiceInfos.isEmpty()) {
266             return true;
267         }
268 
269         setActivityResult(Activity.RESULT_CANCELED);
270         return false;
271     }
272 
273     @VisibleForTesting
setDelegate(Delegate delegate)274     void setDelegate(Delegate delegate) {
275         mDelegate = delegate;
276     }
277 
setActivityResult(int resultCode)278     private void setActivityResult(int resultCode) {
279         if (mDelegate == null) {
280             Log.e(TAG, "Missing delegate");
281             return;
282         }
283         mDelegate.setActivityResult(resultCode);
284     }
285 
handleIntent()286     private void handleIntent() {
287         List<ServiceInfo> pendingServiceInfos = new ArrayList<>(mPendingServiceInfos);
288         mPendingServiceInfos.clear();
289         if (pendingServiceInfos.isEmpty()) {
290             return;
291         }
292 
293         ServiceInfo serviceInfo = pendingServiceInfos.get(0);
294         ApplicationInfo appInfo = serviceInfo.applicationInfo;
295         CharSequence appName = "";
296         if (appInfo.nonLocalizedLabel != null) {
297             appName = appInfo.loadLabel(mPm);
298         }
299 
300         // Stop if there is no name.
301         if (TextUtils.isEmpty(appName)) {
302             return;
303         }
304 
305         NewProviderConfirmationDialogFragment fragment =
306                 newNewProviderConfirmationDialogFragment(
307                         serviceInfo.packageName, appName, /* shouldSetActivityResult= */ true);
308         if (fragment == null || mFragmentManager == null) {
309             return;
310         }
311 
312         fragment.show(mFragmentManager, NewProviderConfirmationDialogFragment.TAG);
313     }
314 
315     @OnLifecycleEvent(ON_CREATE)
onCreate(LifecycleOwner lifecycleOwner)316     void onCreate(LifecycleOwner lifecycleOwner) {
317         update();
318     }
319 
update()320     private void update() {
321         if (mCredentialManager == null) {
322             return;
323         }
324 
325         setAvailableServices(
326                 mCredentialManager.getCredentialProviderServices(
327                         getUser(),
328                         CredentialManager.PROVIDER_FILTER_USER_PROVIDERS_INCLUDING_HIDDEN),
329                 null);
330     }
331 
buildComponentNameSet( List<CredentialProviderInfo> providers, boolean removeNonPrimary)332     private Set<ComponentName> buildComponentNameSet(
333             List<CredentialProviderInfo> providers, boolean removeNonPrimary) {
334         Set<ComponentName> output = new HashSet<>();
335 
336         for (CredentialProviderInfo cpi : providers) {
337             if (removeNonPrimary && !cpi.isPrimary()) {
338                 continue;
339             }
340 
341             output.add(cpi.getComponentName());
342         }
343 
344         return output;
345     }
346 
updateFromExternal()347     private void updateFromExternal() {
348         if (mCredentialManager == null) {
349             return;
350         }
351 
352         // Get the list of new providers and components.
353         setAvailableServices(
354                 mCredentialManager.getCredentialProviderServices(
355                         getUser(),
356                         CredentialManager.PROVIDER_FILTER_USER_PROVIDERS_INCLUDING_HIDDEN),
357                 null);
358 
359         if (mPreferenceScreen != null) {
360             displayPreference(mPreferenceScreen);
361         }
362 
363         if (mDelegate != null) {
364             mDelegate.forceDelegateRefresh();
365         }
366     }
367 
368     @VisibleForTesting
forceDelegateRefresh()369     public void forceDelegateRefresh() {
370         if (mDelegate != null) {
371             mDelegate.forceDelegateRefresh();
372         }
373     }
374 
375     @VisibleForTesting
setSimulateHiddenForTests(Optional<Boolean> simulateHiddenForTests)376     public void setSimulateHiddenForTests(Optional<Boolean> simulateHiddenForTests) {
377         mSimulateHiddenForTests = simulateHiddenForTests;
378     }
379 
380     @VisibleForTesting
isHiddenDueToNoProviderSet( Pair<List<CombinedProviderInfo>, CombinedProviderInfo> providerPair)381     public boolean isHiddenDueToNoProviderSet(
382             Pair<List<CombinedProviderInfo>, CombinedProviderInfo> providerPair) {
383         if (mSimulateHiddenForTests.isPresent()) {
384             return mSimulateHiddenForTests.get();
385         }
386 
387         return (providerPair.first.size() == 0 || providerPair.second == null);
388     }
389 
390     @VisibleForTesting
setAvailableServices( List<CredentialProviderInfo> availableServices, String flagOverrideForTest)391     void setAvailableServices(
392             List<CredentialProviderInfo> availableServices, String flagOverrideForTest) {
393         mFlagOverrideForTest = flagOverrideForTest;
394         mServices.clear();
395         mServices.addAll(availableServices);
396 
397         // If there is a pending dialog then show it.
398         handleIntent();
399 
400         mEnabledPackageNames.clear();
401         for (CredentialProviderInfo cpi : availableServices) {
402             if (cpi.isEnabled() && !cpi.isPrimary()) {
403                 mEnabledPackageNames.add(cpi.getServiceInfo().packageName);
404             }
405         }
406 
407         for (String packageName : mPrefs.keySet()) {
408             mPrefs.get(packageName).setChecked(mEnabledPackageNames.contains(packageName));
409         }
410     }
411 
412     @VisibleForTesting
hasNonPrimaryServices()413     public boolean hasNonPrimaryServices() {
414         for (CredentialProviderInfo availableService : mServices) {
415             if (!availableService.isPrimary()) {
416                 return true;
417             }
418         }
419 
420         return false;
421     }
422 
423     @Override
displayPreference(PreferenceScreen screen)424     public void displayPreference(PreferenceScreen screen) {
425         final String prefKey = getPreferenceKey();
426         if (TextUtils.isEmpty(prefKey)) {
427             Log.w(TAG, "Skipping displayPreference because key is empty");
428             return;
429         }
430 
431         // Store this reference for later.
432         if (mPreferenceScreen == null) {
433             mPreferenceScreen = screen;
434             mPreferenceGroup = screen.findPreference(prefKey);
435         }
436 
437         final Pair<List<CombinedProviderInfo>, CombinedProviderInfo> providerPair = getProviders();
438 
439         maybeUpdateListOfPrefs(providerPair);
440         maybeUpdatePreferenceVisibility(providerPair);
441     }
442 
maybeUpdateListOfPrefs( Pair<List<CombinedProviderInfo>, CombinedProviderInfo> providerPair)443     private void maybeUpdateListOfPrefs(
444             Pair<List<CombinedProviderInfo>, CombinedProviderInfo> providerPair) {
445         if (mPreferenceScreen == null || mPreferenceGroup == null) {
446             return;
447         }
448 
449         // Build the new list of prefs.
450         Map<String, CombiPreference> newPrefs =
451                 buildPreferenceList(mPreferenceScreen.getContext(), providerPair);
452 
453         // Determine if we need to update the prefs.
454         Set<String> existingPrefPackageNames = mPrefs.keySet();
455         if (existingPrefPackageNames.equals(newPrefs.keySet())) {
456             return;
457         }
458 
459         // Since the UI is being cleared, clear any refs and prefs.
460         mPrefs.clear();
461         mPreferenceGroup.removeAll();
462 
463         // Populate the preference list with new data.
464         mPrefs.putAll(newPrefs);
465         for (CombiPreference pref : newPrefs.values()) {
466             mPreferenceGroup.addPreference(pref);
467         }
468     }
469 
maybeUpdatePreferenceVisibility( Pair<List<CombinedProviderInfo>, CombinedProviderInfo> providerPair)470     private void maybeUpdatePreferenceVisibility(
471             Pair<List<CombinedProviderInfo>, CombinedProviderInfo> providerPair) {
472         if (mPreferenceScreen == null || mPreferenceGroup == null) {
473             return;
474         }
475 
476         final boolean isAvailable =
477                 (getAvailabilityStatus() == AVAILABLE) && !isHiddenDueToNoProviderSet(providerPair);
478 
479         if (isAvailable) {
480             mPreferenceScreen.addPreference(mPreferenceGroup);
481             mPreferenceGroup.setVisible(true);
482         } else {
483             mPreferenceScreen.removePreference(mPreferenceGroup);
484             mPreferenceGroup.setVisible(false);
485         }
486     }
487 
488     /**
489      * Gets the preference that allows to add a new cred man service.
490      *
491      * @return the pref to be added
492      */
493     @VisibleForTesting
newAddServicePreference(String searchUri, Context context)494     public Preference newAddServicePreference(String searchUri, Context context) {
495         final Intent addNewServiceIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(searchUri));
496         final Preference preference = new Preference(context);
497         preference.setOnPreferenceClickListener(
498                 p -> {
499                     context.startActivityAsUser(addNewServiceIntent, UserHandle.of(getUser()));
500                     return true;
501                 });
502         preference.setTitle(R.string.print_menu_item_add_service);
503         preference.setOrder(Integer.MAX_VALUE - 1);
504         preference.setPersistent(false);
505 
506         // Try to set the icon this should fail in a test environment but work
507         // in the actual app.
508         try {
509             preference.setIcon(R.drawable.ic_add_24dp);
510         } catch (Resources.NotFoundException e) {
511             Log.e(TAG, "Failed to find icon for add services link", e);
512         }
513         return preference;
514     }
515 
516     /**
517      * Returns a pair that contains a list of the providers in the first position and the top
518      * provider in the second position.
519      */
getProviders()520     private Pair<List<CombinedProviderInfo>, CombinedProviderInfo> getProviders() {
521         // Get the selected autofill provider. If it is the placeholder then replace it with an
522         // empty string.
523         String selectedAutofillProvider =
524                 getSelectedAutofillProvider(mContext, getUser(), TAG);
525 
526         // Get the list of combined providers.
527         List<CombinedProviderInfo> providers =
528                 CombinedProviderInfo.buildMergedList(
529                         AutofillServiceInfo.getAvailableServices(mContext, getUser()),
530                         mServices,
531                         selectedAutofillProvider);
532         return new Pair<>(providers, CombinedProviderInfo.getTopProvider(providers));
533     }
534 
535     /** Aggregates the list of services and builds a list of UI prefs to show. */
536     @VisibleForTesting
buildPreferenceList( @onNull Context context, @NonNull Pair<List<CombinedProviderInfo>, CombinedProviderInfo> providerPair)537     public @NonNull Map<String, CombiPreference> buildPreferenceList(
538             @NonNull Context context,
539             @NonNull Pair<List<CombinedProviderInfo>, CombinedProviderInfo> providerPair) {
540         // Extract the values.
541         CombinedProviderInfo topProvider = providerPair.second;
542         List<CombinedProviderInfo> providers = providerPair.first;
543 
544         // If the provider is set to "none" or there are no providers then we should not
545         // return any providers.
546         if (isHiddenDueToNoProviderSet(providerPair)) {
547             forceDelegateRefresh();
548             return new HashMap<>();
549         }
550 
551         Map<String, CombiPreference> output = new HashMap<>();
552         for (CombinedProviderInfo combinedInfo : providers) {
553             final String packageName = combinedInfo.getApplicationInfo().packageName;
554 
555             // If this provider is displayed at the top then we should not show it.
556             if (topProvider != null
557                     && topProvider.getApplicationInfo() != null
558                     && topProvider.getApplicationInfo().packageName.equals(packageName)) {
559                 continue;
560             }
561 
562             // If this is an autofill provider then don't show it here.
563             if (combinedInfo.getCredentialProviderInfos().isEmpty()) {
564                 continue;
565             }
566 
567             Drawable icon = combinedInfo.getAppIcon(context, getUser());
568             CharSequence title = combinedInfo.getAppName(context);
569 
570             // Build the pref and add it to the output & group.
571             CombiPreference pref =
572                     addProviderPreference(
573                             context,
574                             title == null ? "" : title,
575                             icon,
576                             packageName,
577                             combinedInfo.getSettingsSubtitle(),
578                             combinedInfo.getSettingsActivity(),
579                             combinedInfo.getDeviceAdminRestrictions(context, getUser()));
580             output.put(packageName, pref);
581         }
582 
583         // Set the visibility if we have services.
584         forceDelegateRefresh();
585 
586         return output;
587     }
588 
589     /** Creates a preference object based on the provider info. */
590     @VisibleForTesting
createPreference(Context context, CredentialProviderInfo service)591     public CombiPreference createPreference(Context context, CredentialProviderInfo service) {
592         CharSequence label = service.getLabel(context);
593         return addProviderPreference(
594                 context,
595                 label == null ? "" : label,
596                 service.getServiceIcon(mContext),
597                 service.getServiceInfo().packageName,
598                 service.getSettingsSubtitle(),
599                 service.getSettingsActivity(),
600                 /* enforcedCredManAdmin= */ null);
601     }
602 
603     /**
604      * Enables the package name as an enabled credential manager provider.
605      *
606      * @param packageName the package name to enable
607      */
608     @VisibleForTesting
togglePackageNameEnabled(String packageName)609     public boolean togglePackageNameEnabled(String packageName) {
610         if (hasProviderLimitBeenReached()) {
611             return false;
612         } else {
613             mEnabledPackageNames.add(packageName);
614             commitEnabledPackages();
615             return true;
616         }
617     }
618 
619     /**
620      * Disables the package name as a credential manager provider.
621      *
622      * @param packageName the package name to disable
623      */
624     @VisibleForTesting
togglePackageNameDisabled(String packageName)625     public void togglePackageNameDisabled(String packageName) {
626         mEnabledPackageNames.remove(packageName);
627         commitEnabledPackages();
628     }
629 
630     /** Returns the enabled credential manager provider package names. */
631     @VisibleForTesting
getEnabledProviders()632     public Set<String> getEnabledProviders() {
633         return mEnabledPackageNames;
634     }
635 
636     /**
637      * Returns the enabled credential manager provider flattened component names that can be stored
638      * in the setting.
639      */
640     @VisibleForTesting
getEnabledSettings()641     public List<String> getEnabledSettings() {
642         // Get all the component names that match the enabled package names.
643         List<String> enabledServices = new ArrayList<>();
644         for (CredentialProviderInfo service : mServices) {
645             ComponentName cn = service.getServiceInfo().getComponentName();
646             if (mEnabledPackageNames.contains(service.getServiceInfo().packageName)) {
647                 enabledServices.add(cn.flattenToString());
648             }
649         }
650 
651         return enabledServices;
652     }
653 
654     @VisibleForTesting
processIcon(@ullable Drawable icon)655     public @NonNull Drawable processIcon(@Nullable Drawable icon) {
656         // If we didn't get an icon then we should use the default app icon.
657         if (icon == null) {
658             icon = mPm.getDefaultActivityIcon();
659         }
660 
661         Drawable providerIcon = Utils.getSafeIcon(icon);
662         return mIconResizer.createIconThumbnail(providerIcon);
663     }
664 
hasProviderLimitBeenReached()665     private boolean hasProviderLimitBeenReached() {
666         return hasProviderLimitBeenReached(mEnabledPackageNames.size());
667     }
668 
669     @VisibleForTesting
hasProviderLimitBeenReached(int enabledAdditionalProviderCount)670     public static boolean hasProviderLimitBeenReached(int enabledAdditionalProviderCount) {
671         // If the number of package names has reached the maximum limit then
672         // we should stop any new packages from being added. We will also
673         // reserve one place for the primary provider so if the max limit is
674         // five providers this will be four additional plus the primary.
675         return (enabledAdditionalProviderCount + 1) >= MAX_SELECTABLE_PROVIDERS;
676     }
677 
678     /** Gets the credential autofill service component name. */
getCredentialAutofillService(Context context, String tag)679     public static String getCredentialAutofillService(Context context, String tag) {
680         try {
681             return context.getResources().getString(
682                     com.android.internal.R.string.config_defaultCredentialManagerAutofillService);
683         } catch (Resources.NotFoundException e) {
684             Log.e(tag, "Failed to find credential autofill service.", e);
685         }
686         return "";
687     }
688 
689     /** Gets the selected autofill provider name. This will filter out place holder names. **/
getSelectedAutofillProvider( Context context, int userId, String tag)690     public static @Nullable String getSelectedAutofillProvider(
691             Context context, int userId, String tag) {
692         String providerName = Settings.Secure.getStringForUser(
693                 context.getContentResolver(), Settings.Secure.AUTOFILL_SERVICE, userId);
694 
695         if (TextUtils.isEmpty(providerName)) {
696             return providerName;
697         }
698 
699         if (providerName.equals(AUTOFILL_CREDMAN_ONLY_PROVIDER_PLACEHOLDER)) {
700             return "";
701         }
702 
703         String credentialAutofillService = "";
704         if (android.service.autofill.Flags.autofillCredmanDevIntegration()) {
705             credentialAutofillService = getCredentialAutofillService(context, tag);
706         }
707         if (providerName.equals(credentialAutofillService)) {
708             return "";
709         }
710 
711         return providerName;
712     }
713 
addProviderPreference( @onNull Context prefContext, @NonNull CharSequence title, @Nullable Drawable icon, @NonNull String packageName, @Nullable CharSequence subtitle, @Nullable CharSequence settingsActivity, @Nullable RestrictedLockUtils.EnforcedAdmin enforcedCredManAdmin)714     private CombiPreference addProviderPreference(
715             @NonNull Context prefContext,
716             @NonNull CharSequence title,
717             @Nullable Drawable icon,
718             @NonNull String packageName,
719             @Nullable CharSequence subtitle,
720             @Nullable CharSequence settingsActivity,
721             @Nullable RestrictedLockUtils.EnforcedAdmin enforcedCredManAdmin) {
722         final CombiPreference pref =
723                 new CombiPreference(prefContext, mEnabledPackageNames.contains(packageName));
724         pref.setTitle(title);
725         pref.setLayoutResource(R.layout.preference_icon_credman);
726 
727         if (Flags.newSettingsUi()) {
728             pref.setIcon(processIcon(icon));
729         } else if (icon != null) {
730             pref.setIcon(icon);
731         }
732 
733         if (subtitle != null) {
734             pref.setSummary(subtitle);
735         }
736 
737         pref.setDisabledByAdmin(enforcedCredManAdmin);
738 
739         pref.setPreferenceListener(
740                 new CombiPreference.OnCombiPreferenceClickListener() {
741                     @Override
742                     public boolean onCheckChanged(CombiPreference p, boolean isChecked) {
743                         if (isChecked) {
744                             if (hasProviderLimitBeenReached()) {
745                                 // Show the error if too many enabled.
746                                 final DialogFragment fragment = newErrorDialogFragment();
747 
748                                 if (fragment == null || mFragmentManager == null) {
749                                     return false;
750                                 }
751 
752                                 fragment.show(mFragmentManager, ErrorDialogFragment.TAG);
753                                 return false;
754                             }
755 
756                             togglePackageNameEnabled(packageName);
757 
758                             // Enable all prefs.
759                             if (mPrefs.containsKey(packageName)) {
760                                 mPrefs.get(packageName).setChecked(true);
761                             }
762                         } else {
763                             togglePackageNameDisabled(packageName);
764                         }
765 
766                         return true;
767                     }
768 
769                     @Override
770                     public void onLeftSideClicked() {
771                         CombinedProviderInfo.launchSettingsActivityIntent(
772                                 mContext, packageName, settingsActivity, getUser());
773                     }
774                 });
775 
776         return pref;
777     }
778 
commitEnabledPackages()779     private void commitEnabledPackages() {
780         // Commit using the CredMan API.
781         if (mCredentialManager == null) {
782             return;
783         }
784 
785         // Get the existing primary providers since we don't touch them in
786         // this part of the UI we should just copy them over.
787         Set<String> primaryServices = new HashSet<>();
788         List<String> enabledServices = getEnabledSettings();
789         for (CredentialProviderInfo service : mServices) {
790             if (service.isPrimary()) {
791                 String flattened = service.getServiceInfo().getComponentName().flattenToString();
792                 primaryServices.add(flattened);
793                 enabledServices.add(flattened);
794             }
795         }
796 
797         mCredentialManager.setEnabledProviders(
798                 new ArrayList<>(primaryServices),
799                 enabledServices,
800                 getUser(),
801                 mExecutor,
802                 new OutcomeReceiver<Void, SetEnabledProvidersException>() {
803                     @Override
804                     public void onResult(Void result) {
805                         Log.i(TAG, "setEnabledProviders success");
806                         updateFromExternal();
807                     }
808 
809                     @Override
810                     public void onError(SetEnabledProvidersException e) {
811                         Log.e(TAG, "setEnabledProviders error: " + e.toString());
812                     }
813                 });
814     }
815 
816     /** Create the new provider confirmation dialog. */
817     private @Nullable NewProviderConfirmationDialogFragment
newNewProviderConfirmationDialogFragment( @onNull String packageName, @NonNull CharSequence appName, boolean shouldSetActivityResult)818             newNewProviderConfirmationDialogFragment(
819                     @NonNull String packageName,
820                     @NonNull CharSequence appName,
821                     boolean shouldSetActivityResult) {
822         DialogHost host =
823                 new DialogHost() {
824                     @Override
825                     public void onDialogClick(int whichButton) {
826                         completeEnableProviderDialogBox(
827                                 whichButton, packageName, shouldSetActivityResult);
828                     }
829 
830                     @Override
831                     public void onCancel() {}
832                 };
833 
834         return new NewProviderConfirmationDialogFragment(host, packageName, appName);
835     }
836 
837     @VisibleForTesting
completeEnableProviderDialogBox( int whichButton, String packageName, boolean shouldSetActivityResult)838     int completeEnableProviderDialogBox(
839             int whichButton, String packageName, boolean shouldSetActivityResult) {
840         int activityResult = -1;
841         if (whichButton == DialogInterface.BUTTON_POSITIVE) {
842             if (togglePackageNameEnabled(packageName)) {
843                 // Enable all prefs.
844                 if (mPrefs.containsKey(packageName)) {
845                     mPrefs.get(packageName).setChecked(true);
846                 }
847                 activityResult = Activity.RESULT_OK;
848             } else {
849                 // There are too many providers so set the result as cancelled.
850                 activityResult = Activity.RESULT_CANCELED;
851 
852                 // Show the error if too many enabled.
853                 final DialogFragment fragment = newErrorDialogFragment();
854 
855                 if (fragment == null || mFragmentManager == null) {
856                     return activityResult;
857                 }
858 
859                 fragment.show(mFragmentManager, ErrorDialogFragment.TAG);
860             }
861         } else {
862             // The user clicked the cancel button so send that result back.
863             activityResult = Activity.RESULT_CANCELED;
864         }
865 
866         // If the dialog is being shown because of the intent we should
867         // return a result.
868         if (activityResult == -1 || !shouldSetActivityResult) {
869             setActivityResult(activityResult);
870         }
871 
872         return activityResult;
873     }
874 
newErrorDialogFragment()875     private @Nullable ErrorDialogFragment newErrorDialogFragment() {
876         DialogHost host =
877                 new DialogHost() {
878                     @Override
879                     public void onDialogClick(int whichButton) {}
880 
881                     @Override
882                     public void onCancel() {}
883                 };
884 
885         return new ErrorDialogFragment(host);
886     }
887 
getUser()888     protected int getUser() {
889         if (mIsWorkProfile) {
890             UserHandle workProfile = getWorkProfileUserHandle();
891             if (workProfile != null) {
892                 return workProfile.getIdentifier();
893             }
894         }
895         return UserHandle.myUserId();
896     }
897 
getWorkProfileUserHandle()898     private @Nullable UserHandle getWorkProfileUserHandle() {
899         if (mIsWorkProfile) {
900             return Utils.getManagedProfile(UserManager.get(mContext));
901         }
902 
903         return null;
904     }
905 
906     /** Called when the dialog button is clicked. */
907     private static interface DialogHost {
onDialogClick(int whichButton)908         void onDialogClick(int whichButton);
909 
onCancel()910         void onCancel();
911     }
912 
913     /** Called to send messages back to the parent fragment. */
914     public static interface Delegate {
setActivityResult(int resultCode)915         void setActivityResult(int resultCode);
916 
forceDelegateRefresh()917         void forceDelegateRefresh();
918     }
919 
920     /**
921      * Monitor coming and going credman services and calls {@link #DefaultCombinedPicker} when
922      * necessary
923      */
924     private final PackageMonitor mSettingsPackageMonitor =
925             new PackageMonitor() {
926                 @Override
927                 public void onPackageAdded(String packageName, int uid) {
928                     ThreadUtils.postOnMainThread(() -> updateFromExternal());
929                 }
930 
931                 @Override
932                 public void onPackageModified(String packageName) {
933                     ThreadUtils.postOnMainThread(() -> updateFromExternal());
934                 }
935 
936                 @Override
937                 public void onPackageRemoved(String packageName, int uid) {
938                     ThreadUtils.postOnMainThread(() -> updateFromExternal());
939                 }
940             };
941 
942     /** Dialog fragment parent class. */
943     private abstract static class CredentialManagerDialogFragment extends DialogFragment
944             implements DialogInterface.OnClickListener {
945 
946         public static final String TAG = "CredentialManagerDialogFragment";
947         public static final String PACKAGE_NAME_KEY = "package_name";
948         public static final String APP_NAME_KEY = "app_name";
949 
950         private DialogHost mDialogHost;
951 
CredentialManagerDialogFragment(DialogHost dialogHost)952         CredentialManagerDialogFragment(DialogHost dialogHost) {
953             super();
954             mDialogHost = dialogHost;
955         }
956 
getDialogHost()957         public DialogHost getDialogHost() {
958             return mDialogHost;
959         }
960 
961         @Override
onCancel(@onNull DialogInterface dialog)962         public void onCancel(@NonNull DialogInterface dialog) {
963             getDialogHost().onCancel();
964         }
965     }
966 
967     /** Dialog showing error when too many providers are selected. */
968     public static class ErrorDialogFragment extends CredentialManagerDialogFragment {
969 
ErrorDialogFragment(DialogHost dialogHost)970         ErrorDialogFragment(DialogHost dialogHost) {
971             super(dialogHost);
972         }
973 
974         @Override
onCreateDialog(Bundle savedInstanceState)975         public Dialog onCreateDialog(Bundle savedInstanceState) {
976             return new AlertDialog.Builder(getActivity())
977                     .setTitle(
978                             getContext()
979                                     .getString(
980                                             Flags.newSettingsUi()
981                                                     ? R.string.credman_limit_error_msg_title
982                                                     : R.string.credman_error_message_title))
983                     .setMessage(
984                             getContext()
985                                     .getString(
986                                             Flags.newSettingsUi()
987                                                     ? R.string.credman_limit_error_msg
988                                                     : R.string.credman_error_message))
989                     .setPositiveButton(android.R.string.ok, this)
990                     .create();
991         }
992 
993         @Override
onClick(DialogInterface dialog, int which)994         public void onClick(DialogInterface dialog, int which) {}
995     }
996 
997     /**
998      * Confirmation dialog fragment shows a dialog to the user to confirm that they would like to
999      * enable the new provider.
1000      */
1001     public static class NewProviderConfirmationDialogFragment
1002             extends CredentialManagerDialogFragment {
1003 
NewProviderConfirmationDialogFragment( DialogHost dialogHost, @NonNull String packageName, @NonNull CharSequence appName)1004         NewProviderConfirmationDialogFragment(
1005                 DialogHost dialogHost, @NonNull String packageName, @NonNull CharSequence appName) {
1006             super(dialogHost);
1007 
1008             final Bundle argument = new Bundle();
1009             argument.putString(PACKAGE_NAME_KEY, packageName);
1010             argument.putCharSequence(APP_NAME_KEY, appName);
1011             setArguments(argument);
1012         }
1013 
1014         @Override
onCreateDialog(Bundle savedInstanceState)1015         public Dialog onCreateDialog(Bundle savedInstanceState) {
1016             final Bundle bundle = getArguments();
1017             final Context context = getContext();
1018             final CharSequence appName =
1019                     bundle.getCharSequence(CredentialManagerDialogFragment.APP_NAME_KEY);
1020             final String title =
1021                     context.getString(R.string.credman_enable_confirmation_message_title, appName);
1022             final String message =
1023                     context.getString(R.string.credman_enable_confirmation_message, appName);
1024 
1025             return new AlertDialog.Builder(getActivity())
1026                     .setTitle(title)
1027                     .setMessage(message)
1028                     .setPositiveButton(android.R.string.ok, this)
1029                     .setNegativeButton(android.R.string.cancel, this)
1030                     .create();
1031         }
1032 
1033         @Override
onClick(DialogInterface dialog, int which)1034         public void onClick(DialogInterface dialog, int which) {
1035             getDialogHost().onDialogClick(which);
1036         }
1037     }
1038 
1039     /** Updates the list if setting content changes. */
1040     private final class SettingContentObserver extends ContentObserver {
1041 
1042         private final Uri mAutofillService =
1043                 Settings.Secure.getUriFor(Settings.Secure.AUTOFILL_SERVICE);
1044 
1045         private final Uri mCredentialService =
1046                 Settings.Secure.getUriFor(Settings.Secure.CREDENTIAL_SERVICE);
1047 
1048         private final Uri mCredentialPrimaryService =
1049                 Settings.Secure.getUriFor(Settings.Secure.CREDENTIAL_SERVICE_PRIMARY);
1050 
1051         private ContentResolver mContentResolver;
1052 
SettingContentObserver(Handler handler, ContentResolver contentResolver)1053         public SettingContentObserver(Handler handler, ContentResolver contentResolver) {
1054             super(handler);
1055             mContentResolver = contentResolver;
1056         }
1057 
register()1058         public void register() {
1059             mContentResolver.registerContentObserver(mAutofillService, false, this, getUser());
1060             mContentResolver.registerContentObserver(mCredentialService, false, this, getUser());
1061             mContentResolver.registerContentObserver(
1062                     mCredentialPrimaryService, false, this, getUser());
1063         }
1064 
unregister()1065         public void unregister() {
1066             mContentResolver.unregisterContentObserver(this);
1067         }
1068 
1069         @Override
onChange(boolean selfChange, Uri uri)1070         public void onChange(boolean selfChange, Uri uri) {
1071             updateFromExternal();
1072         }
1073     }
1074 
1075     /** CombiPreference is a combination of RestrictedPreference and SwitchPreference. */
1076     public static class CombiPreference extends RestrictedPreference {
1077 
1078         private final Listener mListener = new Listener();
1079 
1080         private class Listener implements View.OnClickListener {
1081             @Override
onClick(View buttonView)1082             public void onClick(View buttonView) {
1083                 // Forward the event.
1084                 if (mSwitch != null && mOnClickListener != null) {
1085                     if (!mOnClickListener.onCheckChanged(
1086                             CombiPreference.this, mSwitch.isChecked())) {
1087                         // The update was not successful since there were too
1088                         // many enabled providers to manually reset any state.
1089                         mChecked = false;
1090                         mSwitch.setChecked(false);
1091                     }
1092                 }
1093             }
1094         }
1095 
1096         // Stores a reference to the switch view.
1097         private @Nullable CompoundButton mSwitch;
1098 
1099         // Switch text for on and off states
1100         private @NonNull boolean mChecked = false;
1101         private @Nullable OnCombiPreferenceClickListener mOnClickListener = null;
1102 
1103         public interface OnCombiPreferenceClickListener {
1104             /** Called when the check is updated */
onCheckChanged(CombiPreference p, boolean isChecked)1105             boolean onCheckChanged(CombiPreference p, boolean isChecked);
1106 
1107             /** Called when the left side is clicked. */
onLeftSideClicked()1108             void onLeftSideClicked();
1109         }
1110 
CombiPreference(Context context, boolean initialValue)1111         public CombiPreference(Context context, boolean initialValue) {
1112             super(context);
1113             mChecked = initialValue;
1114         }
1115 
1116         /** Set the new checked value */
setChecked(boolean isChecked)1117         public void setChecked(boolean isChecked) {
1118             // Don't update if we don't need too.
1119             if (mChecked == isChecked) {
1120                 return;
1121             }
1122 
1123             mChecked = isChecked;
1124 
1125             if (mSwitch != null) {
1126                 mSwitch.setChecked(isChecked);
1127             }
1128         }
1129 
1130         @VisibleForTesting
isChecked()1131         public boolean isChecked() {
1132             return mChecked;
1133         }
1134 
1135         @Override
setTitle(@ullable CharSequence title)1136         public void setTitle(@Nullable CharSequence title) {
1137             super.setTitle(title);
1138             maybeUpdateContentDescription();
1139         }
1140 
maybeUpdateContentDescription()1141         private void maybeUpdateContentDescription() {
1142             final CharSequence appName = getTitle();
1143 
1144             if (mSwitch != null && !TextUtils.isEmpty(appName)) {
1145                 mSwitch.setContentDescription(
1146                         getContext()
1147                                 .getString(
1148                                         R.string.credman_on_off_switch_content_description,
1149                                         appName));
1150             }
1151         }
1152 
setPreferenceListener(OnCombiPreferenceClickListener onClickListener)1153         public void setPreferenceListener(OnCombiPreferenceClickListener onClickListener) {
1154             mOnClickListener = onClickListener;
1155         }
1156 
1157         @Override
getSecondTargetResId()1158         protected int getSecondTargetResId() {
1159             return com.android.settingslib.R.layout.preference_widget_primary_switch;
1160         }
1161 
1162         @Override
onBindViewHolder(PreferenceViewHolder view)1163         public void onBindViewHolder(PreferenceViewHolder view) {
1164             super.onBindViewHolder(view);
1165 
1166             // Setup the switch.
1167             View checkableView =
1168                     view.itemView.findViewById(com.android.settingslib.R.id.switchWidget);
1169             if (checkableView instanceof CompoundButton switchView) {
1170                 switchView.setChecked(mChecked);
1171                 switchView.setOnClickListener(mListener);
1172 
1173                 // Store this for later.
1174                 mSwitch = switchView;
1175 
1176                 // Update the content description.
1177                 maybeUpdateContentDescription();
1178             }
1179 
1180             super.setOnPreferenceClickListener(
1181                     new Preference.OnPreferenceClickListener() {
1182                         @Override
1183                         public boolean onPreferenceClick(Preference preference) {
1184                             if (mOnClickListener != null) {
1185                                 mOnClickListener.onLeftSideClicked();
1186                             }
1187 
1188                             return true;
1189                         }
1190                     });
1191         }
1192     }
1193 }
1194