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.viewmodel
19
20 import android.content.Context
21 import android.content.res.Configuration
22 import android.graphics.Color
23 import android.graphics.PixelFormat
24 import android.graphics.Point
25 import android.graphics.Rect
26 import android.view.Gravity
27 import android.view.WindowManager
28 import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION
29 import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
30 import com.airbnb.lottie.model.KeyPath
31 import com.android.systemui.Flags.constraintBp
32 import com.android.systemui.biometrics.Utils
33 import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
34 import com.android.systemui.biometrics.domain.interactor.SideFpsSensorInteractor
35 import com.android.systemui.biometrics.domain.model.SideFpsSensorLocation
36 import com.android.systemui.biometrics.shared.model.DisplayRotation
37 import com.android.systemui.biometrics.shared.model.LottieCallback
38 import com.android.systemui.dagger.qualifiers.Application
39 import com.android.systemui.keyguard.domain.interactor.DeviceEntrySideFpsOverlayInteractor
40 import com.android.systemui.res.R
41 import com.android.systemui.util.kotlin.sample
42 import javax.inject.Inject
43 import kotlinx.coroutines.ExperimentalCoroutinesApi
44 import kotlinx.coroutines.flow.Flow
45 import kotlinx.coroutines.flow.MutableStateFlow
46 import kotlinx.coroutines.flow.combine
47 import kotlinx.coroutines.flow.distinctUntilChanged
48
49 /** Models UI of the side fingerprint sensor indicator view. */
50 @OptIn(ExperimentalCoroutinesApi::class)
51 class SideFpsOverlayViewModel
52 @Inject
53 constructor(
54 @Application private val applicationContext: Context,
55 deviceEntrySideFpsOverlayInteractor: DeviceEntrySideFpsOverlayInteractor,
56 displayStateInteractor: DisplayStateInteractor,
57 sfpsSensorInteractor: SideFpsSensorInteractor,
58 ) {
59 /** Contains properties of the side fingerprint sensor indicator */
60 data class OverlayViewProperties(
61 /** The raw asset for the indicator animation */
62 val indicatorAsset: Int,
63 /** Rotation of the overlayView */
64 val overlayViewRotation: Float,
65 )
66
67 private val _lottieBounds: MutableStateFlow<Rect?> = MutableStateFlow(null)
68
69 /** Used for setting lottie bounds once the composition has loaded. */
70 fun setLottieBounds(bounds: Rect) {
71 _lottieBounds.value = bounds
72 }
73
74 private val displayRotation = displayStateInteractor.currentRotation
75 private val sensorLocation = sfpsSensorInteractor.sensorLocation
76
77 /** Default LayoutParams for the overlayView */
78 val defaultOverlayViewParams: WindowManager.LayoutParams
79 get() =
80 WindowManager.LayoutParams(
81 WindowManager.LayoutParams.WRAP_CONTENT,
82 WindowManager.LayoutParams.WRAP_CONTENT,
83 WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
84 Utils.FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS,
85 PixelFormat.TRANSLUCENT
86 )
87 .apply {
88 title = TAG
89 fitInsetsTypes = 0 // overrides default, avoiding status bars during layout
90 gravity = Gravity.TOP or Gravity.LEFT
91 layoutInDisplayCutoutMode =
92 WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
93 privateFlags = PRIVATE_FLAG_TRUSTED_OVERLAY or PRIVATE_FLAG_NO_MOVE_ANIMATION
94 }
95
96 private val indicatorAsset: Flow<Int> =
97 combine(displayRotation, sensorLocation) { rotation: DisplayRotation, sensorLocation ->
98 val yAligned = sensorLocation.isSensorVerticalInDefaultOrientation
99 val newAsset: Int =
100 when (rotation) {
101 DisplayRotation.ROTATION_0 ->
102 if (yAligned) {
103 R.raw.sfps_pulse
104 } else {
105 R.raw.sfps_pulse_landscape
106 }
107 DisplayRotation.ROTATION_180 ->
108 if (yAligned) {
109 R.raw.sfps_pulse
110 } else {
111 R.raw.sfps_pulse_landscape
112 }
113 else ->
114 if (yAligned) {
115 R.raw.sfps_pulse_landscape
116 } else {
117 R.raw.sfps_pulse
118 }
119 }
120 newAsset
121 }
122 .distinctUntilChanged()
123
124 private val overlayViewRotation: Flow<Float> =
125 combine(
126 displayRotation,
127 sensorLocation,
128 ) { rotation: DisplayRotation, sensorLocation ->
129 val yAligned = sensorLocation.isSensorVerticalInDefaultOrientation
130 when (rotation) {
131 DisplayRotation.ROTATION_90 -> if (yAligned) 0f else 180f
132 DisplayRotation.ROTATION_180 -> 180f
133 DisplayRotation.ROTATION_270 -> if (yAligned) 180f else 0f
134 else -> 0f
135 }
136 }
137 .distinctUntilChanged()
138
139 /** Contains properties (animation asset and view rotation) for overlayView */
140 val overlayViewProperties: Flow<OverlayViewProperties> =
141 combine(indicatorAsset, overlayViewRotation) { asset: Int, rotation: Float ->
142 OverlayViewProperties(asset, rotation)
143 }
144
145 /** LayoutParams for placement of overlayView (the side fingerprint sensor indicator view) */
146 val overlayViewParams: Flow<WindowManager.LayoutParams> =
147 combine(
148 _lottieBounds,
149 sensorLocation,
150 displayRotation,
151 ) { bounds: Rect?, sensorLocation: SideFpsSensorLocation, displayRotation: DisplayRotation
152 ->
153 val topLeft = Point(sensorLocation.left, sensorLocation.top)
154
155 if (!constraintBp()) {
156 if (sensorLocation.isSensorVerticalInDefaultOrientation) {
157 if (displayRotation == DisplayRotation.ROTATION_0) {
158 topLeft.x -= bounds!!.width()
159 } else if (displayRotation == DisplayRotation.ROTATION_270) {
160 topLeft.y -= bounds!!.height()
161 }
162 } else {
163 if (displayRotation == DisplayRotation.ROTATION_180) {
164 topLeft.y -= bounds!!.height()
165 } else if (displayRotation == DisplayRotation.ROTATION_270) {
166 topLeft.x -= bounds!!.width()
167 }
168 }
169 }
170 defaultOverlayViewParams.apply {
171 x = topLeft.x
172 y = topLeft.y
173 }
174 }
175
176 /** List of LottieCallbacks use for adding dynamic color to the overlayView */
177 val lottieCallbacks: Flow<List<LottieCallback>> =
178 _lottieBounds.sample(deviceEntrySideFpsOverlayInteractor.showIndicatorForDeviceEntry) {
179 _,
180 showIndicatorForDeviceEntry: Boolean ->
181 val callbacks = mutableListOf<LottieCallback>()
182 if (showIndicatorForDeviceEntry) {
183 val indicatorColor =
184 com.android.settingslib.Utils.getColorAttrDefaultColor(
185 applicationContext,
186 com.android.internal.R.attr.materialColorPrimaryFixed
187 )
188 val outerRimColor =
189 com.android.settingslib.Utils.getColorAttrDefaultColor(
190 applicationContext,
191 com.android.internal.R.attr.materialColorPrimaryFixedDim
192 )
193 val chevronFill =
194 com.android.settingslib.Utils.getColorAttrDefaultColor(
195 applicationContext,
196 com.android.internal.R.attr.materialColorOnPrimaryFixed
197 )
198 callbacks.add(LottieCallback(KeyPath(".blue600", "**"), indicatorColor))
199 callbacks.add(LottieCallback(KeyPath(".blue400", "**"), outerRimColor))
200 callbacks.add(LottieCallback(KeyPath(".black", "**"), chevronFill))
201 } else {
202 if (!isDarkMode(applicationContext)) {
203 callbacks.add(LottieCallback(KeyPath(".black", "**"), Color.WHITE))
204 }
205 for (key in listOf(".blue600", ".blue400")) {
206 callbacks.add(
207 LottieCallback(
208 KeyPath(key, "**"),
209 applicationContext.getColor(
210 com.android.settingslib.color.R.color.settingslib_color_blue400
211 ),
212 )
213 )
214 }
215 }
216 callbacks
217 }
218
219 companion object {
220 private const val TAG = "SideFpsOverlayViewModel"
221 }
222 }
223
isDarkModenull224 private fun isDarkMode(context: Context): Boolean {
225 val darkMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
226 return darkMode == Configuration.UI_MODE_NIGHT_YES
227 }
228