diff options
11 files changed, 150 insertions, 4 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index 04f907a59535..77e460038866 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -18792,6 +18792,7 @@ package android.hardware.biometrics { method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @Nullable public android.hardware.biometrics.PromptContentView getContentView(); method @Nullable public CharSequence getDescription(); method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @Nullable @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_LOGO) public android.graphics.Bitmap getLogoBitmap(); + method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @Nullable @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_LOGO) public String getLogoDescription(); method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @DrawableRes @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_LOGO) public int getLogoRes(); method @Nullable public CharSequence getNegativeButtonText(); method @Nullable public CharSequence getSubtitle(); @@ -18843,6 +18844,7 @@ package android.hardware.biometrics { method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setDescription(@NonNull CharSequence); method @Deprecated @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setDeviceCredentialAllowed(boolean); method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @NonNull @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_LOGO) public android.hardware.biometrics.BiometricPrompt.Builder setLogoBitmap(@NonNull android.graphics.Bitmap); + method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @NonNull @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_LOGO) public android.hardware.biometrics.BiometricPrompt.Builder setLogoDescription(@NonNull String); method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @NonNull @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_LOGO) public android.hardware.biometrics.BiometricPrompt.Builder setLogoRes(@DrawableRes int); method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setNegativeButton(@NonNull CharSequence, @NonNull java.util.concurrent.Executor, @NonNull android.content.DialogInterface.OnClickListener); method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setSubtitle(@NonNull CharSequence); diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java index bdaf9d789960..d4c58b239c84 100644 --- a/core/java/android/hardware/biometrics/BiometricPrompt.java +++ b/core/java/android/hardware/biometrics/BiometricPrompt.java @@ -200,6 +200,25 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan return this; } + /** + * Optional: Sets logo description text that will be shown on the prompt. + * + * <p> Note that using this method is not recommended in most scenarios because the calling + * application's name will be used by default. Setting the logo description is intended for + * large bundled applications that perform a wide range of functions and need to show + * distinct description for each function. + * + * @param logoDescription The logo description text that will be shown on the prompt. + * @return This builder. + */ + @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) + @RequiresPermission(SET_BIOMETRIC_DIALOG_LOGO) + @NonNull + public BiometricPrompt.Builder setLogoDescription(@NonNull String logoDescription) { + mPromptInfo.setLogoDescription(logoDescription); + return this; + } + /** * Required: Sets the title that will be shown on the prompt. @@ -743,7 +762,20 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan return mPromptInfo.getLogoBitmap(); } - + /** + * Gets the logo description for the prompt, as set by + * {@link Builder#setDescription(CharSequence)}. + * Currently for system applications use only. + * + * @return The logo description of the prompt, or null if the prompt has no logo description + * set. + */ + @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) + @RequiresPermission(SET_BIOMETRIC_DIALOG_LOGO) + @Nullable + public String getLogoDescription() { + return mPromptInfo.getLogoDescription(); + } /** * Gets the title for the prompt, as set by {@link Builder#setTitle(CharSequence)}. diff --git a/core/java/android/hardware/biometrics/PromptInfo.java b/core/java/android/hardware/biometrics/PromptInfo.java index 0f9cadc52608..2236660ee388 100644 --- a/core/java/android/hardware/biometrics/PromptInfo.java +++ b/core/java/android/hardware/biometrics/PromptInfo.java @@ -34,6 +34,7 @@ public class PromptInfo implements Parcelable { @DrawableRes private int mLogoRes = -1; @Nullable private Bitmap mLogoBitmap; + @Nullable private String mLogoDescription; @NonNull private CharSequence mTitle; private boolean mUseDefaultTitle; @Nullable private CharSequence mSubtitle; @@ -62,6 +63,7 @@ public class PromptInfo implements Parcelable { PromptInfo(Parcel in) { mLogoRes = in.readInt(); mLogoBitmap = in.readTypedObject(Bitmap.CREATOR); + mLogoDescription = in.readString(); mTitle = in.readCharSequence(); mUseDefaultTitle = in.readBoolean(); mSubtitle = in.readCharSequence(); @@ -106,6 +108,7 @@ public class PromptInfo implements Parcelable { public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mLogoRes); dest.writeTypedObject(mLogoBitmap, 0); + dest.writeString(mLogoDescription); dest.writeCharSequence(mTitle); dest.writeBoolean(mUseDefaultTitle); dest.writeCharSequence(mSubtitle); @@ -173,6 +176,8 @@ public class PromptInfo implements Parcelable { return true; } else if (mLogoBitmap != null) { return true; + } else if (mLogoDescription != null) { + return true; } return false; } @@ -189,6 +194,10 @@ public class PromptInfo implements Parcelable { checkOnlyOneLogoSet(); } + public void setLogoDescription(@NonNull String logoDescription) { + mLogoDescription = logoDescription; + } + public void setTitle(CharSequence title) { mTitle = title; } @@ -282,6 +291,10 @@ public class PromptInfo implements Parcelable { return mLogoBitmap; } + public String getLogoDescription() { + return mLogoDescription; + } + public CharSequence getTitle() { return mTitle; } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt index 72e884e9e5d6..9c2791f5a257 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt @@ -120,6 +120,7 @@ internal fun Collection<SensorPropertiesInternal?>.extractAuthenticatorTypes(): internal fun promptInfo( logoRes: Int = -1, logoBitmap: Bitmap? = null, + logoDescription: String? = null, title: String = "title", subtitle: String = "sub", description: String = "desc", @@ -132,6 +133,7 @@ internal fun promptInfo( val info = PromptInfo() info.logoRes = logoRes info.logoBitmap = logoBitmap + info.logoDescription = logoDescription info.title = title info.subtitle = subtitle info.description = description diff --git a/packages/SystemUI/res/layout/biometric_prompt_constraint_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_constraint_layout.xml index a877853eaec8..0ecdcfcbdd83 100644 --- a/packages/SystemUI/res/layout/biometric_prompt_constraint_layout.xml +++ b/packages/SystemUI/res/layout/biometric_prompt_constraint_layout.xml @@ -13,6 +13,16 @@ android:layout_height="match_parent"> android:scaleType="fitXY" android:visibility="gone" /> + <TextView + android:id="@+id/logo_description" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="@integer/biometric_dialog_text_gravity" + android:singleLine="true" + android:marqueeRepeatLimit="1" + android:ellipsize="marquee" + android:visibility="gone"/> + <ImageView android:id="@+id/background" android:layout_width="0dp" diff --git a/packages/SystemUI/res/layout/biometric_prompt_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_layout.xml index 10f71134c4cc..e7590746207d 100644 --- a/packages/SystemUI/res/layout/biometric_prompt_layout.xml +++ b/packages/SystemUI/res/layout/biometric_prompt_layout.xml @@ -28,6 +28,15 @@ android:scaleType="fitXY"/> <TextView + android:id="@+id/logo_description" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="@integer/biometric_dialog_text_gravity" + android:singleLine="true" + android:marqueeRepeatLimit="1" + android:ellipsize="marquee"/> + + <TextView android:id="@+id/title" android:layout_width="match_parent" android:layout_height="wrap_content" diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt index c17c8dced668..6133a51c6497 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt @@ -40,6 +40,7 @@ sealed class BiometricPromptRequest( val contentView: PromptContentView? = info.contentView val logoRes: Int = info.logoRes val logoBitmap: Bitmap? = info.logoBitmap + val logoDescription: String? = info.logoDescription val negativeButtonText: String = info.negativeButtonText?.toString() ?: "" } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt index efad21bda380..31aadf51c4f2 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt @@ -95,6 +95,7 @@ object BiometricViewBinder { view.resources.getColor(R.color.biometric_dialog_gray, view.context.theme) val logoView = view.requireViewById<ImageView>(R.id.logo) + val logoDescriptionView = view.requireViewById<TextView>(R.id.logo_description) val titleView = view.requireViewById<TextView>(R.id.title) val subtitleView = view.requireViewById<TextView>(R.id.subtitle) val descriptionView = view.requireViewById<TextView>(R.id.description) @@ -104,6 +105,8 @@ object BiometricViewBinder { // set selected to enable marquee unless a screen reader is enabled logoView.isSelected = !accessibilityManager.isEnabled || !accessibilityManager.isTouchExplorationEnabled + logoDescriptionView.isSelected = + !accessibilityManager.isEnabled || !accessibilityManager.isTouchExplorationEnabled titleView.isSelected = !accessibilityManager.isEnabled || !accessibilityManager.isTouchExplorationEnabled subtitleView.isSelected = @@ -165,6 +168,7 @@ object BiometricViewBinder { } logoView.setImageDrawable(viewModel.logo.first()) + logoDescriptionView.text = viewModel.logoDescription.first() titleView.text = viewModel.title.first() subtitleView.text = viewModel.subtitle.first() descriptionView.text = viewModel.description.first() @@ -197,6 +201,7 @@ object BiometricViewBinder { viewsToHideWhenSmall = listOf( logoView, + logoDescriptionView, titleView, subtitleView, descriptionView, @@ -205,6 +210,7 @@ object BiometricViewBinder { viewsToFadeInOnSizeChange = listOf( logoView, + logoDescriptionView, titleView, subtitleView, descriptionView, diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt index ef5c37eaa953..788991d2e45b 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt @@ -17,6 +17,7 @@ package com.android.systemui.biometrics.ui.viewmodel import android.content.Context +import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.graphics.Rect import android.graphics.drawable.BitmapDrawable @@ -280,8 +281,9 @@ constructor( it.logoBitmap != null -> BitmapDrawable(context.resources, it.logoBitmap) else -> try { - context.packageManager.getApplicationIcon(it.opPackageName) - } catch (e: PackageManager.NameNotFoundException) { + val info = context.getApplicationInfo(it.opPackageName) + context.packageManager.getApplicationIcon(info) + } catch (e: Exception) { Log.w(TAG, "Cannot find icon for package " + it.opPackageName, e) null } @@ -289,6 +291,25 @@ constructor( } .distinctUntilChanged() + /** Logo description for the prompt. */ + val logoDescription: Flow<String> = + promptSelectorInteractor.prompt + .map { + when { + !customBiometricPrompt() || it == null -> "" + it.logoDescription != null -> it.logoDescription + else -> + try { + val info = context.getApplicationInfo(it.opPackageName) + context.packageManager.getApplicationLabel(info).toString() + } catch (e: Exception) { + Log.w(TAG, "Cannot find name for package " + it.opPackageName, e) + "" + } + } + } + .distinctUntilChanged() + /** Title for the prompt. */ val title: Flow<String> = promptSelectorInteractor.prompt.map { it?.title ?: "" }.distinctUntilChanged() @@ -682,6 +703,12 @@ constructor( } } +private fun Context.getApplicationInfo(packageName: String): ApplicationInfo = + packageManager.getApplicationInfo( + packageName, + PackageManager.MATCH_DISABLED_COMPONENTS or PackageManager.MATCH_ANY_USER + ) + /** How the fingerprint sensor was started for the prompt. */ enum class FingerprintStartMode { /** Fingerprint sensor has not started. */ diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt index a46167a423f1..8fab2332c00e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt @@ -26,6 +26,7 @@ class BiometricPromptRequestTest : SysuiTestCase() { @Test fun biometricRequestFromPromptInfo() { val logoRes = R.drawable.ic_cake + val logoDescription = "test cake" val title = "what" val subtitle = "a" val description = "request" @@ -41,6 +42,7 @@ class BiometricPromptRequestTest : SysuiTestCase() { BiometricPromptRequest.Biometric( promptInfo( logoRes = logoRes, + logoDescription = logoDescription, title = title, subtitle = subtitle, description = description, @@ -53,6 +55,7 @@ class BiometricPromptRequestTest : SysuiTestCase() { ) assertThat(request.logoRes).isEqualTo(logoRes) + assertThat(request.logoDescription).isEqualTo(logoDescription) assertThat(request.title).isEqualTo(title) assertThat(request.subtitle).isEqualTo(subtitle) assertThat(request.description).isEqualTo(description) diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt index 2e94d381b8dc..ff68fe3df9e9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.biometrics.ui.viewmodel +import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.content.res.Configuration import android.graphics.Bitmap @@ -74,6 +75,8 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.eq import org.mockito.Mock import org.mockito.junit.MockitoJUnit @@ -95,6 +98,8 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa @Mock private lateinit var selectedUserInteractor: SelectedUserInteractor @Mock private lateinit var udfpsUtils: UdfpsUtils @Mock private lateinit var packageManager: PackageManager + @Mock private lateinit var applicationInfoWithIcon: ApplicationInfo + @Mock private lateinit var applicationInfoNoIcon: ApplicationInfo private val fakeExecutor = FakeExecutor(FakeSystemClock()) private val testScope = TestScope() @@ -102,6 +107,8 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa private val logoResFromApp = R.drawable.ic_cake private val logoFromApp = context.getDrawable(logoResFromApp) private val logoBitmapFromApp = Bitmap.createBitmap(400, 400, Bitmap.Config.RGB_565) + private val defaultLogoDescription = "Test Android App" + private val logoDescriptionFromApp = "Test Cake App" private lateinit var fingerprintRepository: FakeFingerprintPropertyRepository private lateinit var promptRepository: FakePromptRepository @@ -166,7 +173,14 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa iconViewModel = viewModel.iconViewModel // Set up default logo icon and app customized icon - whenever(packageManager.getApplicationIcon(OP_PACKAGE_NAME)).thenReturn(defaultLogoIcon) + whenever(packageManager.getApplicationInfo(eq(OP_PACKAGE_NAME_NO_ICON), anyInt())) + .thenReturn(applicationInfoNoIcon) + whenever(packageManager.getApplicationInfo(eq(OP_PACKAGE_NAME), anyInt())) + .thenReturn(applicationInfoWithIcon) + whenever(packageManager.getApplicationIcon(applicationInfoWithIcon)) + .thenReturn(defaultLogoIcon) + whenever(packageManager.getApplicationLabel(applicationInfoWithIcon)) + .thenReturn(defaultLogoDescription) context.setMockPackageManager(packageManager) val resources = context.getOrCreateTestableResources() resources.addOverride(logoResFromApp, logoFromApp) @@ -1277,6 +1291,29 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa assertThat((logo as BitmapDrawable).bitmap).isEqualTo(logoBitmapFromApp) } + @Test + fun logoDescriptionIsEmptyIfPackageNameNotFound() = + runGenericTest(packageName = OP_PACKAGE_NAME_NO_ICON) { + mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) + val logoDescription by collectLastValue(viewModel.logoDescription) + assertThat(logoDescription).isEqualTo("") + } + + @Test + fun defaultLogoDescriptionIfNoLogoDescriptionSet() = runGenericTest { + mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) + val logoDescription by collectLastValue(viewModel.logoDescription) + assertThat(logoDescription).isEqualTo(defaultLogoDescription) + } + + @Test + fun logoDescriptionSetByApp() = + runGenericTest(logoDescription = logoDescriptionFromApp) { + mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) + val logoDescription by collectLastValue(viewModel.logoDescription) + assertThat(logoDescription).isEqualTo(logoDescriptionFromApp) + } + /** Asserts that the selected buttons are visible now. */ private suspend fun TestScope.assertButtonsVisible( tryAgain: Boolean = false, @@ -1300,6 +1337,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa contentView: PromptContentView? = null, logoRes: Int = -1, logoBitmap: Bitmap? = null, + logoDescription: String? = null, packageName: String = OP_PACKAGE_NAME, block: suspend TestScope.() -> Unit, ) { @@ -1312,6 +1350,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa contentViewFromApp = contentView, logoResFromApp = logoRes, logoBitmapFromApp = logoBitmap, + logoDescriptionFromApp = logoDescription, packageName = packageName, ) @@ -1492,12 +1531,14 @@ private fun PromptSelectorInteractor.initializePrompt( contentViewFromApp: PromptContentView? = null, logoResFromApp: Int = -1, logoBitmapFromApp: Bitmap? = null, + logoDescriptionFromApp: String? = null, packageName: String = OP_PACKAGE_NAME, ) { val info = PromptInfo().apply { logoRes = logoResFromApp logoBitmap = logoBitmapFromApp + logoDescription = logoDescriptionFromApp title = "t" subtitle = "s" description = descriptionFromApp |