• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 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 com.android.settings.applications.credentials.CredentialManagerPreferenceController.getCredentialAutofillService;
20 
21 import android.app.Activity;
22 import android.app.settings.SettingsEnums;
23 import android.content.Context;
24 import android.content.DialogInterface;
25 import android.content.Intent;
26 import android.content.pm.ApplicationInfo;
27 import android.content.pm.PackageItemInfo;
28 import android.content.pm.PackageManager;
29 import android.content.pm.ServiceInfo;
30 import android.credentials.CredentialManager;
31 import android.credentials.CredentialProviderInfo;
32 import android.credentials.SetEnabledProvidersException;
33 import android.credentials.flags.Flags;
34 import android.net.Uri;
35 import android.os.Bundle;
36 import android.os.Handler;
37 import android.os.Looper;
38 import android.os.OutcomeReceiver;
39 import android.os.UserHandle;
40 import android.provider.Settings;
41 import android.service.autofill.AutofillServiceInfo;
42 import android.text.Html;
43 import android.text.TextUtils;
44 import android.util.Log;
45 
46 import androidx.annotation.NonNull;
47 import androidx.annotation.Nullable;
48 import androidx.core.content.ContextCompat;
49 import androidx.preference.Preference;
50 
51 import com.android.internal.content.PackageMonitor;
52 import com.android.settings.R;
53 import com.android.settings.applications.defaultapps.DefaultAppPickerFragment;
54 import com.android.settingslib.RestrictedSelectorWithWidgetPreference;
55 import com.android.settingslib.applications.DefaultAppInfo;
56 import com.android.settingslib.widget.CandidateInfo;
57 import com.android.settingslib.widget.SelectorWithWidgetPreference;
58 
59 import java.util.ArrayList;
60 import java.util.List;
61 
62 public class DefaultCombinedPicker extends DefaultAppPickerFragment {
63 
64     private boolean mIsWorkProfile;
65     private boolean mIsPrivateSpace;
66     private static final String TAG = "DefaultCombinedPicker";
67 
68     public static final String AUTOFILL_SETTING = Settings.Secure.AUTOFILL_SERVICE;
69     public static final String CREDENTIAL_SETTING = Settings.Secure.CREDENTIAL_SERVICE;
70 
71     /** Extra set when the fragment is implementing ACTION_REQUEST_SET_AUTOFILL_SERVICE. */
72     public static final String EXTRA_PACKAGE_NAME = "package_name";
73 
74     /** Set when the fragment is implementing ACTION_REQUEST_SET_AUTOFILL_SERVICE. */
75     private DialogInterface.OnClickListener mCancelListener;
76 
77     private CredentialManager mCredentialManager;
78     private int mIntentSenderUserId = -1;
79 
80     private static final Handler sMainHandler = new Handler(Looper.getMainLooper());
81 
82     @Override
onCreate(Bundle savedInstanceState)83     public void onCreate(Bundle savedInstanceState) {
84         super.onCreate(savedInstanceState);
85 
86         final Activity activity = getActivity();
87         mIsWorkProfile = activity.getIntent().getBooleanExtra(
88                 UserUtils.EXTRA_IS_WORK_PROFILE, false);
89         mIsPrivateSpace = activity.getIntent().getBooleanExtra(
90                 UserUtils.EXTRA_IS_PRIVATE_SPACE, false);
91         if (activity != null && activity.getIntent().getStringExtra(EXTRA_PACKAGE_NAME) != null) {
92             mCancelListener =
93                     (d, w) -> {
94                         activity.setResult(Activity.RESULT_CANCELED);
95                         activity.finish();
96                     };
97             // If mCancelListener is not null, fragment is started from
98             // ACTION_REQUEST_SET_AUTOFILL_SERVICE and we should always use the calling uid.
99             mIntentSenderUserId = UserHandle.myUserId();
100         }
101 
102         getUser();
103 
104         mSettingsPackageMonitor.register(activity, activity.getMainLooper(), false);
105         update();
106     }
107 
108     @Override
newConfirmationDialogFragment( String selectedKey, CharSequence confirmationMessage)109     protected DefaultAppPickerFragment.ConfirmationDialogFragment newConfirmationDialogFragment(
110             String selectedKey, CharSequence confirmationMessage) {
111         final AutofillPickerConfirmationDialogFragment fragment =
112                 new AutofillPickerConfirmationDialogFragment();
113         fragment.init(this, selectedKey, confirmationMessage);
114         return fragment;
115     }
116 
117     /**
118      * Custom dialog fragment that has a cancel listener used to propagate the result back to caller
119      * (for the cases where the picker is launched by {@code
120      * android.settings.REQUEST_SET_AUTOFILL_SERVICE}.
121      */
122     public static class AutofillPickerConfirmationDialogFragment
123             extends DefaultAppPickerFragment.ConfirmationDialogFragment {
124 
125         @Override
onCreate(Bundle savedInstanceState)126         public void onCreate(Bundle savedInstanceState) {
127             final DefaultCombinedPicker target = (DefaultCombinedPicker) getTargetFragment();
128             setCancelListener(target.mCancelListener);
129             super.onCreate(savedInstanceState);
130         }
131 
132         @Override
getPositiveButtonText()133         protected CharSequence getPositiveButtonText() {
134             final Bundle bundle = getArguments();
135             if (TextUtils.isEmpty(bundle.getString(EXTRA_KEY))) {
136                 return getContext()
137                         .getString(R.string.credman_confirmation_turn_off_positive_button);
138             }
139 
140             return getContext()
141                     .getString(R.string.credman_confirmation_change_provider_positive_button);
142         }
143     }
144 
145     @Override
getPreferenceScreenResId()146     protected int getPreferenceScreenResId() {
147         return R.xml.default_credman_picker;
148     }
149 
150     @Override
getMetricsCategory()151     public int getMetricsCategory() {
152         return SettingsEnums.DEFAULT_AUTOFILL_PICKER;
153     }
154 
155     @Override
shouldShowItemNone()156     protected boolean shouldShowItemNone() {
157         return true;
158     }
159 
160     /** Monitor coming and going auto fill services and calls {@link #update()} when necessary */
161     private final PackageMonitor mSettingsPackageMonitor =
162             new PackageMonitor() {
163                 @Override
164                 public void onPackageAdded(String packageName, int uid) {
165                     sMainHandler.post(
166                             () -> {
167                                 // See b/296164461 for context
168                                 if (getContext() == null) {
169                                     Log.w(TAG, "context is null");
170                                     return;
171                                 }
172 
173                                 update();
174                             });
175                 }
176 
177                 @Override
178                 public void onPackageModified(String packageName) {
179                     sMainHandler.post(
180                             () -> {
181                                 // See b/296164461 for context
182                                 if (getContext() == null) {
183                                     Log.w(TAG, "context is null");
184                                     return;
185                                 }
186 
187                                 update();
188                             });
189                 }
190 
191                 @Override
192                 public void onPackageRemoved(String packageName, int uid) {
193                     sMainHandler.post(
194                             () -> {
195                                 // See b/296164461 for context
196                                 if (getContext() == null) {
197                                     Log.w(TAG, "context is null");
198                                     return;
199                                 }
200 
201                                 update();
202                             });
203                 }
204             };
205 
206     /** Update the data in this UI. */
update()207     private void update() {
208         updateCandidates();
209         addAddServicePreference();
210     }
211 
212     @Override
onDestroy()213     public void onDestroy() {
214         mSettingsPackageMonitor.unregister();
215         super.onDestroy();
216     }
217 
218     /**
219      * Gets the preference that allows to add a new autofill service.
220      *
221      * @return The preference or {@code null} if no service can be added
222      */
newAddServicePreferenceOrNull()223     private Preference newAddServicePreferenceOrNull() {
224         final String searchUri =
225                 Settings.Secure.getStringForUser(
226                         getActivity().getContentResolver(),
227                         Settings.Secure.AUTOFILL_SERVICE_SEARCH_URI,
228                         getUser());
229         if (TextUtils.isEmpty(searchUri)) {
230             return null;
231         }
232 
233         final Intent addNewServiceIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(searchUri));
234         final Context context = getPrefContext();
235         final Preference preference = new Preference(context);
236         preference.setOnPreferenceClickListener(
237                 p -> {
238                     context.startActivityAsUser(addNewServiceIntent, UserHandle.of(getUser()));
239                     return true;
240                 });
241         preference.setTitle(R.string.print_menu_item_add_service);
242         preference.setIcon(R.drawable.ic_add_24dp);
243         preference.setOrder(Integer.MAX_VALUE - 1);
244         preference.setPersistent(false);
245         return preference;
246     }
247 
248     /**
249      * Add a preference that allows the user to add a service if the market link for that is
250      * configured.
251      */
addAddServicePreference()252     private void addAddServicePreference() {
253         final Preference addNewServicePreference = newAddServicePreferenceOrNull();
254         if (addNewServicePreference != null) {
255             getPreferenceScreen().addPreference(addNewServicePreference);
256         }
257     }
258 
259     /**
260      * Get the Credential Manager service if we haven't already got it. We need to get the service
261      * later because if we do it in onCreate it will fail.
262      */
getCredentialProviderService()263     private @Nullable CredentialManager getCredentialProviderService() {
264         if (mCredentialManager == null) {
265             mCredentialManager = getContext().getSystemService(CredentialManager.class);
266         }
267         return mCredentialManager;
268     }
269 
getAllProviders(int userId)270     private List<CombinedProviderInfo> getAllProviders(int userId) {
271         final Context context = getContext();
272         final List<AutofillServiceInfo> autofillProviders =
273                 AutofillServiceInfo.getAvailableServices(context, userId);
274 
275         final CredentialManager service = getCredentialProviderService();
276         final List<CredentialProviderInfo> credManProviders = new ArrayList<>();
277         if (service != null) {
278             credManProviders.addAll(
279                     service.getCredentialProviderServices(
280                             userId,
281                             CredentialManager.PROVIDER_FILTER_USER_PROVIDERS_INCLUDING_HIDDEN));
282         }
283 
284         final String selectedAutofillProvider =
285                 CredentialManagerPreferenceController
286                     .getSelectedAutofillProvider(context, userId, TAG);
287         return CombinedProviderInfo.buildMergedList(
288                 autofillProviders, credManProviders, selectedAutofillProvider);
289     }
290 
291 
getCandidates()292     protected List<DefaultAppInfo> getCandidates() {
293         final Context context = getContext();
294         final int userId = getUser();
295         final List<CombinedProviderInfo> allProviders = getAllProviders(userId);
296         final List<DefaultAppInfo> candidates = new ArrayList<>();
297 
298         for (CombinedProviderInfo cpi : allProviders) {
299             ServiceInfo brandingService = cpi.getBrandingService();
300             ApplicationInfo appInfo = cpi.getApplicationInfo();
301 
302             if (brandingService != null) {
303                 candidates.add(
304                         new CredentialManagerDefaultAppInfo(
305                                 context, mPm, userId, brandingService, cpi));
306             } else if (appInfo != null) {
307                 candidates.add(
308                         new CredentialManagerDefaultAppInfo(context, mPm, userId, appInfo, cpi));
309             }
310         }
311 
312         return candidates;
313     }
314 
315     @Override
bindPreferenceExtra( SelectorWithWidgetPreference pref, String key, CandidateInfo info, String defaultKey, String systemDefaultKey)316     public void bindPreferenceExtra(
317             SelectorWithWidgetPreference pref,
318             String key,
319             CandidateInfo info,
320             String defaultKey,
321             String systemDefaultKey) {
322         super.bindPreferenceExtra(pref, key, info, defaultKey, systemDefaultKey);
323 
324         if (!(info instanceof CredentialManagerDefaultAppInfo)) {
325             Log.e(TAG, "Candidate info should be a subclass of CredentialManagerDefaultAppInfo");
326             return;
327         }
328 
329         if (!(pref instanceof RestrictedSelectorWithWidgetPreference)) {
330             Log.e(TAG, "Preference should be a subclass of RestrictedSelectorWithWidgetPreference");
331             return;
332         }
333 
334         CredentialManagerDefaultAppInfo credmanAppInfo = (CredentialManagerDefaultAppInfo) info;
335         RestrictedSelectorWithWidgetPreference rp = (RestrictedSelectorWithWidgetPreference) pref;
336 
337         // Apply policy transparency.
338         rp.setDisabledByAdmin(
339                 credmanAppInfo
340                         .getCombinedProviderInfo()
341                         .getDeviceAdminRestrictions(getContext(), getUser()));
342     }
343 
344     @Override
createPreference()345     protected SelectorWithWidgetPreference createPreference() {
346         return new RestrictedSelectorWithWidgetPreference(getPrefContext());
347     }
348 
349     /** This extends DefaultAppInfo with custom CredMan app info. */
350     public static class CredentialManagerDefaultAppInfo extends DefaultAppInfo {
351 
352         private final CombinedProviderInfo mCombinedProviderInfo;
353 
CredentialManagerDefaultAppInfo( Context context, PackageManager pm, int uid, PackageItemInfo info, CombinedProviderInfo cpi)354         CredentialManagerDefaultAppInfo(
355                 Context context,
356                 PackageManager pm,
357                 int uid,
358                 PackageItemInfo info,
359                 CombinedProviderInfo cpi) {
360             super(context, pm, uid, info, cpi.getSettingsSubtitle(), /* enabled= */ true);
361             mCombinedProviderInfo = cpi;
362         }
363 
getCombinedProviderInfo()364         public @NonNull CombinedProviderInfo getCombinedProviderInfo() {
365             return mCombinedProviderInfo;
366         }
367     }
368 
369     @Override
getDefaultKey()370     protected String getDefaultKey() {
371         final int userId = getUser();
372         final @Nullable CombinedProviderInfo topProvider =
373                 CombinedProviderInfo.getTopProvider(getAllProviders(userId));
374 
375         if (topProvider != null) {
376             // Apply device admin restrictions to top provider.
377             if (topProvider.getDeviceAdminRestrictions(getContext(), userId) != null) {
378                 return "";
379             }
380 
381             ApplicationInfo appInfo = topProvider.getApplicationInfo();
382             if (appInfo != null) {
383                 return appInfo.packageName;
384             }
385         }
386 
387         return "";
388     }
389 
390     @Override
getConfirmationMessage(CandidateInfo appInfo)391     protected CharSequence getConfirmationMessage(CandidateInfo appInfo) {
392         // If we are selecting none then show a warning label.
393         if (appInfo == null) {
394             final String message =
395                     getContext()
396                             .getString(
397                                     Flags.newSettingsUi()
398                                             ? R.string.credman_confirmation_message_new_ui
399                                             : R.string.credman_confirmation_message);
400             return Html.fromHtml(message);
401         }
402         final CharSequence appName = appInfo.loadLabel();
403         final String message =
404                 getContext()
405                         .getString(
406                                 Flags.newSettingsUi()
407                                         ? R.string.credman_autofill_confirmation_message_new_ui
408                                         : R.string.credman_autofill_confirmation_message,
409                                 Html.escapeHtml(appName));
410         return Html.fromHtml(message);
411     }
412 
413     @Override
setDefaultKey(String key)414     protected boolean setDefaultKey(String key) {
415         // Get the list of providers and see if any match the key (package name).
416         final List<CombinedProviderInfo> allProviders = getAllProviders(getUser());
417         CombinedProviderInfo matchedProvider = null;
418         for (CombinedProviderInfo cpi : allProviders) {
419             if (cpi.getApplicationInfo().packageName.equals(key)) {
420                 matchedProvider = cpi;
421                 break;
422             }
423         }
424 
425         // If there were none then clear the stored providers.
426         if (matchedProvider == null) {
427             setProviders(null, new ArrayList<>());
428             return true;
429         }
430 
431         // Get the component names and save them.
432         final List<String> credManComponents = new ArrayList<>();
433         for (CredentialProviderInfo pi : matchedProvider.getCredentialProviderInfos()) {
434             credManComponents.add(pi.getServiceInfo().getComponentName().flattenToString());
435         }
436 
437         String autofillValue = null;
438         if (matchedProvider.getAutofillServiceInfo() != null) {
439             autofillValue =
440                     matchedProvider
441                             .getAutofillServiceInfo()
442                             .getServiceInfo()
443                             .getComponentName()
444                             .flattenToString();
445         }
446 
447         setProviders(autofillValue, credManComponents);
448 
449         // Check if activity was launched from Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE
450         // intent, and set proper result if so...
451         final Activity activity = getActivity();
452         if (activity != null) {
453             final String packageName = activity.getIntent().getStringExtra(EXTRA_PACKAGE_NAME);
454             if (packageName != null) {
455                 final int result =
456                         key != null && key.startsWith(packageName)
457                                 ? Activity.RESULT_OK
458                                 : Activity.RESULT_CANCELED;
459                 activity.setResult(result);
460                 activity.finish();
461             }
462         }
463 
464         // TODO: Notify the rest
465 
466         return true;
467     }
468 
setProviders(String autofillProvider, List<String> primaryCredManProviders)469     private void setProviders(String autofillProvider, List<String> primaryCredManProviders) {
470         if (TextUtils.isEmpty(autofillProvider)) {
471             if (primaryCredManProviders.size() > 0) {
472                 if (android.service.autofill.Flags.autofillCredmanDevIntegration()) {
473                     autofillProvider = getCredentialAutofillService(getContext(), TAG);
474                 } else {
475                     autofillProvider =
476                             CredentialManagerPreferenceController
477                                     .AUTOFILL_CREDMAN_ONLY_PROVIDER_PLACEHOLDER;
478                 }
479             }
480         }
481 
482         Settings.Secure.putStringForUser(
483                 getContext().getContentResolver(), AUTOFILL_SETTING, autofillProvider, getUser());
484 
485         final CredentialManager service = getCredentialProviderService();
486         if (service == null) {
487             return;
488         }
489 
490         // Get the existing secondary providers since we don't touch them in
491         // this part of the UI we should just copy them over.
492         final List<String> credManProviders = new ArrayList<>();
493         for (CredentialProviderInfo cpi :
494                 service.getCredentialProviderServices(
495                         getUser(),
496                         CredentialManager.PROVIDER_FILTER_USER_PROVIDERS_INCLUDING_HIDDEN)) {
497 
498             if (cpi.isEnabled() && !cpi.isPrimary()) {
499                 credManProviders.add(cpi.getServiceInfo().getComponentName().flattenToString());
500             }
501         }
502 
503         credManProviders.addAll(primaryCredManProviders);
504 
505         // If there is no provider then clear all the providers.
506         if (TextUtils.isEmpty(autofillProvider) && primaryCredManProviders.isEmpty()) {
507             credManProviders.clear();
508         }
509 
510         service.setEnabledProviders(
511                 primaryCredManProviders,
512                 credManProviders,
513                 getUser(),
514                 ContextCompat.getMainExecutor(getContext()),
515                 new OutcomeReceiver<Void, SetEnabledProvidersException>() {
516                     @Override
517                     public void onResult(Void result) {
518                         Log.i(TAG, "setEnabledProviders success");
519                     }
520 
521                     @Override
522                     public void onError(SetEnabledProvidersException e) {
523                         Log.e(TAG, "setEnabledProviders error: " + e.toString());
524                     }
525                 });
526     }
527 
getUser()528     protected int getUser() {
529        return  UserUtils.getUser(mIsWorkProfile, mIsPrivateSpace, getContext());
530     }
531 }
532