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