If the cred man provider has the isPrimary bit set then we should use it as top provider. Test: ondevice Bug: 280454916 Change-Id: I8c5651909d3926f09549c64af68185f1ef633198
390 lines
14 KiB
Java
390 lines
14 KiB
Java
/*
|
|
* 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());
|
|
}
|
|
});
|
|
}
|
|
}
|