• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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