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