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.graphics.Outline
22 import android.graphics.Rect
23 import android.transition.AutoTransition
24 import android.transition.TransitionManager
25 import android.util.TypedValue
26 import android.view.View
27 import android.view.ViewGroup
28 import android.view.ViewOutlineProvider
29 import android.view.WindowInsets
30 import android.view.accessibility.AccessibilityManager
31 import android.widget.ImageView
32 import android.widget.TextView
33 import androidx.constraintlayout.widget.ConstraintLayout
34 import androidx.constraintlayout.widget.ConstraintSet
35 import androidx.constraintlayout.widget.Guideline
36 import androidx.core.animation.addListener
37 import androidx.core.view.doOnLayout
38 import androidx.lifecycle.lifecycleScope
39 import com.android.systemui.biometrics.Utils
40 import com.android.systemui.biometrics.ui.viewmodel.PromptPosition
41 import com.android.systemui.biometrics.ui.viewmodel.PromptSize
42 import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel
43 import com.android.systemui.biometrics.ui.viewmodel.isLarge
44 import com.android.systemui.biometrics.ui.viewmodel.isLeft
45 import com.android.systemui.biometrics.ui.viewmodel.isMedium
46 import com.android.systemui.biometrics.ui.viewmodel.isSmall
47 import com.android.systemui.biometrics.ui.viewmodel.isTop
48 import com.android.systemui.lifecycle.repeatWhenAttached
49 import com.android.systemui.res.R
50 import com.android.systemui.utils.windowmanager.WindowManagerUtils
51 import kotlin.math.abs
52 import kotlinx.coroutines.flow.combine
53 import com.android.app.tracing.coroutines.launchTraced as launch
54
55 /** Helper for [BiometricViewBinder] to handle resize transitions. */
56 object BiometricViewSizeBinder {
57
58 private const val ANIMATE_SMALL_TO_MEDIUM_DURATION_MS = 150
59 // TODO(b/201510778): make private when related misuse is fixed
60 const val ANIMATE_MEDIUM_TO_LARGE_DURATION_MS = 450
61
62 /** Resizes [BiometricPromptLayout] and the [panelViewController] via the [PromptViewModel]. */
63 fun bind(
64 view: View,
65 viewModel: PromptViewModel,
66 viewsToHideWhenSmall: List<View>,
67 jankListener: BiometricJankListener,
68 ) {
69 val windowManager = WindowManagerUtils.getWindowManager(view.context)
70 val accessibilityManager =
71 requireNotNull(view.context.getSystemService(AccessibilityManager::class.java))
72
73 fun notifyAccessibilityChanged() {
74 Utils.notifyAccessibilityContentChanged(accessibilityManager, view as ViewGroup)
75 }
76
77 fun startMonitoredAnimation(animators: List<Animator>) {
78 with(AnimatorSet()) {
79 addListener(jankListener)
80 addListener(onEnd = { notifyAccessibilityChanged() })
81 play(animators.first()).apply { animators.drop(1).forEach { next -> with(next) } }
82 start()
83 }
84 }
85
86 val leftGuideline = view.requireViewById<Guideline>(R.id.leftGuideline)
87 val topGuideline = view.requireViewById<Guideline>(R.id.topGuideline)
88 val rightGuideline = view.requireViewById<Guideline>(R.id.rightGuideline)
89 val midGuideline = view.findViewById<Guideline>(R.id.midGuideline)
90
91 val iconHolderView = view.requireViewById<View>(R.id.biometric_icon)
92 val panelView = view.requireViewById<View>(R.id.panel)
93 val cornerRadius = view.resources.getDimension(R.dimen.biometric_dialog_corner_size)
94 val pxToDp =
95 TypedValue.applyDimension(
96 TypedValue.COMPLEX_UNIT_DIP,
97 1f,
98 view.resources.displayMetrics,
99 )
100 val cornerRadiusPx = (pxToDp * cornerRadius).toInt()
101
102 var currentSize: PromptSize? = null
103 var currentPosition: PromptPosition = PromptPosition.Bottom
104 panelView.outlineProvider =
105 object : ViewOutlineProvider() {
106 override fun getOutline(view: View, outline: Outline) {
107 when (currentPosition) {
108 PromptPosition.Right -> {
109 outline.setRoundRect(
110 0,
111 0,
112 view.width + cornerRadiusPx,
113 view.height,
114 cornerRadiusPx.toFloat(),
115 )
116 }
117 PromptPosition.Left -> {
118 outline.setRoundRect(
119 -cornerRadiusPx,
120 0,
121 view.width,
122 view.height,
123 cornerRadiusPx.toFloat(),
124 )
125 }
126 PromptPosition.Bottom,
127 PromptPosition.Top -> {
128 outline.setRoundRect(
129 0,
130 0,
131 view.width,
132 view.height + cornerRadiusPx,
133 cornerRadiusPx.toFloat(),
134 )
135 }
136 }
137 }
138 }
139
140 // ConstraintSets for animating between prompt sizes
141 val mediumConstraintSet = ConstraintSet()
142 mediumConstraintSet.clone(view as ConstraintLayout)
143
144 val smallConstraintSet = ConstraintSet()
145 smallConstraintSet.clone(mediumConstraintSet)
146
147 val largeConstraintSet = ConstraintSet()
148 largeConstraintSet.clone(mediumConstraintSet)
149 largeConstraintSet.constrainMaxWidth(R.id.panel, 0)
150 largeConstraintSet.setGuidelineBegin(R.id.leftGuideline, 0)
151 largeConstraintSet.setGuidelineEnd(R.id.rightGuideline, 0)
152
153 // TODO: Investigate better way to handle 180 rotations
154 val flipConstraintSet = ConstraintSet()
155
156 view.doOnLayout {
157 fun setVisibilities(hideSensorIcon: Boolean, size: PromptSize) {
158 viewsToHideWhenSmall.forEach { it.showContentOrHide(forceHide = size.isSmall) }
159 largeConstraintSet.setVisibility(iconHolderView.id, View.GONE)
160 largeConstraintSet.setVisibility(R.id.indicator, View.GONE)
161 largeConstraintSet.setVisibility(R.id.scrollView, View.GONE)
162
163 if (hideSensorIcon) {
164 smallConstraintSet.setVisibility(iconHolderView.id, View.GONE)
165 smallConstraintSet.setVisibility(R.id.indicator, View.GONE)
166 mediumConstraintSet.setVisibility(iconHolderView.id, View.GONE)
167 mediumConstraintSet.setVisibility(R.id.indicator, View.GONE)
168 }
169 }
170
171 view.repeatWhenAttached {
172 lifecycleScope.launch {
173 viewModel.iconPosition.collect { position ->
174 if (position != Rect()) {
175 val iconParams =
176 iconHolderView.layoutParams as ConstraintLayout.LayoutParams
177
178 if (position.left != 0) {
179 iconParams.endToEnd = ConstraintSet.UNSET
180 iconParams.leftMargin = position.left
181 mediumConstraintSet.clear(R.id.biometric_icon, ConstraintSet.RIGHT)
182 mediumConstraintSet.connect(
183 R.id.biometric_icon,
184 ConstraintSet.LEFT,
185 ConstraintSet.PARENT_ID,
186 ConstraintSet.LEFT,
187 )
188 mediumConstraintSet.setMargin(
189 R.id.biometric_icon,
190 ConstraintSet.LEFT,
191 position.left,
192 )
193 smallConstraintSet.clear(R.id.biometric_icon, ConstraintSet.RIGHT)
194 smallConstraintSet.connect(
195 R.id.biometric_icon,
196 ConstraintSet.LEFT,
197 ConstraintSet.PARENT_ID,
198 ConstraintSet.LEFT,
199 )
200 smallConstraintSet.setMargin(
201 R.id.biometric_icon,
202 ConstraintSet.LEFT,
203 position.left,
204 )
205 }
206 if (position.top != 0) {
207 iconParams.bottomToBottom = ConstraintSet.UNSET
208 iconParams.topMargin = position.top
209 mediumConstraintSet.clear(R.id.biometric_icon, ConstraintSet.BOTTOM)
210 mediumConstraintSet.setMargin(
211 R.id.biometric_icon,
212 ConstraintSet.TOP,
213 position.top,
214 )
215 smallConstraintSet.clear(R.id.biometric_icon, ConstraintSet.BOTTOM)
216 smallConstraintSet.setMargin(
217 R.id.biometric_icon,
218 ConstraintSet.TOP,
219 position.top,
220 )
221 }
222 if (position.right != 0) {
223 iconParams.startToStart = ConstraintSet.UNSET
224 iconParams.rightMargin = position.right
225 mediumConstraintSet.clear(R.id.biometric_icon, ConstraintSet.LEFT)
226 mediumConstraintSet.connect(
227 R.id.biometric_icon,
228 ConstraintSet.RIGHT,
229 ConstraintSet.PARENT_ID,
230 ConstraintSet.RIGHT,
231 )
232 mediumConstraintSet.setMargin(
233 R.id.biometric_icon,
234 ConstraintSet.RIGHT,
235 position.right,
236 )
237 smallConstraintSet.clear(R.id.biometric_icon, ConstraintSet.LEFT)
238 smallConstraintSet.connect(
239 R.id.biometric_icon,
240 ConstraintSet.RIGHT,
241 ConstraintSet.PARENT_ID,
242 ConstraintSet.RIGHT,
243 )
244 smallConstraintSet.setMargin(
245 R.id.biometric_icon,
246 ConstraintSet.RIGHT,
247 position.right,
248 )
249 }
250 if (position.bottom != 0) {
251 iconParams.topToTop = ConstraintSet.UNSET
252 iconParams.bottomMargin = position.bottom
253 mediumConstraintSet.clear(R.id.biometric_icon, ConstraintSet.TOP)
254 mediumConstraintSet.setMargin(
255 R.id.biometric_icon,
256 ConstraintSet.BOTTOM,
257 position.bottom,
258 )
259 smallConstraintSet.clear(R.id.biometric_icon, ConstraintSet.TOP)
260 smallConstraintSet.setMargin(
261 R.id.biometric_icon,
262 ConstraintSet.BOTTOM,
263 position.bottom,
264 )
265 }
266 iconHolderView.layoutParams = iconParams
267 }
268 }
269 }
270
271 lifecycleScope.launch {
272 viewModel.iconSize.collect { iconSize ->
273 iconHolderView.layoutParams.width = iconSize.first
274 iconHolderView.layoutParams.height = iconSize.second
275 mediumConstraintSet.constrainWidth(R.id.biometric_icon, iconSize.first)
276 mediumConstraintSet.constrainHeight(R.id.biometric_icon, iconSize.second)
277 }
278 }
279
280 lifecycleScope.launch {
281 viewModel.guidelineBounds.collect { bounds ->
282 val bottomInset =
283 windowManager.maximumWindowMetrics.windowInsets
284 .getInsets(WindowInsets.Type.navigationBars())
285 .bottom
286 mediumConstraintSet.setGuidelineEnd(R.id.bottomGuideline, bottomInset)
287
288 if (bounds.left >= 0) {
289 mediumConstraintSet.setGuidelineBegin(leftGuideline.id, bounds.left)
290 smallConstraintSet.setGuidelineBegin(leftGuideline.id, bounds.left)
291 } else if (bounds.left < 0) {
292 mediumConstraintSet.setGuidelineEnd(leftGuideline.id, abs(bounds.left))
293 smallConstraintSet.setGuidelineEnd(leftGuideline.id, abs(bounds.left))
294 }
295
296 if (bounds.right >= 0) {
297 mediumConstraintSet.setGuidelineEnd(rightGuideline.id, bounds.right)
298 smallConstraintSet.setGuidelineEnd(rightGuideline.id, bounds.right)
299 } else if (bounds.right < 0) {
300 mediumConstraintSet.setGuidelineBegin(
301 rightGuideline.id,
302 abs(bounds.right),
303 )
304 smallConstraintSet.setGuidelineBegin(
305 rightGuideline.id,
306 abs(bounds.right),
307 )
308 }
309
310 if (bounds.top >= 0) {
311 mediumConstraintSet.setGuidelineBegin(topGuideline.id, bounds.top)
312 smallConstraintSet.setGuidelineBegin(topGuideline.id, bounds.top)
313 } else if (bounds.top < 0) {
314 mediumConstraintSet.setGuidelineEnd(topGuideline.id, abs(bounds.top))
315 smallConstraintSet.setGuidelineEnd(topGuideline.id, abs(bounds.top))
316 }
317
318 if (midGuideline != null) {
319 val left =
320 if (bounds.left >= 0) {
321 abs(bounds.left)
322 } else {
323 view.width - abs(bounds.left)
324 }
325 val right =
326 if (bounds.right >= 0) {
327 view.width - abs(bounds.right)
328 } else {
329 abs(bounds.right)
330 }
331 val mid = (left + right) / 2
332 mediumConstraintSet.setGuidelineBegin(midGuideline.id, mid)
333 }
334 }
335 }
336
337 lifecycleScope.launch {
338 combine(viewModel.hideSensorIcon, viewModel.size, ::Pair).collect {
339 (hideSensorIcon, size) ->
340 setVisibilities(hideSensorIcon, size)
341 }
342 }
343
344 lifecycleScope.launch {
345 combine(viewModel.position, viewModel.size, ::Pair).collect { (position, size)
346 ->
347 if (position.isLeft) {
348 if (size.isSmall) {
349 flipConstraintSet.clone(smallConstraintSet)
350 } else {
351 flipConstraintSet.clone(mediumConstraintSet)
352 }
353
354 // Move all content to other panel
355 flipConstraintSet.connect(
356 R.id.scrollView,
357 ConstraintSet.LEFT,
358 R.id.midGuideline,
359 ConstraintSet.LEFT,
360 )
361 flipConstraintSet.connect(
362 R.id.scrollView,
363 ConstraintSet.RIGHT,
364 R.id.rightGuideline,
365 ConstraintSet.RIGHT,
366 )
367 } else if (position.isTop) {
368 // Top position is only used for 180 rotation Udfps
369 // Requires repositioning due to sensor location at top of screen
370 mediumConstraintSet.connect(
371 R.id.scrollView,
372 ConstraintSet.TOP,
373 R.id.indicator,
374 ConstraintSet.BOTTOM,
375 )
376 mediumConstraintSet.connect(
377 R.id.scrollView,
378 ConstraintSet.BOTTOM,
379 R.id.button_bar,
380 ConstraintSet.TOP,
381 )
382 mediumConstraintSet.connect(
383 R.id.panel,
384 ConstraintSet.TOP,
385 R.id.biometric_icon,
386 ConstraintSet.TOP,
387 )
388 mediumConstraintSet.setMargin(
389 R.id.panel,
390 ConstraintSet.TOP,
391 (-24 * pxToDp).toInt(),
392 )
393 mediumConstraintSet.setVerticalBias(R.id.scrollView, 0f)
394 }
395
396 when {
397 size.isSmall -> {
398 if (position.isLeft) {
399 flipConstraintSet.applyTo(view)
400 } else {
401 smallConstraintSet.applyTo(view)
402 }
403 }
404 size.isMedium && currentSize.isSmall -> {
405 val autoTransition = AutoTransition()
406 autoTransition.setDuration(
407 ANIMATE_SMALL_TO_MEDIUM_DURATION_MS.toLong()
408 )
409
410 if (position.isLeft) {
411 flipConstraintSet.applyTo(view)
412 } else {
413 mediumConstraintSet.applyTo(view)
414 }
415 TransitionManager.beginDelayedTransition(view, autoTransition)
416 }
417 size.isMedium -> {
418 if (position.isLeft) {
419 flipConstraintSet.applyTo(view)
420 } else {
421 mediumConstraintSet.applyTo(view)
422 }
423 }
424 size.isLarge -> {
425 val autoTransition = AutoTransition()
426 autoTransition.setDuration(
427 if (currentSize.isSmall) {
428 ANIMATE_SMALL_TO_MEDIUM_DURATION_MS.toLong()
429 } else {
430 ANIMATE_MEDIUM_TO_LARGE_DURATION_MS.toLong()
431 }
432 )
433
434 largeConstraintSet.applyTo(view)
435 TransitionManager.beginDelayedTransition(view, autoTransition)
436 }
437 }
438
439 currentSize = size
440 currentPosition = position
441 notifyAccessibilityChanged()
442
443 panelView.invalidateOutline()
444 view.invalidate()
445 view.requestLayout()
446 }
447 }
448 }
449 }
450 }
451 }
452
showContentOrHidenull453 private fun View.showContentOrHide(forceHide: Boolean = false) {
454 val isTextViewWithBlankText = this is TextView && this.text.isBlank()
455 val isImageViewWithoutImage = this is ImageView && this.drawable == null
456 visibility =
457 if (forceHide || isTextViewWithBlankText || isImageViewWithoutImage) {
458 View.GONE
459 } else {
460 View.VISIBLE
461 }
462 }
463