Revert "Revert "Add Credential Manager settings""

This reverts commit 7a231eaba04c43697d9843c4dd94f320a6c0b3d8.

Reason for revert: Adding in fix for issue that caused initial revert

Change-Id: I395c13fb46fc570a6b8a663d4b4e5537866325ce
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 5352562..37c9a7f 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -126,6 +126,7 @@
     <uses-permission android:name="android.permission.SEND_SAFETY_CENTER_UPDATE" />
     <uses-permission android:name="android.permission.START_VIEW_APP_FEATURES" />
     <uses-permission android:name="android.permission.ACCESS_KEYGUARD_QUICK_AFFORDANCES" />
+    <uses-permission android:name="android.permission.LIST_ENABLED_CREDENTIAL_PROVIDERS" />
 
     <application
             android:name=".SettingsApplication"
diff --git a/res/values/strings.xml b/res/values/strings.xml
index bf39c8b..304e0e4 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -9726,8 +9726,12 @@
     <!-- AutoFill strings -->
     <!-- Preference label for choosing auto-fill service. [CHAR LIMIT=60] -->
     <string name="autofill_app">Autofill service</string>
+    <!-- Preference label for choosing auto-fill service. [CHAR LIMIT=60] -->
+    <string name="default_autofill_app">Default autofill service</string>
     <!-- Preference category for showing auto-fill services with saved passwords. [CHAR LIMIT=60] -->
     <string name="autofill_passwords">Passwords</string>
+    <!-- Preference category for showing credman services with saved credentials. [CHAR LIMIT=60] -->
+    <string name="credman_credentials">Password and identity services</string>
     <!-- Summary for passwords settings that shows how many passwords are saved for each autofill
          service. [CHAR LIMIT=NONE] -->
     <plurals name="autofill_passwords_count">
@@ -9739,6 +9743,8 @@
     <string name="autofill_passwords_count_placeholder" translatable="false">\u2014</string>
     <!-- Keywords for the auto-fill feature. [CHAR LIMIT=NONE] -->
     <string name="autofill_keywords">auto, fill, autofill, password</string>
+    <!-- Keywords for the credman feature. [CHAR LIMIT=NONE] -->
+    <string name="credman_keywords">credentials, passkey, password</string>
 
     <!-- Message of the warning dialog for setting the auto-fill app. [CHAR_LIMIT=NONE] -->
     <string name="autofill_confirmation_message">
@@ -9751,6 +9757,21 @@
         ]]>
     </string>
 
+    <!-- Title of the warning dialog for disabling the credential provider. [CHAR_LIMIT=NONE] -->
+    <string name="credman_confirmation_message_title">Turn off %1$s\?</string>
+
+    <!-- Message of the warning dialog for disabling the credential provider. [CHAR_LIMIT=NONE] -->
+    <string name="credman_confirmation_message">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.</string>
+
+    <!-- Title of the error dialog when too many credential providers are selected. [CHAR_LIMIT=NONE] -->
+    <string name="credman_error_message_title">Password and identity services limit</string>
+
+    <!-- Message of the error dialog when too many credential providers are selected. [CHAR_LIMIT=NONE] -->
+    <string name="credman_error_message">You can have up to 5 autofill and password services active at the same time. Turn off a service to add more.</string>
+
+    <!-- Positive button to turn off credential manager provider (confirmation). [CHAR LIMIT=60] -->
+    <string name="credman_confirmation_message_positive_button">Turn off</string>
+
     <!-- Preference category for autofill debugging development settings. [CHAR LIMIT=25] -->
     <string name="debug_autofill_category">Autofill</string>
 
diff --git a/res/xml/accounts_dashboard_settings_credman.xml b/res/xml/accounts_dashboard_settings_credman.xml
new file mode 100644
index 0000000..605d315
--- /dev/null
+++ b/res/xml/accounts_dashboard_settings_credman.xml
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<PreferenceScreen
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:settings="http://schemas.android.com/apk/res-auto"
+    android:key="user_and_account_settings_screen"
+    android:title="@string/account_dashboard_title"
+    settings:keywords="@string/keywords_accounts">
+
+    <PreferenceCategory
+        android:key="default_service_category"
+        android:order="10"
+        android:title="@string/default_autofill_app">
+
+        <com.android.settings.widget.GearPreference
+            android:fragment="com.android.settings.applications.defaultapps.DefaultAutofillPicker"
+            android:key="default_autofill_main"
+            android:title="@string/default_autofill_app"
+            settings:keywords="@string/autofill_keywords">
+            <extra
+                android:name="for_work"
+                android:value="false" />
+        </com.android.settings.widget.GearPreference>
+    </PreferenceCategory>
+
+    <PreferenceCategory
+        android:key="credman_category"
+        android:order="20"
+        android:persistent="false"
+        android:title="@string/credman_credentials"
+        settings:controller="com.android.settings.applications.credentials.CredentialManagerPreferenceController"
+        settings:keywords="@string/credman_keywords" />
+
+    <PreferenceCategory
+        android:key="passwords_category"
+        android:order="30"
+        android:persistent="false"
+        settings:controller="com.android.settings.applications.autofill.PasswordsPreferenceController"
+        settings:keywords="@string/autofill_keywords" />
+
+    <PreferenceCategory
+        android:key="dashboard_tile_placeholder"
+        android:order="130"/>
+
+    <SwitchPreference
+        android:key="auto_sync_account_data"
+        android:title="@string/auto_sync_account_title"
+        android:summary="@string/auto_sync_account_summary"
+        android:order="202"
+        settings:allowDividerAbove="true"/>
+
+    <SwitchPreference
+        android:key="auto_sync_work_account_data"
+        android:title="@string/account_settings_menu_auto_sync_work"
+        android:summary="@string/auto_sync_account_summary"
+        settings:forWork="true"
+        android:order="203"/>
+
+    <SwitchPreference
+        android:key="auto_sync_personal_account_data"
+        android:title="@string/account_settings_menu_auto_sync_personal"
+        android:summary="@string/auto_sync_account_summary"
+        android:order="204"/>
+
+</PreferenceScreen>
\ 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 0000000..a5188dd
--- /dev/null
+++ b/res/xml/accounts_personal_dashboard_settings_credman.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+
+<PreferenceScreen
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:settings="http://schemas.android.com/apk/res-auto"
+    android:key="user_and_account_settings_screen"
+    android:title="@string/account_dashboard_title"
+    settings:keywords="@string/keywords_accounts">
+
+    <PreferenceCategory
+        android:key="default_service_category"
+        android:order="10"
+        android:title="@string/default_autofill_app">
+
+        <com.android.settings.widget.GearPreference
+            android:fragment="com.android.settings.applications.defaultapps.DefaultAutofillPicker"
+            android:key="default_autofill_main"
+            android:title="@string/default_autofill_app"
+            settings:keywords="@string/autofill_keywords">
+            <extra
+                android:name="for_work"
+                android:value="false" />
+        </com.android.settings.widget.GearPreference>
+    </PreferenceCategory>
+
+    <PreferenceCategory
+        android:key="credman_category"
+        android:order="20"
+        android:persistent="false"
+        android:title="@string/credman_credentials"
+        settings:controller="com.android.settings.applications.credentials.CredentialManagerPreferenceController"
+        settings:keywords="@string/credman_keywords" />
+
+    <PreferenceCategory
+        android:key="passwords_category"
+        android:order="30"
+        android:persistent="false"
+        settings:controller="com.android.settings.applications.autofill.PasswordsPreferenceController"
+        settings:keywords="@string/autofill_keywords" />
+
+    <PreferenceCategory
+        android:key="dashboard_tile_placeholder"
+        android:order="130"/>
+
+    <SwitchPreference
+        android:key="auto_sync_account_data"
+        android:title="@string/auto_sync_account_title"
+        android:summary="@string/auto_sync_account_summary"
+        android:order="200"
+        settings:allowDividerAbove="true"/>
+
+    <SwitchPreference
+        android:key="auto_sync_personal_account_data"
+        android:title="@string/account_settings_menu_auto_sync_personal"
+        android:summary="@string/auto_sync_account_summary"
+        android:order="210"/>
+
+</PreferenceScreen>
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 0000000..f4e8af2
--- /dev/null
+++ b/res/xml/accounts_work_dashboard_settings_credman.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+
+<PreferenceScreen
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:settings="http://schemas.android.com/apk/res-auto"
+    android:key="user_and_account_settings_screen"
+    android:title="@string/account_dashboard_title"
+    settings:keywords="@string/keywords_accounts">
+
+    <com.android.settings.widget.WorkOnlyCategory
+        android:key="autofill_work_app_defaults"
+        android:order="30"
+        android:title="@string/default_autofill_app">
+
+        <com.android.settings.widget.GearPreference
+            android:fragment="com.android.settings.applications.defaultapps.DefaultAutofillPicker"
+            android:key="default_autofill_work"
+            android:title="@string/default_autofill_app"
+            settings:searchable="false">
+            <extra
+                android:name="for_work"
+                android:value="true" />
+        </com.android.settings.widget.GearPreference>
+    </com.android.settings.widget.WorkOnlyCategory>
+
+    <PreferenceCategory
+        android:key="credman_category"
+        android:order="20"
+        android:persistent="false"
+        android:title="@string/credman_credentials"
+        settings:controller="com.android.settings.applications.credentials.CredentialManagerPreferenceController"
+        settings:keywords="@string/credman_keywords" />
+
+    <PreferenceCategory
+        android:key="passwords_category"
+        android:order="30"
+        android:persistent="false"
+        settings:controller="com.android.settings.applications.autofill.PasswordsPreferenceController"
+        settings:keywords="@string/autofill_keywords" />
+
+    <PreferenceCategory
+        android:key="dashboard_tile_placeholder"
+        android:order="130"/>
+
+    <SwitchPreference
+        android:key="auto_sync_account_data"
+        android:title="@string/auto_sync_account_title"
+        android:summary="@string/auto_sync_account_summary"
+        android:order="200"
+        settings:allowDividerAbove="true"/>
+
+    <SwitchPreference
+        android:key="auto_sync_work_account_data"
+        android:title="@string/account_settings_menu_auto_sync_work"
+        android:summary="@string/auto_sync_account_summary"
+        android:order="210"/>
+
+</PreferenceScreen>
diff --git a/src/com/android/settings/accounts/AccountDashboardFragment.java b/src/com/android/settings/accounts/AccountDashboardFragment.java
index 3e83d6f..107df94 100644
--- a/src/com/android/settings/accounts/AccountDashboardFragment.java
+++ b/src/com/android/settings/accounts/AccountDashboardFragment.java
@@ -22,11 +22,13 @@
 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 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;
@@ -47,7 +49,6 @@
 
     private static final String TAG = "AccountDashboardFrag";
 
-
     @Override
     public int getMetricsCategory() {
         return SettingsEnums.ACCOUNT;
@@ -60,7 +61,7 @@
 
     @Override
     protected int getPreferenceScreenResId() {
-        return R.xml.accounts_dashboard_settings;
+        return getPreferenceLayoutResId();
     }
 
     @Override
@@ -71,6 +72,12 @@
     @Override
     public void onAttach(Context context) {
         super.onAttach(context);
+        if (CredentialManager.isServiceEnabled()) {
+            CredentialManagerPreferenceController cmpp =
+                    use(CredentialManagerPreferenceController.class);
+            cmpp.setParentFragment(this);
+        }
+
         getSettingsLifecycle().addObserver(use(PasswordsPreferenceController.class));
     }
 
@@ -95,11 +102,13 @@
     }
 
     private static void buildAccountPreferenceControllers(
-            Context context, DashboardFragment parent, String[] authorities,
+            Context context,
+            DashboardFragment parent,
+            String[] authorities,
             List<AbstractPreferenceController> 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,14 @@
         controllers.add(new AutoSyncWorkDataPreferenceController(context, parent));
     }
 
+    public static int getPreferenceLayoutResId() {
+        return CredentialManager.isServiceEnabled()
+                ? 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(getPreferenceLayoutResId()) {
 
                 @Override
                 public List<AbstractPreferenceController> createPreferenceControllers(
@@ -124,11 +139,11 @@
 
                 @SuppressWarnings("MissingSuperCall") // TODO: Fix me
                 @Override
-                public List<SearchIndexableRaw> getDynamicRawDataToIndex(Context context,
-                        boolean enabled) {
+                public List<SearchIndexableRaw> getDynamicRawDataToIndex(
+                        Context context, boolean enabled) {
                     final List<SearchIndexableRaw> indexRaws = new ArrayList<>();
-                    final UserManager userManager = (UserManager) context.getSystemService(
-                            Context.USER_SERVICE);
+                    final UserManager userManager =
+                            (UserManager) context.getSystemService(Context.USER_SERVICE);
                     final List<UserInfo> 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 4661c64..e061102 100644
--- a/src/com/android/settings/accounts/AccountPersonalDashboardFragment.java
+++ b/src/com/android/settings/accounts/AccountPersonalDashboardFragment.java
@@ -22,9 +22,11 @@
 
 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,9 +36,7 @@
 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";
@@ -53,6 +53,9 @@
 
     @Override
     protected int getPreferenceScreenResId() {
+        if (CredentialManager.isServiceEnabled()) {
+            return R.xml.accounts_personal_dashboard_settings_credman;
+        }
         return R.xml.accounts_personal_dashboard_settings;
     }
 
@@ -64,6 +67,13 @@
     @Override
     public void onAttach(Context context) {
         super.onAttach(context);
+
+        if (CredentialManager.isServiceEnabled()) {
+            CredentialManagerPreferenceController cmpp =
+                    use(CredentialManagerPreferenceController.class);
+            cmpp.setParentFragment(this);
+        }
+
         getSettingsLifecycle().addObserver(use(PasswordsPreferenceController.class));
     }
 
@@ -77,11 +87,13 @@
     }
 
     private static void buildAccountPreferenceControllers(
-            Context context, DashboardFragment parent, String[] authorities,
+            Context context,
+            DashboardFragment parent,
+            String[] authorities,
             List<AbstractPreferenceController> 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 +103,15 @@
     }
 
     // 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<AbstractPreferenceController> 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<AbstractPreferenceController> 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 f64e041..027d1f7 100644
--- a/src/com/android/settings/accounts/AccountWorkProfileDashboardFragment.java
+++ b/src/com/android/settings/accounts/AccountWorkProfileDashboardFragment.java
@@ -22,9 +22,11 @@
 
 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,9 +36,7 @@
 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";
@@ -53,6 +53,9 @@
 
     @Override
     protected int getPreferenceScreenResId() {
+        if (CredentialManager.isServiceEnabled()) {
+            return R.xml.accounts_work_dashboard_settings_credman;
+        }
         return R.xml.accounts_work_dashboard_settings;
     }
 
@@ -64,6 +67,13 @@
     @Override
     public void onAttach(Context context) {
         super.onAttach(context);
+
+        if (CredentialManager.isServiceEnabled()) {
+            CredentialManagerPreferenceController cmpp =
+                    use(CredentialManagerPreferenceController.class);
+            cmpp.setParentFragment(this);
+        }
+
         getSettingsLifecycle().addObserver(use(PasswordsPreferenceController.class));
     }
 
@@ -77,11 +87,13 @@
     }
 
     private static void buildAccountPreferenceControllers(
-            Context context, DashboardFragment parent, String[] authorities,
+            Context context,
+            DashboardFragment parent,
+            String[] authorities,
             List<AbstractPreferenceController> 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 +103,15 @@
     }
 
     // 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<AbstractPreferenceController> 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<AbstractPreferenceController> 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 0000000..7abe904
--- /dev/null
+++ b/src/com/android/settings/applications/credentials/CredentialManagerPreferenceController.java
@@ -0,0 +1,475 @@
+/*
+ * 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<ServiceInfo> mServices;
+    private final Set<String> 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()) {
+            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<ListEnabledProvidersResponse, ListEnabledProvidersException>() {
+                    @Override
+                    public void onResult(ListEnabledProvidersResponse result) {
+                        Set<String> enabledPackages = new HashSet<>();
+                        for (String flattenedComponentName : result.getProviderComponentNames()) {
+                            ComponentName cn =
+                                    ComponentName.unflattenFromString(flattenedComponentName);
+                            if (cn != null) {
+                                enabledPackages.add(cn.getPackageName());
+                            }
+                        }
+
+                        List<ServiceInfo> 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<ServiceInfo> availableServices,
+            Set<String> 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<String> getEnabledProviders() {
+        return mEnabledPackageNames;
+    }
+
+    /**
+     * Returns the enabled credential manager provider flattened component names that can be stored
+     * in the setting.
+     */
+    @VisibleForTesting
+    public List<String> getEnabledSettings() {
+        // Get all the component names that match the enabled package names.
+        List<String> 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<String> enabledServices = getEnabledSettings();
+        mCredentialManager.setEnabledProviders(
+                enabledServices,
+                getUser(),
+                mExecutor,
+                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());
+                    }
+                });
+    }
+
+    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 0000000..5848326
--- /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<String> 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<String> 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<String> 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<String> 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<ServiceInfo> 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;
+    }
+}