Add buttons for new settings design
This adds a new class called PrimaryProviderPreference
which is responsible for laying out the old cog view if
the feature is disabled and the new button view if it
is enabled.
Screenshots:
https://hsv.googleplex.com/5109836204212224
https://hsv.googleplex.com/5574754636398592
https://hsv.googleplex.com/6737135727017984
Change-Id: I1ad0c59a4afc5be3694b499f66bbd7306dfbee69
Test: Manual test with flag on + off & unit tests
Bug: 300979487
diff --git a/res/layout/preference_credential_manager_with_buttons.xml b/res/layout/preference_credential_manager_with_buttons.xml
new file mode 100644
index 0000000..1889cea
--- /dev/null
+++ b/res/layout/preference_credential_manager_with_buttons.xml
@@ -0,0 +1,127 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2024 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.
+ -->
+
+<!-- Based off preference_single_target.xml with buttons added below text. -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:settings="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?android:attr/listPreferredItemHeightSmall"
+ android:gravity="center_vertical"
+ android:orientation="vertical"
+ android:background="?android:attr/selectableItemBackground"
+ android:clipToPadding="false">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="horizontal"
+ android:gravity="start|center_vertical"
+ android:clipToPadding="false"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
+
+ <LinearLayout
+ android:id="@+id/icon_frame"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="start|center_vertical"
+ android:minWidth="56dp"
+ android:orientation="horizontal"
+ android:clipToPadding="false"
+ android:paddingTop="4dp"
+ android:paddingBottom="4dp">
+ <androidx.preference.internal.PreferenceImageView
+ android:id="@android:id/icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ settings:maxWidth="48dp"
+ settings:maxHeight="48dp" />
+ </LinearLayout>
+
+ <RelativeLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:paddingTop="16dp"
+ android:paddingBottom="16dp">
+
+ <TextView
+ android:id="@android:id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:textAppearance="?android:attr/textAppearanceListItem"
+ android:ellipsize="marquee" />
+
+ <TextView
+ android:id="@android:id/summary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@android:id/title"
+ android:layout_alignStart="@android:id/title"
+ android:textAppearance="?android:attr/textAppearanceListItemSecondary"
+ android:textColor="?android:attr/textColorSecondary"
+ android:hyphenationFrequency="normalFast"
+ android:lineBreakWordStyle="phrase"
+ android:maxLines="10" />
+
+ </RelativeLayout>
+
+ </LinearLayout>
+
+ <!-- Preference should place its actual preference widget here. -->
+ <LinearLayout
+ android:id="@android:id/widget_frame"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:minWidth="@dimen/two_target_min_width"
+ android:gravity="center"
+ android:orientation="vertical" />
+
+ <LinearLayout
+ android:id="@+id/credman_button_frame"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="start|center_vertical"
+ android:minWidth="56dp"
+ android:orientation="horizontal"
+ android:clipToPadding="false"
+ android:paddingTop="4dp"
+ android:paddingLeft="80dp"
+ android:paddingBottom="4dp">
+
+ <Button
+ android:id="@+id/change_button"
+ android:layout_width="match_parent"
+ style="@style/CredentialManagerChangeButton"
+ android:layout_height="wrap_content"
+ android:text="@string/credman_button_change"/>
+
+ <Button
+ android:id="@+id/open_button"
+ style="@style/CredentialManagerOpenButton"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ android:text="@string/credman_button_open"/>
+
+ </LinearLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 402f526..9db5d8020 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -10739,7 +10739,7 @@
<!-- Preference category for showing auto-fill services with saved passwords. [CHAR LIMIT=60] -->
<string name="autofill_passwords">Passwords</string>
<!-- Preference category for showing autofill and credman services with saved credentials. [CHAR LIMIT=60] -->
- <string name="credman_chosen_app_title">Passwords, passkeys, and data services</string>
+ <string name="credman_chosen_app_title">Preferred service</string>
<!-- Preference category for showing additional credential providers. [CHAR LIMIT=60] -->
<string name="credman_credentials">Additional providers</string>
<!-- Summary for passwords settings that shows how many passwords are saved for each autofill
@@ -10757,6 +10757,10 @@
<string name="credman_keywords">data, passkey, password</string>
<!-- Keywords for the credman feature. [CHAR LIMIT=NONE] -->
<string name="credman_autofill_keywords">auto, fill, autofill, data, passkey, password</string>
+ <!-- Button for choosing credman service. [CHAR LIMIT=40] -->
+ <string name="credman_button_change">Change</string>
+ <!-- Button for opening credman service settings. [CHAR LIMIT=40] -->
+ <string name="credman_button_open">Open</string>
<!-- Message of the warning dialog for setting the auto-fill app. [CHAR_LIMIT=NONE] -->
<string name="autofill_confirmation_message">
@@ -12706,4 +12710,4 @@
<!-- Authority of the content provider that support methods restartPhoneProcess and restartRild. Will be overlaid by OEM.-->
<string name="reset_telephony_stack_content_provider_authority" translatable="false"></string>
-</resources>
+</resources>
\ No newline at end of file
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 8df990b..fbc6d7f 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -989,4 +989,22 @@
<item name="android:layout_alignParentTop">true</item>
</style>
+ <style name="CredentialManagerChangeButton" parent="@style/ActionPrimaryButton">
+ <item name="android:fontFamily">google-sans-medium</item>
+ <item name="android:textSize">14sp</item>
+ <item name="android:textAllCaps">false</item>
+ <item name="android:singleLine">true</item>
+ <item name="android:paddingHorizontal">24dp</item>
+ </style>
+
+ <style name="CredentialManagerOpenButton"
+ parent="@style/Widget.AppCompat.Button">
+ <item name="android:theme">@style/RoundedCornerThemeOverlay</item>
+ <item name="android:fontFamily">google-sans-medium</item>
+ <item name="android:textSize">14sp</item>
+ <item name="android:textAllCaps">false</item>
+ <item name="android:singleLine">true</item>
+ <item name="android:background">@null</item>
+ </style>
+
</resources>
diff --git a/res/xml/accounts_dashboard_settings_credman.xml b/res/xml/accounts_dashboard_settings_credman.xml
index 7bcf62d..7266bda 100644
--- a/res/xml/accounts_dashboard_settings_credman.xml
+++ b/res/xml/accounts_dashboard_settings_credman.xml
@@ -26,15 +26,14 @@
android:order="10"
android:title="@string/credman_chosen_app_title">
- <com.android.settings.widget.GearPreference
- android:fragment="com.android.settings.applications.credentials.DefaultCombinedPicker"
+ <com.android.settings.applications.credentials.PrimaryProviderPreference
android:key="default_credman_autofill_main"
android:title="@string/credman_chosen_app_title"
settings:keywords="@string/credman_autofill_keywords">
<extra
android:name="for_work"
android:value="false" />
- </com.android.settings.widget.GearPreference>
+ </com.android.settings.applications.credentials.PrimaryProviderPreference>
</PreferenceCategory>
<PreferenceCategory
diff --git a/res/xml/accounts_personal_dashboard_settings_credman.xml b/res/xml/accounts_personal_dashboard_settings_credman.xml
index 835fcb7..9473e17 100644
--- a/res/xml/accounts_personal_dashboard_settings_credman.xml
+++ b/res/xml/accounts_personal_dashboard_settings_credman.xml
@@ -27,15 +27,14 @@
android:order="10"
android:title="@string/credman_chosen_app_title">
- <com.android.settings.widget.GearPreference
- android:fragment="com.android.settings.applications.credentials.DefaultCombinedPicker"
+ <com.android.settings.applications.credentials.PrimaryProviderPreference
android:key="default_credman_autofill_main"
android:title="@string/credman_chosen_app_title"
settings:keywords="@string/credman_autofill_keywords">
<extra
android:name="for_work"
android:value="false" />
- </com.android.settings.widget.GearPreference>
+ </com.android.settings.applications.credentials.PrimaryProviderPreference>
</PreferenceCategory>
<PreferenceCategory
diff --git a/res/xml/accounts_private_dashboard_settings_credman.xml b/res/xml/accounts_private_dashboard_settings_credman.xml
index e9abcf8..54db839 100644
--- a/res/xml/accounts_private_dashboard_settings_credman.xml
+++ b/res/xml/accounts_private_dashboard_settings_credman.xml
@@ -27,8 +27,7 @@
android:order="10"
android:title="@string/credman_chosen_app_title">
- <com.android.settings.widget.GearPreference
- android:fragment="com.android.settings.applications.credentials.DefaultCombinedPickerPrivate"
+ <com.android.settings.applications.credentials.PrimaryProviderPreference
android:key="default_credman_autofill_private"
android:title="@string/credman_chosen_app_title"
settings:searchable="false">
@@ -36,7 +35,7 @@
<extra
android:name="for_work"
android:value="false" />
- </com.android.settings.widget.GearPreference>
+ </com.android.settings.applications.credentials.PrimaryProviderPreference>
</PreferenceCategory>
<PreferenceCategory
diff --git a/res/xml/accounts_work_dashboard_settings_credman.xml b/res/xml/accounts_work_dashboard_settings_credman.xml
index 8afbba8..d649940 100644
--- a/res/xml/accounts_work_dashboard_settings_credman.xml
+++ b/res/xml/accounts_work_dashboard_settings_credman.xml
@@ -27,15 +27,14 @@
android:order="10"
android:title="@string/credman_chosen_app_title">
- <com.android.settings.widget.GearPreference
- android:fragment="com.android.settings.applications.credentials.DefaultCombinedPickerWork"
+ <com.android.settings.applications.credentials.PrimaryProviderPreference
android:key="default_credman_autofill_main_work"
android:title="@string/credman_chosen_app_title"
settings:searchable="false">
<extra
android:name="for_work"
android:value="true" />
- </com.android.settings.widget.GearPreference>
+ </com.android.settings.applications.credentials.PrimaryProviderPreference>
</PreferenceCategory>
<PreferenceCategory
diff --git a/src/com/android/settings/applications/credentials/CombinedProviderInfo.java b/src/com/android/settings/applications/credentials/CombinedProviderInfo.java
index e7a391e..f8a3b0f 100644
--- a/src/com/android/settings/applications/credentials/CombinedProviderInfo.java
+++ b/src/com/android/settings/applications/credentials/CombinedProviderInfo.java
@@ -18,13 +18,17 @@
import android.content.ComponentName;
import android.content.Context;
+import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.ServiceInfo;
import android.credentials.CredentialProviderInfo;
import android.graphics.drawable.Drawable;
+import android.os.UserHandle;
+import android.os.UserManager;
import android.service.autofill.AutofillServiceInfo;
import android.text.TextUtils;
import android.util.IconDrawableFactory;
+import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -42,6 +46,11 @@
* logic for each row in settings.
*/
public final class CombinedProviderInfo {
+ private static final String TAG = "CombinedProviderInfo";
+ private static final String SETTINGS_ACTIVITY_INTENT_ACTION = "android.intent.action.MAIN";
+ private static final String SETTINGS_ACTIVITY_INTENT_CATEGORY =
+ "android.intent.category.LAUNCHER";
+
private final List<CredentialProviderInfo> mCredentialProviderInfos;
private final @Nullable AutofillServiceInfo mAutofillServiceInfo;
private final boolean mIsDefaultAutofillProvider;
@@ -316,4 +325,44 @@
return cmpi;
}
+
+ public static @Nullable Intent createSettingsActivityIntent(
+ @NonNull Context context,
+ @Nullable CharSequence packageName,
+ @Nullable CharSequence settingsActivity,
+ int currentUserId) {
+ if (TextUtils.isEmpty(packageName) || TextUtils.isEmpty(settingsActivity)) {
+ return null;
+ }
+
+ ComponentName cn =
+ new ComponentName(String.valueOf(packageName), String.valueOf(settingsActivity));
+ if (cn == null) {
+ Log.e(
+ TAG,
+ "Failed to deserialize settingsActivity attribute, we got: "
+ + String.valueOf(packageName)
+ + " and "
+ + String.valueOf(settingsActivity));
+ return null;
+ }
+
+ Intent intent = new Intent(SETTINGS_ACTIVITY_INTENT_ACTION);
+ intent.addCategory(SETTINGS_ACTIVITY_INTENT_CATEGORY);
+ intent.setComponent(cn);
+
+ int contextUserId = context.getUser().getIdentifier();
+ if (currentUserId != contextUserId && UserManager.isHeadlessSystemUserMode()) {
+ Log.w(
+ TAG,
+ "onLeftSideClicked(): using context for current user ("
+ + currentUserId
+ + ") instead of user "
+ + contextUserId
+ + " on headless system user mode");
+ context = context.createContextAsUser(UserHandle.of(currentUserId), /* flags= */ 0);
+ }
+
+ return intent;
+ }
}
diff --git a/src/com/android/settings/applications/credentials/CredentialManagerPreferenceController.java b/src/com/android/settings/applications/credentials/CredentialManagerPreferenceController.java
index e186206..4e5d7f0 100644
--- a/src/com/android/settings/applications/credentials/CredentialManagerPreferenceController.java
+++ b/src/com/android/settings/applications/credentials/CredentialManagerPreferenceController.java
@@ -95,9 +95,6 @@
private static final String ALTERNATE_INTENT = "android.settings.SYNC_SETTINGS";
private static final String PRIMARY_INTENT = "android.settings.CREDENTIAL_PROVIDER";
private static final int MAX_SELECTABLE_PROVIDERS = 5;
- private static final String SETTINGS_ACTIVITY_INTENT_ACTION = "android.intent.action.MAIN";
- private static final String SETTINGS_ACTIVITY_INTENT_CATEGORY =
- "android.intent.category.LAUNCHER";
private final PackageManager mPm;
private final List<CredentialProviderInfo> mServices;
@@ -475,6 +472,7 @@
// If this provider is displayed at the top then we should not show it.
if (topProvider != null
+ && topProvider.getApplicationInfo() != null
&& topProvider.getApplicationInfo().packageName.equals(packageName)) {
continue;
}
@@ -484,10 +482,6 @@
continue;
}
- // Get the settings activity.
- CharSequence settingsActivity =
- combinedInfo.getCredentialProviderInfos().get(0).getSettingsActivity();
-
Drawable icon = combinedInfo.getAppIcon(context, getUser());
CharSequence title = combinedInfo.getAppName(context);
@@ -499,7 +493,7 @@
icon,
packageName,
combinedInfo.getSettingsSubtitle(),
- settingsActivity);
+ combinedInfo.getSettingsActivity());
output.put(packageName, pref);
group.addPreference(pref);
}
@@ -626,43 +620,12 @@
@Override
public void onLeftSideClicked() {
- if (settingsActivity == null) {
- Log.w(TAG, "settingsActivity was null");
- return;
+ Intent settingsIntent =
+ CombinedProviderInfo.createSettingsActivityIntent(
+ mContext, packageName, settingsActivity, getUser());
+ if (settingsIntent != null) {
+ mContext.startActivity(settingsIntent);
}
-
- String settingsActivityStr = String.valueOf(settingsActivity);
- ComponentName cn = ComponentName.unflattenFromString(settingsActivityStr);
- if (cn == null) {
- Log.w(
- TAG,
- "Failed to deserialize settingsActivity attribute, we got: "
- + settingsActivityStr);
- return;
- }
-
- Intent intent = new Intent(SETTINGS_ACTIVITY_INTENT_ACTION);
- intent.addCategory(SETTINGS_ACTIVITY_INTENT_CATEGORY);
- intent.setComponent(cn);
-
- Context context = mContext;
- int currentUserId = getUser();
- int contextUserId = context.getUser().getIdentifier();
-
- if (currentUserId != contextUserId) {
- Log.d(
- TAG,
- "onLeftSideClicked(): using context for current user ("
- + currentUserId
- + ") instead of user "
- + contextUserId
- + " on headless system user mode");
- context =
- context.createContextAsUser(
- UserHandle.of(currentUserId), /* flags= */ 0);
- }
-
- context.startActivity(intent);
}
});
diff --git a/src/com/android/settings/applications/credentials/DefaultCombinedPreferenceController.java b/src/com/android/settings/applications/credentials/DefaultCombinedPreferenceController.java
index 567bc31..d2400bb 100644
--- a/src/com/android/settings/applications/credentials/DefaultCombinedPreferenceController.java
+++ b/src/com/android/settings/applications/credentials/DefaultCombinedPreferenceController.java
@@ -16,31 +16,32 @@
package com.android.settings.applications.credentials;
-import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
-import android.content.pm.ServiceInfo;
import android.credentials.CredentialManager;
import android.credentials.CredentialProviderInfo;
+import android.graphics.drawable.Drawable;
import android.os.UserHandle;
import android.provider.Settings;
import android.service.autofill.AutofillService;
import android.service.autofill.AutofillServiceInfo;
-import android.text.TextUtils;
import android.view.autofill.AutofillManager;
import androidx.annotation.Nullable;
+import androidx.annotation.NonNull;
import androidx.preference.Preference;
-import androidx.preference.PreferenceScreen;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.settings.R;
+import com.android.settings.Utils;
import com.android.settings.applications.defaultapps.DefaultAppPreferenceController;
import com.android.settingslib.applications.DefaultAppInfo;
+import com.android.settingslib.widget.TwoTargetPreference;
import java.util.ArrayList;
import java.util.List;
-public class DefaultCombinedPreferenceController extends DefaultAppPreferenceController
- implements Preference.OnPreferenceClickListener {
+public class DefaultCombinedPreferenceController extends DefaultAppPreferenceController {
private static final Intent AUTOFILL_PROBE = new Intent(AutofillService.SERVICE_INTERFACE);
private static final String TAG = "DefaultCombinedPreferenceController";
@@ -78,72 +79,80 @@
// Despite this method being called getSettingIntent this intent actually
// opens the primary picker. This is so that we can swap the cog and the left
// hand side presses to align the UX.
- return new Intent(mContext, CredentialsPickerActivity.class);
- }
-
- @Override
- public void displayPreference(PreferenceScreen screen) {
- super.displayPreference(screen);
-
- final String prefKey = getPreferenceKey();
- final Preference preference = screen.findPreference(prefKey);
- if (preference != null) {
- preference.setOnPreferenceClickListener((Preference.OnPreferenceClickListener) this);
+ if (PrimaryProviderPreference.shouldUseNewSettingsUi()) {
+ // We need to return an empty intent here since the class we inherit
+ // from will throw an NPE if we return null and we don't want it to
+ // open anything since we added the buttons.
+ return new Intent();
}
+ return createIntentToOpenPicker();
}
@Override
- public boolean onPreferenceClick(Preference preference) {
- // Get the selected provider.
+ public void updateState(@NonNull Preference preference) {
final CombinedProviderInfo topProvider = getTopProvider();
- if (topProvider == null) {
- return false;
+ if (topProvider != null && mContext != null) {
+ updatePreferenceForProvider(
+ preference,
+ topProvider.getAppName(mContext),
+ topProvider.getSettingsSubtitle(),
+ topProvider.getAppIcon(mContext, getUser()),
+ createSettingsActivityIntent(
+ topProvider.getPackageName(), topProvider.getSettingsActivity()));
+ } else {
+ updatePreferenceForProvider(preference, null, null, null, null);
+ }
+ }
+
+ @VisibleForTesting
+ public void updatePreferenceForProvider(
+ Preference preference,
+ @Nullable CharSequence appName,
+ @Nullable String appSubtitle,
+ @Nullable Drawable appIcon,
+ @Nullable Intent settingsActivityIntent) {
+ if (appName == null) {
+ preference.setTitle(R.string.app_list_preference_none);
+ } else {
+ preference.setTitle(appName);
}
- // If the top provider has a defined Credential Manager settings
- // provider then we should open that up.
- final String settingsActivity = topProvider.getSettingsActivity();
- if (!TextUtils.isEmpty(settingsActivity)) {
- final Intent intent =
- new Intent(Intent.ACTION_MAIN)
- .setComponent(
- new ComponentName(
- topProvider.getPackageName(), settingsActivity));
- startActivity(intent);
- return true;
+ if (appIcon == null) {
+ preference.setIcon(null);
+ } else {
+ preference.setIcon(Utils.getSafeIcon(appIcon));
}
- return false;
+ preference.setSummary(appSubtitle);
+
+ if (preference instanceof PrimaryProviderPreference) {
+ PrimaryProviderPreference primaryPref = (PrimaryProviderPreference) preference;
+ primaryPref.setIconSize(TwoTargetPreference.ICON_SIZE_MEDIUM);
+ primaryPref.setDelegate(
+ new PrimaryProviderPreference.Delegate() {
+ public void onOpenButtonClicked() {
+ if (settingsActivityIntent != null) {
+ startActivity(settingsActivityIntent);
+ }
+ }
+
+ public void onChangeButtonClicked() {
+ startActivity(createIntentToOpenPicker());
+ }
+ });
+
+ // Hide the open button if there is no defined settings activity.
+ primaryPref.setOpenButtonVisible(settingsActivityIntent != null);
+ primaryPref.setButtonsVisible(appName != null);
+ }
}
private @Nullable CombinedProviderInfo getTopProvider() {
- List<CombinedProviderInfo> providers = getAllProviders(getUser());
- return CombinedProviderInfo.getTopProvider(providers);
+ return CombinedProviderInfo.getTopProvider(getAllProviders(getUser()));
}
@Override
protected DefaultAppInfo getDefaultAppInfo() {
- CombinedProviderInfo topProvider = getTopProvider();
- if (topProvider != null) {
- ServiceInfo brandingService = topProvider.getBrandingService();
- if (brandingService == null) {
- return new DefaultAppInfo(
- mContext,
- mPackageManager,
- getUser(),
- topProvider.getApplicationInfo(),
- topProvider.getSettingsSubtitle(),
- true);
- } else {
- return new DefaultAppInfo(
- mContext,
- mPackageManager,
- getUser(),
- brandingService,
- topProvider.getSettingsSubtitle(),
- true);
- }
- }
return null;
}
@@ -180,4 +189,16 @@
protected int getUser() {
return UserHandle.myUserId();
}
+
+ /** Creates an intent to open the credential picker. */
+ private Intent createIntentToOpenPicker() {
+ return new Intent(mContext, CredentialsPickerActivity.class);
+ }
+
+ /** Creates an intent to open the settings activity of the primary provider (if available). */
+ public @Nullable Intent createSettingsActivityIntent(
+ @Nullable String packageName, @Nullable String settingsActivity) {
+ return CombinedProviderInfo.createSettingsActivityIntent(
+ mContext, packageName, settingsActivity, getUser());
+ }
}
diff --git a/src/com/android/settings/applications/credentials/PrimaryProviderPreference.java b/src/com/android/settings/applications/credentials/PrimaryProviderPreference.java
new file mode 100644
index 0000000..b8e2529
--- /dev/null
+++ b/src/com/android/settings/applications/credentials/PrimaryProviderPreference.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.applications.credentials;
+
+import android.content.Context;
+import android.credentials.flags.Flags;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.Button;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceViewHolder;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.settings.R;
+import com.android.settings.widget.GearPreference;
+
+/**
+ * This preference is shown at the top of the "passwords & accounts" screen and allows the user to
+ * pick their primary credential manager provider.
+ */
+public class PrimaryProviderPreference extends GearPreference {
+
+ public static boolean shouldUseNewSettingsUi() {
+ return Flags.newSettingsUi();
+ }
+
+ private @Nullable Button mChangeButton = null;
+ private @Nullable Button mOpenButton = null;
+ private @Nullable View mButtonFrameView = null;
+ private @Nullable View mGearView = null;
+ private @Nullable Delegate mDelegate = null;
+ private boolean mButtonsVisible = false;
+ private boolean mOpenButtonVisible = false;
+
+ /** Called to send messages back to the parent controller. */
+ public static interface Delegate {
+ void onOpenButtonClicked();
+
+ void onChangeButtonClicked();
+ }
+
+ public PrimaryProviderPreference(
+ @NonNull Context context,
+ @NonNull AttributeSet attrs,
+ int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ initializeNewSettingsUi();
+ }
+
+ public PrimaryProviderPreference(
+ @NonNull Context context,
+ @NonNull AttributeSet attrs) {
+ super(context, attrs);
+ initializeNewSettingsUi();
+ }
+
+ private void initializeNewSettingsUi() {
+ if (!shouldUseNewSettingsUi()) {
+ return;
+ }
+
+ // Change the layout to the new settings ui.
+ setLayoutResource(R.layout.preference_credential_manager_with_buttons);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull PreferenceViewHolder holder) {
+ super.onBindViewHolder(holder);
+
+ if (shouldUseNewSettingsUi()) {
+ onBindViewHolderNewSettingsUi(holder);
+ } else {
+ onBindViewHolderOldSettingsUi(holder);
+ }
+ }
+
+ private void onBindViewHolderOldSettingsUi(PreferenceViewHolder holder) {
+ setOnPreferenceClickListener(
+ new Preference.OnPreferenceClickListener() {
+ public boolean onPreferenceClick(@NonNull Preference preference) {
+ if (mDelegate != null) {
+ mDelegate.onOpenButtonClicked();
+ return true;
+ }
+
+ return false;
+ }
+ });
+
+ // Setup the gear icon to handle opening the change provider scenario.
+ mGearView = holder.findViewById(R.id.settings_button);
+ mGearView.setVisibility(View.VISIBLE);
+ mGearView.setOnClickListener(
+ new View.OnClickListener() {
+ public void onClick(@NonNull View v) {
+ if (mDelegate != null) {
+ mDelegate.onChangeButtonClicked();
+ }
+ }
+ });
+ }
+
+ private void onBindViewHolderNewSettingsUi(PreferenceViewHolder holder) {
+ mOpenButton = (Button) holder.findViewById(R.id.open_button);
+ mOpenButton.setOnClickListener(
+ new View.OnClickListener() {
+ public void onClick(@NonNull View v) {
+ if (mDelegate != null) {
+ mDelegate.onOpenButtonClicked();
+ }
+ }
+ });
+ setVisibility(mOpenButton, mOpenButtonVisible);
+
+ mChangeButton = (Button) holder.findViewById(R.id.change_button);
+ mChangeButton.setOnClickListener(
+ new View.OnClickListener() {
+ public void onClick(@NonNull View v) {
+ if (mDelegate != null) {
+ mDelegate.onChangeButtonClicked();
+ }
+ }
+ });
+
+ mButtonFrameView = holder.findViewById(R.id.credman_button_frame);
+ mButtonFrameView.setVisibility(mButtonsVisible ? View.VISIBLE : View.GONE);
+
+ // There is a special case where if the provider == none then we should
+ // hide the buttons and when the preference is tapped we can open the
+ // provider selection dialog.
+ setOnPreferenceClickListener(
+ new Preference.OnPreferenceClickListener() {
+ public boolean onPreferenceClick(@NonNull Preference preference) {
+ return handlePreferenceClickNewSettingsUi();
+ }
+ });
+ }
+
+ private boolean handlePreferenceClickNewSettingsUi() {
+ if (mDelegate != null && !mButtonsVisible) {
+ mDelegate.onChangeButtonClicked();
+ return true;
+ }
+
+ return false;
+ }
+
+ public void setOpenButtonVisible(boolean isVisible) {
+ if (mOpenButton != null) {
+ mOpenButton.setVisibility(isVisible ? View.VISIBLE : View.GONE);
+ setVisibility(mOpenButton, isVisible);
+ }
+
+ mOpenButtonVisible = isVisible;
+ }
+
+ public void setButtonsVisible(boolean isVisible) {
+ if (mButtonFrameView != null) {
+ setVisibility(mButtonFrameView, isVisible);
+ }
+
+ mButtonsVisible = isVisible;
+ }
+
+ public void setDelegate(@NonNull Delegate delegate) {
+ mDelegate = delegate;
+ }
+
+ @Override
+ protected boolean shouldHideSecondTarget() {
+ return shouldUseNewSettingsUi();
+ }
+
+ @VisibleForTesting
+ public @Nullable Button getOpenButton() {
+ return mOpenButton;
+ }
+
+ @VisibleForTesting
+ public @Nullable Button getChangeButton() {
+ return mChangeButton;
+ }
+
+ @VisibleForTesting
+ public @Nullable View getButtonFrameView() {
+ return mButtonFrameView;
+ }
+
+ @VisibleForTesting
+ public @Nullable View getGearView() {
+ return mGearView;
+ }
+
+ private static void setVisibility(View view, boolean isVisible) {
+ view.setVisibility(isVisible ? View.VISIBLE : View.GONE);
+ }
+}
diff --git a/tests/unit/src/com/android/settings/applications/credentials/DefaultCombinedPreferenceControllerTest.java b/tests/unit/src/com/android/settings/applications/credentials/DefaultCombinedPreferenceControllerTest.java
new file mode 100644
index 0000000..301fcfa
--- /dev/null
+++ b/tests/unit/src/com/android/settings/applications/credentials/DefaultCombinedPreferenceControllerTest.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2024 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.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Looper;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+
+import androidx.preference.PreferenceViewHolder;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.settings.R;
+import com.android.settings.testutils.ResourcesUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class DefaultCombinedPreferenceControllerTest {
+
+ private Context mContext;
+ private PrimaryProviderPreference.Delegate mDelegate;
+ private AttributeSet mAttributes;
+
+ @Before
+ public void setUp() {
+ mContext = spy(ApplicationProvider.getApplicationContext());
+ if (Looper.myLooper() == null) {
+ Looper.prepare(); // needed to create the preference screen
+ }
+ mDelegate =
+ new PrimaryProviderPreference.Delegate() {
+ public void onOpenButtonClicked() {}
+
+ public void onChangeButtonClicked() {}
+ };
+ }
+
+ @Test
+ public void ensureSettingIntentNullForNewDesign() {
+ if (!PrimaryProviderPreference.shouldUseNewSettingsUi()) {
+ return;
+ }
+
+ // The setting intent should be null for the new design since this
+ // is handled by the delegate for the PrimaryProviderPreference.
+ DefaultCombinedPreferenceController dcpc =
+ new DefaultCombinedPreferenceController(mContext);
+ assertThat(dcpc.getSettingIntent(null).getPackage()).isNull();
+ }
+
+ @Test
+ public void ensureSettingIntentNotNullForOldDesign() {
+ if (PrimaryProviderPreference.shouldUseNewSettingsUi()) {
+ return;
+ }
+
+ // For the old design the setting intent should still be used.
+ DefaultCombinedPreferenceController dcpc =
+ new DefaultCombinedPreferenceController(mContext);
+ assertThat(dcpc.getSettingIntent(null).getPackage()).isNotNull();
+ }
+
+ @Test
+ public void ensureSettingsActivityIntentCreatedSuccessfully() {
+ DefaultCombinedPreferenceController dcpc =
+ new DefaultCombinedPreferenceController(mContext);
+
+ // Ensure that the settings activity is only created if we haved the right combination
+ // of package and class name.
+ assertThat(dcpc.createSettingsActivityIntent(null, null)).isNull();
+ assertThat(dcpc.createSettingsActivityIntent("", null)).isNull();
+ assertThat(dcpc.createSettingsActivityIntent("", "")).isNull();
+ assertThat(dcpc.createSettingsActivityIntent("com.test", "")).isNull();
+ assertThat(dcpc.createSettingsActivityIntent("com.test", "ClassName")).isNotNull();
+ }
+
+ @Test
+ public void ensureUpdatePreferenceForProviderPopulatesInfo() {
+ if (!PrimaryProviderPreference.shouldUseNewSettingsUi()) {
+ return;
+ }
+
+ DefaultCombinedPreferenceController dcpc =
+ new DefaultCombinedPreferenceController(mContext);
+ PrimaryProviderPreference ppp = createTestPreference();
+ Drawable appIcon = mContext.getResources().getDrawable(R.drawable.ic_settings_delete);
+
+ // Update the preference to use the provider and make sure the view
+ // was updated.
+ dcpc.updatePreferenceForProvider(ppp, "App Name", "Subtitle", appIcon, null);
+ assertThat(ppp.getTitle().toString()).isEqualTo("App Name");
+ assertThat(ppp.getSummary().toString()).isEqualTo("Subtitle");
+ assertThat(ppp.getIcon()).isEqualTo(appIcon);
+
+ // Set the preference back to none and make sure the view was updated.
+ dcpc.updatePreferenceForProvider(ppp, null, null, null, null);
+ assertThat(ppp.getTitle().toString()).isEqualTo("None");
+ assertThat(ppp.getSummary()).isNull();
+ assertThat(ppp.getIcon()).isNull();
+ }
+
+ private PrimaryProviderPreference createTestPreference() {
+ int layoutId =
+ ResourcesUtils.getResourcesId(
+ mContext, "layout", "preference_credential_manager_with_buttons");
+ PreferenceViewHolder holder =
+ PreferenceViewHolder.createInstanceForTests(
+ LayoutInflater.from(mContext).inflate(layoutId, null));
+ PreferenceViewHolder holderForTest = spy(holder);
+ View gearView = new View(mContext, null);
+ int gearId = ResourcesUtils.getResourcesId(mContext, "id", "settings_button");
+ when(holderForTest.findViewById(gearId)).thenReturn(gearView);
+
+ PrimaryProviderPreference ppp = new PrimaryProviderPreference(mContext, mAttributes);
+ ppp.setDelegate(mDelegate);
+ ppp.onBindViewHolder(holderForTest);
+ return ppp;
+ }
+}
diff --git a/tests/unit/src/com/android/settings/applications/credentials/PrimaryProviderPreferenceTest.java b/tests/unit/src/com/android/settings/applications/credentials/PrimaryProviderPreferenceTest.java
new file mode 100644
index 0000000..51a1fc4
--- /dev/null
+++ b/tests/unit/src/com/android/settings/applications/credentials/PrimaryProviderPreferenceTest.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2024 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.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.os.Looper;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+
+import androidx.preference.PreferenceViewHolder;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.settings.testutils.ResourcesUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class PrimaryProviderPreferenceTest {
+
+ private Context mContext;
+ private PrimaryProviderPreference.Delegate mDelegate;
+ private boolean mReceivedOpenButtonClicked = false;
+ private boolean mReceivedChangeButtonClicked = false;
+ private AttributeSet mAttributes;
+
+ @Before
+ public void setUp() {
+ mContext = spy(ApplicationProvider.getApplicationContext());
+ if (Looper.myLooper() == null) {
+ Looper.prepare(); // needed to create the preference screen
+ }
+ mReceivedOpenButtonClicked = false;
+ mReceivedChangeButtonClicked = false;
+ mDelegate =
+ new PrimaryProviderPreference.Delegate() {
+ public void onOpenButtonClicked() {
+ mReceivedOpenButtonClicked = true;
+ }
+
+ public void onChangeButtonClicked() {
+ mReceivedChangeButtonClicked = true;
+ }
+ };
+ }
+
+ @Test
+ public void ensureButtonsClicksCallDelegate_newDesign() {
+ if (!PrimaryProviderPreference.shouldUseNewSettingsUi()) {
+ return;
+ }
+
+ PrimaryProviderPreference ppp = createTestPreferenceWithNewLayout();
+
+ // Test that all the views & buttons are bound correctly.
+ assertThat(ppp.getOpenButton()).isNotNull();
+ assertThat(ppp.getChangeButton()).isNotNull();
+ assertThat(ppp.getButtonFrameView()).isNotNull();
+
+ // Test that clicking the open button results in the delegate being
+ // called.
+ assertThat(mReceivedOpenButtonClicked).isFalse();
+ ppp.getOpenButton().performClick();
+ assertThat(mReceivedOpenButtonClicked).isTrue();
+
+ // Test that clicking the change button results in the delegate being
+ // called.
+ assertThat(mReceivedChangeButtonClicked).isFalse();
+ ppp.getChangeButton().performClick();
+ assertThat(mReceivedChangeButtonClicked).isTrue();
+ }
+
+ @Test
+ public void ensureButtonsClicksCallDelegate_newDesign_openButtonVisibility() {
+ if (!PrimaryProviderPreference.shouldUseNewSettingsUi()) {
+ return;
+ }
+
+ PrimaryProviderPreference ppp = createTestPreferenceWithNewLayout();
+
+ // Test that the open button is visible.
+ assertThat(ppp.getOpenButton()).isNotNull();
+ assertThat(ppp.getOpenButton().getVisibility()).isEqualTo(View.GONE);
+
+ // Show the button and make sure the view was updated.
+ ppp.setOpenButtonVisible(true);
+ assertThat(ppp.getOpenButton().getVisibility()).isEqualTo(View.VISIBLE);
+
+ // Hide the button and make sure the view was updated.
+ ppp.setOpenButtonVisible(false);
+ assertThat(ppp.getOpenButton().getVisibility()).isEqualTo(View.GONE);
+ }
+
+ @Test
+ public void ensureButtonsClicksCallDelegate_newDesign_buttonsHidden() {
+ if (!PrimaryProviderPreference.shouldUseNewSettingsUi()) {
+ return;
+ }
+
+ PrimaryProviderPreference ppp = createTestPreferenceWithNewLayout();
+
+ // Test that the buttons are visible.
+ assertThat(ppp.getButtonFrameView()).isNotNull();
+ assertThat(ppp.getButtonFrameView().getVisibility()).isEqualTo(View.GONE);
+ assertThat(mReceivedChangeButtonClicked).isFalse();
+
+ // If we show the buttons the visiblility should be updated.
+ ppp.setButtonsVisible(true);
+ assertThat(ppp.getButtonFrameView().getVisibility()).isEqualTo(View.VISIBLE);
+
+ // If we hide the buttons the visibility should be updated.
+ ppp.setButtonsVisible(false);
+ assertThat(ppp.getButtonFrameView().getVisibility()).isEqualTo(View.GONE);
+ }
+
+ @Test
+ public void ensureButtonsClicksCallDelegate_oldDesign() {
+ if (PrimaryProviderPreference.shouldUseNewSettingsUi()) {
+ return;
+ }
+
+ PrimaryProviderPreference ppp = createTestPreference("preference_widget_gear");
+
+ // Test that clicking the preference results in the delegate being
+ // called.
+ assertThat(mReceivedOpenButtonClicked).isFalse();
+ ppp.getOnPreferenceClickListener().onPreferenceClick(ppp);
+ assertThat(mReceivedOpenButtonClicked).isTrue();
+
+ // Test that the gear button is present and visible.
+ assertThat(ppp.getGearView()).isNotNull();
+ assertThat(ppp.getGearView().getVisibility()).isEqualTo(View.VISIBLE);
+
+ // Test that clicking the gear button results in the delegate being
+ // called.
+ assertThat(mReceivedChangeButtonClicked).isFalse();
+ ppp.getGearView().performClick();
+ assertThat(mReceivedChangeButtonClicked).isTrue();
+ }
+
+ private PrimaryProviderPreference createTestPreferenceWithNewLayout() {
+ return createTestPreference("preference_credential_manager_with_buttons");
+ }
+
+ private PrimaryProviderPreference createTestPreference(String layoutName) {
+ int layoutId = ResourcesUtils.getResourcesId(mContext, "layout", layoutName);
+ PreferenceViewHolder holder =
+ PreferenceViewHolder.createInstanceForTests(
+ LayoutInflater.from(mContext).inflate(layoutId, null));
+ PreferenceViewHolder holderForTest = spy(holder);
+ View gearView = new View(mContext, null);
+ int gearId = ResourcesUtils.getResourcesId(mContext, "id", "settings_button");
+ when(holderForTest.findViewById(gearId)).thenReturn(gearView);
+
+ PrimaryProviderPreference ppp = new PrimaryProviderPreference(mContext, mAttributes);
+ ppp.setDelegate(mDelegate);
+ ppp.onBindViewHolder(holderForTest);
+ return ppp;
+ }
+}