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