Add setLogo() API on Biometric Prompt.

Adds APIs to set the logo on Biometric Prompt for internal apps. For
other apps, show the app launcher logo by default.

Test: atest PromptViewModelTest
Test: atest BiometricPromptRequestTest
Test: atest PromptSelectorInteractorImplTest
Test: atest PromptRepositoryImplTest
Test: manual test on biometric test app
Flag: ACONFIG android.hardware.biometrics.custom_biometric_prompt DEVELOPMENT
Bug: 302735104
API-Coverage-Bug: 302735104

Change-Id: Ic2fbd243c0ba3fe6813addf52270019e68fbee73
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 507d9c4..e065187 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 4a39799..72e884e 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 promptInfo(
+    logoRes: Int = -1,
+    logoBitmap: Bitmap? = null,
     title: String = "title",
     subtitle: String = "sub",
     description: String = "desc",
@@ -127,6 +130,8 @@
     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 23fbb12..10f7113 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 ab23564..57e308f 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 @@
                     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 @@
         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 86802a5b..02eae9ced 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 @@
                     || 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 b35fbbc..ad7bb0e 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 @@
     /** 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 @@
         userId: Int,
         gatekeeperChallenge: Long?,
         kind: PromptKind,
+        opPackageName: String,
     )
 
     /** Unset the prompt info. */
@@ -108,6 +112,9 @@
     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 @@
         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 @@
         _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 ac4b717..359e2e7 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 @@
         @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 65a2c0a..b3f9574 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 @@
         userId: Int,
         challenge: Long,
         modalities: BiometricModalities,
+        opPackageName: String,
     )
 
     /** Use credential-based authentication instead of biometrics. */
@@ -84,6 +85,7 @@
         @Utils.CredentialType kind: Int,
         userId: Int,
         challenge: Long,
+        opPackageName: String,
     )
 
     /** Unset the current authentication request. */
@@ -104,9 +106,12 @@
             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 @@
                         userInfo = BiometricUserInfo(userId = userId),
                         operationInfo = BiometricOperationInfo(gatekeeperChallenge = challenge),
                         modalities = kind.activeModalities,
+                        opPackageName = opPackageName,
                     )
                 else -> null
             }
@@ -152,13 +158,15 @@
         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 @@
         @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 4377937..c17c8dc 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 @@
         userInfo: BiometricUserInfo,
         operationInfo: BiometricOperationInfo,
         val modalities: BiometricModalities,
+        val opPackageName: String,
     ) :
         BiometricPromptRequest(
             title = info.title?.toString() ?: "",
@@ -36,6 +38,8 @@
             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 60b454e..b450896 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 @@
                                 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 04dc7a8..285ab4a 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.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 @@
         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 @@
             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 @@
                 }
             }
 
+            logoView.setImageDrawable(viewModel.logo.first())
             titleView.text = viewModel.title.first()
             subtitleView.text = viewModel.subtitle.first()
             descriptionView.text = viewModel.description.first()
@@ -183,6 +188,7 @@
                     viewModel = viewModel,
                     viewsToHideWhenSmall =
                         listOf(
+                            logoView,
                             titleView,
                             subtitleView,
                             descriptionView,
@@ -190,6 +196,7 @@
                         ),
                     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 c3bbaed..d5695f3 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.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.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 1c78928..dca0338 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 @@
 
 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 @@
             }
         }
 
+    /** 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 0ee0939..43f7c60 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.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 @@
     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 @@
     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 @@
                         selectedUserInteractor,
                         testScope.backgroundScope,
                 )
+        // Set up default logo icon
+        whenever(packageManager.getApplicationIcon(OP_PACKAGE_NAME)).thenReturn(defaultLogoIcon)
+        context.setMockPackageManager(packageManager)
     }
 
     @After
@@ -533,6 +542,7 @@
             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 ec7ce63..b39e09d 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 @@
 
 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 @@
                     PromptInfo().apply { isConfirmationRequested = case },
                     USER_ID,
                     CHALLENGE,
-                    PromptKind.Biometric()
+                    PromptKind.Biometric(),
+                    OP_PACKAGE_NAME
                 )
 
                 assertThat(isConfirmationRequired).isEqualTo(case)
@@ -120,7 +122,8 @@
                     PromptInfo().apply { isConfirmationRequested = case },
                     USER_ID,
                     CHALLENGE,
-                    PromptKind.Biometric()
+                    PromptKind.Biometric(),
+                    OP_PACKAGE_NAME
                 )
 
                 assertThat(isConfirmationRequired).isTrue()
@@ -133,17 +136,19 @@
             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 dcefea2..8a46c0c 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 @@
 
 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 @@
                 },
                 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 f15b738..52b4275 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 USER_ID = 8
 private const val CHALLENGE = 999L
+private const val OP_PACKAGE_NAME = "biometric.testapp"
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
@@ -113,13 +114,20 @@
 
         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 @@
 
         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 a57b890..a46167a42 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.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 @@
 
 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 @@
 
     @Test
     fun biometricRequestFromPromptInfo() {
+        val logoRes = R.drawable.ic_cake
         val title = "what"
         val subtitle = "a"
         val description = "request"
@@ -36,6 +40,7 @@
         val request =
             BiometricPromptRequest.Biometric(
                 promptInfo(
+                    logoRes = logoRes,
                     title = title,
                     subtitle = subtitle,
                     description = description,
@@ -44,8 +49,10 @@
                 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 @@
     }
 
     @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 3944054..3888f2b 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 @@
 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 @@
     @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 @@
                 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 @@
             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 @@
         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 @@
             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 @@
     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 @@
             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 42ec8fed..f192de2 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 @@
     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 @@
         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() {