/* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.settings.applications.credentials; import android.annotation.Nullable; import android.app.Activity; import android.app.settings.SettingsEnums; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.ServiceInfo; import android.credentials.CredentialManager; import android.credentials.CredentialProviderInfo; import android.credentials.SetEnabledProvidersException; import android.net.Uri; import android.os.Bundle; import android.os.OutcomeReceiver; import android.os.UserHandle; import android.provider.Settings; import android.service.autofill.AutofillServiceInfo; import android.text.Html; import android.text.TextUtils; import android.util.Log; import androidx.core.content.ContextCompat; import androidx.preference.Preference; import com.android.internal.content.PackageMonitor; import com.android.settings.R; import com.android.settings.applications.defaultapps.DefaultAppPickerFragment; import com.android.settingslib.applications.DefaultAppInfo; import com.android.settingslib.utils.ThreadUtils; import com.android.settingslib.widget.CandidateInfo; import java.util.ArrayList; import java.util.List; public class DefaultCombinedPicker extends DefaultAppPickerFragment { private static final String TAG = "DefaultCombinedPicker"; public static final String AUTOFILL_SETTING = Settings.Secure.AUTOFILL_SERVICE; public static final String CREDENTIAL_SETTING = Settings.Secure.CREDENTIAL_SERVICE; /** Extra set when the fragment is implementing ACTION_REQUEST_SET_AUTOFILL_SERVICE. */ public static final String EXTRA_PACKAGE_NAME = "package_name"; /** Set when the fragment is implementing ACTION_REQUEST_SET_AUTOFILL_SERVICE. */ private DialogInterface.OnClickListener mCancelListener; private CredentialManager mCredentialManager; private int mIntentSenderUserId = -1; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); final Activity activity = getActivity(); if (activity != null && activity.getIntent().getStringExtra(EXTRA_PACKAGE_NAME) != null) { mCancelListener = (d, w) -> { activity.setResult(Activity.RESULT_CANCELED); activity.finish(); }; // If mCancelListener is not null, fragment is started from // ACTION_REQUEST_SET_AUTOFILL_SERVICE and we should always use the calling uid. mIntentSenderUserId = UserHandle.myUserId(); } getUser(); mSettingsPackageMonitor.register(activity, activity.getMainLooper(), false); update(); } @Override protected DefaultAppPickerFragment.ConfirmationDialogFragment newConfirmationDialogFragment( String selectedKey, CharSequence confirmationMessage) { final AutofillPickerConfirmationDialogFragment fragment = new AutofillPickerConfirmationDialogFragment(); fragment.init(this, selectedKey, confirmationMessage); return fragment; } /** * Custom dialog fragment that has a cancel listener used to propagate the result back to caller * (for the cases where the picker is launched by {@code * android.settings.REQUEST_SET_AUTOFILL_SERVICE}. */ public static class AutofillPickerConfirmationDialogFragment extends DefaultAppPickerFragment.ConfirmationDialogFragment { @Override public void onCreate(Bundle savedInstanceState) { final DefaultCombinedPicker target = (DefaultCombinedPicker) getTargetFragment(); setCancelListener(target.mCancelListener); super.onCreate(savedInstanceState); } } @Override protected int getPreferenceScreenResId() { return R.xml.default_credman_picker; } @Override public int getMetricsCategory() { return SettingsEnums.DEFAULT_AUTOFILL_PICKER; } @Override protected boolean shouldShowItemNone() { return true; } /** Monitor coming and going auto fill services and calls {@link #update()} when necessary */ private final PackageMonitor mSettingsPackageMonitor = new PackageMonitor() { @Override public void onPackageAdded(String packageName, int uid) { ThreadUtils.postOnMainThread(() -> update()); } @Override public void onPackageModified(String packageName) { ThreadUtils.postOnMainThread(() -> update()); } @Override public void onPackageRemoved(String packageName, int uid) { ThreadUtils.postOnMainThread(() -> update()); } }; /** Update the data in this UI. */ private void update() { updateCandidates(); addAddServicePreference(); } @Override public void onDestroy() { mSettingsPackageMonitor.unregister(); super.onDestroy(); } /** * Gets the preference that allows to add a new autofill service. * * @return The preference or {@code null} if no service can be added */ private Preference newAddServicePreferenceOrNull() { final String searchUri = Settings.Secure.getStringForUser( getActivity().getContentResolver(), Settings.Secure.AUTOFILL_SERVICE_SEARCH_URI, getUser()); if (TextUtils.isEmpty(searchUri)) { return null; } final Intent addNewServiceIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(searchUri)); final Context context = getPrefContext(); final Preference preference = new Preference(context); preference.setOnPreferenceClickListener( p -> { context.startActivityAsUser(addNewServiceIntent, UserHandle.of(getUser())); return true; }); preference.setTitle(R.string.print_menu_item_add_service); preference.setIcon(R.drawable.ic_add_24dp); preference.setOrder(Integer.MAX_VALUE - 1); preference.setPersistent(false); return preference; } /** * Add a preference that allows the user to add a service if the market link for that is * configured. */ private void addAddServicePreference() { final Preference addNewServicePreference = newAddServicePreferenceOrNull(); if (addNewServicePreference != null) { getPreferenceScreen().addPreference(addNewServicePreference); } } /** * Get the Credential Manager service if we haven't already got it. We need to get the service * later because if we do it in onCreate it will fail. */ private @Nullable CredentialManager getCredentialProviderService() { if (mCredentialManager == null) { mCredentialManager = getContext().getSystemService(CredentialManager.class); } return mCredentialManager; } private List getAllProviders() { final Context context = getContext(); final List autofillProviders = AutofillServiceInfo.getAvailableServices(context, getUser()); final CredentialManager service = getCredentialProviderService(); final List credManProviders = new ArrayList<>(); if (service != null) { credManProviders.addAll( service.getCredentialProviderServices( getUser(), CredentialManager.PROVIDER_FILTER_USER_PROVIDERS_ONLY)); } final String selectedAutofillProvider = getSelectedAutofillProvider(context, getUser()); return CombinedProviderInfo.buildMergedList( autofillProviders, credManProviders, selectedAutofillProvider); } public static String getSelectedAutofillProvider(Context context, int userId) { return Settings.Secure.getStringForUser( context.getContentResolver(), AUTOFILL_SETTING, userId); } protected List getCandidates() { final Context context = getContext(); final List allProviders = getAllProviders(); final List candidates = new ArrayList<>(); for (CombinedProviderInfo cpi : allProviders) { ServiceInfo brandingService = cpi.getBrandingService(); if (brandingService == null) { candidates.add( new DefaultAppInfo( context, mPm, getUser(), cpi.getApplicationInfo(), cpi.getSettingsSubtitle(), true)); } else { candidates.add( new DefaultAppInfo( context, mPm, getUser(), brandingService, cpi.getSettingsSubtitle(), true)); } } return candidates; } @Override protected String getDefaultKey() { final CombinedProviderInfo topProvider = CombinedProviderInfo.getTopProvider(getAllProviders()); return topProvider == null ? "" : topProvider.getApplicationInfo().packageName; } @Override protected CharSequence getConfirmationMessage(CandidateInfo appInfo) { // If we are selecting none then show a warning label. if (appInfo == null) { final String message = getContext() .getString( R.string.credman_confirmation_message); return Html.fromHtml(message); } final CharSequence appName = appInfo.loadLabel(); final String message = getContext() .getString( R.string.credman_autofill_confirmation_message, Html.escapeHtml(appName)); return Html.fromHtml(message); } @Override protected boolean setDefaultKey(String key) { // Get the list of providers and see if any match the key (package name). final List allProviders = getAllProviders(); CombinedProviderInfo matchedProvider = null; for (CombinedProviderInfo cpi : allProviders) { if (cpi.getApplicationInfo().packageName.equals(key)) { matchedProvider = cpi; break; } } // If there were none then clear the stored providers. if (matchedProvider == null) { setProviders(null, new ArrayList<>()); return true; } // Get the component names and save them. final List credManComponents = new ArrayList<>(); for (CredentialProviderInfo pi : matchedProvider.getCredentialProviderInfos()) { credManComponents.add(pi.getServiceInfo().getComponentName().flattenToString()); } String autofillValue = null; if (matchedProvider.getAutofillServiceInfo() != null) { autofillValue = matchedProvider .getAutofillServiceInfo() .getServiceInfo() .getComponentName() .flattenToString(); } setProviders(autofillValue, credManComponents); // Check if activity was launched from Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE // intent, and set proper result if so... final Activity activity = getActivity(); if (activity != null) { final String packageName = activity.getIntent().getStringExtra(EXTRA_PACKAGE_NAME); if (packageName != null) { final int result = key != null && key.startsWith(packageName) ? Activity.RESULT_OK : Activity.RESULT_CANCELED; activity.setResult(result); activity.finish(); } } // TODO: Notify the rest return true; } private void setProviders(String autofillProvider, List primaryCredManProviders) { if (TextUtils.isEmpty(autofillProvider)) { if (primaryCredManProviders.size() > 0) { autofillProvider = CredentialManagerPreferenceController .AUTOFILL_CREDMAN_ONLY_PROVIDER_PLACEHOLDER; } } Settings.Secure.putStringForUser( getContext().getContentResolver(), AUTOFILL_SETTING, autofillProvider, getUser()); final CredentialManager service = getCredentialProviderService(); if (service == null) { return; } // Get the existing secondary providers since we don't touch them in // this part of the UI we should just copy them over. final List credManProviders = new ArrayList<>(); for (CredentialProviderInfo cpi : service.getCredentialProviderServices( getUser(), CredentialManager.PROVIDER_FILTER_USER_PROVIDERS_ONLY)) { if (cpi.isEnabled() && !cpi.isPrimary()) { credManProviders.add(cpi.getServiceInfo().getComponentName().flattenToString()); } } credManProviders.addAll(primaryCredManProviders); // If there is no provider then clear all the providers. if (TextUtils.isEmpty(autofillProvider) && primaryCredManProviders.isEmpty()) { credManProviders.clear(); } service.setEnabledProviders( primaryCredManProviders, credManProviders, getUser(), ContextCompat.getMainExecutor(getContext()), new OutcomeReceiver() { @Override public void onResult(Void result) { Log.i(TAG, "setEnabledProviders success"); } @Override public void onError(SetEnabledProvidersException e) { Log.e(TAG, "setEnabledProviders error: " + e.toString()); } }); } protected int getUser() { if (mIntentSenderUserId >= 0) { return mIntentSenderUserId; } return UserHandle.myUserId(); } }