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