diff options
25 files changed, 335 insertions, 15 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index 5fb94dfe47bf..f052b85a9f44 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -18679,6 +18679,8 @@ package android.hardware.biometrics { method @Nullable public int getAllowedAuthenticators(); 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.permission.MANAGE_BIOMETRIC_DIALOG") public android.graphics.Bitmap getLogoBitmap(); + method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @DrawableRes @RequiresPermission("android.permission.MANAGE_BIOMETRIC_DIALOG") public int getLogoRes(); method @Nullable public CharSequence getNegativeButtonText(); method @Nullable public CharSequence getSubtitle(); method @NonNull public CharSequence getTitle(); @@ -18728,6 +18730,8 @@ package android.hardware.biometrics { method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setContentView(@NonNull android.hardware.biometrics.PromptContentView); 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.permission.MANAGE_BIOMETRIC_DIALOG") public android.hardware.biometrics.BiometricPrompt.Builder setLogo(@DrawableRes int); + method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @NonNull @RequiresPermission("android.permission.MANAGE_BIOMETRIC_DIALOG") public android.hardware.biometrics.BiometricPrompt.Builder setLogo(@NonNull android.graphics.Bitmap); 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); method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setTitle(@NonNull CharSequence); diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java index a0f4d8df0bd5..c0424dbeb813 100644 --- a/core/java/android/hardware/biometrics/BiometricPrompt.java +++ b/core/java/android/hardware/biometrics/BiometricPrompt.java @@ -16,6 +16,7 @@ package android.hardware.biometrics; +import static android.Manifest.permission.MANAGE_BIOMETRIC_DIALOG; import static android.Manifest.permission.TEST_BIOMETRIC; import static android.Manifest.permission.USE_BIOMETRIC; import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL; @@ -25,6 +26,7 @@ import static android.hardware.biometrics.Flags.FLAG_GET_OP_ID_CRYPTO_OBJECT; import static android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT; import android.annotation.CallbackExecutor; +import android.annotation.DrawableRes; import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; @@ -33,6 +35,7 @@ import android.annotation.RequiresPermission; import android.annotation.TestApi; import android.content.Context; import android.content.DialogInterface; +import android.graphics.Bitmap; import android.hardware.face.FaceManager; import android.hardware.fingerprint.FingerprintManager; import android.os.Binder; @@ -160,6 +163,45 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan } /** + * Optional: Sets the drawable resource of the logo that will be shown on the prompt. + * + * <p> Note that using this method is not recommended in most scenarios because the calling + * application's icon will be used by default. Setting the logo is intended for large + * bundled applications that perform a wide range of functions and need to show distinct + * icons for each function. + * + * @param logoRes A drawable resource of the logo that will be shown on the prompt. + * @return This builder. + */ + @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) + @RequiresPermission(MANAGE_BIOMETRIC_DIALOG) + @NonNull + public BiometricPrompt.Builder setLogo(@DrawableRes int logoRes) { + mPromptInfo.setLogoRes(logoRes); + return this; + } + + /** + * Optional: Sets the bitmap drawable of the logo that will be shown on the prompt. + * + * <p> Note that using this method is not recommended in most scenarios because the calling + * application's icon will be used by default. Setting the logo is intended for large + * bundled applications that perform a wide range of functions and need to show distinct + * icons for each function. + * + * @param logoBitmap A bitmap drawable of the logo that will be shown on the prompt. + * @return This builder. + */ + @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) + @RequiresPermission(MANAGE_BIOMETRIC_DIALOG) + @NonNull + public BiometricPrompt.Builder setLogo(@NonNull Bitmap logoBitmap) { + mPromptInfo.setLogoBitmap(logoBitmap); + return this; + } + + + /** * Required: Sets the title that will be shown on the prompt. * @param title The title to display. * @return This builder. @@ -676,6 +718,34 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan } /** + * Gets the drawable resource of the logo for the prompt, as set by + * {@link Builder#setLogo(int)}. Currently for system applications use only. + * + * @return The drawable resource of the logo, or -1 if the prompt has no logo resource set. + */ + @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) + @RequiresPermission(MANAGE_BIOMETRIC_DIALOG) + @DrawableRes + public int getLogoRes() { + return mPromptInfo.getLogoRes(); + } + + /** + * Gets the logo bitmap for the prompt, as set by {@link Builder#setLogo(Bitmap)}. Currently for + * system applications use only. + * + * @return The logo bitmap of the prompt, or null if the prompt has no logo bitmap set. + */ + @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) + @RequiresPermission(MANAGE_BIOMETRIC_DIALOG) + @Nullable + public Bitmap getLogoBitmap() { + return mPromptInfo.getLogoBitmap(); + } + + + + /** * Gets the title for the prompt, as set by {@link Builder#setTitle(CharSequence)}. * @return The title of the prompt, which is guaranteed to be non-null. */ diff --git a/core/java/android/hardware/biometrics/PromptInfo.java b/core/java/android/hardware/biometrics/PromptInfo.java index c73ebd4dbe76..d788b37c781d 100644 --- a/core/java/android/hardware/biometrics/PromptInfo.java +++ b/core/java/android/hardware/biometrics/PromptInfo.java @@ -16,8 +16,10 @@ package android.hardware.biometrics; +import android.annotation.DrawableRes; import android.annotation.NonNull; import android.annotation.Nullable; +import android.graphics.Bitmap; import android.os.Parcel; import android.os.Parcelable; @@ -30,6 +32,8 @@ import java.util.List; */ public class PromptInfo implements Parcelable { + @DrawableRes private int mLogoRes = -1; + @Nullable private Bitmap mLogoBitmap; @NonNull private CharSequence mTitle; private boolean mUseDefaultTitle; @Nullable private CharSequence mSubtitle; @@ -56,6 +60,8 @@ public class PromptInfo implements Parcelable { } PromptInfo(Parcel in) { + mLogoRes = in.readInt(); + mLogoBitmap = in.readTypedObject(Bitmap.CREATOR); mTitle = in.readCharSequence(); mUseDefaultTitle = in.readBoolean(); mSubtitle = in.readCharSequence(); @@ -98,6 +104,8 @@ public class PromptInfo implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mLogoRes); + dest.writeTypedObject(mLogoBitmap, 0); dest.writeCharSequence(mTitle); dest.writeBoolean(mUseDefaultTitle); dest.writeCharSequence(mSubtitle); @@ -156,9 +164,30 @@ public class PromptInfo implements Parcelable { } return false; } + + /** + * Returns whether MANAGE_BIOMETRIC_DIALOG is contained. + */ + public boolean containsManageBioApiConfigurations() { + if (mLogoRes != -1) { + return true; + } else if (mLogoBitmap != null) { + return true; + } + return false; + } // LINT.ThenChange(frameworks/base/core/java/android/hardware/biometrics/BiometricPrompt.java) // Setters + public void setLogoRes(@DrawableRes int logoRes) { + mLogoRes = logoRes; + checkOnlyOneLogoSet(); + } + + public void setLogoBitmap(@NonNull Bitmap logoBitmap) { + mLogoBitmap = logoBitmap; + checkOnlyOneLogoSet(); + } public void setTitle(CharSequence title) { mTitle = title; @@ -244,6 +273,14 @@ public class PromptInfo implements Parcelable { } // Getters + @DrawableRes + public int getLogoRes() { + return mLogoRes; + } + + public Bitmap getLogoBitmap() { + return mLogoBitmap; + } public CharSequence getTitle() { return mTitle; @@ -337,4 +374,11 @@ public class PromptInfo implements Parcelable { public boolean isShowEmergencyCallButton() { return mShowEmergencyCallButton; } + + private void checkOnlyOneLogoSet() { + if (mLogoRes != -1 && mLogoBitmap != null) { + throw new IllegalStateException( + "Exclusively one of logo resource or logo bitmap can be set"); + } + } } diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 42068aa14cda..4be75f83422e 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -426,6 +426,7 @@ applications that come with the platform <!-- Permissions required for CTS test - android.server.biometrics --> <permission name="android.permission.USE_BIOMETRIC" /> <permission name="android.permission.TEST_BIOMETRIC" /> + <permission name="android.permission.MANAGE_BIOMETRIC_DIALOG" /> <!-- Permissions required for CTS test - CtsContactsProviderTestCases --> <permission name="android.contacts.permission.MANAGE_SIM_ACCOUNTS" /> <!-- Permissions required for CTS test - CtsHdmiCecHostTestCases --> diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index f204e4814579..3dfc4540d6e7 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -559,6 +559,9 @@ <!-- Permission required for CTS test - android.server.biometrics --> <uses-permission android:name="android.permission.TEST_BIOMETRIC" /> + <!-- Permission required for CTS test - android.server.biometrics --> + <uses-permission android:name="android.permission.MANAGE_BIOMETRIC_DIALOG" /> + <!-- Permissions required for CTS test - NotificationManagerTest --> <uses-permission android:name="android.permission.MANAGE_NOTIFICATION_LISTENERS" /> 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 4a39799fd64f..72e884e9e5d6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt @@ -16,6 +16,7 @@ package com.android.systemui.biometrics +import android.graphics.Bitmap import android.hardware.biometrics.BiometricManager.Authenticators import android.hardware.biometrics.ComponentInfoInternal import android.hardware.biometrics.PromptContentView @@ -117,6 +118,8 @@ internal fun Collection<SensorPropertiesInternal?>.extractAuthenticatorTypes(): } internal fun promptInfo( + logoRes: Int = -1, + logoBitmap: Bitmap? = null, title: String = "title", subtitle: String = "sub", description: String = "desc", @@ -127,6 +130,8 @@ internal fun promptInfo( negativeButton: String = "neg", ): PromptInfo { val info = PromptInfo() + info.logoRes = logoRes + info.logoBitmap = logoBitmap info.title = title info.subtitle = subtitle info.description = description diff --git a/packages/SystemUI/res/layout/biometric_prompt_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_layout.xml index 23fbb12f3036..10f71134c4cc 100644 --- a/packages/SystemUI/res/layout/biometric_prompt_layout.xml +++ b/packages/SystemUI/res/layout/biometric_prompt_layout.xml @@ -20,6 +20,13 @@ android:layout_height="wrap_content" android:orientation="vertical"> + <ImageView + android:id="@+id/logo" + android:layout_width="@dimen/biometric_auth_icon_size" + android:layout_height="@dimen/biometric_auth_icon_size" + android:layout_gravity="center" + android:scaleType="fitXY"/> + <TextView android:id="@+id/title" android:layout_width="match_parent" diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java index ab23564a1df4..57e308ff16e8 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java @@ -399,7 +399,8 @@ public class AuthContainerView extends LinearLayout config.mPromptInfo, config.mUserId, config.mOperationId, - new BiometricModalities(fpProps, faceProps)); + new BiometricModalities(fpProps, faceProps), + config.mOpPackageName); final BiometricPromptLayout view = (BiometricPromptLayout) layoutInflater.inflate( R.layout.biometric_prompt_layout, null, false); @@ -470,7 +471,8 @@ public class AuthContainerView extends LinearLayout mBackgroundView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); mPromptSelectorInteractorProvider.get().useCredentialsForAuthentication( - mConfig.mPromptInfo, credentialType, mConfig.mUserId, mConfig.mOperationId); + mConfig.mPromptInfo, credentialType, mConfig.mUserId, mConfig.mOperationId, + mConfig.mOpPackageName); final CredentialViewModel vm = mCredentialViewModelProvider.get(); vm.setAnimateContents(animateContents); ((CredentialView) mCredentialView).init(vm, this, mPanelController, animatePanel); diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java index 86802a5b58b0..02eae9cedf74 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java @@ -148,6 +148,12 @@ public class UdfpsDialogMeasureAdapter { || child.getId() == R.id.customized_view_container) { //skip description view and compute later continue; + } else if (child.getId() == R.id.logo) { + child.measure( + MeasureSpec.makeMeasureSpec(child.getLayoutParams().width, + MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(child.getLayoutParams().height, + MeasureSpec.EXACTLY)); } else { child.measure( MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt index b35fbbc7bb32..ad7bb0e61178 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt @@ -55,6 +55,9 @@ interface PromptRepository { /** The kind of credential to use (biometric, pin, pattern, etc.). */ val kind: StateFlow<PromptKind> + /** The package name that the prompt is called from. */ + val opPackageName: StateFlow<String?> + /** * If explicit confirmation is required. * @@ -68,6 +71,7 @@ interface PromptRepository { userId: Int, gatekeeperChallenge: Long?, kind: PromptKind, + opPackageName: String, ) /** Unset the prompt info. */ @@ -108,6 +112,9 @@ constructor( private val _kind: MutableStateFlow<PromptKind> = MutableStateFlow(PromptKind.Biometric()) override val kind = _kind.asStateFlow() + private val _opPackageName: MutableStateFlow<String?> = MutableStateFlow(null) + override val opPackageName = _opPackageName.asStateFlow() + private val _faceSettings = _userId.map { id -> faceSettings.forUser(id) }.distinctUntilChanged() private val _faceSettingAlwaysRequireConfirmation = @@ -127,11 +134,13 @@ constructor( userId: Int, gatekeeperChallenge: Long?, kind: PromptKind, + opPackageName: String, ) { _kind.value = kind _userId.value = userId _challenge.value = gatekeeperChallenge _promptInfo.value = promptInfo + _opPackageName.value = opPackageName } override fun unsetPrompt() { @@ -139,6 +148,7 @@ constructor( _userId.value = null _challenge.value = null _kind.value = PromptKind.Biometric() + _opPackageName.value = null } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt index ac4b717a23ec..359e2e703ee6 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt @@ -115,12 +115,14 @@ constructor( @Utils.CredentialType kind: Int, userId: Int, challenge: Long, + opPackageName: String, ) { biometricPromptRepository.setPrompt( promptInfo, userId, challenge, - kind.asBiometricPromptCredential() + kind.asBiometricPromptCredential(), + opPackageName, ) } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt index 65a2c0a2490f..b3f95748ccdc 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt @@ -76,6 +76,7 @@ interface PromptSelectorInteractor { userId: Int, challenge: Long, modalities: BiometricModalities, + opPackageName: String, ) /** Use credential-based authentication instead of biometrics. */ @@ -84,6 +85,7 @@ interface PromptSelectorInteractor { @Utils.CredentialType kind: Int, userId: Int, challenge: Long, + opPackageName: String, ) /** Unset the current authentication request. */ @@ -104,9 +106,12 @@ constructor( promptRepository.promptInfo, promptRepository.challenge, promptRepository.userId, - promptRepository.kind - ) { promptInfo, challenge, userId, kind -> - if (promptInfo == null || userId == null || challenge == null) { + promptRepository.kind, + promptRepository.opPackageName, + ) { promptInfo, challenge, userId, kind, opPackageName -> + if ( + promptInfo == null || userId == null || challenge == null || opPackageName == null + ) { return@combine null } @@ -117,6 +122,7 @@ constructor( userInfo = BiometricUserInfo(userId = userId), operationInfo = BiometricOperationInfo(gatekeeperChallenge = challenge), modalities = kind.activeModalities, + opPackageName = opPackageName, ) else -> null } @@ -152,13 +158,15 @@ constructor( promptInfo: PromptInfo, userId: Int, challenge: Long, - modalities: BiometricModalities + modalities: BiometricModalities, + opPackageName: String, ) { promptRepository.setPrompt( promptInfo = promptInfo, userId = userId, gatekeeperChallenge = challenge, kind = PromptKind.Biometric(modalities), + opPackageName = opPackageName, ) } @@ -167,12 +175,14 @@ constructor( @Utils.CredentialType kind: Int, userId: Int, challenge: Long, + opPackageName: String, ) { promptRepository.setPrompt( promptInfo = promptInfo, userId = userId, gatekeeperChallenge = challenge, kind = kind.asBiometricPromptCredential(), + opPackageName = opPackageName, ) } 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 437793798567..c17c8dced668 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 @@ -1,5 +1,6 @@ package com.android.systemui.biometrics.domain.model +import android.graphics.Bitmap import android.hardware.biometrics.PromptContentView import android.hardware.biometrics.PromptInfo import com.android.systemui.biometrics.shared.model.BiometricModalities @@ -26,6 +27,7 @@ sealed class BiometricPromptRequest( userInfo: BiometricUserInfo, operationInfo: BiometricOperationInfo, val modalities: BiometricModalities, + val opPackageName: String, ) : BiometricPromptRequest( title = info.title?.toString() ?: "", @@ -36,6 +38,8 @@ sealed class BiometricPromptRequest( showEmergencyCallButton = info.isShowEmergencyCallButton ) { val contentView: PromptContentView? = info.contentView + val logoRes: Int = info.logoRes + val logoBitmap: Bitmap? = info.logoBitmap val negativeButtonText: String = info.negativeButtonText?.toString() ?: "" } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java index 60b454e9670e..b450896729b7 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java @@ -115,6 +115,12 @@ public class BiometricPromptLayout extends LinearLayout { MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(iconView.getLayoutParams().height, MeasureSpec.EXACTLY)); + } else if (child.getId() == R.id.logo) { + child.measure( + MeasureSpec.makeMeasureSpec(child.getLayoutParams().width, + MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(child.getLayoutParams().height, + MeasureSpec.EXACTLY)); } else if (child.getId() == R.id.biometric_icon) { child.measure( MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST), 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 04dc7a8da9f3..285ab4a800b6 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 @@ -31,6 +31,7 @@ import android.view.View import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO import android.view.accessibility.AccessibilityManager import android.widget.Button +import android.widget.ImageView import android.widget.ScrollView import android.widget.TextView import androidx.lifecycle.DefaultLifecycleObserver @@ -92,6 +93,7 @@ object BiometricViewBinder { val textColorHint = view.resources.getColor(R.color.biometric_dialog_gray, view.context.theme) + val logoView = view.requireViewById<ImageView>(R.id.logo) val titleView = view.requireViewById<TextView>(R.id.title) val subtitleView = view.requireViewById<TextView>(R.id.subtitle) val descriptionView = view.requireViewById<TextView>(R.id.description) @@ -99,6 +101,8 @@ object BiometricViewBinder { view.requireViewById<ScrollView>(R.id.customized_view_container) // set selected to enable marquee unless a screen reader is enabled + logoView.isSelected = + !accessibilityManager.isEnabled || !accessibilityManager.isTouchExplorationEnabled titleView.isSelected = !accessibilityManager.isEnabled || !accessibilityManager.isTouchExplorationEnabled subtitleView.isSelected = @@ -152,6 +156,7 @@ object BiometricViewBinder { } } + logoView.setImageDrawable(viewModel.logo.first()) titleView.text = viewModel.title.first() subtitleView.text = viewModel.subtitle.first() descriptionView.text = viewModel.description.first() @@ -183,6 +188,7 @@ object BiometricViewBinder { viewModel = viewModel, viewsToHideWhenSmall = listOf( + logoView, titleView, subtitleView, descriptionView, @@ -190,6 +196,7 @@ object BiometricViewBinder { ), viewsToFadeInOnSizeChange = listOf( + logoView, titleView, subtitleView, descriptionView, diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt index c3bbaedb2670..d5695f31f121 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt @@ -25,6 +25,7 @@ import android.view.ViewGroup import android.view.WindowInsets import android.view.WindowManager import android.view.accessibility.AccessibilityManager +import android.widget.ImageView import android.widget.TextView import androidx.core.animation.addListener import androidx.core.view.doOnLayout @@ -234,7 +235,13 @@ private fun View.isLandscape(): Boolean { private fun View.showContentOrHide(forceHide: Boolean = false) { val isTextViewWithBlankText = this is TextView && this.text.isBlank() - visibility = if (forceHide || isTextViewWithBlankText) View.GONE else View.VISIBLE + val isImageViewWithoutImage = this is ImageView && this.drawable == null + visibility = + if (forceHide || isTextViewWithBlankText || isImageViewWithoutImage) { + View.GONE + } else { + View.VISIBLE + } } private fun View.asVerticalAnimator( 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 1c789283ec70..dca0338dc8e7 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 @@ -18,6 +18,8 @@ package com.android.systemui.biometrics.ui.viewmodel import android.content.Context import android.graphics.Rect +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable import android.hardware.biometrics.BiometricPrompt import android.hardware.biometrics.PromptContentView import android.util.Log @@ -233,6 +235,19 @@ constructor( } } + /** Logo for the prompt. */ + val logo: Flow<Drawable?> = + promptSelectorInteractor.prompt + .map { + when { + it == null -> null + it.logoRes != -1 -> context.resources.getDrawable(it.logoRes, context.theme) + it.logoBitmap != null -> BitmapDrawable(context.resources, it.logoBitmap) + else -> context.packageManager.getApplicationIcon(it.opPackageName) + } + } + .distinctUntilChanged() + /** Title for the prompt. */ val title: Flow<String> = promptSelectorInteractor.prompt.map { it?.title ?: "" }.distinctUntilChanged() diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt index 0ee09390d03a..43f7c60721ee 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.biometrics import android.app.admin.DevicePolicyManager +import android.content.pm.PackageManager import android.hardware.biometrics.BiometricAuthenticator import android.hardware.biometrics.BiometricConstants import android.hardware.biometrics.BiometricManager @@ -79,6 +80,8 @@ import org.mockito.Mockito.verify import org.mockito.junit.MockitoJUnit import org.mockito.Mockito.`when` as whenever +private const val OP_PACKAGE_NAME = "biometric.testapp" + @RunWith(AndroidJUnit4::class) @RunWithLooper(setAsMainLooper = true) @SmallTest @@ -109,6 +112,8 @@ open class AuthContainerViewTest : SysuiTestCase() { lateinit var authController: AuthController @Mock lateinit var selectedUserInteractor: SelectedUserInteractor + @Mock + private lateinit var packageManager: PackageManager private val testScope = TestScope(StandardTestDispatcher()) private val fakeExecutor = FakeExecutor(FakeSystemClock()) @@ -134,6 +139,7 @@ open class AuthContainerViewTest : SysuiTestCase() { private lateinit var udfpsOverlayInteractor: UdfpsOverlayInteractor private val credentialViewModel = CredentialViewModel(mContext, bpCredentialInteractor) + private val defaultLogoIcon = context.getDrawable(R.drawable.ic_android) private var authContainer: TestAuthContainerView? = null @@ -156,6 +162,9 @@ open class AuthContainerViewTest : SysuiTestCase() { selectedUserInteractor, testScope.backgroundScope, ) + // Set up default logo icon + whenever(packageManager.getApplicationIcon(OP_PACKAGE_NAME)).thenReturn(defaultLogoIcon) + context.setMockPackageManager(packageManager) } @After @@ -533,6 +542,7 @@ open class AuthContainerViewTest : SysuiTestCase() { mPromptInfo = PromptInfo().apply { this.authenticators = authenticators } + mOpPackageName = OP_PACKAGE_NAME }, testScope.backgroundScope, fingerprintProps, diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt index ec7ce634fd78..b39e09df9d2e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt @@ -43,6 +43,7 @@ import org.mockito.junit.MockitoJUnit private const val USER_ID = 9 private const val CHALLENGE = 90L +private const val OP_PACKAGE_NAME = "biometric.testapp" @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -102,7 +103,8 @@ class PromptRepositoryImplTest : SysuiTestCase() { PromptInfo().apply { isConfirmationRequested = case }, USER_ID, CHALLENGE, - PromptKind.Biometric() + PromptKind.Biometric(), + OP_PACKAGE_NAME ) assertThat(isConfirmationRequired).isEqualTo(case) @@ -120,7 +122,8 @@ class PromptRepositoryImplTest : SysuiTestCase() { PromptInfo().apply { isConfirmationRequested = case }, USER_ID, CHALLENGE, - PromptKind.Biometric() + PromptKind.Biometric(), + OP_PACKAGE_NAME ) assertThat(isConfirmationRequired).isTrue() @@ -133,17 +136,19 @@ class PromptRepositoryImplTest : SysuiTestCase() { val kind = PromptKind.Pin val promptInfo = PromptInfo() - repository.setPrompt(promptInfo, USER_ID, CHALLENGE, kind) + repository.setPrompt(promptInfo, USER_ID, CHALLENGE, kind, OP_PACKAGE_NAME) assertThat(repository.kind.value).isEqualTo(kind) assertThat(repository.userId.value).isEqualTo(USER_ID) assertThat(repository.challenge.value).isEqualTo(CHALLENGE) assertThat(repository.promptInfo.value).isSameInstanceAs(promptInfo) + assertThat(repository.opPackageName.value).isEqualTo(OP_PACKAGE_NAME) repository.unsetPrompt() assertThat(repository.promptInfo.value).isNull() assertThat(repository.userId.value).isNull() assertThat(repository.challenge.value).isNull() + assertThat(repository.opPackageName.value).isNull() } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt index dcefea28d4c8..8a46c0c6da9f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt @@ -30,6 +30,7 @@ import org.mockito.junit.MockitoJUnit private const val USER_ID = 22 private const val OPERATION_ID = 100L +private const val OP_PACKAGE_NAME = "biometric.testapp" @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -114,7 +115,8 @@ class PromptCredentialInteractorTest : SysuiTestCase() { }, kind = kind, userId = USER_ID, - challenge = OPERATION_ID + challenge = OPERATION_ID, + opPackageName = OP_PACKAGE_NAME ) assertThat(prompt?.title).isEqualTo(title) diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt index f15b738f3e95..52b42750847a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt @@ -51,6 +51,7 @@ private const val NEGATIVE_TEXT = "escape" private const val USER_ID = 8 private const val CHALLENGE = 999L +private const val OP_PACKAGE_NAME = "biometric.testapp" @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -113,13 +114,20 @@ class PromptSelectorInteractorImplTest : SysuiTestCase() { assertThat(currentPrompt).isNull() - interactor.useBiometricsForAuthentication(info, USER_ID, CHALLENGE, modalities) + interactor.useBiometricsForAuthentication( + info, + USER_ID, + CHALLENGE, + modalities, + OP_PACKAGE_NAME + ) assertThat(currentPrompt).isNotNull() assertThat(currentPrompt?.title).isEqualTo(TITLE) assertThat(currentPrompt?.description).isEqualTo(DESCRIPTION) assertThat(currentPrompt?.subtitle).isEqualTo(SUBTITLE) assertThat(currentPrompt?.negativeButtonText).isEqualTo(NEGATIVE_TEXT) + assertThat(currentPrompt?.opPackageName).isEqualTo(OP_PACKAGE_NAME) if (allowCredentialFallback) { assertThat(credentialKind).isSameInstanceAs(PromptKind.Password) @@ -167,7 +175,7 @@ class PromptSelectorInteractorImplTest : SysuiTestCase() { assertThat(currentPrompt).isNull() - interactor.useCredentialsForAuthentication(info, kind, USER_ID, CHALLENGE) + interactor.useCredentialsForAuthentication(info, kind, USER_ID, CHALLENGE, OP_PACKAGE_NAME) // not using biometrics, should be null with no fallback option assertThat(currentPrompt).isNull() 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 a57b8905843c..a46167a423f1 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 @@ -1,5 +1,6 @@ package com.android.systemui.biometrics.domain.model +import android.graphics.Bitmap import android.hardware.biometrics.PromptContentItemBulletedText import android.hardware.biometrics.PromptVerticalListContentView import androidx.test.filters.SmallTest @@ -8,6 +9,7 @@ import com.android.systemui.biometrics.fingerprintSensorPropertiesInternal import com.android.systemui.biometrics.promptInfo import com.android.systemui.biometrics.shared.model.BiometricModalities import com.android.systemui.biometrics.shared.model.BiometricUserInfo +import com.android.systemui.res.R import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith @@ -15,6 +17,7 @@ import org.junit.runners.JUnit4 private const val USER_ID = 2 private const val OPERATION_ID = 8L +private const val OP_PACKAGE_NAME = "biometric.testapp" @SmallTest @RunWith(JUnit4::class) @@ -22,6 +25,7 @@ class BiometricPromptRequestTest : SysuiTestCase() { @Test fun biometricRequestFromPromptInfo() { + val logoRes = R.drawable.ic_cake val title = "what" val subtitle = "a" val description = "request" @@ -36,6 +40,7 @@ class BiometricPromptRequestTest : SysuiTestCase() { val request = BiometricPromptRequest.Biometric( promptInfo( + logoRes = logoRes, title = title, subtitle = subtitle, description = description, @@ -44,8 +49,10 @@ class BiometricPromptRequestTest : SysuiTestCase() { BiometricUserInfo(USER_ID), BiometricOperationInfo(OPERATION_ID), BiometricModalities(fingerprintProperties = fpPros), + OP_PACKAGE_NAME, ) + assertThat(request.logoRes).isEqualTo(logoRes) assertThat(request.title).isEqualTo(title) assertThat(request.subtitle).isEqualTo(subtitle) assertThat(request.description).isEqualTo(description) @@ -57,6 +64,23 @@ class BiometricPromptRequestTest : SysuiTestCase() { } @Test + fun biometricRequestLogoBitmapFromPromptInfo() { + val logoBitmap = Bitmap.createBitmap(400, 400, Bitmap.Config.ARGB_8888) + val fpPros = fingerprintSensorPropertiesInternal().first() + val request = + BiometricPromptRequest.Biometric( + promptInfo( + logoBitmap = logoBitmap, + ), + BiometricUserInfo(USER_ID), + BiometricOperationInfo(OPERATION_ID), + BiometricModalities(fingerprintProperties = fpPros), + OP_PACKAGE_NAME, + ) + assertThat(request.logoBitmap).isEqualTo(logoBitmap) + } + + @Test fun credentialRequestFromPromptInfo() { val title = "what" val subtitle = "a" 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 394405440d7e..3888f2b940b3 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,8 +16,11 @@ package com.android.systemui.biometrics.ui.viewmodel +import android.content.pm.PackageManager import android.content.res.Configuration +import android.graphics.Bitmap import android.graphics.Point +import android.graphics.drawable.BitmapDrawable import android.hardware.biometrics.PromptContentItemBulletedText import android.hardware.biometrics.PromptContentView import android.hardware.biometrics.PromptInfo @@ -76,6 +79,7 @@ import org.mockito.junit.MockitoJUnit private const val USER_ID = 4 private const val CHALLENGE = 2L private const val DELAY = 1000L +private const val OP_PACKAGE_NAME = "biometric.testapp" @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -88,9 +92,14 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa @Mock private lateinit var authController: AuthController @Mock private lateinit var selectedUserInteractor: SelectedUserInteractor @Mock private lateinit var udfpsUtils: UdfpsUtils + @Mock private lateinit var packageManager: PackageManager private val fakeExecutor = FakeExecutor(FakeSystemClock()) private val testScope = TestScope() + private val defaultLogoIcon = context.getDrawable(R.drawable.ic_android) + 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 lateinit var fingerprintRepository: FakeFingerprintPropertyRepository private lateinit var promptRepository: FakePromptRepository @@ -153,6 +162,12 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa udfpsUtils ) iconViewModel = viewModel.iconViewModel + + // Set up default logo icon and app customized icon + whenever(packageManager.getApplicationIcon(OP_PACKAGE_NAME)).thenReturn(defaultLogoIcon) + context.setMockPackageManager(packageManager) + val resources = context.getOrCreateTestableResources() + resources.addOverride(logoResFromApp, logoFromApp) } @Test @@ -1227,6 +1242,26 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa assertThat(contentView).isNull() } + @Test + fun defaultLogoIfNoLogoSet() = runGenericTest { + val logo by collectLastValue(viewModel.logo) + assertThat(logo).isEqualTo(defaultLogoIcon) + } + + @Test + fun logoResSetByApp() = + runGenericTest(logoRes = logoResFromApp) { + val logo by collectLastValue(viewModel.logo) + assertThat(logo).isEqualTo(logoFromApp) + } + + @Test + fun logoBitmapSetByApp() = + runGenericTest(logoBitmap = logoBitmapFromApp) { + val logo by collectLastValue(viewModel.logo) + assertThat((logo as BitmapDrawable).bitmap).isEqualTo(logoBitmapFromApp) + } + /** Asserts that the selected buttons are visible now. */ private suspend fun TestScope.assertButtonsVisible( tryAgain: Boolean = false, @@ -1248,6 +1283,8 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa allowCredentialFallback: Boolean = false, description: String? = null, contentView: PromptContentView? = null, + logoRes: Int = -1, + logoBitmap: Bitmap? = null, block: suspend TestScope.() -> Unit ) { selector.initializePrompt( @@ -1257,6 +1294,8 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa face = testCase.face, descriptionFromApp = description, contentViewFromApp = contentView, + logoResFromApp = logoRes, + logoBitmapFromApp = logoBitmap, ) // put the view model in the initial authenticating state, unless explicitly skipped @@ -1434,9 +1473,13 @@ private fun PromptSelectorInteractor.initializePrompt( allowCredentialFallback: Boolean = false, descriptionFromApp: String? = null, contentViewFromApp: PromptContentView? = null, + logoResFromApp: Int = -1, + logoBitmapFromApp: Bitmap? = null, ) { val info = PromptInfo().apply { + logoRes = logoResFromApp + logoBitmap = logoBitmapFromApp title = "t" subtitle = "s" description = descriptionFromApp @@ -1445,11 +1488,13 @@ private fun PromptSelectorInteractor.initializePrompt( isDeviceCredentialAllowed = allowCredentialFallback isConfirmationRequested = requireConfirmation } + useBiometricsForAuthentication( info, USER_ID, CHALLENGE, BiometricModalities(fingerprintProperties = fingerprint, faceProperties = face), + OP_PACKAGE_NAME, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt index 42ec8fed0127..f192de23fecc 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt @@ -26,12 +26,24 @@ class FakePromptRepository : PromptRepository { private val _isConfirmationRequired = MutableStateFlow(false) override val isConfirmationRequired = _isConfirmationRequired.asStateFlow() + private val _opPackageName: MutableStateFlow<String?> = MutableStateFlow(null) + override val opPackageName = _opPackageName.asStateFlow() + override fun setPrompt( promptInfo: PromptInfo, userId: Int, gatekeeperChallenge: Long?, kind: PromptKind, - ) = setPrompt(promptInfo, userId, gatekeeperChallenge, kind, forceConfirmation = false) + opPackageName: String, + ) = + setPrompt( + promptInfo, + userId, + gatekeeperChallenge, + kind, + forceConfirmation = false, + opPackageName = opPackageName + ) fun setPrompt( promptInfo: PromptInfo, @@ -39,12 +51,14 @@ class FakePromptRepository : PromptRepository { gatekeeperChallenge: Long?, kind: PromptKind, forceConfirmation: Boolean = false, + opPackageName: String? = null, ) { _promptInfo.value = promptInfo _userId.value = userId _challenge.value = gatekeeperChallenge _kind.value = kind _isConfirmationRequired.value = promptInfo.isConfirmationRequested || forceConfirmation + _opPackageName.value = opPackageName } override fun unsetPrompt() { diff --git a/services/core/java/com/android/server/biometrics/AuthService.java b/services/core/java/com/android/server/biometrics/AuthService.java index d5d8fd22314b..8fd2ee2bdc33 100644 --- a/services/core/java/com/android/server/biometrics/AuthService.java +++ b/services/core/java/com/android/server/biometrics/AuthService.java @@ -20,6 +20,7 @@ package com.android.server.biometrics; // TODO(b/141025588): Create separate internal and external permissions for AuthService. // TODO(b/141025588): Get rid of the USE_FINGERPRINT permission. +import static android.Manifest.permission.MANAGE_BIOMETRIC_DIALOG; import static android.Manifest.permission.TEST_BIOMETRIC; import static android.Manifest.permission.USE_BIOMETRIC; import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL; @@ -304,6 +305,9 @@ public class AuthService extends SystemService { if (promptInfo.containsPrivateApiConfigurations()) { checkInternalPermission(); } + if (promptInfo.containsManageBioApiConfigurations()) { + checkManageBiometricPermission(); + } final long identity = Binder.clearCallingIdentity(); try { @@ -984,6 +988,11 @@ public class AuthService extends SystemService { "Must have USE_BIOMETRIC_INTERNAL permission"); } + private void checkManageBiometricPermission() { + getContext().enforceCallingOrSelfPermission(MANAGE_BIOMETRIC_DIALOG, + "Must have MANAGE_BIOMETRIC_DIALOG permission"); + } + private void checkPermission() { if (getContext().checkCallingOrSelfPermission(USE_FINGERPRINT) != PackageManager.PERMISSION_GRANTED) { |