1 /*
2  * 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 
207 private fun View.isLandscape(): Boolean {
208     val r = context.display?.rotation
209     return r == Surface.ROTATION_90 || r == Surface.ROTATION_270
210 }
211 
212 private fun TextView.showTextOrHide(forceHide: Boolean = false) {
213     visibility = if (forceHide || text.isBlank()) View.GONE else View.VISIBLE
214 }
215 
216 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 
227 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