• 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.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.app.Activity;
24 import android.app.Dialog;
25 import android.content.ComponentName;
26 import android.content.ContentResolver;
27 import android.content.Context;
28 import android.content.DialogInterface;
29 import android.content.Intent;
30 import android.content.pm.ApplicationInfo;
31 import android.content.pm.PackageManager;
32 import android.content.pm.ServiceInfo;
33 import android.content.res.Resources;
34 import android.credentials.CredentialManager;
35 import android.credentials.CredentialProviderInfo;
36 import android.credentials.SetEnabledProvidersException;
37 import android.database.ContentObserver;
38 import android.graphics.drawable.Drawable;
39 import android.net.Uri;
40 import android.os.Bundle;
41 import android.os.Handler;
42 import android.os.OutcomeReceiver;
43 import android.os.UserHandle;
44 import android.os.UserManager;
45 import android.provider.Settings;
46 import android.service.autofill.AutofillServiceInfo;
47 import android.text.TextUtils;
48 import android.util.IconDrawableFactory;
49 import android.util.Log;
50 
51 import androidx.appcompat.app.AlertDialog;
52 import androidx.core.content.ContextCompat;
53 import androidx.fragment.app.DialogFragment;
54 import androidx.fragment.app.FragmentManager;
55 import androidx.lifecycle.LifecycleObserver;
56 import androidx.lifecycle.LifecycleOwner;
57 import androidx.lifecycle.OnLifecycleEvent;
58 import androidx.preference.Preference;
59 import androidx.preference.PreferenceGroup;
60 import androidx.preference.PreferenceScreen;
61 import androidx.preference.SwitchPreference;
62 
63 import com.android.internal.annotations.VisibleForTesting;
64 import com.android.internal.content.PackageMonitor;
65 import com.android.settings.R;
66 import com.android.settings.Utils;
67 import com.android.settings.core.BasePreferenceController;
68 import com.android.settings.dashboard.DashboardFragment;
69 import com.android.settingslib.utils.ThreadUtils;
70 
71 import java.util.ArrayList;
72 import java.util.HashMap;
73 import java.util.HashSet;
74 import java.util.List;
75 import java.util.Map;
76 import java.util.Set;
77 import java.util.concurrent.Executor;
78 
79 /** Queries available credential manager providers and adds preferences for them. */
80 public class CredentialManagerPreferenceController extends BasePreferenceController
81         implements LifecycleObserver {
82     public static final String ADD_SERVICE_DEVICE_CONFIG = "credential_manager_service_search_uri";
83 
84     /**
85      * In the settings logic we should hide the list of additional credman providers if there is no
86      * provider selected at the top. The current logic relies on checking whether the autofill
87      * provider is set which won't work for cred-man only providers. Therefore when a CM only
88      * provider is set we will set the autofill setting to be this placeholder.
89      */
90     public static final String AUTOFILL_CREDMAN_ONLY_PROVIDER_PLACEHOLDER = "credential-provider";
91 
92     private static final String TAG = "CredentialManagerPreferenceController";
93     private static final String ALTERNATE_INTENT = "android.settings.SYNC_SETTINGS";
94     private static final String PRIMARY_INTENT = "android.settings.CREDENTIAL_PROVIDER";
95     private static final int MAX_SELECTABLE_PROVIDERS = 5;
96 
97     private final PackageManager mPm;
98     private final IconDrawableFactory mIconFactory;
99     private final List<CredentialProviderInfo> mServices;
100     private final Set<String> mEnabledPackageNames;
101     private final @Nullable CredentialManager mCredentialManager;
102     private final Executor mExecutor;
103     private final Map<String, SwitchPreference> mPrefs = new HashMap<>(); // key is package name
104     private final List<ServiceInfo> mPendingServiceInfos = new ArrayList<>();
105     private final Handler mHandler = new Handler();
106     private final SettingContentObserver mSettingsContentObserver;
107 
108     private @Nullable FragmentManager mFragmentManager = null;
109     private @Nullable Delegate mDelegate = null;
110     private @Nullable String mFlagOverrideForTest = null;
111     private @Nullable PreferenceScreen mPreferenceScreen = null;
112 
113     private boolean mVisibility = false;
114     private boolean mIsWorkProfile = false;
115 
CredentialManagerPreferenceController(Context context, String preferenceKey)116     public CredentialManagerPreferenceController(Context context, String preferenceKey) {
117         super(context, preferenceKey);
118         mPm = context.getPackageManager();
119         mIconFactory = IconDrawableFactory.newInstance(mContext);
120         mServices = new ArrayList<>();
121         mEnabledPackageNames = new HashSet<>();
122         mExecutor = ContextCompat.getMainExecutor(mContext);
123         mCredentialManager =
124                 getCredentialManager(context, preferenceKey.equals("credentials_test"));
125         mSettingsContentObserver =
126                 new SettingContentObserver(mHandler, context.getContentResolver());
127         mSettingsContentObserver.register();
128         mSettingsPackageMonitor.register(context, context.getMainLooper(), false);
129     }
130 
getCredentialManager(Context context, boolean isTest)131     private @Nullable CredentialManager getCredentialManager(Context context, boolean isTest) {
132         if (isTest) {
133             return null;
134         }
135 
136         Object service = context.getSystemService(Context.CREDENTIAL_SERVICE);
137 
138         if (service != null && CredentialManager.isServiceEnabled(context)) {
139             return (CredentialManager) service;
140         }
141 
142         return null;
143     }
144 
145     @Override
getAvailabilityStatus()146     public int getAvailabilityStatus() {
147         if (mCredentialManager == null) {
148             return UNSUPPORTED_ON_DEVICE;
149         }
150 
151         if (!mVisibility) {
152             return CONDITIONALLY_UNAVAILABLE;
153         }
154 
155         if (mServices.isEmpty()) {
156             return CONDITIONALLY_UNAVAILABLE;
157         }
158 
159         return AVAILABLE;
160     }
161 
162     @VisibleForTesting
isConnected()163     public boolean isConnected() {
164         return mCredentialManager != null;
165     }
166 
167     /**
168      * Initializes the controller with the parent fragment and adds the controller to observe its
169      * lifecycle. Also stores the fragment manager which is used to open dialogs.
170      *
171      * @param fragment the fragment to use as the parent
172      * @param fragmentManager the fragment manager to use
173      * @param intent the intent used to start the activity
174      * @param delegate the delegate to send results back to
175      * @param isWorkProfile whether this controller is under a work profile user
176      */
init( DashboardFragment fragment, FragmentManager fragmentManager, @Nullable Intent launchIntent, @NonNull Delegate delegate, boolean isWorkProfile)177     public void init(
178             DashboardFragment fragment,
179             FragmentManager fragmentManager,
180             @Nullable Intent launchIntent,
181             @NonNull Delegate delegate,
182             boolean isWorkProfile) {
183         fragment.getSettingsLifecycle().addObserver(this);
184         mFragmentManager = fragmentManager;
185         mIsWorkProfile = isWorkProfile;
186         setDelegate(delegate);
187         verifyReceivedIntent(launchIntent);
188 
189         // Recreate the content observers because the user might have changed.
190         mSettingsContentObserver.unregister();
191         mSettingsContentObserver.register();
192     }
193 
194     /**
195      * Parses and sets the package component name. Returns a boolean as to whether this was
196      * successful.
197      */
198     @VisibleForTesting
verifyReceivedIntent(Intent launchIntent)199     boolean verifyReceivedIntent(Intent launchIntent) {
200         if (launchIntent == null || launchIntent.getAction() == null) {
201             return false;
202         }
203 
204         final String action = launchIntent.getAction();
205         final boolean isCredProviderAction = TextUtils.equals(action, PRIMARY_INTENT);
206         final boolean isExistingAction = TextUtils.equals(action, ALTERNATE_INTENT);
207         final boolean isValid = isCredProviderAction || isExistingAction;
208 
209         if (!isValid) {
210             return false;
211         }
212 
213         // After this point we have received a set credential manager provider intent
214         // so we should return a cancelled result if the data we got is no good.
215         if (launchIntent.getData() == null) {
216             setActivityResult(Activity.RESULT_CANCELED);
217             return false;
218         }
219 
220         String packageName = launchIntent.getData().getSchemeSpecificPart();
221         if (packageName == null) {
222             setActivityResult(Activity.RESULT_CANCELED);
223             return false;
224         }
225 
226         mPendingServiceInfos.clear();
227         for (CredentialProviderInfo cpi : mServices) {
228             final ServiceInfo serviceInfo = cpi.getServiceInfo();
229             if (serviceInfo.packageName.equals(packageName)) {
230                 mPendingServiceInfos.add(serviceInfo);
231             }
232         }
233 
234         // Don't set the result as RESULT_OK here because we should wait for the user to
235         // enable the provider.
236         if (!mPendingServiceInfos.isEmpty()) {
237             return true;
238         }
239 
240         setActivityResult(Activity.RESULT_CANCELED);
241         return false;
242     }
243 
244     @VisibleForTesting
setDelegate(Delegate delegate)245     void setDelegate(Delegate delegate) {
246         mDelegate = delegate;
247     }
248 
setActivityResult(int resultCode)249     private void setActivityResult(int resultCode) {
250         if (mDelegate == null) {
251             Log.e(TAG, "Missing delegate");
252             return;
253         }
254         mDelegate.setActivityResult(resultCode);
255     }
256 
handleIntent()257     private void handleIntent() {
258         List<ServiceInfo> pendingServiceInfos = new ArrayList<>(mPendingServiceInfos);
259         mPendingServiceInfos.clear();
260         if (pendingServiceInfos.isEmpty()) {
261             return;
262         }
263 
264         ServiceInfo serviceInfo = pendingServiceInfos.get(0);
265         ApplicationInfo appInfo = serviceInfo.applicationInfo;
266         CharSequence appName = "";
267         if (appInfo.nonLocalizedLabel != null) {
268             appName = appInfo.loadLabel(mPm);
269         }
270 
271         // Stop if there is no name.
272         if (TextUtils.isEmpty(appName)) {
273             return;
274         }
275 
276         NewProviderConfirmationDialogFragment fragment =
277                 newNewProviderConfirmationDialogFragment(
278                         serviceInfo.packageName, appName, /* setActivityResult= */ true);
279         if (fragment == null || mFragmentManager == null) {
280             return;
281         }
282 
283         fragment.show(mFragmentManager, NewProviderConfirmationDialogFragment.TAG);
284     }
285 
286     @OnLifecycleEvent(ON_CREATE)
onCreate(LifecycleOwner lifecycleOwner)287     void onCreate(LifecycleOwner lifecycleOwner) {
288         update();
289     }
290 
update()291     private void update() {
292         if (mCredentialManager == null) {
293             return;
294         }
295 
296         setAvailableServices(
297                 mCredentialManager.getCredentialProviderServices(
298                         getUser(), CredentialManager.PROVIDER_FILTER_USER_PROVIDERS_ONLY),
299                 null);
300     }
301 
updateFromExternal()302     private void updateFromExternal() {
303         update();
304 
305         if (mPreferenceScreen != null) {
306             displayPreference(mPreferenceScreen);
307         }
308 
309         if (mDelegate != null) {
310             mDelegate.forceDelegateRefresh();
311         }
312     }
313 
setVisibility(boolean newVisibility)314     private void setVisibility(boolean newVisibility) {
315         if (newVisibility == mVisibility) {
316             return;
317         }
318 
319         mVisibility = newVisibility;
320         if (mDelegate != null) {
321             mDelegate.forceDelegateRefresh();
322         }
323     }
324 
325     @VisibleForTesting
setAvailableServices( List<CredentialProviderInfo> availableServices, String flagOverrideForTest)326     void setAvailableServices(
327             List<CredentialProviderInfo> availableServices, String flagOverrideForTest) {
328         mFlagOverrideForTest = flagOverrideForTest;
329         mServices.clear();
330         mServices.addAll(availableServices);
331 
332         // If there is a pending dialog then show it.
333         handleIntent();
334 
335         mEnabledPackageNames.clear();
336         for (CredentialProviderInfo cpi : availableServices) {
337             if (cpi.isEnabled() && !cpi.isPrimary()) {
338                 mEnabledPackageNames.add(cpi.getServiceInfo().packageName);
339             }
340         }
341 
342         for (String packageName : mPrefs.keySet()) {
343             mPrefs.get(packageName).setChecked(mEnabledPackageNames.contains(packageName));
344         }
345     }
346 
347     @Override
displayPreference(PreferenceScreen screen)348     public void displayPreference(PreferenceScreen screen) {
349         super.displayPreference(screen);
350 
351         // Since the UI is being cleared, clear any refs.
352         mPrefs.clear();
353 
354         mPreferenceScreen = screen;
355         PreferenceGroup group = screen.findPreference(getPreferenceKey());
356         group.removeAll();
357 
358         Context context = screen.getContext();
359         mPrefs.putAll(buildPreferenceList(context, group));
360     }
361 
362     /**
363      * Gets the preference that allows to add a new cred man service.
364      *
365      * @return the pref to be added
366      */
367     @VisibleForTesting
newAddServicePreference(String searchUri, Context context)368     public Preference newAddServicePreference(String searchUri, Context context) {
369         final Intent addNewServiceIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(searchUri));
370         final Preference preference = new Preference(context);
371         preference.setOnPreferenceClickListener(
372                 p -> {
373                     context.startActivityAsUser(addNewServiceIntent, UserHandle.of(getUser()));
374                     return true;
375                 });
376         preference.setTitle(R.string.print_menu_item_add_service);
377         preference.setOrder(Integer.MAX_VALUE - 1);
378         preference.setPersistent(false);
379 
380         // Try to set the icon this should fail in a test environment but work
381         // in the actual app.
382         try {
383             preference.setIcon(R.drawable.ic_add_24dp);
384         } catch (Resources.NotFoundException e) {
385             Log.e(TAG, "Failed to find icon for add services link", e);
386         }
387         return preference;
388     }
389 
390     /** Aggregates the list of services and builds a list of UI prefs to show. */
391     @VisibleForTesting
buildPreferenceList( Context context, PreferenceGroup group)392     public Map<String, SwitchPreference> buildPreferenceList(
393             Context context, PreferenceGroup group) {
394         // Get the selected autofill provider. If it is the placeholder then replace it with an
395         // empty string.
396         String selectedAutofillProvider =
397                 DefaultCombinedPicker.getSelectedAutofillProvider(mContext, getUser());
398         if (TextUtils.equals(
399                 selectedAutofillProvider, AUTOFILL_CREDMAN_ONLY_PROVIDER_PLACEHOLDER)) {
400             selectedAutofillProvider = "";
401         }
402 
403         // Get the list of combined providers.
404         List<CombinedProviderInfo> providers =
405                 CombinedProviderInfo.buildMergedList(
406                         AutofillServiceInfo.getAvailableServices(context, getUser()),
407                         mServices,
408                         selectedAutofillProvider);
409 
410         // Get the provider that is displayed at the top. If there is none then hide
411         // everything.
412         CombinedProviderInfo topProvider = CombinedProviderInfo.getTopProvider(providers);
413         if (topProvider == null) {
414             setVisibility(false);
415             return new HashMap<>();
416         }
417 
418         Map<String, SwitchPreference> output = new HashMap<>();
419         for (CombinedProviderInfo combinedInfo : providers) {
420             final String packageName = combinedInfo.getApplicationInfo().packageName;
421 
422             // If this provider is displayed at the top then we should not show it.
423             if (topProvider != null
424                     && topProvider.getApplicationInfo().packageName.equals(packageName)) {
425                 continue;
426             }
427 
428             // If this is an autofill provider then don't show it here.
429             if (combinedInfo.getCredentialProviderInfos().isEmpty()) {
430                 continue;
431             }
432 
433             Drawable icon = combinedInfo.getAppIcon(context, getUser());
434             CharSequence title = combinedInfo.getAppName(context);
435 
436             // Build the pref and add it to the output & group.
437             SwitchPreference pref =
438                     addProviderPreference(
439                             context, title, icon, packageName, combinedInfo.getSettingsSubtitle());
440             output.put(packageName, pref);
441             group.addPreference(pref);
442         }
443 
444         // Set the visibility if we have services.
445         setVisibility(!output.isEmpty());
446 
447         return output;
448     }
449 
450     /** Creates a preference object based on the provider info. */
451     @VisibleForTesting
createPreference(Context context, CredentialProviderInfo service)452     public SwitchPreference createPreference(Context context, CredentialProviderInfo service) {
453         CharSequence label = service.getLabel(context);
454         return addProviderPreference(
455                 context,
456                 label == null ? "" : label,
457                 service.getServiceIcon(mContext),
458                 service.getServiceInfo().packageName,
459                 service.getSettingsSubtitle());
460     }
461 
462     /**
463      * Enables the package name as an enabled credential manager provider.
464      *
465      * @param packageName the package name to enable
466      */
467     @VisibleForTesting
togglePackageNameEnabled(String packageName)468     public boolean togglePackageNameEnabled(String packageName) {
469         if (mEnabledPackageNames.size() >= MAX_SELECTABLE_PROVIDERS) {
470             return false;
471         } else {
472             mEnabledPackageNames.add(packageName);
473             commitEnabledPackages();
474             return true;
475         }
476     }
477 
478     /**
479      * Disables the package name as a credential manager provider.
480      *
481      * @param packageName the package name to disable
482      */
483     @VisibleForTesting
togglePackageNameDisabled(String packageName)484     public void togglePackageNameDisabled(String packageName) {
485         mEnabledPackageNames.remove(packageName);
486         commitEnabledPackages();
487     }
488 
489     /** Returns the enabled credential manager provider package names. */
490     @VisibleForTesting
getEnabledProviders()491     public Set<String> getEnabledProviders() {
492         return mEnabledPackageNames;
493     }
494 
495     /**
496      * Returns the enabled credential manager provider flattened component names that can be stored
497      * in the setting.
498      */
499     @VisibleForTesting
getEnabledSettings()500     public List<String> getEnabledSettings() {
501         // Get all the component names that match the enabled package names.
502         List<String> enabledServices = new ArrayList<>();
503         for (CredentialProviderInfo service : mServices) {
504             ComponentName cn = service.getServiceInfo().getComponentName();
505             if (mEnabledPackageNames.contains(service.getServiceInfo().packageName)) {
506                 enabledServices.add(cn.flattenToString());
507             }
508         }
509 
510         return enabledServices;
511     }
512 
addProviderPreference( @onNull Context prefContext, @NonNull CharSequence title, @Nullable Drawable icon, @NonNull String packageName, @Nullable CharSequence subtitle)513     private SwitchPreference addProviderPreference(
514             @NonNull Context prefContext,
515             @NonNull CharSequence title,
516             @Nullable Drawable icon,
517             @NonNull String packageName,
518             @Nullable CharSequence subtitle) {
519         final SwitchPreference pref = new SwitchPreference(prefContext);
520         pref.setTitle(title);
521         pref.setChecked(mEnabledPackageNames.contains(packageName));
522 
523         if (icon != null) {
524             pref.setIcon(Utils.getSafeIcon(icon));
525         }
526 
527         if (subtitle != null) {
528             pref.setSummary(subtitle);
529         }
530 
531         pref.setOnPreferenceClickListener(
532                 p -> {
533                     boolean isChecked = pref.isChecked();
534 
535                     if (isChecked) {
536                         if (mEnabledPackageNames.size() >= MAX_SELECTABLE_PROVIDERS) {
537                             // Show the error if too many enabled.
538                             pref.setChecked(false);
539                             final DialogFragment fragment = newErrorDialogFragment();
540 
541                             if (fragment == null || mFragmentManager == null) {
542                                 return true;
543                             }
544 
545                             fragment.show(mFragmentManager, ErrorDialogFragment.TAG);
546                             return true;
547                         }
548 
549                         togglePackageNameEnabled(packageName);
550 
551                         // Enable all prefs.
552                         if (mPrefs.containsKey(packageName)) {
553                             mPrefs.get(packageName).setChecked(true);
554                         }
555                         return true;
556                     } else {
557                         togglePackageNameDisabled(packageName);
558                     }
559 
560                     return true;
561                 });
562 
563         return pref;
564     }
565 
commitEnabledPackages()566     private void commitEnabledPackages() {
567         // Commit using the CredMan API.
568         if (mCredentialManager == null) {
569             return;
570         }
571 
572         // Get the existing primary providers since we don't touch them in
573         // this part of the UI we should just copy them over.
574         Set<String> primaryServices = new HashSet<>();
575         List<String> enabledServices = getEnabledSettings();
576         for (CredentialProviderInfo service : mServices) {
577             if (service.isPrimary()) {
578                 String flattened = service.getServiceInfo().getComponentName().flattenToString();
579                 primaryServices.add(flattened);
580                 enabledServices.add(flattened);
581             }
582         }
583 
584         mCredentialManager.setEnabledProviders(
585                 new ArrayList<>(primaryServices),
586                 enabledServices,
587                 getUser(),
588                 mExecutor,
589                 new OutcomeReceiver<Void, SetEnabledProvidersException>() {
590                     @Override
591                     public void onResult(Void result) {
592                         Log.i(TAG, "setEnabledProviders success");
593                         updateFromExternal();
594                     }
595 
596                     @Override
597                     public void onError(SetEnabledProvidersException e) {
598                         Log.e(TAG, "setEnabledProviders error: " + e.toString());
599                     }
600                 });
601     }
602 
603     /** Create the new provider confirmation dialog. */
604     private @Nullable NewProviderConfirmationDialogFragment
newNewProviderConfirmationDialogFragment( @onNull String packageName, @NonNull CharSequence appName, boolean setActivityResult)605             newNewProviderConfirmationDialogFragment(
606                     @NonNull String packageName,
607                     @NonNull CharSequence appName,
608                     boolean setActivityResult) {
609         DialogHost host =
610                 new DialogHost() {
611                     @Override
612                     public void onDialogClick(int whichButton) {
613                         completeEnableProviderDialogBox(
614                                 whichButton, packageName, setActivityResult);
615                     }
616 
617                     @Override
618                     public void onCancel() {}
619                 };
620 
621         return new NewProviderConfirmationDialogFragment(host, packageName, appName);
622     }
623 
624     @VisibleForTesting
completeEnableProviderDialogBox( int whichButton, String packageName, boolean setActivityResult)625     void completeEnableProviderDialogBox(
626             int whichButton, String packageName, boolean setActivityResult) {
627         int activityResult = -1;
628         if (whichButton == DialogInterface.BUTTON_POSITIVE) {
629             if (togglePackageNameEnabled(packageName)) {
630                 // Enable all prefs.
631                 if (mPrefs.containsKey(packageName)) {
632                     mPrefs.get(packageName).setChecked(true);
633                 }
634                 activityResult = Activity.RESULT_OK;
635             } else {
636                 // There are too many providers so set the result as cancelled.
637                 activityResult = Activity.RESULT_CANCELED;
638 
639                 // Show the error if too many enabled.
640                 final DialogFragment fragment = newErrorDialogFragment();
641 
642                 if (fragment == null || mFragmentManager == null) {
643                     return;
644                 }
645 
646                 fragment.show(mFragmentManager, ErrorDialogFragment.TAG);
647             }
648         } else {
649             // The user clicked the cancel button so send that result back.
650             activityResult = Activity.RESULT_CANCELED;
651         }
652 
653         // If the dialog is being shown because of the intent we should
654         // return a result.
655         if (activityResult == -1 || !setActivityResult) {
656             setActivityResult(activityResult);
657         }
658     }
659 
newErrorDialogFragment()660     private @Nullable ErrorDialogFragment newErrorDialogFragment() {
661         DialogHost host =
662                 new DialogHost() {
663                     @Override
664                     public void onDialogClick(int whichButton) {}
665 
666                     @Override
667                     public void onCancel() {}
668                 };
669 
670         return new ErrorDialogFragment(host);
671     }
672 
getUser()673     protected int getUser() {
674         if (mIsWorkProfile) {
675             UserHandle workProfile = Utils.getManagedProfile(UserManager.get(mContext));
676             return workProfile.getIdentifier();
677         }
678         return UserHandle.myUserId();
679     }
680 
681     /** Called when the dialog button is clicked. */
682     private static interface DialogHost {
onDialogClick(int whichButton)683         void onDialogClick(int whichButton);
684 
onCancel()685         void onCancel();
686     }
687 
688     /** Called to send messages back to the parent fragment. */
689     public static interface Delegate {
setActivityResult(int resultCode)690         void setActivityResult(int resultCode);
691 
forceDelegateRefresh()692         void forceDelegateRefresh();
693     }
694 
695     /**
696      * Monitor coming and going credman services and calls {@link #DefaultCombinedPicker} when
697      * necessary
698      */
699     private final PackageMonitor mSettingsPackageMonitor =
700             new PackageMonitor() {
701                 @Override
702                 public void onPackageAdded(String packageName, int uid) {
703                     ThreadUtils.postOnMainThread(() -> updateFromExternal());
704                 }
705 
706                 @Override
707                 public void onPackageModified(String packageName) {
708                     ThreadUtils.postOnMainThread(() -> updateFromExternal());
709                 }
710 
711                 @Override
712                 public void onPackageRemoved(String packageName, int uid) {
713                     ThreadUtils.postOnMainThread(() -> updateFromExternal());
714                 }
715             };
716 
717     /** Dialog fragment parent class. */
718     private abstract static class CredentialManagerDialogFragment extends DialogFragment
719             implements DialogInterface.OnClickListener {
720 
721         public static final String TAG = "CredentialManagerDialogFragment";
722         public static final String PACKAGE_NAME_KEY = "package_name";
723         public static final String APP_NAME_KEY = "app_name";
724 
725         private DialogHost mDialogHost;
726 
CredentialManagerDialogFragment(DialogHost dialogHost)727         CredentialManagerDialogFragment(DialogHost dialogHost) {
728             super();
729             mDialogHost = dialogHost;
730         }
731 
getDialogHost()732         public DialogHost getDialogHost() {
733             return mDialogHost;
734         }
735 
736         @Override
onCancel(@onNull DialogInterface dialog)737         public void onCancel(@NonNull DialogInterface dialog) {
738             getDialogHost().onCancel();
739         }
740     }
741 
742     /** Dialog showing error when too many providers are selected. */
743     public static class ErrorDialogFragment extends CredentialManagerDialogFragment {
744 
ErrorDialogFragment(DialogHost dialogHost)745         ErrorDialogFragment(DialogHost dialogHost) {
746             super(dialogHost);
747         }
748 
749         @Override
onCreateDialog(Bundle savedInstanceState)750         public Dialog onCreateDialog(Bundle savedInstanceState) {
751             return new AlertDialog.Builder(getActivity())
752                     .setTitle(getContext().getString(R.string.credman_error_message_title))
753                     .setMessage(getContext().getString(R.string.credman_error_message))
754                     .setPositiveButton(android.R.string.ok, this)
755                     .create();
756         }
757 
758         @Override
onClick(DialogInterface dialog, int which)759         public void onClick(DialogInterface dialog, int which) {}
760     }
761 
762     /**
763      * Confirmation dialog fragment shows a dialog to the user to confirm that they would like to
764      * enable the new provider.
765      */
766     public static class NewProviderConfirmationDialogFragment
767             extends CredentialManagerDialogFragment {
768 
NewProviderConfirmationDialogFragment( DialogHost dialogHost, @NonNull String packageName, @NonNull CharSequence appName)769         NewProviderConfirmationDialogFragment(
770                 DialogHost dialogHost, @NonNull String packageName, @NonNull CharSequence appName) {
771             super(dialogHost);
772 
773             final Bundle argument = new Bundle();
774             argument.putString(PACKAGE_NAME_KEY, packageName);
775             argument.putCharSequence(APP_NAME_KEY, appName);
776             setArguments(argument);
777         }
778 
779         @Override
onCreateDialog(Bundle savedInstanceState)780         public Dialog onCreateDialog(Bundle savedInstanceState) {
781             final Bundle bundle = getArguments();
782             final Context context = getContext();
783             final CharSequence appName =
784                     bundle.getCharSequence(CredentialManagerDialogFragment.APP_NAME_KEY);
785             final String title =
786                     context.getString(R.string.credman_enable_confirmation_message_title, appName);
787             final String message =
788                     context.getString(R.string.credman_enable_confirmation_message, appName);
789 
790             return new AlertDialog.Builder(getActivity())
791                     .setTitle(title)
792                     .setMessage(message)
793                     .setPositiveButton(android.R.string.ok, this)
794                     .setNegativeButton(android.R.string.cancel, this)
795                     .create();
796         }
797 
798         @Override
onClick(DialogInterface dialog, int which)799         public void onClick(DialogInterface dialog, int which) {
800             getDialogHost().onDialogClick(which);
801         }
802     }
803 
804     /** Updates the list if setting content changes. */
805     private final class SettingContentObserver extends ContentObserver {
806 
807         private final Uri mAutofillService =
808                 Settings.Secure.getUriFor(Settings.Secure.AUTOFILL_SERVICE);
809 
810         private final Uri mCredentialService =
811                 Settings.Secure.getUriFor(Settings.Secure.CREDENTIAL_SERVICE);
812 
813         private final Uri mCredentialPrimaryService =
814                 Settings.Secure.getUriFor(Settings.Secure.CREDENTIAL_SERVICE_PRIMARY);
815 
816         private ContentResolver mContentResolver;
817 
SettingContentObserver(Handler handler, ContentResolver contentResolver)818         public SettingContentObserver(Handler handler, ContentResolver contentResolver) {
819             super(handler);
820             mContentResolver = contentResolver;
821         }
822 
register()823         public void register() {
824             mContentResolver.registerContentObserver(mAutofillService, false, this, getUser());
825             mContentResolver.registerContentObserver(mCredentialService, false, this, getUser());
826             mContentResolver.registerContentObserver(
827                     mCredentialPrimaryService, false, this, getUser());
828         }
829 
unregister()830         public void unregister() {
831             mContentResolver.unregisterContentObserver(this);
832         }
833 
834         @Override
onChange(boolean selfChange, Uri uri)835         public void onChange(boolean selfChange, Uri uri) {
836             updateFromExternal();
837         }
838     }
839 }
840