diff options
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) |