diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 53525627a0e..37c9a7f4357 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -126,6 +126,7 @@ + Autofill service + + Default autofill service Passwords + + Password and identity services @@ -9818,6 +9822,8 @@ \u2014 auto, fill, autofill, password + + credentials, passkey, password @@ -9830,6 +9836,21 @@ ]]> + + Turn off %1$s\? + + + Saved info like addresses or payment methods won\'t be filled in when you sign in. To keep your saved info filled in, set a default autofill service. + + + Password and identity services limit + + + You can have up to 5 autofill and password services active at the same time. Turn off a service to add more. + + + Turn off + Autofill diff --git a/res/xml/accounts_dashboard_settings_credman.xml b/res/xml/accounts_dashboard_settings_credman.xml new file mode 100644 index 00000000000..605d315bcbc --- /dev/null +++ b/res/xml/accounts_dashboard_settings_credman.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/xml/accounts_personal_dashboard_settings_credman.xml b/res/xml/accounts_personal_dashboard_settings_credman.xml new file mode 100644 index 00000000000..a5188dd6885 --- /dev/null +++ b/res/xml/accounts_personal_dashboard_settings_credman.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/xml/accounts_work_dashboard_settings_credman.xml b/res/xml/accounts_work_dashboard_settings_credman.xml new file mode 100644 index 00000000000..f4e8af2f27a --- /dev/null +++ b/res/xml/accounts_work_dashboard_settings_credman.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/com/android/settings/accounts/AccountDashboardFragment.java b/src/com/android/settings/accounts/AccountDashboardFragment.java index 3e83d6f0ebd..e252688fd18 100644 --- a/src/com/android/settings/accounts/AccountDashboardFragment.java +++ b/src/com/android/settings/accounts/AccountDashboardFragment.java @@ -22,11 +22,14 @@ import android.accounts.AccountManager; import android.app.settings.SettingsEnums; import android.content.Context; import android.content.pm.UserInfo; +import android.credentials.CredentialManager; import android.os.UserHandle; import android.os.UserManager; +import android.provider.SearchIndexableResource; import com.android.settings.R; import com.android.settings.applications.autofill.PasswordsPreferenceController; +import com.android.settings.applications.credentials.CredentialManagerPreferenceController; import com.android.settings.applications.defaultapps.DefaultAutofillPreferenceController; import com.android.settings.applications.defaultapps.DefaultWorkAutofillPreferenceController; import com.android.settings.dashboard.DashboardFragment; @@ -44,10 +47,8 @@ import java.util.List; @SearchIndexable public class AccountDashboardFragment extends DashboardFragment { - private static final String TAG = "AccountDashboardFrag"; - @Override public int getMetricsCategory() { return SettingsEnums.ACCOUNT; @@ -60,7 +61,7 @@ public class AccountDashboardFragment extends DashboardFragment { @Override protected int getPreferenceScreenResId() { - return R.xml.accounts_dashboard_settings; + return getPreferenceLayoutResId(this.getContext()); } @Override @@ -71,6 +72,12 @@ public class AccountDashboardFragment extends DashboardFragment { @Override public void onAttach(Context context) { super.onAttach(context); + if (CredentialManager.isServiceEnabled(context)) { + CredentialManagerPreferenceController cmpp = + use(CredentialManagerPreferenceController.class); + cmpp.setParentFragment(this); + } + getSettingsLifecycle().addObserver(use(PasswordsPreferenceController.class)); } @@ -95,11 +102,13 @@ public class AccountDashboardFragment extends DashboardFragment { } private static void buildAccountPreferenceControllers( - Context context, DashboardFragment parent, String[] authorities, + Context context, + DashboardFragment parent, + String[] authorities, List controllers) { final AccountPreferenceController accountPrefController = - new AccountPreferenceController(context, parent, authorities, - ProfileSelectFragment.ProfileType.ALL); + new AccountPreferenceController( + context, parent, authorities, ProfileSelectFragment.ProfileType.ALL); if (parent != null) { parent.getSettingsLifecycle().addObserver(accountPrefController); } @@ -109,8 +118,21 @@ public class AccountDashboardFragment extends DashboardFragment { controllers.add(new AutoSyncWorkDataPreferenceController(context, parent)); } + private static int getPreferenceLayoutResId(Context context) { + return (context != null && CredentialManager.isServiceEnabled(context)) + ? R.xml.accounts_dashboard_settings_credman + : R.xml.accounts_dashboard_settings; + } + public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = - new BaseSearchIndexProvider(R.xml.accounts_dashboard_settings) { + new BaseSearchIndexProvider() { + @Override + public List getXmlResourcesToIndex(Context context, + boolean enabled) { + final SearchIndexableResource sir = new SearchIndexableResource(context); + sir.xmlResId = getPreferenceLayoutResId(context); + return List.of(sir); + } @Override public List createPreferenceControllers( @@ -124,11 +146,11 @@ public class AccountDashboardFragment extends DashboardFragment { @SuppressWarnings("MissingSuperCall") // TODO: Fix me @Override - public List getDynamicRawDataToIndex(Context context, - boolean enabled) { + public List getDynamicRawDataToIndex( + Context context, boolean enabled) { final List indexRaws = new ArrayList<>(); - final UserManager userManager = (UserManager) context.getSystemService( - Context.USER_SERVICE); + final UserManager userManager = + (UserManager) context.getSystemService(Context.USER_SERVICE); final List profiles = userManager.getProfiles(UserHandle.myUserId()); for (final UserInfo userInfo : profiles) { if (userInfo.isManagedProfile()) { diff --git a/src/com/android/settings/accounts/AccountPersonalDashboardFragment.java b/src/com/android/settings/accounts/AccountPersonalDashboardFragment.java index 4661c6416e7..5f9a288034d 100644 --- a/src/com/android/settings/accounts/AccountPersonalDashboardFragment.java +++ b/src/com/android/settings/accounts/AccountPersonalDashboardFragment.java @@ -22,9 +22,11 @@ import static com.android.settings.accounts.AccountDashboardFragment.buildAutofi import android.app.settings.SettingsEnums; import android.content.Context; +import android.credentials.CredentialManager; import com.android.settings.R; import com.android.settings.applications.autofill.PasswordsPreferenceController; +import com.android.settings.applications.credentials.CredentialManagerPreferenceController; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.dashboard.profileselector.ProfileSelectFragment; import com.android.settings.users.AutoSyncDataPreferenceController; @@ -34,11 +36,8 @@ import com.android.settingslib.core.AbstractPreferenceController; import java.util.ArrayList; import java.util.List; -/** - * Account Setting page for personal profile. - */ +/** Account Setting page for personal profile. */ public class AccountPersonalDashboardFragment extends DashboardFragment { - private static final String TAG = "AccountPersonalFrag"; @Override @@ -53,6 +52,9 @@ public class AccountPersonalDashboardFragment extends DashboardFragment { @Override protected int getPreferenceScreenResId() { + if (this.getContext() != null && CredentialManager.isServiceEnabled(this.getContext())) { + return R.xml.accounts_personal_dashboard_settings_credman; + } return R.xml.accounts_personal_dashboard_settings; } @@ -64,6 +66,12 @@ public class AccountPersonalDashboardFragment extends DashboardFragment { @Override public void onAttach(Context context) { super.onAttach(context); + if (CredentialManager.isServiceEnabled(context)) { + CredentialManagerPreferenceController cmpp = + use(CredentialManagerPreferenceController.class); + cmpp.setParentFragment(this); + } + getSettingsLifecycle().addObserver(use(PasswordsPreferenceController.class)); } @@ -77,11 +85,13 @@ public class AccountPersonalDashboardFragment extends DashboardFragment { } private static void buildAccountPreferenceControllers( - Context context, DashboardFragment parent, String[] authorities, + Context context, + DashboardFragment parent, + String[] authorities, List controllers) { final AccountPreferenceController accountPrefController = - new AccountPreferenceController(context, parent, authorities, - ProfileSelectFragment.ProfileType.PERSONAL); + new AccountPreferenceController( + context, parent, authorities, ProfileSelectFragment.ProfileType.PERSONAL); if (parent != null) { parent.getSettingsLifecycle().addObserver(accountPrefController); } @@ -91,15 +101,15 @@ public class AccountPersonalDashboardFragment extends DashboardFragment { } // TODO: b/141601408. After featureFlag settings_work_profile is launched, unmark this -// public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = -// new BaseSearchIndexProvider(R.xml.accounts_personal_dashboard_settings) { -// -// @Override -// public List createPreferenceControllers( -// Context context) { -// ..Add autofill here too.. -// return buildPreferenceControllers( -// context, null /* parent */, null /* authorities*/); -// } -// }; + // public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + // new BaseSearchIndexProvider(R.xml.accounts_personal_dashboard_settings) { + // + // @Override + // public List createPreferenceControllers( + // Context context) { + // ..Add autofill here too.. + // return buildPreferenceControllers( + // context, null /* parent */, null /* authorities*/); + // } + // }; } diff --git a/src/com/android/settings/accounts/AccountWorkProfileDashboardFragment.java b/src/com/android/settings/accounts/AccountWorkProfileDashboardFragment.java index f64e0416163..4835b173355 100644 --- a/src/com/android/settings/accounts/AccountWorkProfileDashboardFragment.java +++ b/src/com/android/settings/accounts/AccountWorkProfileDashboardFragment.java @@ -22,9 +22,11 @@ import static com.android.settings.accounts.AccountDashboardFragment.buildAutofi import android.app.settings.SettingsEnums; import android.content.Context; +import android.credentials.CredentialManager; import com.android.settings.R; import com.android.settings.applications.autofill.PasswordsPreferenceController; +import com.android.settings.applications.credentials.CredentialManagerPreferenceController; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.dashboard.profileselector.ProfileSelectFragment; import com.android.settings.users.AutoSyncDataPreferenceController; @@ -34,11 +36,8 @@ import com.android.settingslib.core.AbstractPreferenceController; import java.util.ArrayList; import java.util.List; -/** - * Account Setting page for work profile. - */ +/** Account Setting page for work profile. */ public class AccountWorkProfileDashboardFragment extends DashboardFragment { - private static final String TAG = "AccountWorkProfileFrag"; @Override @@ -53,6 +52,9 @@ public class AccountWorkProfileDashboardFragment extends DashboardFragment { @Override protected int getPreferenceScreenResId() { + if (this.getContext() != null && CredentialManager.isServiceEnabled(this.getContext())) { + return R.xml.accounts_work_dashboard_settings_credman; + } return R.xml.accounts_work_dashboard_settings; } @@ -64,6 +66,12 @@ public class AccountWorkProfileDashboardFragment extends DashboardFragment { @Override public void onAttach(Context context) { super.onAttach(context); + if (CredentialManager.isServiceEnabled(context)) { + CredentialManagerPreferenceController cmpp = + use(CredentialManagerPreferenceController.class); + cmpp.setParentFragment(this); + } + getSettingsLifecycle().addObserver(use(PasswordsPreferenceController.class)); } @@ -77,11 +85,13 @@ public class AccountWorkProfileDashboardFragment extends DashboardFragment { } private static void buildAccountPreferenceControllers( - Context context, DashboardFragment parent, String[] authorities, + Context context, + DashboardFragment parent, + String[] authorities, List controllers) { final AccountPreferenceController accountPrefController = - new AccountPreferenceController(context, parent, authorities, - ProfileSelectFragment.ProfileType.WORK); + new AccountPreferenceController( + context, parent, authorities, ProfileSelectFragment.ProfileType.WORK); if (parent != null) { parent.getSettingsLifecycle().addObserver(accountPrefController); } @@ -91,15 +101,15 @@ public class AccountWorkProfileDashboardFragment extends DashboardFragment { } // TODO: b/141601408. After featureFlag settings_work_profile is launched, unmark this -// public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = -// new BaseSearchIndexProvider(R.xml.accounts_work_dashboard_settings) { -// -// @Override -// public List createPreferenceControllers( -// Context context) { -// ..Add autofill here too.. -// return buildPreferenceControllers( -// context, null /* parent */, null /* authorities*/); -// } -// }; + // public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + // new BaseSearchIndexProvider(R.xml.accounts_work_dashboard_settings) { + // + // @Override + // public List createPreferenceControllers( + // Context context) { + // ..Add autofill here too.. + // return buildPreferenceControllers( + // context, null /* parent */, null /* authorities*/); + // } + // }; } diff --git a/src/com/android/settings/applications/credentials/CredentialManagerPreferenceController.java b/src/com/android/settings/applications/credentials/CredentialManagerPreferenceController.java new file mode 100644 index 00000000000..8150604c280 --- /dev/null +++ b/src/com/android/settings/applications/credentials/CredentialManagerPreferenceController.java @@ -0,0 +1,476 @@ +/* + * Copyright (C) 2022 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 static androidx.lifecycle.Lifecycle.Event.ON_CREATE; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.Dialog; +import android.app.settings.SettingsEnums; +import android.content.ComponentName; +import android.content.Context; +import android.content.DialogInterface; +import android.content.pm.PackageManager; +import android.content.pm.ServiceInfo; +import android.credentials.CredentialManager; +import android.credentials.ListEnabledProvidersException; +import android.credentials.ListEnabledProvidersResponse; +import android.credentials.SetEnabledProvidersException; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.CancellationSignal; +import android.os.OutcomeReceiver; +import android.os.UserHandle; +import android.service.credentials.CredentialProviderInfo; +import android.util.IconDrawableFactory; +import android.util.Log; + +import androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.DialogFragment; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.OnLifecycleEvent; +import androidx.preference.PreferenceGroup; +import androidx.preference.PreferenceScreen; +import androidx.preference.SwitchPreference; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.settings.R; +import com.android.settings.Utils; +import com.android.settings.core.BasePreferenceController; +import com.android.settings.core.instrumentation.InstrumentedDialogFragment; +import com.android.settings.dashboard.DashboardFragment; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Executor; + +/** Queries available credential manager providers and adds preferences for them. */ +public class CredentialManagerPreferenceController extends BasePreferenceController + implements LifecycleObserver { + private static final String TAG = "CredentialManagerPreferenceController"; + private static final int MAX_SELECTABLE_PROVIDERS = 5; + + private final PackageManager mPm; + private final IconDrawableFactory mIconFactory; + private final List mServices; + private final Set mEnabledPackageNames; + private final @Nullable CredentialManager mCredentialManager; + private final CancellationSignal mCancellationSignal = new CancellationSignal(); + private final Executor mExecutor; + + private @Nullable DashboardFragment mParentFragment = null; + + public CredentialManagerPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + mPm = context.getPackageManager(); + mIconFactory = IconDrawableFactory.newInstance(mContext); + mServices = new ArrayList<>(); + mEnabledPackageNames = new HashSet<>(); + mExecutor = ContextCompat.getMainExecutor(mContext); + mCredentialManager = + getCredentialManager(context, preferenceKey.equals("credentials_test")); + } + + private @Nullable CredentialManager getCredentialManager(Context context, boolean isTest) { + if (isTest) { + return null; + } + + Object service = context.getSystemService(Context.CREDENTIAL_SERVICE); + + if (service != null && CredentialManager.isServiceEnabled(context)) { + return (CredentialManager) service; + } + + return null; + } + + @VisibleForTesting + public boolean isConnected() { + return mCredentialManager != null; + } + + /** + * Sets the parent fragment and attaches this controller to the settings lifecycle. + * + * @param fragment the fragment to use as the parent + */ + public void setParentFragment(DashboardFragment fragment) { + mParentFragment = fragment; + fragment.getSettingsLifecycle().addObserver(this); + } + + @OnLifecycleEvent(ON_CREATE) + void onCreate(LifecycleOwner lifecycleOwner) { + if (mCredentialManager == null) { + return; + } + + mCredentialManager.listEnabledProviders( + mCancellationSignal, + mExecutor, + new OutcomeReceiver() { + @Override + public void onResult(ListEnabledProvidersResponse result) { + Set enabledPackages = new HashSet<>(); + for (String flattenedComponentName : result.getProviderComponentNames()) { + ComponentName cn = + ComponentName.unflattenFromString(flattenedComponentName); + if (cn != null) { + enabledPackages.add(cn.getPackageName()); + } + } + + List services = new ArrayList<>(); + for (CredentialProviderInfo cpi : + CredentialProviderInfo.getAvailableServices(mContext, getUser())) { + services.add(cpi.getServiceInfo()); + } + + init(lifecycleOwner, services, enabledPackages); + } + + @Override + public void onError(ListEnabledProvidersException e) { + Log.e(TAG, "listEnabledProviders error: " + e.toString()); + } + }); + } + + @VisibleForTesting + void init( + LifecycleOwner lifecycleOwner, + List availableServices, + Set enabledPackages) { + mServices.clear(); + mServices.addAll(availableServices); + + mEnabledPackageNames.clear(); + mEnabledPackageNames.addAll(enabledPackages); + } + + @Override + public int getAvailabilityStatus() { + return mServices.isEmpty() ? CONDITIONALLY_UNAVAILABLE : AVAILABLE; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + + PreferenceGroup group = screen.findPreference(getPreferenceKey()); + Context context = screen.getContext(); + + for (ServiceInfo serviceInfo : mServices) { + CharSequence title = ""; + if (serviceInfo.nonLocalizedLabel != null) { + title = serviceInfo.loadLabel(mPm); + } + + group.addPreference( + addProviderPreference( + context, + title, + mIconFactory.getBadgedIcon( + serviceInfo, serviceInfo.applicationInfo, getUser()), + serviceInfo.packageName)); + } + } + + /** + * Enables the package name as an enabled credential manager provider. + * + * @param packageName the package name to enable + */ + @VisibleForTesting + public boolean togglePackageNameEnabled(String packageName) { + if (mEnabledPackageNames.size() >= MAX_SELECTABLE_PROVIDERS) { + return false; + } else { + mEnabledPackageNames.add(packageName); + commitEnabledPackages(); + return true; + } + } + + /** + * Disables the package name as a credential manager provider. + * + * @param packageName the package name to disable + */ + @VisibleForTesting + public void togglePackageNameDisabled(String packageName) { + mEnabledPackageNames.remove(packageName); + commitEnabledPackages(); + } + + /** Returns the enabled credential manager provider package names. */ + @VisibleForTesting + public Set getEnabledProviders() { + return mEnabledPackageNames; + } + + /** + * Returns the enabled credential manager provider flattened component names that can be stored + * in the setting. + */ + @VisibleForTesting + public List getEnabledSettings() { + // Get all the component names that match the enabled package names. + List enabledServices = new ArrayList<>(); + for (ServiceInfo service : mServices) { + if (mEnabledPackageNames.contains(service.packageName)) { + enabledServices.add(service.getComponentName().flattenToString()); + } + } + + return enabledServices; + } + + private SwitchPreference addProviderPreference( + @NonNull Context prefContext, + @NonNull CharSequence title, + @Nullable Drawable icon, + @NonNull String packageName) { + final SwitchPreference pref = new SwitchPreference(prefContext); + pref.setTitle(title); + pref.setChecked(mEnabledPackageNames.contains(packageName)); + + if (icon != null) { + pref.setIcon(Utils.getSafeIcon(icon)); + } + + pref.setOnPreferenceClickListener( + p -> { + boolean isChecked = pref.isChecked(); + + if (isChecked) { + // Show the error if too many enabled. + if (!togglePackageNameEnabled(packageName)) { + final DialogFragment fragment = newErrorDialogFragment(); + + if (fragment == null || mParentFragment == null) { + return true; + } + + fragment.show( + mParentFragment.getActivity().getSupportFragmentManager(), + ErrorDialogFragment.TAG); + + // The user set the check to true so we need to set it back. + pref.setChecked(false); + } + + return true; + } else { + // Show the confirm disable dialog. + final DialogFragment fragment = + newConfirmationDialogFragment(packageName, title, pref); + + if (fragment == null || mParentFragment == null) { + return true; + } + + fragment.show( + mParentFragment.getActivity().getSupportFragmentManager(), + ConfirmationDialogFragment.TAG); + } + + return true; + }); + + return pref; + } + + private void commitEnabledPackages() { + // Commit using the CredMan API. + if (mCredentialManager == null) { + return; + } + + List enabledServices = getEnabledSettings(); + mCredentialManager.setEnabledProviders( + enabledServices, + getUser(), + mExecutor, + 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()); + } + }); + } + + private @Nullable ConfirmationDialogFragment newConfirmationDialogFragment( + @NonNull String packageName, + @NonNull CharSequence appName, + @NonNull SwitchPreference pref) { + DialogHost host = + new DialogHost() { + @Override + public DashboardFragment getParentFragment() { + return mParentFragment; + } + + @Override + public void onDialogClick(int whichButton) { + if (whichButton == DialogInterface.BUTTON_POSITIVE) { + // Since the package is now enabled then we + // should remove it from the enabled list. + togglePackageNameDisabled(packageName); + } else if (whichButton == DialogInterface.BUTTON_NEGATIVE) { + // Set the checked back to true because we + // backed out of turning this off. + pref.setChecked(true); + } + } + }; + + if (host.getParentFragment() == null) { + return null; + } + + return new ConfirmationDialogFragment(host, packageName, appName); + } + + private @Nullable ErrorDialogFragment newErrorDialogFragment() { + DialogHost host = + new DialogHost() { + @Override + public DashboardFragment getParentFragment() { + return mParentFragment; + } + + @Override + public void onDialogClick(int whichButton) {} + }; + + if (host.getParentFragment() == null) { + return null; + } + + return new ErrorDialogFragment(host); + } + + private int getUser() { + UserHandle workUser = getWorkProfileUser(); + return workUser != null ? workUser.getIdentifier() : UserHandle.myUserId(); + } + + /** Called when the dialog button is clicked. */ + private interface DialogHost { + void onDialogClick(int whichButton); + + DashboardFragment getParentFragment(); + } + + /** Dialog fragment parent class. */ + private abstract static class CredentialManagerDialogFragment extends InstrumentedDialogFragment + implements DialogInterface.OnClickListener { + + public static final String TAG = "CredentialManagerDialogFragment"; + public static final String PACKAGE_NAME_KEY = "package_name"; + public static final String APP_NAME_KEY = "app_name"; + + private DialogHost mDialogHost; + + CredentialManagerDialogFragment(DialogHost dialogHost) { + super(); + setTargetFragment(dialogHost.getParentFragment(), 0); + mDialogHost = dialogHost; + } + + public DialogHost getDialogHost() { + return mDialogHost; + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.ACCOUNT; + } + } + + /** Dialog showing error when too many providers are selected. */ + private static class ErrorDialogFragment extends CredentialManagerDialogFragment { + + ErrorDialogFragment(DialogHost dialogHost) { + super(dialogHost); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + return new AlertDialog.Builder(getActivity()) + .setTitle(getContext().getString(R.string.credman_error_message_title)) + .setMessage(getContext().getString(R.string.credman_error_message)) + .setPositiveButton(android.R.string.ok, this) + .create(); + } + + @Override + public void onClick(DialogInterface dialog, int which) {} + } + + /** + * Confirmation dialog fragment shows a dialog to the user to confirm that they are disabling a + * provider. + */ + private static class ConfirmationDialogFragment extends CredentialManagerDialogFragment { + + ConfirmationDialogFragment( + DialogHost dialogHost, @NonNull String packageName, @NonNull CharSequence appName) { + super(dialogHost); + + final Bundle argument = new Bundle(); + argument.putString(PACKAGE_NAME_KEY, packageName); + argument.putCharSequence(APP_NAME_KEY, appName); + setArguments(argument); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Bundle bundle = getArguments(); + final String title = + getContext() + .getString( + R.string.credman_confirmation_message_title, + bundle.getCharSequence( + CredentialManagerDialogFragment.APP_NAME_KEY)); + + return new AlertDialog.Builder(getActivity()) + .setTitle(title) + .setMessage(getContext().getString(R.string.credman_confirmation_message)) + .setPositiveButton(R.string.credman_confirmation_message_positive_button, this) + .setNegativeButton(android.R.string.cancel, this) + .create(); + } + + @Override + public void onClick(DialogInterface dialog, int which) { + getDialogHost().onDialogClick(which); + } + } +} diff --git a/tests/unit/src/com/android/settings/applications/credentials/CredentialManagerPreferenceControllerTest.java b/tests/unit/src/com/android/settings/applications/credentials/CredentialManagerPreferenceControllerTest.java new file mode 100644 index 00000000000..584832686ab --- /dev/null +++ b/tests/unit/src/com/android/settings/applications/credentials/CredentialManagerPreferenceControllerTest.java @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2022 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 static com.android.settings.core.BasePreferenceController.AVAILABLE; +import static com.android.settings.core.BasePreferenceController.CONDITIONALLY_UNAVAILABLE; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.ServiceInfo; +import android.os.Looper; + +import androidx.lifecycle.Lifecycle; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceScreen; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.google.android.collect.Lists; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@RunWith(AndroidJUnit4.class) +public class CredentialManagerPreferenceControllerTest { + + private Context mContext; + private PreferenceScreen mScreen; + private PreferenceCategory mCredentialsPreferenceCategory; + + @Before + public void setUp() { + mContext = spy(ApplicationProvider.getApplicationContext()); + if (Looper.myLooper() == null) { + Looper.prepare(); // needed to create the preference screen + } + mScreen = new PreferenceManager(mContext).createPreferenceScreen(mContext); + mCredentialsPreferenceCategory = new PreferenceCategory(mContext); + mCredentialsPreferenceCategory.setKey("credentials_test"); + mScreen.addPreference(mCredentialsPreferenceCategory); + } + + @Test + // Tests that getAvailabilityStatus() does not throw an exception if it's called before the + // Controller is initialized (this can happen during indexing). + public void getAvailabilityStatus_withoutInit_returnsUnavailable() { + CredentialManagerPreferenceController controller = + new CredentialManagerPreferenceController( + mContext, mCredentialsPreferenceCategory.getKey()); + assertThat(controller.isConnected()).isFalse(); + assertThat(controller.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE); + } + + @Test + public void getAvailabilityStatus_noServices_returnsUnavailable() { + CredentialManagerPreferenceController controller = + createControllerWithServices(Collections.emptyList()); + assertThat(controller.isConnected()).isFalse(); + assertThat(controller.getAvailabilityStatus()).isEqualTo(CONDITIONALLY_UNAVAILABLE); + } + + @Test + public void getAvailabilityStatus_withServices_returnsAvailable() { + CredentialManagerPreferenceController controller = + createControllerWithServices(Lists.newArrayList(createServiceInfo())); + assertThat(controller.isConnected()).isFalse(); + assertThat(controller.getAvailabilityStatus()).isEqualTo(AVAILABLE); + } + + @Test + public void displayPreference_noServices_noPreferencesAdded() { + CredentialManagerPreferenceController controller = + createControllerWithServices(Collections.emptyList()); + controller.displayPreference(mScreen); + assertThat(mCredentialsPreferenceCategory.getPreferenceCount()).isEqualTo(0); + } + + @Test + public void displayPreference_withServices_preferencesAdded() { + CredentialManagerPreferenceController controller = + createControllerWithServices(Lists.newArrayList(createServiceInfo())); + controller.displayPreference(mScreen); + assertThat(controller.isConnected()).isFalse(); + assertThat(mCredentialsPreferenceCategory.getPreferenceCount()).isEqualTo(1); + } + + @Test + public void getAvailabilityStatus_handlesToggleAndSave() { + CredentialManagerPreferenceController controller = + createControllerWithServices( + Lists.newArrayList( + createServiceInfo("com.android.provider1", "ClassA"), + createServiceInfo("com.android.provider1", "ClassB"), + createServiceInfo("com.android.provider2", "ClassA"), + createServiceInfo("com.android.provider3", "ClassA"), + createServiceInfo("com.android.provider4", "ClassA"), + createServiceInfo("com.android.provider5", "ClassA"), + createServiceInfo("com.android.provider6", "ClassA"))); + assertThat(controller.getAvailabilityStatus()).isEqualTo(AVAILABLE); + assertThat(controller.isConnected()).isFalse(); + + // Ensure that we stay under 5 providers. + assertThat(controller.togglePackageNameEnabled("com.android.provider1")).isTrue(); + assertThat(controller.togglePackageNameEnabled("com.android.provider2")).isTrue(); + assertThat(controller.togglePackageNameEnabled("com.android.provider3")).isTrue(); + assertThat(controller.togglePackageNameEnabled("com.android.provider4")).isTrue(); + assertThat(controller.togglePackageNameEnabled("com.android.provider5")).isTrue(); + assertThat(controller.togglePackageNameEnabled("com.android.provider6")).isFalse(); + + // Check that they are all actually registered. + Set enabledProviders = controller.getEnabledProviders(); + assertThat(enabledProviders.size()).isEqualTo(5); + assertThat(enabledProviders.contains("com.android.provider1")).isTrue(); + assertThat(enabledProviders.contains("com.android.provider2")).isTrue(); + assertThat(enabledProviders.contains("com.android.provider3")).isTrue(); + assertThat(enabledProviders.contains("com.android.provider4")).isTrue(); + assertThat(enabledProviders.contains("com.android.provider5")).isTrue(); + assertThat(enabledProviders.contains("com.android.provider6")).isFalse(); + + // Check that the settings string has the right component names. + List enabledServices = controller.getEnabledSettings(); + assertThat(enabledServices.size()).isEqualTo(6); + assertThat(enabledServices.contains("com.android.provider1/ClassA")).isTrue(); + assertThat(enabledServices.contains("com.android.provider1/ClassB")).isTrue(); + assertThat(enabledServices.contains("com.android.provider2/ClassA")).isTrue(); + assertThat(enabledServices.contains("com.android.provider3/ClassA")).isTrue(); + assertThat(enabledServices.contains("com.android.provider4/ClassA")).isTrue(); + assertThat(enabledServices.contains("com.android.provider5/ClassA")).isTrue(); + assertThat(enabledServices.contains("com.android.provider6/ClassA")).isFalse(); + + // Toggle the provider disabled. + controller.togglePackageNameDisabled("com.android.provider2"); + + // Check that the provider was removed from the list of providers. + Set currentlyEnabledProviders = controller.getEnabledProviders(); + assertThat(currentlyEnabledProviders.size()).isEqualTo(4); + assertThat(currentlyEnabledProviders.contains("com.android.provider1")).isTrue(); + assertThat(currentlyEnabledProviders.contains("com.android.provider2")).isFalse(); + assertThat(currentlyEnabledProviders.contains("com.android.provider3")).isTrue(); + assertThat(currentlyEnabledProviders.contains("com.android.provider4")).isTrue(); + assertThat(currentlyEnabledProviders.contains("com.android.provider5")).isTrue(); + assertThat(currentlyEnabledProviders.contains("com.android.provider6")).isFalse(); + + // Check that the provider was removed from the list of services stored in the setting. + List currentlyEnabledServices = controller.getEnabledSettings(); + assertThat(currentlyEnabledServices.size()).isEqualTo(5); + assertThat(currentlyEnabledServices.contains("com.android.provider1/ClassA")).isTrue(); + assertThat(currentlyEnabledServices.contains("com.android.provider1/ClassB")).isTrue(); + assertThat(currentlyEnabledServices.contains("com.android.provider3/ClassA")).isTrue(); + assertThat(currentlyEnabledServices.contains("com.android.provider4/ClassA")).isTrue(); + assertThat(currentlyEnabledServices.contains("com.android.provider5/ClassA")).isTrue(); + assertThat(currentlyEnabledServices.contains("com.android.provider6/ClassA")).isFalse(); + } + + private CredentialManagerPreferenceController createControllerWithServices( + List availableServices) { + CredentialManagerPreferenceController controller = + new CredentialManagerPreferenceController( + mContext, mCredentialsPreferenceCategory.getKey()); + controller.init(() -> mock(Lifecycle.class), availableServices, new HashSet<>()); + return controller; + } + + private ServiceInfo createServiceInfo() { + return createServiceInfo("com.android.provider", "CredManProvider"); + } + + private ServiceInfo createServiceInfo(String packageName, String className) { + ServiceInfo si = new ServiceInfo(); + si.packageName = packageName; + si.name = className; + si.nonLocalizedLabel = "test"; + + si.applicationInfo = new ApplicationInfo(); + si.applicationInfo.packageName = packageName; + si.applicationInfo.nonLocalizedLabel = "test"; + + return si; + } +}