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;
+ }
+}