Add Credential Manager settings
Autofill is evolving into CredMan which means we need
to update the settings to have CredMan providers.
This CL adds CredMan equivalent classes to list the
Credential Manager providers and allow the user
to select a number of providers.
Test: Manual & atest SettingsUnitTests & make RunSettingsRoboTests -j
Bug: 253157366
Change-Id: Ice76187cfee91d844d211205b44b661acf2f6a44
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 733d0e1..eb222d5 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -9728,8 +9728,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">
@@ -9741,6 +9745,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">
@@ -9753,6 +9759,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;
+ }
+}