• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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