diff options
4 files changed, 252 insertions, 50 deletions
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index d979abbe0af9..e825fc5c5b9a 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1122,6 +1122,8 @@ <dimen name="biometric_prompt_panel_max_width">640dp</dimen> <dimen name="biometric_prompt_land_small_horizontal_guideline_padding">344dp</dimen> <dimen name="biometric_prompt_two_pane_udfps_horizontal_guideline_padding">114dp</dimen> + <dimen name="biometric_prompt_two_pane_udfps_shorter_content_width">216dp</dimen> + <dimen name="biometric_prompt_two_pane_udfps_shorter_horizontal_guideline_padding">661dp</dimen> <dimen name="biometric_prompt_two_pane_medium_horizontal_guideline_padding">640dp</dimen> <dimen name="biometric_prompt_one_pane_medium_top_guideline_padding">119dp</dimen> <dimen name="biometric_prompt_one_pane_medium_horizontal_guideline_padding">0dp</dimen> 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 b4d53d02448d..a87ee24da2f7 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 @@ -364,7 +364,7 @@ object BiometricViewSizeBinder { if (midGuideline != null) { val left = if (bounds.left >= 0) { - bounds.left + abs(bounds.left) } else { view.width - abs(bounds.left) } @@ -372,7 +372,7 @@ object BiometricViewSizeBinder { if (bounds.right >= 0) { view.width - abs(bounds.right) } else { - bounds.right + abs(bounds.right) } val mid = (left + right) / 2 mediumConstraintSet.setGuidelineBegin(midGuideline.id, mid) 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 68a3f5d11fb6..7d494a5cf229 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 @@ -30,6 +30,7 @@ import android.hardware.biometrics.BiometricPrompt import android.hardware.biometrics.Flags.customBiometricPrompt import android.hardware.biometrics.PromptContentView import android.os.UserHandle +import android.text.TextPaint import android.util.Log import android.util.RotationUtils import android.view.HapticFeedbackConstants @@ -52,6 +53,7 @@ import com.android.systemui.biometrics.shared.model.PromptKind import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.shared.model.AcquiredFingerprintAuthenticationStatus import com.android.systemui.res.R +import com.android.systemui.util.kotlin.combine import javax.inject.Inject import kotlinx.coroutines.Job import kotlinx.coroutines.coroutineScope @@ -260,15 +262,15 @@ constructor( val position: Flow<PromptPosition> = combine( _forceLargeSize, + promptKind, displayStateInteractor.isLargeScreen, displayStateInteractor.currentRotation, modalities - ) { forceLarge, isLargeScreen, rotation, modalities -> + ) { forceLarge, promptKind, isLargeScreen, rotation, modalities -> when { forceLarge || isLargeScreen || - promptKind.value.isOnePaneNoSensorLandscapeBiometric() -> - PromptPosition.Bottom + promptKind.isOnePaneNoSensorLandscapeBiometric() -> PromptPosition.Bottom rotation == DisplayRotation.ROTATION_90 -> PromptPosition.Right rotation == DisplayRotation.ROTATION_270 -> PromptPosition.Left rotation == DisplayRotation.ROTATION_180 && modalities.hasUdfps -> @@ -308,6 +310,10 @@ constructor( context.resources.getDimensionPixelSize( R.dimen.biometric_prompt_two_pane_udfps_horizontal_guideline_padding ) + private val udfpsHorizontalShorterGuidelinePadding = + context.resources.getDimensionPixelSize( + R.dimen.biometric_prompt_two_pane_udfps_shorter_horizontal_guideline_padding + ) private val mediumTopGuidelinePadding = context.resources.getDimensionPixelSize( R.dimen.biometric_prompt_one_pane_medium_top_guideline_padding @@ -449,47 +455,6 @@ constructor( } } - /** - * Rect for positioning prompt guidelines (left, top, right, unused) - * - * Negative values are used to signify that guideline measuring should be flipped, measuring - * from opposite side of the screen - */ - val guidelineBounds: Flow<Rect> = - combine(iconPosition, promptKind, size, position, modalities) { - _, - promptKind, - size, - position, - modalities -> - when (position) { - PromptPosition.Bottom -> - if (promptKind.isOnePaneNoSensorLandscapeBiometric()) { - Rect(0, 0, 0, 0) - } else { - Rect(0, mediumTopGuidelinePadding, 0, 0) - } - PromptPosition.Right -> - if (size.isSmall) { - Rect(-smallHorizontalGuidelinePadding, 0, 0, 0) - } else if (modalities.hasUdfps) { - Rect(udfpsHorizontalGuidelinePadding, 0, 0, 0) - } else { - Rect(-mediumHorizontalGuidelinePadding, 0, 0, 0) - } - PromptPosition.Left -> - if (size.isSmall) { - Rect(0, 0, -smallHorizontalGuidelinePadding, 0) - } else if (modalities.hasUdfps) { - Rect(0, 0, udfpsHorizontalGuidelinePadding, 0) - } else { - Rect(0, 0, -mediumHorizontalGuidelinePadding, 0) - } - PromptPosition.Top -> Rect() - } - } - .distinctUntilChanged() - /** Padding for prompt UI elements */ val promptPadding: Flow<Rect> = combine(size, displayStateInteractor.currentRotation) { size, rotation -> @@ -556,6 +521,81 @@ constructor( if (contentView == null) description else "" } + private val hasOnlyOneLineTitle: Flow<Boolean> = + combine(title, subtitle, contentView, description) { + title, + subtitle, + contentView, + description -> + if (subtitle.isNotEmpty() || contentView != null || description.isNotEmpty()) { + false + } else { + val maxWidth = + context.resources.getDimensionPixelSize( + R.dimen.biometric_prompt_two_pane_udfps_shorter_content_width + ) + val attributes = + context.obtainStyledAttributes( + R.style.TextAppearance_AuthCredential_Title, + intArrayOf(android.R.attr.textSize) + ) + val paint = TextPaint() + paint.textSize = attributes.getDimensionPixelSize(0, 0).toFloat() + val textWidth = paint.measureText(title) + attributes.recycle() + textWidth / maxWidth <= 1 + } + } + + /** + * Rect for positioning prompt guidelines (left, top, right, unused) + * + * Negative values are used to signify that guideline measuring should be flipped, measuring + * from opposite side of the screen + */ + val guidelineBounds: Flow<Rect> = + combine(iconPosition, promptKind, size, position, modalities, hasOnlyOneLineTitle) { + _, + promptKind, + size, + position, + modalities, + hasOnlyOneLineTitle -> + var left = 0 + var top = 0 + var right = 0 + when (position) { + PromptPosition.Bottom -> { + val noSensorLandscape = promptKind.isOnePaneNoSensorLandscapeBiometric() + top = if (noSensorLandscape) 0 else mediumTopGuidelinePadding + } + PromptPosition.Right -> + left = getHorizontalPadding(size, modalities, hasOnlyOneLineTitle) + PromptPosition.Left -> + right = getHorizontalPadding(size, modalities, hasOnlyOneLineTitle) + PromptPosition.Top -> {} + } + Rect(left, top, right, 0) + } + .distinctUntilChanged() + + private fun getHorizontalPadding( + size: PromptSize, + modalities: BiometricModalities, + hasOnlyOneLineTitle: Boolean + ) = + if (size.isSmall) { + -smallHorizontalGuidelinePadding + } else if (modalities.hasUdfps) { + if (hasOnlyOneLineTitle) { + -udfpsHorizontalShorterGuidelinePadding + } else { + udfpsHorizontalGuidelinePadding + } + } else { + -mediumHorizontalGuidelinePadding + } + /** If the indicator (help, error) message should be shown. */ val isIndicatorMessageVisible: Flow<Boolean> = combine( 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 707695441fb5..766fb05e21ff 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 @@ -26,6 +26,7 @@ import android.content.pm.PackageManager.NameNotFoundException import android.content.res.Configuration import android.graphics.Bitmap import android.graphics.Point +import android.graphics.Rect import android.graphics.drawable.BitmapDrawable import android.hardware.biometrics.BiometricFingerprintConstants import android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT @@ -87,9 +88,6 @@ import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before -import platform.test.runner.parameterized.ParameterizedAndroidJunit4 -import platform.test.runner.parameterized.Parameter -import platform.test.runner.parameterized.Parameters import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -97,6 +95,8 @@ import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.eq import org.mockito.Mock import org.mockito.junit.MockitoJUnit +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters private const val USER_ID = 4 private const val REQUEST_ID = 4L @@ -135,6 +135,27 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa private val defaultLogoDescription = "Test Android App" private val logoDescriptionFromApp = "Test Cake App" private val packageNameForLogoWithOverrides = "should.use.overridden.logo" + /** Prompt panel size padding */ + private val smallHorizontalGuidelinePadding = + context.resources.getDimensionPixelSize( + R.dimen.biometric_prompt_land_small_horizontal_guideline_padding + ) + private val udfpsHorizontalGuidelinePadding = + context.resources.getDimensionPixelSize( + R.dimen.biometric_prompt_two_pane_udfps_horizontal_guideline_padding + ) + private val udfpsHorizontalShorterGuidelinePadding = + context.resources.getDimensionPixelSize( + R.dimen.biometric_prompt_two_pane_udfps_shorter_horizontal_guideline_padding + ) + private val mediumTopGuidelinePadding = + context.resources.getDimensionPixelSize( + R.dimen.biometric_prompt_one_pane_medium_top_guideline_padding + ) + private val mediumHorizontalGuidelinePadding = + context.resources.getDimensionPixelSize( + R.dimen.biometric_prompt_two_pane_medium_horizontal_guideline_padding + ) private lateinit var fingerprintRepository: FakeFingerprintPropertyRepository private lateinit var promptRepository: FakePromptRepository @@ -1370,6 +1391,142 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test + @EnableFlags(FLAG_CONSTRAINT_BP) + fun position_bottom_rotation0() = runGenericTest { + displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_0) + val position by collectLastValue(viewModel.position) + assertThat(position).isEqualTo(PromptPosition.Bottom) + } // TODO(b/335278136): Add test for no sensor landscape + + @Test + @EnableFlags(FLAG_CONSTRAINT_BP) + fun position_bottom_forceLarge() = runGenericTest { + displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270) + viewModel.onSwitchToCredential() + val position by collectLastValue(viewModel.position) + assertThat(position).isEqualTo(PromptPosition.Bottom) + } + + @Test + @EnableFlags(FLAG_CONSTRAINT_BP) + fun position_bottom_largeScreen() = runGenericTest { + displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270) + displayStateRepository.setIsLargeScreen(true) + val position by collectLastValue(viewModel.position) + assertThat(position).isEqualTo(PromptPosition.Bottom) + } + + @Test + @EnableFlags(FLAG_CONSTRAINT_BP) + fun position_right_rotation90() = runGenericTest { + displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_90) + val position by collectLastValue(viewModel.position) + assertThat(position).isEqualTo(PromptPosition.Right) + } + + @Test + @EnableFlags(FLAG_CONSTRAINT_BP) + fun position_left_rotation270() = runGenericTest { + displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270) + val position by collectLastValue(viewModel.position) + assertThat(position).isEqualTo(PromptPosition.Left) + } + + @Test + @EnableFlags(FLAG_CONSTRAINT_BP) + fun position_top_rotation180() = runGenericTest { + displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_180) + val position by collectLastValue(viewModel.position) + if (testCase.modalities.hasUdfps) { + assertThat(position).isEqualTo(PromptPosition.Top) + } else { + assertThat(position).isEqualTo(PromptPosition.Bottom) + } + } + + @Test + @EnableFlags(FLAG_CONSTRAINT_BP) + fun guideline_bottom() = runGenericTest { + displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_0) + val guidelineBounds by collectLastValue(viewModel.guidelineBounds) + assertThat(guidelineBounds).isEqualTo(Rect(0, mediumTopGuidelinePadding, 0, 0)) + } // TODO(b/335278136): Add test for no sensor landscape + + @Test + @EnableFlags(FLAG_CONSTRAINT_BP) + fun guideline_right() = runGenericTest { + displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_90) + + val isSmall = testCase.shouldStartAsImplicitFlow + val guidelineBounds by collectLastValue(viewModel.guidelineBounds) + + if (isSmall) { + assertThat(guidelineBounds).isEqualTo(Rect(-smallHorizontalGuidelinePadding, 0, 0, 0)) + } else if (testCase.modalities.hasUdfps) { + assertThat(guidelineBounds).isEqualTo(Rect(udfpsHorizontalGuidelinePadding, 0, 0, 0)) + } else { + assertThat(guidelineBounds).isEqualTo(Rect(-mediumHorizontalGuidelinePadding, 0, 0, 0)) + } + } + + @Test + @EnableFlags(FLAG_CONSTRAINT_BP) + fun guideline_right_onlyShortTitle() = + runGenericTest(subtitle = "") { + displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_90) + + val isSmall = testCase.shouldStartAsImplicitFlow + val guidelineBounds by collectLastValue(viewModel.guidelineBounds) + + if (!isSmall && testCase.modalities.hasUdfps) { + assertThat(guidelineBounds) + .isEqualTo(Rect(-udfpsHorizontalShorterGuidelinePadding, 0, 0, 0)) + } + } + + @Test + @EnableFlags(FLAG_CONSTRAINT_BP) + fun guideline_left() = runGenericTest { + displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270) + + val isSmall = testCase.shouldStartAsImplicitFlow + val guidelineBounds by collectLastValue(viewModel.guidelineBounds) + + if (isSmall) { + assertThat(guidelineBounds).isEqualTo(Rect(0, 0, -smallHorizontalGuidelinePadding, 0)) + } else if (testCase.modalities.hasUdfps) { + assertThat(guidelineBounds).isEqualTo(Rect(0, 0, udfpsHorizontalGuidelinePadding, 0)) + } else { + assertThat(guidelineBounds).isEqualTo(Rect(0, 0, -mediumHorizontalGuidelinePadding, 0)) + } + } + + @Test + @EnableFlags(FLAG_CONSTRAINT_BP) + fun guideline_left_onlyShortTitle() = + runGenericTest(subtitle = "") { + displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270) + + val isSmall = testCase.shouldStartAsImplicitFlow + val guidelineBounds by collectLastValue(viewModel.guidelineBounds) + + if (!isSmall && testCase.modalities.hasUdfps) { + assertThat(guidelineBounds) + .isEqualTo(Rect(0, 0, -udfpsHorizontalShorterGuidelinePadding, 0)) + } + } + + @Test + @EnableFlags(FLAG_CONSTRAINT_BP) + fun guideline_top() = runGenericTest { + displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_180) + val guidelineBounds by collectLastValue(viewModel.guidelineBounds) + if (testCase.modalities.hasUdfps) { + assertThat(guidelineBounds).isEqualTo(Rect(0, 0, 0, 0)) + } + } + + @Test fun iconViewLoaded() = runGenericTest { val isIconViewLoaded by collectLastValue(viewModel.isIconViewLoaded) // TODO(b/328677869): Add test for noIcon logic. @@ -1399,6 +1556,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa private fun runGenericTest( doNotStart: Boolean = false, allowCredentialFallback: Boolean = false, + subtitle: String? = "s", description: String? = null, contentView: PromptContentView? = null, logoRes: Int = -1, @@ -1437,6 +1595,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa allowCredentialFallback = allowCredentialFallback, fingerprint = testCase.fingerprint, face = testCase.face, + subtitleFromApp = subtitle, descriptionFromApp = description, contentViewFromApp = contentView, logoResFromApp = logoRes, @@ -1625,6 +1784,7 @@ private fun PromptSelectorInteractor.initializePrompt( face: FaceSensorPropertiesInternal? = null, requireConfirmation: Boolean = false, allowCredentialFallback: Boolean = false, + subtitleFromApp: String? = "s", descriptionFromApp: String? = null, contentViewFromApp: PromptContentView? = null, logoResFromApp: Int = -1, @@ -1636,7 +1796,7 @@ private fun PromptSelectorInteractor.initializePrompt( PromptInfo().apply { logoDescription = logoDescriptionFromApp title = "t" - subtitle = "s" + subtitle = subtitleFromApp description = descriptionFromApp contentView = contentViewFromApp authenticators = listOf(face, fingerprint).extractAuthenticatorTypes() |