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
18 package com.android.systemui.biometrics.ui.binder
19
20 import android.content.Context
21 import android.graphics.PorterDuff
22 import android.graphics.PorterDuffColorFilter
23 import android.util.Log
24 import android.view.LayoutInflater
25 import android.view.View
26 import android.view.WindowManager
27 import android.view.accessibility.AccessibilityEvent
28 import androidx.core.view.AccessibilityDelegateCompat
29 import androidx.core.view.ViewCompat
30 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
31 import androidx.lifecycle.Lifecycle
32 import androidx.lifecycle.repeatOnLifecycle
33 import com.airbnb.lottie.LottieAnimationView
34 import com.airbnb.lottie.LottieComposition
35 import com.airbnb.lottie.LottieProperty
36 import com.android.app.animation.Interpolators
37 import com.android.app.tracing.coroutines.launchTraced as launch
38 import com.android.keyguard.KeyguardPINView
39 import com.android.systemui.CoreStartable
40 import com.android.systemui.biometrics.domain.interactor.BiometricStatusInteractor
41 import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
42 import com.android.systemui.biometrics.domain.interactor.SideFpsSensorInteractor
43 import com.android.systemui.biometrics.shared.model.AuthenticationReason.NotRunning
44 import com.android.systemui.biometrics.shared.model.LottieCallback
45 import com.android.systemui.biometrics.ui.viewmodel.SideFpsOverlayViewModel
46 import com.android.systemui.dagger.SysUISingleton
47 import com.android.systemui.dagger.qualifiers.Application
48 import com.android.systemui.keyguard.domain.interactor.DeviceEntrySideFpsOverlayInteractor
49 import com.android.systemui.keyguard.ui.viewmodel.SideFpsProgressBarViewModel
50 import com.android.systemui.lifecycle.repeatWhenAttached
51 import com.android.systemui.res.R
52 import com.android.systemui.util.kotlin.sample
53 import dagger.Lazy
54 import javax.inject.Inject
55 import kotlinx.coroutines.CoroutineScope
56 import kotlinx.coroutines.flow.combine
57
58 /** Binds the side fingerprint sensor indicator view to [SideFpsOverlayViewModel]. */
59 @SysUISingleton
60 class SideFpsOverlayViewBinder
61 @Inject
62 constructor(
63 @Application private val applicationScope: CoroutineScope,
64 @Application private val applicationContext: Context,
65 private val biometricStatusInteractor: Lazy<BiometricStatusInteractor>,
66 private val displayStateInteractor: Lazy<DisplayStateInteractor>,
67 private val deviceEntrySideFpsOverlayInteractor: Lazy<DeviceEntrySideFpsOverlayInteractor>,
68 private val layoutInflater: Lazy<LayoutInflater>,
69 private val sideFpsProgressBarViewModel: Lazy<SideFpsProgressBarViewModel>,
70 private val sfpsSensorInteractor: Lazy<SideFpsSensorInteractor>,
71 private val windowManager: Lazy<WindowManager>,
72 ) : CoreStartable {
73 private val pauseDelegate: AccessibilityDelegateCompat =
74 object : AccessibilityDelegateCompat() {
75 override fun onInitializeAccessibilityNodeInfo(
76 host: View,
77 info: AccessibilityNodeInfoCompat,
78 ) {
79 super.onInitializeAccessibilityNodeInfo(host, info)
80 info.addAction(
81 AccessibilityNodeInfoCompat.AccessibilityActionCompat(
82 AccessibilityNodeInfoCompat.ACTION_CLICK,
83 host.context.getString(R.string.pause_animation),
84 )
85 )
86 }
87
88 override fun dispatchPopulateAccessibilityEvent(
89 host: View,
90 event: AccessibilityEvent,
91 ): Boolean {
92 return if (event.getEventType() === AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
93 true
94 } else {
95 super.dispatchPopulateAccessibilityEvent(host, event)
96 }
97 }
98 }
99
100 private val resumeDelegate: AccessibilityDelegateCompat =
101 object : AccessibilityDelegateCompat() {
102 override fun onInitializeAccessibilityNodeInfo(
103 host: View,
104 info: AccessibilityNodeInfoCompat,
105 ) {
106 super.onInitializeAccessibilityNodeInfo(host, info)
107 info.addAction(
108 AccessibilityNodeInfoCompat.AccessibilityActionCompat(
109 AccessibilityNodeInfoCompat.ACTION_CLICK,
110 host.context.getString(R.string.resume_animation),
111 )
112 )
113 }
114
115 override fun dispatchPopulateAccessibilityEvent(
116 host: View,
117 event: AccessibilityEvent,
118 ): Boolean {
119 return if (event.getEventType() === AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
120 true
121 } else {
122 super.dispatchPopulateAccessibilityEvent(host, event)
123 }
124 }
125 }
126
127 override fun start() {
128 applicationScope.launch {
129 sfpsSensorInteractor.get().isAvailable.collect { isSfpsAvailable ->
130 if (isSfpsAvailable) {
131 combine(
132 biometricStatusInteractor.get().sfpsAuthenticationReason,
133 deviceEntrySideFpsOverlayInteractor.get().showIndicatorForDeviceEntry,
134 sideFpsProgressBarViewModel.get().isVisible,
135 ::Triple,
136 )
137 .sample(displayStateInteractor.get().isInRearDisplayMode, ::Pair)
138 .collect { (combinedFlows, isInRearDisplayMode: Boolean) ->
139 val (
140 systemServerAuthReason,
141 showIndicatorForDeviceEntry,
142 progressBarIsVisible) =
143 combinedFlows
144 Log.d(
145 TAG,
146 "systemServerAuthReason = $systemServerAuthReason, " +
147 "showIndicatorForDeviceEntry = " +
148 "$showIndicatorForDeviceEntry, " +
149 "progressBarIsVisible = $progressBarIsVisible",
150 )
151 if (!isInRearDisplayMode) {
152 if (progressBarIsVisible) {
153 hide()
154 } else if (systemServerAuthReason != NotRunning) {
155 show()
156 } else if (showIndicatorForDeviceEntry) {
157 show()
158 } else {
159 hide()
160 }
161 }
162 }
163 }
164 }
165 }
166 }
167
168 private var overlayView: View? = null
169
170 /** Show the side fingerprint sensor indicator */
171 private fun show() {
172 if (overlayView?.isAttachedToWindow == true) {
173 Log.d(
174 TAG,
175 "show(): overlayView $overlayView isAttachedToWindow already, ignoring show request",
176 )
177 return
178 }
179
180 overlayView = layoutInflater.get().inflate(R.layout.sidefps_view, null, false)
181
182 val overlayViewModel =
183 SideFpsOverlayViewModel(
184 applicationContext,
185 deviceEntrySideFpsOverlayInteractor.get(),
186 displayStateInteractor.get(),
187 sfpsSensorInteractor.get(),
188 )
189 bind(overlayView!!, overlayViewModel, windowManager.get())
190 overlayView!!.visibility = View.INVISIBLE
191 overlayView!!.setOnClickListener { v ->
192 v.requireViewById<LottieAnimationView>(R.id.sidefps_animation).toggleAnimation()
193 }
194 ViewCompat.setAccessibilityDelegate(overlayView!!, pauseDelegate)
195 Log.d(TAG, "show(): adding overlayView $overlayView")
196 windowManager.get().addView(overlayView, overlayViewModel.defaultOverlayViewParams)
197 }
198
199 /** Hide the side fingerprint sensor indicator */
200 private fun hide() {
201 if (overlayView != null) {
202 val lottie = overlayView!!.requireViewById<LottieAnimationView>(R.id.sidefps_animation)
203 lottie.pauseAnimation()
204 lottie.removeAllLottieOnCompositionLoadedListener()
205 Log.d(TAG, "hide(): removing overlayView $overlayView, setting to null")
206 windowManager.get().removeView(overlayView)
207 overlayView = null
208 }
209 }
210
211 companion object {
212 private const val TAG = "SideFpsOverlayViewBinder"
213
214 /** Binds overlayView (side fingerprint sensor indicator view) to SideFpsOverlayViewModel */
215 fun bind(
216 overlayView: View,
217 viewModel: SideFpsOverlayViewModel,
218 windowManager: WindowManager,
219 ) {
220 overlayView.repeatWhenAttached {
221 val lottie = it.requireViewById<LottieAnimationView>(R.id.sidefps_animation)
222 lottie.addLottieOnCompositionLoadedListener { composition: LottieComposition ->
223 if (overlayView.visibility != View.VISIBLE) {
224 viewModel.setLottieBounds(composition.bounds)
225 overlayView.visibility = View.VISIBLE
226 }
227 }
228 it.alpha = 0f
229 val overlayShowAnimator =
230 it.animate()
231 .alpha(1f)
232 .setDuration(KeyguardPINView.ANIMATION_DURATION)
233 .setInterpolator(Interpolators.ALPHA_IN)
234
235 overlayShowAnimator.start()
236
237 repeatOnLifecycle(Lifecycle.State.STARTED) {
238 launch {
239 viewModel.lottieCallbacks.collect { callbacks ->
240 lottie.addOverlayDynamicColor(callbacks)
241 }
242 }
243
244 launch {
245 viewModel.overlayViewParams.collect { params ->
246 windowManager.updateViewLayout(it, params)
247 lottie.resumeAnimation()
248 }
249 }
250
251 launch {
252 viewModel.overlayViewProperties.collect { properties ->
253 it.rotation = properties.overlayViewRotation
254 lottie.setAnimation(properties.indicatorAsset)
255 }
256 }
257 }
258 }
259 }
260 }
261
262 private fun LottieAnimationView.toggleAnimation() {
263 if (isAnimating) {
264 pauseAnimation()
265 ViewCompat.setAccessibilityDelegate(this, resumeDelegate)
266 } else {
267 resumeAnimation()
268 ViewCompat.setAccessibilityDelegate(this, pauseDelegate)
269 }
270 }
271 }
272
LottieAnimationViewnull273 private fun LottieAnimationView.addOverlayDynamicColor(colorCallbacks: List<LottieCallback>) {
274 addLottieOnCompositionLoadedListener {
275 for (callback in colorCallbacks) {
276 addValueCallback(callback.keypath, LottieProperty.COLOR_FILTER) {
277 PorterDuffColorFilter(callback.color, PorterDuff.Mode.SRC_ATOP)
278 }
279 }
280 resumeAnimation()
281 }
282 }
283