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
This commit is contained in:
Becca Hughes
2024-01-10 19:20:22 +00:00
parent 306df3beaa
commit 782fcb9bd5
13 changed files with 833 additions and 114 deletions

View File

@@ -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>

View File

@@ -10739,7 +10739,7 @@
<!-- Preference category for showing auto-fill services with saved passwords. [CHAR LIMIT=60] --> <!-- Preference category for showing auto-fill services with saved passwords. [CHAR LIMIT=60] -->
<string name="autofill_passwords">Passwords</string> <string name="autofill_passwords">Passwords</string>
<!-- Preference category for showing autofill and credman services with saved credentials. [CHAR LIMIT=60] --> <!-- 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] --> <!-- Preference category for showing additional credential providers. [CHAR LIMIT=60] -->
<string name="credman_credentials">Additional providers</string> <string name="credman_credentials">Additional providers</string>
<!-- Summary for passwords settings that shows how many passwords are saved for each autofill <!-- 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> <string name="credman_keywords">data, passkey, password</string>
<!-- Keywords for the credman feature. [CHAR LIMIT=NONE] --> <!-- Keywords for the credman feature. [CHAR LIMIT=NONE] -->
<string name="credman_autofill_keywords">auto, fill, autofill, data, passkey, password</string> <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] --> <!-- Message of the warning dialog for setting the auto-fill app. [CHAR_LIMIT=NONE] -->
<string name="autofill_confirmation_message"> <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.--> <!-- 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> <string name="reset_telephony_stack_content_provider_authority" translatable="false"></string>
</resources> </resources>

View File

@@ -989,4 +989,22 @@
<item name="android:layout_alignParentTop">true</item> <item name="android:layout_alignParentTop">true</item>
</style> </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> </resources>

View File

@@ -26,15 +26,14 @@
android:order="10" android:order="10"
android:title="@string/credman_chosen_app_title"> android:title="@string/credman_chosen_app_title">
<com.android.settings.widget.GearPreference <com.android.settings.applications.credentials.PrimaryProviderPreference
android:fragment="com.android.settings.applications.credentials.DefaultCombinedPicker"
android:key="default_credman_autofill_main" android:key="default_credman_autofill_main"
android:title="@string/credman_chosen_app_title" android:title="@string/credman_chosen_app_title"
settings:keywords="@string/credman_autofill_keywords"> settings:keywords="@string/credman_autofill_keywords">
<extra <extra
android:name="for_work" android:name="for_work"
android:value="false" /> android:value="false" />
</com.android.settings.widget.GearPreference> </com.android.settings.applications.credentials.PrimaryProviderPreference>
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory <PreferenceCategory

View File

@@ -27,15 +27,14 @@
android:order="10" android:order="10"
android:title="@string/credman_chosen_app_title"> android:title="@string/credman_chosen_app_title">
<com.android.settings.widget.GearPreference <com.android.settings.applications.credentials.PrimaryProviderPreference
android:fragment="com.android.settings.applications.credentials.DefaultCombinedPicker"
android:key="default_credman_autofill_main" android:key="default_credman_autofill_main"
android:title="@string/credman_chosen_app_title" android:title="@string/credman_chosen_app_title"
settings:keywords="@string/credman_autofill_keywords"> settings:keywords="@string/credman_autofill_keywords">
<extra <extra
android:name="for_work" android:name="for_work"
android:value="false" /> android:value="false" />
</com.android.settings.widget.GearPreference> </com.android.settings.applications.credentials.PrimaryProviderPreference>
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory <PreferenceCategory

View File

@@ -27,8 +27,7 @@
android:order="10" android:order="10"
android:title="@string/credman_chosen_app_title"> android:title="@string/credman_chosen_app_title">
<com.android.settings.widget.GearPreference <com.android.settings.applications.credentials.PrimaryProviderPreference
android:fragment="com.android.settings.applications.credentials.DefaultCombinedPickerPrivate"
android:key="default_credman_autofill_private" android:key="default_credman_autofill_private"
android:title="@string/credman_chosen_app_title" android:title="@string/credman_chosen_app_title"
settings:searchable="false"> settings:searchable="false">
@@ -36,7 +35,7 @@
<extra <extra
android:name="for_work" android:name="for_work"
android:value="false" /> android:value="false" />
</com.android.settings.widget.GearPreference> </com.android.settings.applications.credentials.PrimaryProviderPreference>
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory <PreferenceCategory

View File

@@ -27,15 +27,14 @@
android:order="10" android:order="10"
android:title="@string/credman_chosen_app_title"> android:title="@string/credman_chosen_app_title">
<com.android.settings.widget.GearPreference <com.android.settings.applications.credentials.PrimaryProviderPreference
android:fragment="com.android.settings.applications.credentials.DefaultCombinedPickerWork"
android:key="default_credman_autofill_main_work" android:key="default_credman_autofill_main_work"
android:title="@string/credman_chosen_app_title" android:title="@string/credman_chosen_app_title"
settings:searchable="false"> settings:searchable="false">
<extra <extra
android:name="for_work" android:name="for_work"
android:value="true" /> android:value="true" />
</com.android.settings.widget.GearPreference> </com.android.settings.applications.credentials.PrimaryProviderPreference>
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory <PreferenceCategory

View File

@@ -18,13 +18,17 @@ package com.android.settings.applications.credentials;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo; import android.content.pm.ApplicationInfo;
import android.content.pm.ServiceInfo; import android.content.pm.ServiceInfo;
import android.credentials.CredentialProviderInfo; import android.credentials.CredentialProviderInfo;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.UserHandle;
import android.os.UserManager;
import android.service.autofill.AutofillServiceInfo; import android.service.autofill.AutofillServiceInfo;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.IconDrawableFactory; import android.util.IconDrawableFactory;
import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@@ -42,6 +46,11 @@ import java.util.Set;
* logic for each row in settings. * logic for each row in settings.
*/ */
public final class CombinedProviderInfo { 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 List<CredentialProviderInfo> mCredentialProviderInfos;
private final @Nullable AutofillServiceInfo mAutofillServiceInfo; private final @Nullable AutofillServiceInfo mAutofillServiceInfo;
private final boolean mIsDefaultAutofillProvider; private final boolean mIsDefaultAutofillProvider;
@@ -316,4 +325,44 @@ public final class CombinedProviderInfo {
return cmpi; 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;
}
} }

View File

@@ -95,9 +95,6 @@ public class CredentialManagerPreferenceController extends BasePreferenceControl
private static final String ALTERNATE_INTENT = "android.settings.SYNC_SETTINGS"; private static final String ALTERNATE_INTENT = "android.settings.SYNC_SETTINGS";
private static final String PRIMARY_INTENT = "android.settings.CREDENTIAL_PROVIDER"; private static final String PRIMARY_INTENT = "android.settings.CREDENTIAL_PROVIDER";
private static final int MAX_SELECTABLE_PROVIDERS = 5; 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 PackageManager mPm;
private final List<CredentialProviderInfo> mServices; private final List<CredentialProviderInfo> mServices;
@@ -475,6 +472,7 @@ public class CredentialManagerPreferenceController extends BasePreferenceControl
// If this provider is displayed at the top then we should not show it. // If this provider is displayed at the top then we should not show it.
if (topProvider != null if (topProvider != null
&& topProvider.getApplicationInfo() != null
&& topProvider.getApplicationInfo().packageName.equals(packageName)) { && topProvider.getApplicationInfo().packageName.equals(packageName)) {
continue; continue;
} }
@@ -484,10 +482,6 @@ public class CredentialManagerPreferenceController extends BasePreferenceControl
continue; continue;
} }
// Get the settings activity.
CharSequence settingsActivity =
combinedInfo.getCredentialProviderInfos().get(0).getSettingsActivity();
Drawable icon = combinedInfo.getAppIcon(context, getUser()); Drawable icon = combinedInfo.getAppIcon(context, getUser());
CharSequence title = combinedInfo.getAppName(context); CharSequence title = combinedInfo.getAppName(context);
@@ -499,7 +493,7 @@ public class CredentialManagerPreferenceController extends BasePreferenceControl
icon, icon,
packageName, packageName,
combinedInfo.getSettingsSubtitle(), combinedInfo.getSettingsSubtitle(),
settingsActivity); combinedInfo.getSettingsActivity());
output.put(packageName, pref); output.put(packageName, pref);
group.addPreference(pref); group.addPreference(pref);
} }
@@ -626,43 +620,12 @@ public class CredentialManagerPreferenceController extends BasePreferenceControl
@Override @Override
public void onLeftSideClicked() { public void onLeftSideClicked() {
if (settingsActivity == null) { Intent settingsIntent =
Log.w(TAG, "settingsActivity was null"); CombinedProviderInfo.createSettingsActivityIntent(
return; 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);
} }
}); });

View File

@@ -16,31 +16,32 @@
package com.android.settings.applications.credentials; package com.android.settings.applications.credentials;
import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.pm.ServiceInfo;
import android.credentials.CredentialManager; import android.credentials.CredentialManager;
import android.credentials.CredentialProviderInfo; import android.credentials.CredentialProviderInfo;
import android.graphics.drawable.Drawable;
import android.os.UserHandle; import android.os.UserHandle;
import android.provider.Settings; import android.provider.Settings;
import android.service.autofill.AutofillService; import android.service.autofill.AutofillService;
import android.service.autofill.AutofillServiceInfo; import android.service.autofill.AutofillServiceInfo;
import android.text.TextUtils;
import android.view.autofill.AutofillManager; import android.view.autofill.AutofillManager;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.NonNull;
import androidx.preference.Preference; 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.settings.applications.defaultapps.DefaultAppPreferenceController;
import com.android.settingslib.applications.DefaultAppInfo; import com.android.settingslib.applications.DefaultAppInfo;
import com.android.settingslib.widget.TwoTargetPreference;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
public class DefaultCombinedPreferenceController extends DefaultAppPreferenceController public class DefaultCombinedPreferenceController extends DefaultAppPreferenceController {
implements Preference.OnPreferenceClickListener {
private static final Intent AUTOFILL_PROBE = new Intent(AutofillService.SERVICE_INTERFACE); private static final Intent AUTOFILL_PROBE = new Intent(AutofillService.SERVICE_INTERFACE);
private static final String TAG = "DefaultCombinedPreferenceController"; private static final String TAG = "DefaultCombinedPreferenceController";
@@ -78,72 +79,80 @@ public class DefaultCombinedPreferenceController extends DefaultAppPreferenceCon
// Despite this method being called getSettingIntent this intent actually // 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 // opens the primary picker. This is so that we can swap the cog and the left
// hand side presses to align the UX. // hand side presses to align the UX.
return new Intent(mContext, CredentialsPickerActivity.class); 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
@Override // open anything since we added the buttons.
public void displayPreference(PreferenceScreen screen) { return new Intent();
super.displayPreference(screen);
final String prefKey = getPreferenceKey();
final Preference preference = screen.findPreference(prefKey);
if (preference != null) {
preference.setOnPreferenceClickListener((Preference.OnPreferenceClickListener) this);
} }
return createIntentToOpenPicker();
} }
@Override @Override
public boolean onPreferenceClick(Preference preference) { public void updateState(@NonNull Preference preference) {
// Get the selected provider.
final CombinedProviderInfo topProvider = getTopProvider(); final CombinedProviderInfo topProvider = getTopProvider();
if (topProvider == null) { if (topProvider != null && mContext != null) {
return false; 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 if (appIcon == null) {
// provider then we should open that up. preference.setIcon(null);
final String settingsActivity = topProvider.getSettingsActivity(); } else {
if (!TextUtils.isEmpty(settingsActivity)) { preference.setIcon(Utils.getSafeIcon(appIcon));
final Intent intent =
new Intent(Intent.ACTION_MAIN)
.setComponent(
new ComponentName(
topProvider.getPackageName(), settingsActivity));
startActivity(intent);
return true;
} }
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() { private @Nullable CombinedProviderInfo getTopProvider() {
List<CombinedProviderInfo> providers = getAllProviders(getUser()); return CombinedProviderInfo.getTopProvider(getAllProviders(getUser()));
return CombinedProviderInfo.getTopProvider(providers);
} }
@Override @Override
protected DefaultAppInfo getDefaultAppInfo() { 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; return null;
} }
@@ -180,4 +189,16 @@ public class DefaultCombinedPreferenceController extends DefaultAppPreferenceCon
protected int getUser() { protected int getUser() {
return UserHandle.myUserId(); 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());
}
} }

View File

@@ -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);
}
}

View File

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

View File

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