| /* |
| * 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.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| |
| 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; |
| |
| @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. |
| mUserId = UserHandle.myUserId(); |
| } |
| |
| 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, |
| mUserId); |
| 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(mUserId)); |
| 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<CombinedProviderInfo> getAllProviders() { |
| final Context context = getContext(); |
| final List<AutofillServiceInfo> autofillProviders = |
| AutofillServiceInfo.getAvailableServices(context, mUserId); |
| |
| final CredentialManager service = getCredentialProviderService(); |
| final List<CredentialProviderInfo> credManProviders = new ArrayList<>(); |
| if (service != null) { |
| credManProviders.addAll( |
| service.getCredentialProviderServices( |
| mUserId, CredentialManager.PROVIDER_FILTER_USER_PROVIDERS_ONLY)); |
| } |
| |
| final String selectedAutofillProvider = getSelectedAutofillProvider(context, mUserId); |
| 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<DefaultAppInfo> getCandidates() { |
| final Context context = getContext(); |
| final List<CombinedProviderInfo> allProviders = getAllProviders(); |
| final List<DefaultAppInfo> candidates = new ArrayList<>(); |
| |
| for (CombinedProviderInfo cpi : allProviders) { |
| ServiceInfo brandingService = cpi.getBrandingService(); |
| if (brandingService == null) { |
| candidates.add( |
| new DefaultAppInfo( |
| context, |
| mPm, |
| mUserId, |
| cpi.getApplicationInfo(), |
| cpi.getSettingsSubtitle(), |
| true)); |
| } else { |
| candidates.add( |
| new DefaultAppInfo( |
| context, |
| mPm, |
| mUserId, |
| 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 (appInfo == null) { |
| return null; |
| } |
| 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<CombinedProviderInfo> 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<String> 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<String> primaryCredManProviders) { |
| if (TextUtils.isEmpty(autofillProvider)) { |
| if (primaryCredManProviders.size() > 0) { |
| autofillProvider = |
| CredentialManagerPreferenceController |
| .AUTOFILL_CREDMAN_ONLY_PROVIDER_PLACEHOLDER; |
| } |
| } |
| |
| Settings.Secure.putStringForUser( |
| getContext().getContentResolver(), AUTOFILL_SETTING, autofillProvider, mUserId); |
| |
| 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<String> credManProviders = new ArrayList<>(); |
| for (CredentialProviderInfo cpi : |
| service.getCredentialProviderServices( |
| mUserId, CredentialManager.PROVIDER_FILTER_USER_PROVIDERS_ONLY)) { |
| |
| if (cpi.isEnabled()) { |
| credManProviders.add(cpi.getServiceInfo().getComponentName().flattenToString()); |
| } |
| } |
| |
| service.setEnabledProviders( |
| primaryCredManProviders, |
| credManProviders, |
| mUserId, |
| ContextCompat.getMainExecutor(getContext()), |
| new OutcomeReceiver<Void, SetEnabledProvidersException>() { |
| @Override |
| public void onResult(Void result) { |
| Log.i(TAG, "setEnabledProviders success"); |
| } |
| |
| @Override |
| public void onError(SetEnabledProvidersException e) { |
| Log.e(TAG, "setEnabledProviders error: " + e.toString()); |
| } |
| }); |
| } |
| } |