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