• 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 package com.android.systemui.keyguard.ui.viewmodel
18 
19 import android.util.Log
20 import android.util.MathUtils
21 import com.android.app.animation.Interpolators
22 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
23 import com.android.systemui.dagger.SysUISingleton
24 import com.android.systemui.dagger.qualifiers.Application
25 import com.android.systemui.keyguard.domain.interactor.BurnInInteractor
26 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
27 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
28 import com.android.systemui.keyguard.shared.model.BurnInModel
29 import com.android.systemui.keyguard.shared.model.Edge
30 import com.android.systemui.keyguard.shared.model.KeyguardState
31 import com.android.systemui.keyguard.ui.StateToValue
32 import com.android.systemui.res.R
33 import com.android.systemui.shade.ShadeDisplayAware
34 import javax.inject.Inject
35 import kotlin.math.max
36 import kotlinx.coroutines.CoroutineScope
37 import kotlinx.coroutines.ExperimentalCoroutinesApi
38 import kotlinx.coroutines.flow.Flow
39 import kotlinx.coroutines.flow.MutableStateFlow
40 import kotlinx.coroutines.flow.SharingStarted
41 import kotlinx.coroutines.flow.StateFlow
42 import kotlinx.coroutines.flow.combine
43 import kotlinx.coroutines.flow.filter
44 import kotlinx.coroutines.flow.flatMapLatest
45 import kotlinx.coroutines.flow.map
46 import kotlinx.coroutines.flow.merge
47 import kotlinx.coroutines.flow.onStart
48 import kotlinx.coroutines.flow.stateIn
49 
50 /**
51  * Models UI state for elements that need to apply anti-burn-in tactics when showing in AOD
52  * (always-on display).
53  */
54 @OptIn(ExperimentalCoroutinesApi::class)
55 @SysUISingleton
56 class AodBurnInViewModel
57 @Inject
58 constructor(
59     @Application private val applicationScope: CoroutineScope,
60     private val burnInInteractor: BurnInInteractor,
61     @ShadeDisplayAware private val configurationInteractor: ConfigurationInteractor,
62     private val keyguardInteractor: KeyguardInteractor,
63     private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
64     private val goneToAodTransitionViewModel: GoneToAodTransitionViewModel,
65     private val lockscreenToAodTransitionViewModel: LockscreenToAodTransitionViewModel,
66     private val aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel,
67     private val occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel,
68     private val keyguardClockViewModel: KeyguardClockViewModel,
69 ) {
70     private val TAG = "AodBurnInViewModel"
71     private val burnInParams = MutableStateFlow(BurnInParameters())
72 
73     fun updateBurnInParams(params: BurnInParameters) {
74         burnInParams.value =
75             if (params.minViewY < params.topInset) {
76                 // minViewY should never be below the inset. Correct it if needed
77                 Log.w(TAG, "minViewY is below topInset: $params")
78                 params.copy(minViewY = params.topInset)
79             } else {
80                 params
81             }
82     }
83 
84     /** All burn-in movement: x,y,scale, to shift items and prevent burn-in */
85     val movement: StateFlow<BurnInModel> =
86         burnInParams
87             .flatMapLatest { params ->
88                 configurationInteractor
89                     .dimensionPixelSize(
90                         setOf(
91                             R.dimen.keyguard_enter_from_top_translation_y,
92                             R.dimen.keyguard_enter_from_side_translation_x,
93                         )
94                     )
95                     .flatMapLatest { dimens ->
96                         combine(
97                             keyguardInteractor.keyguardTranslationY.onStart { emit(0f) },
98                             burnIn(params).onStart { emit(BurnInModel()) },
99                             goneToAodTransitionViewModel
100                                 .enterFromTopTranslationY(
101                                     dimens[R.dimen.keyguard_enter_from_top_translation_y]!!
102                                 )
103                                 .onStart { emit(StateToValue()) },
104                             goneToAodTransitionViewModel
105                                 .enterFromSideTranslationX(
106                                     dimens[R.dimen.keyguard_enter_from_side_translation_x]!!
107                                 )
108                                 .onStart { emit(StateToValue()) },
109                             lockscreenToAodTransitionViewModel
110                                 .enterFromSideTranslationX(
111                                     dimens[R.dimen.keyguard_enter_from_side_translation_x]!!
112                                 )
113                                 .onStart { emit(StateToValue()) },
114                             occludedToLockscreenTransitionViewModel.lockscreenTranslationY.onStart {
115                                 emit(0f)
116                             },
117                             aodToLockscreenTransitionViewModel
118                                 .translationX(params.translationX)
119                                 .onStart { emit(StateToValue()) },
120                             aodToLockscreenTransitionViewModel
121                                 .translationY(params.translationY)
122                                 .onStart { emit(StateToValue()) },
123                         ) { flows ->
124                             val keyguardTranslationY = flows[0] as Float
125                             val burnInModel = flows[1] as BurnInModel
126                             val goneToAodTranslationY = flows[2] as StateToValue
127                             val goneToAodTranslationX = flows[3] as StateToValue
128                             val lockscreenToAodTranslationX = flows[4] as StateToValue
129                             val occludedToLockscreen = flows[5] as Float
130                             val aodToLockscreenTranslationX = flows[6] as StateToValue
131                             val aodToLockscreenTranslationY = flows[7] as StateToValue
132 
133                             val translationY =
134                                 if (aodToLockscreenTranslationY.transitionState.isTransitioning()) {
135                                     aodToLockscreenTranslationY.value ?: 0f
136                                 } else if (
137                                     goneToAodTranslationY.transitionState.isTransitioning()
138                                 ) {
139                                     (goneToAodTranslationY.value ?: 0f) + burnInModel.translationY
140                                 } else {
141                                     burnInModel.translationY +
142                                         occludedToLockscreen +
143                                         keyguardTranslationY
144                                 }
145                             val translationX =
146                                 if (aodToLockscreenTranslationX.transitionState.isTransitioning()) {
147                                     aodToLockscreenTranslationX.value ?: 0f
148                                 } else {
149                                     burnInModel.translationX +
150                                         (goneToAodTranslationX.value ?: 0f) +
151                                         (lockscreenToAodTranslationX.value ?: 0f)
152                                 }
153                             burnInModel.copy(
154                                 translationX = translationX.toInt(),
155                                 translationY = translationY.toInt(),
156                             )
157                         }
158                     }
159             }
160             .stateIn(
161                 scope = applicationScope,
162                 started = SharingStarted.WhileSubscribed(),
163                 initialValue = BurnInModel(),
164             )
165 
166     private fun burnIn(params: BurnInParameters): Flow<BurnInModel> {
167         return combine(
168             merge(
169                     keyguardTransitionInteractor.transition(Edge.create(to = KeyguardState.AOD)),
170                     keyguardTransitionInteractor
171                         .transition(Edge.create(from = KeyguardState.AOD))
172                         .map { it.copy(value = 1f - it.value) },
173                     keyguardTransitionInteractor
174                         .transition(Edge.create(to = KeyguardState.LOCKSCREEN))
175                         .filter { it.from != KeyguardState.AOD }
176                         .map { it.copy(value = 0f) },
177                 )
178                 .map { Interpolators.FAST_OUT_SLOW_IN.getInterpolation(it.value) },
179             burnInInteractor.burnIn(
180                 xDimenResourceId = R.dimen.burn_in_prevention_offset_x,
181                 yDimenResourceId = R.dimen.burn_in_prevention_offset_y,
182             ),
183         ) { interpolated, burnIn ->
184             val useAltAod =
185                 keyguardClockViewModel.currentClock.value
186                     ?.config
187                     ?.useAlternateSmartspaceAODTransition == true
188             // Only scale large non-weather clocks elements in large weather clock will translate
189             // the same as smartspace
190             val useScaleOnly = (!useAltAod) && keyguardClockViewModel.isLargeClockVisible.value
191 
192             val burnInY = MathUtils.lerp(0, burnIn.translationY, interpolated).toInt()
193             val translationY = max(params.topInset - params.minViewY, burnInY)
194             BurnInModel(
195                 translationX = MathUtils.lerp(0, burnIn.translationX, interpolated).toInt(),
196                 translationY = translationY,
197                 scale = MathUtils.lerp(burnIn.scale, 1f, 1f - interpolated),
198                 scaleClockOnly = useScaleOnly,
199             )
200         }
201     }
202 }
203 
204 /** UI-sourced parameters to pass into the various methods of [AodBurnInViewModel]. */
205 data class BurnInParameters(
206     /** System insets that keyguard needs to stay out of */
207     val topInset: Int = 0,
208     /** The min y-value of the visible elements on lockscreen */
209     val minViewY: Int = Int.MAX_VALUE,
210     /** The current y translation of the view */
<lambda>null211     val translationY: () -> Float? = { null },
212     /** The current x translation of the view */
<lambda>null213     val translationX: () -> Float? = { null },
214 )
215 
216 /**
217  * Models UI state of the scaling to apply to elements that need to be scaled for anti-burn-in
218  * purposes.
219  */
220 data class BurnInScaleViewModel(
221     val scale: Float = 1f,
222     /** Whether the scale only applies to clock UI elements. */
223     val scaleClockOnly: Boolean = false,
224 )
225