• 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 
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