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