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 @file:OptIn(ExperimentalCoroutinesApi::class)
18 
19 package com.android.systemui.keyguard.ui.viewmodel
20 
21 import android.util.Log
22 import android.util.MathUtils
23 import com.android.app.animation.Interpolators
24 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
25 import com.android.systemui.dagger.SysUISingleton
26 import com.android.systemui.keyguard.MigrateClocksToBlueprint
27 import com.android.systemui.keyguard.domain.interactor.BurnInInteractor
28 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
29 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
30 import com.android.systemui.keyguard.shared.model.BurnInModel
31 import com.android.systemui.keyguard.shared.model.ClockSize
32 import com.android.systemui.keyguard.shared.model.KeyguardState
33 import com.android.systemui.keyguard.ui.StateToValue
34 import com.android.systemui.res.R
35 import javax.inject.Inject
36 import kotlin.math.max
37 import kotlinx.coroutines.ExperimentalCoroutinesApi
38 import kotlinx.coroutines.flow.Flow
39 import kotlinx.coroutines.flow.combine
40 import kotlinx.coroutines.flow.distinctUntilChanged
41 import kotlinx.coroutines.flow.flatMapLatest
42 import kotlinx.coroutines.flow.map
43 import kotlinx.coroutines.flow.onStart
44 
45 /**
46  * Models UI state for elements that need to apply anti-burn-in tactics when showing in AOD
47  * (always-on display).
48  */
49 @SysUISingleton
50 class AodBurnInViewModel
51 @Inject
52 constructor(
53     private val burnInInteractor: BurnInInteractor,
54     private val configurationInteractor: ConfigurationInteractor,
55     private val keyguardInteractor: KeyguardInteractor,
56     private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
57     private val goneToAodTransitionViewModel: GoneToAodTransitionViewModel,
58     private val aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel,
59     private val occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel,
60     private val keyguardClockViewModel: KeyguardClockViewModel,
61 ) {
62     private val TAG = "AodBurnInViewModel"
63 
64     /** All burn-in movement: x,y,scale, to shift items and prevent burn-in */
65     fun movement(
66         burnInParams: BurnInParameters,
67     ): Flow<BurnInModel> {
68         val params =
69             if (burnInParams.minViewY < burnInParams.topInset) {
70                 // minViewY should never be below the inset. Correct it if needed
71                 Log.w(TAG, "minViewY is below topInset: $burnInParams")
72                 burnInParams.copy(minViewY = burnInParams.topInset)
73             } else {
74                 burnInParams
75             }
76         return configurationInteractor
77             .dimensionPixelSize(R.dimen.keyguard_enter_from_top_translation_y)
78             .flatMapLatest { enterFromTopAmount ->
79                 combine(
80                     keyguardInteractor.keyguardTranslationY.onStart { emit(0f) },
81                     burnIn(params).onStart { emit(BurnInModel()) },
82                     goneToAodTransitionViewModel
83                         .enterFromTopTranslationY(enterFromTopAmount)
84                         .onStart { emit(StateToValue()) },
85                     occludedToLockscreenTransitionViewModel.lockscreenTranslationY.onStart {
86                         emit(0f)
87                     },
88                     aodToLockscreenTransitionViewModel.translationY(params.translationY).onStart {
89                         emit(StateToValue())
90                     },
91                 ) {
92                     keyguardTranslationY,
93                     burnInModel,
94                     goneToAod,
95                     occludedToLockscreen,
96                     aodToLockscreen ->
97                     val translationY =
98                         if (aodToLockscreen.transitionState.isTransitioning()) {
99                             aodToLockscreen.value ?: 0f
100                         } else if (goneToAod.transitionState.isTransitioning()) {
101                             (goneToAod.value ?: 0f) + burnInModel.translationY
102                         } else {
103                             burnInModel.translationY + occludedToLockscreen + keyguardTranslationY
104                         }
105                     burnInModel.copy(translationY = translationY.toInt())
106                 }
107             }
108             .distinctUntilChanged()
109     }
110 
111     private fun burnIn(
112         params: BurnInParameters,
113     ): Flow<BurnInModel> {
114         return combine(
115             keyguardTransitionInteractor.transitionValue(KeyguardState.AOD).map {
116                 Interpolators.FAST_OUT_SLOW_IN.getInterpolation(it)
117             },
118             burnInInteractor.burnIn(
119                 xDimenResourceId = R.dimen.burn_in_prevention_offset_x,
120                 yDimenResourceId = R.dimen.burn_in_prevention_offset_y
121             ),
122         ) { interpolated, burnIn ->
123             val useAltAod =
124                 keyguardClockViewModel.currentClock.value
125                     ?.config
126                     ?.useAlternateSmartspaceAODTransition == true
127             // Only scale large non-weather clocks
128             // elements in large weather clock will translate the same as smartspace
129             val useScaleOnly =
130                 (!useAltAod) && keyguardClockViewModel.clockSize.value == ClockSize.LARGE
131 
132             val burnInY = MathUtils.lerp(0, burnIn.translationY, interpolated).toInt()
133             val translationY =
134                 if (MigrateClocksToBlueprint.isEnabled) {
135                     max(params.topInset - params.minViewY, burnInY)
136                 } else {
137                     max(params.topInset, params.minViewY + burnInY) - params.minViewY
138                 }
139             BurnInModel(
140                 translationX = MathUtils.lerp(0, burnIn.translationX, interpolated).toInt(),
141                 translationY = translationY,
142                 scale = MathUtils.lerp(burnIn.scale, 1f, 1f - interpolated),
143                 scaleClockOnly = useScaleOnly
144             )
145         }
146     }
147 }
148 
149 /** UI-sourced parameters to pass into the various methods of [AodBurnInViewModel]. */
150 data class BurnInParameters(
151     /** System insets that keyguard needs to stay out of */
152     val topInset: Int = 0,
153     /** The min y-value of the visible elements on lockscreen */
154     val minViewY: Int = Int.MAX_VALUE,
155     /** The current y translation of the view */
<lambda>null156     val translationY: () -> Float? = { null }
157 )
158 
159 /**
160  * Models UI state of the scaling to apply to elements that need to be scaled for anti-burn-in
161  * purposes.
162  */
163 data class BurnInScaleViewModel(
164     val scale: Float = 1f,
165     /** Whether the scale only applies to clock UI elements. */
166     val scaleClockOnly: Boolean = false,
167 )
168