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