summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Austin Delgado <austindelgado@google.com> 2023-10-17 13:48:38 -0700
committer Austin Delgado <austindelgado@google.com> 2024-02-01 10:37:38 -0800
commitf8b10452008be80f807ff0ef91adeccd0e4fee32 (patch)
treec5c7c576341f4cd71ab73ef36c83f360bc4ff64c
parent028d0c4a1e6275c7cff7c6e29b495065e10ccebb (diff)
Add Biometric Prompt ConstraintLayout
Test: atest com.android.systemui.biometrics Bug: 288175072 Flag: ACONFIG constraint_bp DEVELOPMENT Change-Id: I69123e491303c3862288bd180eb812efc0d84a37
-rw-r--r--packages/SystemUI/res/layout/biometric_prompt_constraint_layout.xml244
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java73
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt16
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt492
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt22
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt41
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptPosition.kt37
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt35
8 files changed, 794 insertions, 166 deletions
diff --git a/packages/SystemUI/res/layout/biometric_prompt_constraint_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_constraint_layout.xml
new file mode 100644
index 000000000000..a877853eaec8
--- /dev/null
+++ b/packages/SystemUI/res/layout/biometric_prompt_constraint_layout.xml
@@ -0,0 +1,244 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+xmlns:app="http://schemas.android.com/apk/res-auto"
+xmlns:tools="http://schemas.android.com/tools"
+android:layout_width="match_parent"
+android:layout_height="match_parent">
+
+ <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"
+ android:visibility="gone" />
+
+ <ImageView
+ android:id="@+id/background"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:contentDescription="@string/biometric_dialog_empty_space_description"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <View
+ android:id="@+id/panel"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:background="?android:attr/colorBackgroundFloating"
+ android:clickable="true"
+ android:clipToOutline="true"
+ android:importantForAccessibility="no"
+ android:paddingHorizontal="16dp"
+ android:paddingVertical="16dp"
+ android:visibility="visible"
+ app:layout_constraintBottom_toTopOf="@+id/bottomGuideline"
+ app:layout_constraintEnd_toStartOf="@+id/rightGuideline"
+ app:layout_constraintStart_toStartOf="@+id/leftGuideline"
+ app:layout_constraintTop_toTopOf="@+id/title" />
+
+ <com.android.systemui.biometrics.BiometricPromptLottieViewWrapper
+ android:id="@+id/biometric_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_bias="0.8"
+ tools:srcCompat="@tools:sample/avatars" />
+
+ <com.android.systemui.biometrics.BiometricPromptLottieViewWrapper
+ android:id="@+id/biometric_icon_overlay"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_gravity="center"
+ android:contentDescription="@null"
+ android:scaleType="fitXY"
+ app:layout_constraintBottom_toBottomOf="@+id/biometric_icon"
+ app:layout_constraintEnd_toEndOf="@+id/biometric_icon"
+ app:layout_constraintHorizontal_bias="1.0"
+ app:layout_constraintStart_toStartOf="@+id/biometric_icon"
+ app:layout_constraintTop_toTopOf="@+id/biometric_icon"
+ app:layout_constraintVertical_bias="0.0" />
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="@integer/biometric_dialog_text_gravity"
+ android:singleLine="true"
+ android:marqueeRepeatLimit="1"
+ android:ellipsize="marquee"
+ style="@style/TextAppearance.AuthCredential.Title"
+ app:layout_constraintBottom_toTopOf="@+id/subtitle"
+ app:layout_constraintEnd_toEndOf="@+id/panel"
+ app:layout_constraintStart_toStartOf="@+id/panel" />
+
+ <TextView
+ android:id="@+id/subtitle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="@integer/biometric_dialog_text_gravity"
+ android:singleLine="true"
+ android:marqueeRepeatLimit="1"
+ android:ellipsize="marquee"
+ style="@style/TextAppearance.AuthCredential.Subtitle"
+ app:layout_constraintBottom_toTopOf="@+id/description"
+ app:layout_constraintEnd_toEndOf="@+id/panel"
+ app:layout_constraintStart_toStartOf="@+id/panel" />
+
+ <Space
+ android:id="@+id/space_above_content"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/biometric_prompt_space_above_content"
+ android:visibility="gone"
+ app:layout_constraintTop_toBottomOf="@+id/subtitle"
+ app:layout_constraintEnd_toEndOf="@+id/panel"
+ app:layout_constraintStart_toStartOf="@+id/panel"/>
+
+ <ScrollView
+ android:id="@+id/customized_view_container"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:fillViewport="true"
+ android:fadeScrollbars="false"
+ android:gravity="center_vertical"
+ android:orientation="vertical"
+ android:paddingHorizontal="@dimen/biometric_prompt_content_container_padding_horizontal"
+ android:scrollbars="vertical"
+ android:visibility="gone"
+ app:layout_constraintTop_toBottomOf="@+id/space_above_content"
+ app:layout_constraintBottom_toTopOf="@+id/biometric_icon"
+ app:layout_constraintEnd_toEndOf="@+id/panel"
+ app:layout_constraintStart_toStartOf="@+id/panel"/>
+
+ <TextView
+ android:id="@+id/description"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="24dp"
+ android:scrollbars="vertical"
+ android:gravity="@integer/biometric_dialog_text_gravity"
+ style="@style/TextAppearance.AuthCredential.Description"
+ app:layout_constraintBottom_toTopOf="@+id/biometric_icon"
+ app:layout_constraintEnd_toEndOf="@+id/panel"
+ app:layout_constraintStart_toStartOf="@+id/panel" />
+
+ <TextView
+ android:id="@+id/indicator"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:gravity="center_horizontal"
+ android:textColor="@color/biometric_dialog_gray"
+ android:textSize="12sp"
+ android:accessibilityLiveRegion="polite"
+ android:marqueeRepeatLimit="marquee_forever"
+ android:scrollHorizontally="true"
+ android:fadingEdge="horizontal"
+ app:layout_constraintEnd_toEndOf="@+id/panel"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintStart_toStartOf="@+id/panel"
+ app:layout_constraintTop_toBottomOf="@+id/biometric_icon" />
+
+ <!-- Negative Button, reserved for app -->
+ <Button
+ android:id="@+id/button_negative"
+ style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_marginBottom="8dp"
+ android:layout_marginLeft="8dp"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toBottomOf="@+id/panel"
+ app:layout_constraintStart_toStartOf="@+id/panel" />
+
+ <!-- Cancel Button, replaces negative button when biometric is accepted -->
+ <Button
+ android:id="@+id/button_cancel"
+ style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_marginBottom="8dp"
+ android:layout_marginLeft="8dp"
+ android:text="@string/cancel"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toBottomOf="@+id/panel"
+ app:layout_constraintStart_toStartOf="@+id/panel" />
+
+ <!-- "Use Credential" Button, replaces if device credential is allowed -->
+ <Button
+ android:id="@+id/button_use_credential"
+ style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_marginBottom="8dp"
+ android:layout_marginLeft="8dp"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toBottomOf="@+id/panel"
+ app:layout_constraintStart_toStartOf="@+id/panel" />
+
+ <!-- Positive Button -->
+ <Button
+ android:id="@+id/button_confirm"
+ style="@*android:style/Widget.DeviceDefault.Button.Colored"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_marginBottom="8dp"
+ android:layout_marginRight="8dp"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:text="@string/biometric_dialog_confirm"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toBottomOf="@+id/panel"
+ app:layout_constraintEnd_toEndOf="@+id/panel"
+ tools:visibility="invisible" />
+
+ <!-- Try Again Button -->
+ <Button
+ android:id="@+id/button_try_again"
+ style="@*android:style/Widget.DeviceDefault.Button.Colored"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:layout_marginBottom="8dp"
+ android:layout_marginRight="8dp"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:text="@string/biometric_dialog_try_again"
+ android:visibility="invisible"
+ app:layout_constraintBottom_toBottomOf="@+id/panel"
+ app:layout_constraintEnd_toEndOf="@+id/panel" />
+
+ <!-- Guidelines for setting panel border -->
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/leftGuideline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintGuide_begin="@dimen/biometric_dialog_border_padding" />
+
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/rightGuideline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintGuide_end="@dimen/biometric_dialog_border_padding" />
+
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/bottomGuideline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ app:layout_constraintGuide_end="@dimen/biometric_dialog_border_padding" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
index 57e308ff16e8..3397906aa6ea 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
@@ -20,6 +20,7 @@ import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
import static com.android.internal.jank.InteractionJankMonitor.CUJ_BIOMETRIC_PROMPT_TRANSITION;
+import static com.android.systemui.Flags.constraintBp;
import android.animation.Animator;
import android.annotation.IntDef;
@@ -57,6 +58,7 @@ import android.widget.ScrollView;
import android.window.OnBackInvokedCallback;
import android.window.OnBackInvokedDispatcher;
+import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.view.AccessibilityDelegateCompat;
import androidx.core.view.ViewCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
@@ -153,7 +155,7 @@ public class AuthContainerView extends LinearLayout
@Nullable private Spaghetti mBiometricView;
@Nullable private View mCredentialView;
private final AuthPanelController mPanelController;
- private final FrameLayout mFrameLayout;
+ private final ViewGroup mLayout;
private final ImageView mBackgroundView;
private final ScrollView mBiometricScrollView;
private final View mPanelView;
@@ -339,11 +341,16 @@ public class AuthContainerView extends LinearLayout
mBiometricCallback = new BiometricCallback();
final LayoutInflater layoutInflater = LayoutInflater.from(mContext);
- mFrameLayout = (FrameLayout) layoutInflater.inflate(
- R.layout.auth_container_view, this, false /* attachToRoot */);
- addView(mFrameLayout);
- mBiometricScrollView = mFrameLayout.findViewById(R.id.biometric_scrollview);
- mBackgroundView = mFrameLayout.findViewById(R.id.background);
+ if (constraintBp()) {
+ mLayout = (ConstraintLayout) layoutInflater.inflate(
+ R.layout.biometric_prompt_constraint_layout, this, false /* attachToRoot */);
+ } else {
+ mLayout = (FrameLayout) layoutInflater.inflate(
+ R.layout.auth_container_view, this, false /* attachToRoot */);
+ }
+ mBiometricScrollView = mLayout.findViewById(R.id.biometric_scrollview);
+ addView(mLayout);
+ mBackgroundView = mLayout.findViewById(R.id.background);
ViewCompat.setAccessibilityDelegate(mBackgroundView, new AccessibilityDelegateCompat() {
@Override
public void onInitializeAccessibilityNodeInfo(View host,
@@ -358,7 +365,7 @@ public class AuthContainerView extends LinearLayout
}
});
- mPanelView = mFrameLayout.findViewById(R.id.panel);
+ mPanelView = mLayout.findViewById(R.id.panel);
mPanelController = new AuthPanelController(mContext, mPanelView);
mBackgroundExecutor = bgExecutor;
mInteractionJankMonitor = jankMonitor;
@@ -402,20 +409,31 @@ public class AuthContainerView extends LinearLayout
new BiometricModalities(fpProps, faceProps),
config.mOpPackageName);
- final BiometricPromptLayout view = (BiometricPromptLayout) layoutInflater.inflate(
- R.layout.biometric_prompt_layout, null, false);
- mBiometricView = BiometricViewBinder.bind(view, viewModel, mPanelController,
- // TODO(b/201510778): This uses the wrong timeout in some cases
- getJankListener(view, TRANSIT,
- BiometricViewSizeBinder.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS),
- mBackgroundView, mBiometricCallback, mApplicationCoroutineScope,
- vibratorHelper);
-
- // TODO(b/251476085): migrate these dependencies
- if (fpProps != null && fpProps.isAnyUdfpsType()) {
- view.setUdfpsAdapter(new UdfpsDialogMeasureAdapter(view, fpProps),
- config.mScaleProvider);
+ if (constraintBp()) {
+ mBiometricView = BiometricViewBinder.bind(mLayout, viewModel, null,
+ // TODO(b/201510778): This uses the wrong timeout in some cases
+ getJankListener(mLayout, TRANSIT,
+ BiometricViewSizeBinder.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS),
+ mBackgroundView, mBiometricCallback, mApplicationCoroutineScope,
+ vibratorHelper);
+ } else {
+ final BiometricPromptLayout view = (BiometricPromptLayout) layoutInflater.inflate(
+ R.layout.biometric_prompt_layout, null, false);
+ mBiometricView = BiometricViewBinder.bind(view, viewModel, mPanelController,
+ // TODO(b/201510778): This uses the wrong timeout in some cases
+ getJankListener(view, TRANSIT,
+ BiometricViewSizeBinder.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS),
+ mBackgroundView, mBiometricCallback, mApplicationCoroutineScope,
+ vibratorHelper);
+
+ // TODO(b/251476085): migrate these dependencies
+ if (fpProps != null && fpProps.isAnyUdfpsType()) {
+ view.setUdfpsAdapter(new UdfpsDialogMeasureAdapter(view, fpProps),
+ config.mScaleProvider);
+ }
}
+ } else if (constraintBp() && Utils.isDeviceCredentialAllowed(mConfig.mPromptInfo)) {
+ addCredentialView(true, false);
} else {
mPromptSelectorInteractorProvider.get().resetPrompt();
}
@@ -477,7 +495,7 @@ public class AuthContainerView extends LinearLayout
vm.setAnimateContents(animateContents);
((CredentialView) mCredentialView).init(vm, this, mPanelController, animatePanel);
- mFrameLayout.addView(mCredentialView);
+ mLayout.addView(mCredentialView);
}
@Override
@@ -488,7 +506,9 @@ public class AuthContainerView extends LinearLayout
@Override
public void onOrientationChanged() {
- maybeUpdatePositionForUdfps(true /* invalidate */);
+ if (!constraintBp()) {
+ maybeUpdatePositionForUdfps(true /* invalidate */);
+ }
}
@Override
@@ -502,8 +522,9 @@ public class AuthContainerView extends LinearLayout
mWakefulnessLifecycle.addObserver(this);
mPanelInteractionDetector.enable(
() -> animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED));
-
- if (Utils.isBiometricAllowed(mConfig.mPromptInfo)) {
+ if (constraintBp()) {
+ // Do nothing on attachment with constraintLayout
+ } else if (Utils.isBiometricAllowed(mConfig.mPromptInfo)) {
mBiometricScrollView.addView(mBiometricView.asView());
} else if (Utils.isDeviceCredentialAllowed(mConfig.mPromptInfo)) {
addCredentialView(true /* animatePanel */, false /* animateContents */);
@@ -512,7 +533,9 @@ public class AuthContainerView extends LinearLayout
+ mConfig.mPromptInfo.getAuthenticators());
}
- maybeUpdatePositionForUdfps(false /* invalidate */);
+ if (!constraintBp()) {
+ maybeUpdatePositionForUdfps(false /* invalidate */);
+ }
if (mConfig.mSkipIntro) {
mContainerState = STATE_SHOWING;
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 285ab4a800b6..efad21bda380 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
@@ -41,6 +41,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.airbnb.lottie.LottieAnimationView
import com.airbnb.lottie.LottieCompositionFactory
+import com.android.systemui.Flags.constraintBp
import com.android.systemui.biometrics.AuthPanelController
import com.android.systemui.biometrics.shared.model.BiometricModalities
import com.android.systemui.biometrics.shared.model.BiometricModality
@@ -70,9 +71,9 @@ object BiometricViewBinder {
@SuppressLint("ClickableViewAccessibility")
@JvmStatic
fun bind(
- view: BiometricPromptLayout,
+ view: View,
viewModel: PromptViewModel,
- panelViewController: AuthPanelController,
+ panelViewController: AuthPanelController?,
jankListener: BiometricJankListener,
backgroundView: View,
legacyCallback: Spaghetti.Callback,
@@ -112,11 +113,18 @@ object BiometricViewBinder {
val iconOverlayView = view.requireViewById<LottieAnimationView>(R.id.biometric_icon_overlay)
val iconView = view.requireViewById<LottieAnimationView>(R.id.biometric_icon)
+ val iconSizeOverride =
+ if (constraintBp()) {
+ viewModel.fingerprintAffordanceSize
+ } else {
+ (view as BiometricPromptLayout).updatedFingerprintAffordanceSize
+ }
+
PromptIconViewBinder.bind(
iconView,
iconOverlayView,
- view.getUpdatedFingerprintAffordanceSize(),
- viewModel
+ iconSizeOverride,
+ viewModel,
)
val indicatorMessageView = view.requireViewById<TextView>(R.id.indicator)
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 d5695f31f121..2417fe9cd333 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
@@ -19,29 +19,45 @@ package com.android.systemui.biometrics.ui.binder
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ValueAnimator
+import android.graphics.Outline
+import android.graphics.Rect
+import android.transition.AutoTransition
+import android.transition.TransitionManager
import android.view.Surface
import android.view.View
import android.view.ViewGroup
+import android.view.ViewOutlineProvider
import android.view.WindowInsets
import android.view.WindowManager
import android.view.accessibility.AccessibilityManager
import android.widget.ImageView
import android.widget.TextView
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.constraintlayout.widget.ConstraintSet
+import androidx.constraintlayout.widget.Guideline
import androidx.core.animation.addListener
+import androidx.core.view.doOnAttach
import androidx.core.view.doOnLayout
import androidx.core.view.isGone
import androidx.lifecycle.lifecycleScope
+import com.android.systemui.Flags.constraintBp
import com.android.systemui.biometrics.AuthPanelController
import com.android.systemui.biometrics.Utils
-import com.android.systemui.biometrics.ui.BiometricPromptLayout
+import com.android.systemui.biometrics.ui.viewmodel.PromptPosition
import com.android.systemui.biometrics.ui.viewmodel.PromptSize
import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel
+import com.android.systemui.biometrics.ui.viewmodel.isBottom
import com.android.systemui.biometrics.ui.viewmodel.isLarge
+import com.android.systemui.biometrics.ui.viewmodel.isLeft
import com.android.systemui.biometrics.ui.viewmodel.isMedium
import com.android.systemui.biometrics.ui.viewmodel.isNullOrNotSmall
+import com.android.systemui.biometrics.ui.viewmodel.isRight
import com.android.systemui.biometrics.ui.viewmodel.isSmall
+import com.android.systemui.biometrics.ui.viewmodel.isTop
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.res.R
+import kotlin.math.abs
+import kotlin.math.min
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
@@ -54,18 +70,19 @@ object BiometricViewSizeBinder {
/** Resizes [BiometricPromptLayout] and the [panelViewController] via the [PromptViewModel]. */
fun bind(
- view: BiometricPromptLayout,
+ view: View,
viewModel: PromptViewModel,
viewsToHideWhenSmall: List<View>,
viewsToFadeInOnSizeChange: List<View>,
- panelViewController: AuthPanelController,
+ panelViewController: AuthPanelController?,
jankListener: BiometricJankListener,
) {
val windowManager = requireNotNull(view.context.getSystemService(WindowManager::class.java))
val accessibilityManager =
requireNotNull(view.context.getSystemService(AccessibilityManager::class.java))
+
fun notifyAccessibilityChanged() {
- Utils.notifyAccessibilityContentChanged(accessibilityManager, view)
+ Utils.notifyAccessibilityContentChanged(accessibilityManager, view as ViewGroup)
}
fun startMonitoredAnimation(animators: List<Animator>) {
@@ -77,149 +94,342 @@ object BiometricViewSizeBinder {
}
}
- val iconHolderView = view.requireViewById<View>(R.id.biometric_icon_frame)
- val iconPadding = view.resources.getDimension(R.dimen.biometric_dialog_icon_padding)
- val fullSizeYOffset =
- view.resources.getDimension(R.dimen.biometric_dialog_medium_to_large_translation_offset)
-
- // cache the original position of the icon view (as done in legacy view)
- // this must happen before any size changes can be made
- view.doOnLayout {
- // TODO(b/251476085): this old way of positioning has proven itself unreliable
- // remove this and associated thing like (UdfpsDialogMeasureAdapter) and
- // pin to the physical sensor
- val iconHolderOriginalY = iconHolderView.y
-
- // bind to prompt
- // TODO(b/251476085): migrate the legacy panel controller and simplify this
- view.repeatWhenAttached {
- var currentSize: PromptSize? = null
- lifecycleScope.launch {
- /**
- * View is only set visible in BiometricViewSizeBinder once PromptSize is
- * determined that accounts for iconView size, to prevent prompt resizing being
- * visible to the user.
- *
- * TODO(b/288175072): May be able to remove isIconViewLoaded once constraint
- * layout is implemented
- */
- combine(viewModel.isIconViewLoaded, viewModel.size, ::Pair).collect {
- (isIconViewLoaded, size) ->
- if (!isIconViewLoaded) {
- return@collect
- }
+ if (constraintBp()) {
+ val leftGuideline = view.requireViewById<Guideline>(R.id.leftGuideline)
+ val rightGuideline = view.requireViewById<Guideline>(R.id.rightGuideline)
+ val bottomGuideline = view.requireViewById<Guideline>(R.id.bottomGuideline)
+
+ val iconHolderView = view.requireViewById<View>(R.id.biometric_icon)
+ val panelView = view.requireViewById<View>(R.id.panel)
+ val cornerRadius = view.resources.getDimension(R.dimen.biometric_dialog_corner_size)
+
+ // ConstraintSets for animating between prompt sizes
+ val mediumConstraintSet = ConstraintSet()
+ mediumConstraintSet.clone(view as ConstraintLayout)
+
+ val smallConstraintSet = ConstraintSet()
+ smallConstraintSet.clone(mediumConstraintSet)
+ viewsToHideWhenSmall.forEach { smallConstraintSet.setVisibility(it.id, View.GONE) }
+
+ val largeConstraintSet = ConstraintSet()
+ largeConstraintSet.clone(mediumConstraintSet)
+ viewsToHideWhenSmall.forEach { largeConstraintSet.setVisibility(it.id, View.GONE) }
+ largeConstraintSet.setVisibility(iconHolderView.id, View.GONE)
+ largeConstraintSet.setVisibility(R.id.biometric_icon_overlay, View.GONE)
+ largeConstraintSet.setVisibility(R.id.indicator, View.GONE)
+ largeConstraintSet.setGuidelineBegin(leftGuideline.id, 0)
+ largeConstraintSet.setGuidelineEnd(rightGuideline.id, 0)
+ largeConstraintSet.setGuidelineEnd(bottomGuideline.id, 0)
+
+ // Round the panel outline
+ panelView.outlineProvider =
+ object : ViewOutlineProvider() {
+ override fun getOutline(view: View, outline: Outline) {
+ outline.setRoundRect(0, 0, view.width, view.height, cornerRadius)
+ }
+ }
- // prepare for animated size transitions
- for (v in viewsToHideWhenSmall) {
- v.showContentOrHide(forceHide = size.isSmall)
+ view.doOnLayout {
+ val windowBounds = windowManager.maximumWindowMetrics.bounds
+ val bottomInset =
+ windowManager.maximumWindowMetrics.windowInsets
+ .getInsets(WindowInsets.Type.navigationBars())
+ .bottom
+
+ fun measureBounds(position: PromptPosition) {
+ val width = min(windowBounds.height(), windowBounds.width())
+
+ var left = -1
+ var top = -1
+ var right = -1
+ var bottom = -1
+
+ when {
+ position.isTop -> {
+ left = windowBounds.centerX() - width / 2 + viewModel.promptMargin
+ top = viewModel.promptMargin
+ right = windowBounds.centerX() - width / 2 + viewModel.promptMargin
+ bottom = iconHolderView.centerY() * 2 - iconHolderView.centerY() / 4
}
- if (currentSize == null && size.isSmall) {
- iconHolderView.alpha = 0f
+ position.isBottom -> {
+ if (view.isLandscape()) {
+ left = windowBounds.centerX() - width / 2 + viewModel.promptMargin
+ top = iconHolderView.centerY()
+ right = windowBounds.centerX() - width / 2 + viewModel.promptMargin
+ bottom = bottomInset + viewModel.promptMargin
+ } else {
+ left = windowBounds.centerX() - width / 2 + viewModel.promptMargin
+ top =
+ windowBounds.height() -
+ (windowBounds.height() - iconHolderView.centerY()) * 2 +
+ viewModel.promptMargin
+ right = windowBounds.centerX() - width / 2 + viewModel.promptMargin
+ bottom = viewModel.promptMargin
+ }
}
- if ((currentSize.isSmall && size.isMedium) || size.isSmall) {
- viewsToFadeInOnSizeChange.forEach { it.alpha = 0f }
+
+ // For Udfps exclusive left and right, measure guideline to center
+ // icon in BP
+ position.isLeft -> {
+ left = viewModel.promptMargin
+ top =
+ windowBounds.height() -
+ (windowBounds.height() - iconHolderView.centerY()) * 2 +
+ viewModel.promptMargin
+ right =
+ abs(
+ windowBounds.width() - iconHolderView.centerX() * 2 +
+ viewModel.promptMargin
+ )
+ bottom = bottomInset + viewModel.promptMargin
}
+ position.isRight -> {
+ left =
+ abs(
+ iconHolderView.centerX() -
+ (windowBounds.width() - iconHolderView.centerX()) -
+ viewModel.promptMargin
+ )
+ top =
+ windowBounds.height() -
+ (windowBounds.height() - iconHolderView.centerY()) * 2 +
+ viewModel.promptMargin
+ right = viewModel.promptMargin
+ bottom = bottomInset + viewModel.promptMargin
+ }
+ }
- // TODO(b/302735104): Fix wrong height due to the delay of
- // PromptContentView. addOnLayoutChangeListener() will cause crash when
- // showing credential view, since |PromptIconViewModel| won't release the
- // flow.
- // propagate size changes to legacy panel controller and animate transitions
- view.doOnLayout {
- val width = view.measuredWidth
- val height = view.measuredHeight
-
- when {
- size.isSmall -> {
- iconHolderView.alpha = 1f
- val bottomInset =
- windowManager.maximumWindowMetrics.windowInsets
- .getInsets(WindowInsets.Type.navigationBars())
- .bottom
- iconHolderView.y =
- if (view.isLandscape()) {
- (view.height - iconHolderView.height - bottomInset) / 2f
- } else {
- view.height -
- iconHolderView.height -
- iconPadding -
- bottomInset
- }
- val newHeight =
- iconHolderView.height + (2 * iconPadding.toInt()) -
- iconHolderView.paddingTop -
- iconHolderView.paddingBottom
- panelViewController.updateForContentDimensions(
- width,
- newHeight + bottomInset,
- 0, /* animateDurationMs */
- )
- }
- size.isMedium && currentSize.isSmall -> {
- val duration = ANIMATE_SMALL_TO_MEDIUM_DURATION_MS
- panelViewController.updateForContentDimensions(
- width,
- height,
- duration,
- )
- startMonitoredAnimation(
- listOf(
- iconHolderView.asVerticalAnimator(
- duration = duration.toLong(),
- toY =
- iconHolderOriginalY -
- viewsToHideWhenSmall
- .filter { it.isGone }
- .sumOf { it.height },
- ),
- viewsToFadeInOnSizeChange.asFadeInAnimator(
- duration = duration.toLong(),
- delay = duration.toLong(),
- ),
+ val bounds = Rect(left, top, right, bottom)
+ if (bounds.shouldAdjustLeftGuideline()) {
+ leftGuideline.setGuidelineBegin(bounds.left)
+ smallConstraintSet.setGuidelineBegin(leftGuideline.id, bounds.left)
+ mediumConstraintSet.setGuidelineBegin(leftGuideline.id, bounds.left)
+ }
+ if (bounds.shouldAdjustRightGuideline()) {
+ rightGuideline.setGuidelineEnd(bounds.right)
+ smallConstraintSet.setGuidelineEnd(rightGuideline.id, bounds.right)
+ mediumConstraintSet.setGuidelineEnd(rightGuideline.id, bounds.right)
+ }
+ if (bounds.shouldAdjustBottomGuideline()) {
+ bottomGuideline.setGuidelineEnd(bounds.bottom)
+ smallConstraintSet.setGuidelineEnd(bottomGuideline.id, bounds.bottom)
+ mediumConstraintSet.setGuidelineEnd(bottomGuideline.id, bounds.bottom)
+ }
+ }
+
+ view.repeatWhenAttached {
+ var currentSize: PromptSize? = null
+ lifecycleScope.launch {
+ combine(viewModel.position, viewModel.size, ::Pair).collect {
+ (position, size) ->
+ view.doOnAttach {
+ measureBounds(position)
+
+ when {
+ size.isSmall -> {
+ val ratio =
+ if (view.isLandscape()) {
+ (windowBounds.height() -
+ bottomInset -
+ viewModel.promptMargin)
+ .toFloat() / windowBounds.height()
+ } else {
+ (windowBounds.height() - viewModel.promptMargin)
+ .toFloat() / windowBounds.height()
+ }
+ smallConstraintSet.setVerticalBias(iconHolderView.id, ratio)
+
+ smallConstraintSet.applyTo(view as ConstraintLayout?)
+ }
+ size.isMedium && currentSize.isSmall -> {
+ val autoTransition = AutoTransition()
+ autoTransition.setDuration(
+ ANIMATE_SMALL_TO_MEDIUM_DURATION_MS.toLong()
)
- )
- }
- size.isMedium && currentSize.isNullOrNotSmall -> {
- panelViewController.updateForContentDimensions(
- width,
- height,
- 0, /* animateDurationMs */
- )
+
+ TransitionManager.beginDelayedTransition(
+ view,
+ autoTransition
+ )
+ mediumConstraintSet.applyTo(view)
+ }
+ size.isLarge -> {
+ val autoTransition = AutoTransition()
+ autoTransition.setDuration(
+ ANIMATE_MEDIUM_TO_LARGE_DURATION_MS.toLong()
+ )
+
+ TransitionManager.beginDelayedTransition(
+ view,
+ autoTransition
+ )
+ largeConstraintSet.applyTo(view)
+ }
}
- size.isLarge -> {
- val duration = ANIMATE_MEDIUM_TO_LARGE_DURATION_MS
- panelViewController.setUseFullScreen(true)
- panelViewController.updateForContentDimensions(
- panelViewController.containerWidth,
- panelViewController.containerHeight,
- duration,
- )
-
- startMonitoredAnimation(
- listOf(
- view.asVerticalAnimator(
- duration.toLong() * 2 / 3,
- toY = view.y - fullSizeYOffset
- ),
- listOf(view)
- .asFadeInAnimator(
- duration = duration.toLong() / 2,
+
+ currentSize = size
+ view.visibility = View.VISIBLE
+ viewModel.setIsIconViewLoaded(false)
+ notifyAccessibilityChanged()
+
+ view.invalidate()
+ view.requestLayout()
+ }
+ }
+ }
+ }
+ }
+ } else if (panelViewController != null) {
+ val iconHolderView = view.requireViewById<View>(R.id.biometric_icon_frame)
+ val iconPadding = view.resources.getDimension(R.dimen.biometric_dialog_icon_padding)
+ val fullSizeYOffset =
+ view.resources.getDimension(
+ R.dimen.biometric_dialog_medium_to_large_translation_offset
+ )
+
+ // cache the original position of the icon view (as done in legacy view)
+ // this must happen before any size changes can be made
+ view.doOnLayout {
+ // TODO(b/251476085): this old way of positioning has proven itself unreliable
+ // remove this and associated thing like (UdfpsDialogMeasureAdapter) and
+ // pin to the physical sensor
+ val iconHolderOriginalY = iconHolderView.y
+
+ // bind to prompt
+ // TODO(b/251476085): migrate the legacy panel controller and simplify this
+ view.repeatWhenAttached {
+ var currentSize: PromptSize? = null
+ lifecycleScope.launch {
+ /**
+ * View is only set visible in BiometricViewSizeBinder once PromptSize is
+ * determined that accounts for iconView size, to prevent prompt resizing
+ * being visible to the user.
+ *
+ * TODO(b/288175072): May be able to remove isIconViewLoaded once constraint
+ * layout is implemented
+ */
+ combine(viewModel.isIconViewLoaded, viewModel.size, ::Pair).collect {
+ (isIconViewLoaded, size) ->
+ if (!isIconViewLoaded) {
+ return@collect
+ }
+
+ // prepare for animated size transitions
+ for (v in viewsToHideWhenSmall) {
+ v.showContentOrHide(forceHide = size.isSmall)
+ }
+ if (currentSize == null && size.isSmall) {
+ iconHolderView.alpha = 0f
+ }
+ if ((currentSize.isSmall && size.isMedium) || size.isSmall) {
+ viewsToFadeInOnSizeChange.forEach { it.alpha = 0f }
+ }
+
+ // TODO(b/302735104): Fix wrong height due to the delay of
+ // PromptContentView. addOnLayoutChangeListener() will cause crash when
+ // showing credential view, since |PromptIconViewModel| won't release
+ // the
+ // flow.
+ // propagate size changes to legacy panel controller and animate
+ // transitions
+ view.doOnLayout {
+ val width = view.measuredWidth
+ val height = view.measuredHeight
+
+ when {
+ size.isSmall -> {
+ iconHolderView.alpha = 1f
+ val bottomInset =
+ windowManager.maximumWindowMetrics.windowInsets
+ .getInsets(WindowInsets.Type.navigationBars())
+ .bottom
+ iconHolderView.y =
+ if (view.isLandscape()) {
+ (view.height -
+ iconHolderView.height -
+ bottomInset) / 2f
+ } else {
+ view.height -
+ iconHolderView.height -
+ iconPadding -
+ bottomInset
+ }
+ val newHeight =
+ iconHolderView.height + (2 * iconPadding.toInt()) -
+ iconHolderView.paddingTop -
+ iconHolderView.paddingBottom
+ panelViewController.updateForContentDimensions(
+ width,
+ newHeight + bottomInset,
+ 0, /* animateDurationMs */
+ )
+ }
+ size.isMedium && currentSize.isSmall -> {
+ val duration = ANIMATE_SMALL_TO_MEDIUM_DURATION_MS
+ panelViewController.updateForContentDimensions(
+ width,
+ height,
+ duration,
+ )
+ startMonitoredAnimation(
+ listOf(
+ iconHolderView.asVerticalAnimator(
+ duration = duration.toLong(),
+ toY =
+ iconHolderOriginalY -
+ viewsToHideWhenSmall
+ .filter { it.isGone }
+ .sumOf { it.height },
+ ),
+ viewsToFadeInOnSizeChange.asFadeInAnimator(
+ duration = duration.toLong(),
delay = duration.toLong(),
),
+ )
)
- )
- // TODO(b/251476085): clean up (copied from legacy)
- if (view.isAttachedToWindow) {
- val parent = view.parent as? ViewGroup
- parent?.removeView(view)
+ }
+ size.isMedium && currentSize.isNullOrNotSmall -> {
+ panelViewController.updateForContentDimensions(
+ width,
+ height,
+ 0, /* animateDurationMs */
+ )
+ }
+ size.isLarge -> {
+ val duration = ANIMATE_MEDIUM_TO_LARGE_DURATION_MS
+ panelViewController.setUseFullScreen(true)
+ panelViewController.updateForContentDimensions(
+ panelViewController.containerWidth,
+ panelViewController.containerHeight,
+ duration,
+ )
+
+ startMonitoredAnimation(
+ listOf(
+ view.asVerticalAnimator(
+ duration.toLong() * 2 / 3,
+ toY = view.y - fullSizeYOffset
+ ),
+ listOf(view)
+ .asFadeInAnimator(
+ duration = duration.toLong() / 2,
+ delay = duration.toLong(),
+ ),
+ )
+ )
+ // TODO(b/251476085): clean up (copied from legacy)
+ if (view.isAttachedToWindow) {
+ val parent = view.parent as? ViewGroup
+ parent?.removeView(view)
+ }
}
}
- }
- currentSize = size
- view.visibility = View.VISIBLE
- viewModel.setIsIconViewLoaded(false)
- notifyAccessibilityChanged()
+ currentSize = size
+ view.visibility = View.VISIBLE
+ viewModel.setIsIconViewLoaded(false)
+ notifyAccessibilityChanged()
+ }
}
}
}
@@ -244,6 +454,20 @@ private fun View.showContentOrHide(forceHide: Boolean = false) {
}
}
+private fun View.centerX(): Int {
+ return (x + width / 2).toInt()
+}
+
+private fun View.centerY(): Int {
+ return (y + height / 2).toInt()
+}
+
+private fun Rect.shouldAdjustLeftGuideline(): Boolean = left != -1
+
+private fun Rect.shouldAdjustRightGuideline(): Boolean = right != -1
+
+private fun Rect.shouldAdjustBottomGuideline(): Boolean = bottom != -1
+
private fun View.asVerticalAnimator(
duration: Long,
toY: Float,
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt
index 6e3bcf575072..2e47375b69fe 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt
@@ -17,13 +17,17 @@
package com.android.systemui.biometrics.ui.binder
+import android.graphics.Rect
import android.graphics.drawable.Animatable2
import android.graphics.drawable.AnimatedVectorDrawable
import android.graphics.drawable.Drawable
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.constraintlayout.widget.ConstraintSet
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.airbnb.lottie.LottieAnimationView
import com.android.settingslib.widget.LottieColorUtils
+import com.android.systemui.Flags.constraintBp
import com.android.systemui.biometrics.ui.viewmodel.PromptIconViewModel
import com.android.systemui.biometrics.ui.viewmodel.PromptIconViewModel.AuthType
import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel
@@ -119,6 +123,24 @@ object PromptIconViewBinder {
}
launch {
+ viewModel.iconPosition.collect { position ->
+ if (constraintBp() && position != Rect()) {
+ val iconParams = iconView.layoutParams as ConstraintLayout.LayoutParams
+
+ if (position.left != -1) {
+ iconParams.endToEnd = ConstraintSet.UNSET
+ iconParams.leftMargin = position.left
+ }
+ if (position.top != -1) {
+ iconParams.bottomToBottom = ConstraintSet.UNSET
+ iconParams.topMargin = position.top
+ }
+ iconView.layoutParams = iconParams
+ }
+ }
+ }
+
+ launch {
viewModel.iconAsset
.sample(
combine(
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt
index 3defec5ca48d..b7cffaf2bcde 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt
@@ -20,8 +20,11 @@ package com.android.systemui.biometrics.ui.viewmodel
import android.annotation.DrawableRes
import android.annotation.RawRes
import android.content.res.Configuration
+import android.graphics.Rect
+import android.util.RotationUtils
import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor
+import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
import com.android.systemui.biometrics.shared.model.DisplayRotation
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import com.android.systemui.res.R
@@ -42,7 +45,8 @@ class PromptIconViewModel
constructor(
promptViewModel: PromptViewModel,
private val displayStateInteractor: DisplayStateInteractor,
- promptSelectorInteractor: PromptSelectorInteractor
+ promptSelectorInteractor: PromptSelectorInteractor,
+ udfpsOverlayInteractor: UdfpsOverlayInteractor,
) {
/** Auth types for the UI to display. */
@@ -71,7 +75,40 @@ constructor(
} else if (modalities.hasFingerprintOnly) {
AuthType.Fingerprint
} else {
- throw IllegalStateException("unexpected modality: $modalities")
+ // TODO(b/288175072): Remove, currently needed for transition to credential view
+ AuthType.Fingerprint
+ }
+ }
+
+ val udfpsSensorBounds: Flow<Rect> =
+ combine(
+ udfpsOverlayInteractor.udfpsOverlayParams,
+ displayStateInteractor.currentRotation
+ ) { params, rotation ->
+ val rotatedBounds = Rect(params.sensorBounds)
+ RotationUtils.rotateBounds(
+ rotatedBounds,
+ params.naturalDisplayWidth,
+ params.naturalDisplayHeight,
+ rotation.ordinal
+ )
+ rotatedBounds
+ }
+ .distinctUntilChanged()
+
+ val iconPosition: Flow<Rect> =
+ combine(udfpsSensorBounds, promptViewModel.size, promptViewModel.modalities) {
+ sensorBounds,
+ size,
+ modalities ->
+ // If not Udfps, icon does not change from default layout position
+ if (!modalities.hasUdfps) {
+ Rect() // Empty rect, don't offset from default position
+ } else if (size.isSmall) {
+ // When small with Udfps, only set horizontal position
+ Rect(sensorBounds.left, -1, sensorBounds.right, -1)
+ } else {
+ sensorBounds
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptPosition.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptPosition.kt
new file mode 100644
index 000000000000..d45dad616e99
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptPosition.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics.ui.viewmodel
+
+/** The position of a biometric prompt */
+enum class PromptPosition {
+ Top,
+ Bottom,
+ Left,
+ Right,
+}
+
+val PromptPosition?.isBottom: Boolean
+ get() = this != null && this == PromptPosition.Bottom
+
+val PromptPosition?.isLeft: Boolean
+ get() = this != null && this == PromptPosition.Left
+
+val PromptPosition?.isRight: Boolean
+ get() = this != null && this == PromptPosition.Right
+
+val PromptPosition?.isTop: Boolean
+ get() = this != null && this == PromptPosition.Top
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 0f1340a63032..e75f4343da1c 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
@@ -81,11 +81,23 @@ constructor(
val faceIconHeight: Int =
context.resources.getDimensionPixelSize(R.dimen.biometric_dialog_face_icon_size)
+ val fingerprintSensorDiameter: Int =
+ (udfpsOverlayInteractor.udfpsOverlayParams.value.sensorBounds.width() *
+ udfpsOverlayInteractor.udfpsOverlayParams.value.scaleFactor)
+ .toInt()
+ val fingerprintAffordanceSize: Pair<Int, Int>? =
+ if (fingerprintSensorDiameter != 0)
+ Pair(fingerprintSensorDiameter, fingerprintSensorDiameter)
+ else null
+
private val _accessibilityHint = MutableSharedFlow<String>()
/** Hint for talkback directional guidance */
val accessibilityHint: Flow<String> = _accessibilityHint.asSharedFlow()
+ val promptMargin: Int =
+ context.resources.getDimensionPixelSize(R.dimen.biometric_dialog_border_padding)
+
private val _isAuthenticating: MutableStateFlow<Boolean> = MutableStateFlow(false)
/** If the user is currently authenticating (i.e. at least one biometric is scanning). */
@@ -135,6 +147,22 @@ constructor(
/** Event fired to the view indicating a [HapticFeedbackConstants] to be played */
val hapticsToPlay = _hapticsToPlay.asStateFlow()
+ /** The current position of the prompt */
+ val position: Flow<PromptPosition> =
+ combine(_forceLargeSize, modalities, displayStateInteractor.currentRotation) {
+ forceLarge,
+ modalities,
+ rotation ->
+ when {
+ forceLarge || !modalities.hasUdfps -> PromptPosition.Bottom
+ rotation == DisplayRotation.ROTATION_90 -> PromptPosition.Right
+ rotation == DisplayRotation.ROTATION_270 -> PromptPosition.Left
+ rotation == DisplayRotation.ROTATION_180 -> PromptPosition.Top
+ else -> PromptPosition.Bottom
+ }
+ }
+ .distinctUntilChanged()
+
/** The size of the prompt. */
val size: Flow<PromptSize> =
combine(
@@ -195,7 +223,12 @@ constructor(
.distinctUntilChanged()
val iconViewModel: PromptIconViewModel =
- PromptIconViewModel(this, displayStateInteractor, promptSelectorInteractor)
+ PromptIconViewModel(
+ this,
+ displayStateInteractor,
+ promptSelectorInteractor,
+ udfpsOverlayInteractor
+ )
private val _isIconViewLoaded = MutableStateFlow(false)