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