1 /*
<lambda>null2 * Copyright (C) 2023 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package com.android.systemui.biometrics.ui.binder
18
19 import android.animation.Animator
20 import android.animation.AnimatorSet
21 import android.animation.ValueAnimator
22 import android.view.Surface
23 import android.view.View
24 import android.view.ViewGroup
25 import android.view.WindowInsets
26 import android.view.WindowManager
27 import android.view.accessibility.AccessibilityManager
28 import android.widget.TextView
29 import androidx.core.animation.addListener
30 import androidx.core.view.doOnLayout
31 import androidx.core.view.isGone
32 import androidx.lifecycle.lifecycleScope
33 import com.android.systemui.R
34 import com.android.systemui.biometrics.AuthDialog
35 import com.android.systemui.biometrics.AuthPanelController
36 import com.android.systemui.biometrics.Utils
37 import com.android.systemui.biometrics.ui.BiometricPromptLayout
38 import com.android.systemui.biometrics.ui.viewmodel.PromptSize
39 import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel
40 import com.android.systemui.biometrics.ui.viewmodel.isLarge
41 import com.android.systemui.biometrics.ui.viewmodel.isMedium
42 import com.android.systemui.biometrics.ui.viewmodel.isNullOrNotSmall
43 import com.android.systemui.biometrics.ui.viewmodel.isSmall
44 import com.android.systemui.lifecycle.repeatWhenAttached
45 import kotlinx.coroutines.launch
46
47 /** Helper for [BiometricViewBinder] to handle resize transitions. */
48 object BiometricViewSizeBinder {
49
50 /** Resizes [BiometricPromptLayout] and the [panelViewController] via the [PromptViewModel]. */
51 fun bind(
52 view: BiometricPromptLayout,
53 viewModel: PromptViewModel,
54 viewsToHideWhenSmall: List<TextView>,
55 viewsToFadeInOnSizeChange: List<View>,
56 panelViewController: AuthPanelController,
57 jankListener: BiometricJankListener,
58 ) {
59 val windowManager = requireNotNull(view.context.getSystemService(WindowManager::class.java))
60 val accessibilityManager =
61 requireNotNull(view.context.getSystemService(AccessibilityManager::class.java))
62 fun notifyAccessibilityChanged() {
63 Utils.notifyAccessibilityContentChanged(accessibilityManager, view)
64 }
65
66 fun startMonitoredAnimation(animators: List<Animator>) {
67 with(AnimatorSet()) {
68 addListener(jankListener)
69 addListener(onEnd = { notifyAccessibilityChanged() })
70 play(animators.first()).apply { animators.drop(1).forEach { next -> with(next) } }
71 start()
72 }
73 }
74
75 val iconHolderView = view.requireViewById<View>(R.id.biometric_icon_frame)
76 val iconPadding = view.resources.getDimension(R.dimen.biometric_dialog_icon_padding)
77 val fullSizeYOffset =
78 view.resources.getDimension(R.dimen.biometric_dialog_medium_to_large_translation_offset)
79
80 // cache the original position of the icon view (as done in legacy view)
81 // this must happen before any size changes can be made
82 view.doOnLayout {
83 // TODO(b/251476085): this old way of positioning has proven itself unreliable
84 // remove this and associated thing like (UdfpsDialogMeasureAdapter) and
85 // pin to the physical sensor
86 val iconHolderOriginalY = iconHolderView.y
87
88 // bind to prompt
89 // TODO(b/251476085): migrate the legacy panel controller and simplify this
90 view.repeatWhenAttached {
91 var currentSize: PromptSize? = null
92 lifecycleScope.launch {
93 viewModel.size.collect { size ->
94 // prepare for animated size transitions
95 for (v in viewsToHideWhenSmall) {
96 v.showTextOrHide(forceHide = size.isSmall)
97 }
98 if (currentSize == null && size.isSmall) {
99 iconHolderView.alpha = 0f
100 }
101 if ((currentSize.isSmall && size.isMedium) || size.isSmall) {
102 viewsToFadeInOnSizeChange.forEach { it.alpha = 0f }
103 }
104
105 // propagate size changes to legacy panel controller and animate transitions
106 view.doOnLayout {
107 val width = view.measuredWidth
108 val height = view.measuredHeight
109
110 when {
111 size.isSmall -> {
112 iconHolderView.alpha = 1f
113 val bottomInset =
114 windowManager.maximumWindowMetrics.windowInsets
115 .getInsets(WindowInsets.Type.navigationBars())
116 .bottom
117 iconHolderView.y =
118 if (view.isLandscape()) {
119 (view.height - iconHolderView.height - bottomInset) / 2f
120 } else {
121 view.height -
122 iconHolderView.height -
123 iconPadding -
124 bottomInset
125 }
126 val newHeight =
127 iconHolderView.height + (2 * iconPadding.toInt()) -
128 iconHolderView.paddingTop -
129 iconHolderView.paddingBottom
130 panelViewController.updateForContentDimensions(
131 width,
132 newHeight + bottomInset,
133 0, /* animateDurationMs */
134 )
135 }
136 size.isMedium && currentSize.isSmall -> {
137 val duration = AuthDialog.ANIMATE_SMALL_TO_MEDIUM_DURATION_MS
138 panelViewController.updateForContentDimensions(
139 width,
140 height,
141 duration,
142 )
143 startMonitoredAnimation(
144 listOf(
145 iconHolderView.asVerticalAnimator(
146 duration = duration.toLong(),
147 toY =
148 iconHolderOriginalY -
149 viewsToHideWhenSmall
150 .filter { it.isGone }
151 .sumOf { it.height },
152 ),
153 viewsToFadeInOnSizeChange.asFadeInAnimator(
154 duration = duration.toLong(),
155 delay = duration.toLong(),
156 ),
157 )
158 )
159 }
160 size.isMedium && currentSize.isNullOrNotSmall -> {
161 panelViewController.updateForContentDimensions(
162 width,
163 height,
164 0, /* animateDurationMs */
165 )
166 }
167 size.isLarge -> {
168 val duration = AuthDialog.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS
169 panelViewController.setUseFullScreen(true)
170 panelViewController.updateForContentDimensions(
171 panelViewController.containerWidth,
172 panelViewController.containerHeight,
173 duration,
174 )
175
176 startMonitoredAnimation(
177 listOf(
178 view.asVerticalAnimator(
179 duration.toLong() * 2 / 3,
180 toY = view.y - fullSizeYOffset
181 ),
182 listOf(view)
183 .asFadeInAnimator(
184 duration = duration.toLong() / 2,
185 delay = duration.toLong(),
186 ),
187 )
188 )
189 // TODO(b/251476085): clean up (copied from legacy)
190 if (view.isAttachedToWindow) {
191 val parent = view.parent as? ViewGroup
192 parent?.removeView(view)
193 }
194 }
195 }
196
197 currentSize = size
198 notifyAccessibilityChanged()
199 }
200 }
201 }
202 }
203 }
204 }
205 }
206
isLandscapenull207 private fun View.isLandscape(): Boolean {
208 val r = context.display?.rotation
209 return r == Surface.ROTATION_90 || r == Surface.ROTATION_270
210 }
211
TextViewnull212 private fun TextView.showTextOrHide(forceHide: Boolean = false) {
213 visibility = if (forceHide || text.isBlank()) View.GONE else View.VISIBLE
214 }
215
asVerticalAnimatornull216 private fun View.asVerticalAnimator(
217 duration: Long,
218 toY: Float,
219 fromY: Float = this.y
220 ): ValueAnimator {
221 val animator = ValueAnimator.ofFloat(fromY, toY)
222 animator.duration = duration
223 animator.addUpdateListener { y = it.animatedValue as Float }
224 return animator
225 }
226
asFadeInAnimatornull227 private fun List<View>.asFadeInAnimator(duration: Long, delay: Long): ValueAnimator {
228 forEach { it.alpha = 0f }
229 val animator = ValueAnimator.ofFloat(0f, 1f)
230 animator.duration = duration
231 animator.startDelay = delay
232 animator.addUpdateListener {
233 val alpha = it.animatedValue as Float
234 forEach { view -> view.alpha = alpha }
235 }
236 return animator
237 }
238