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.keyguard.ui.viewmodel 19 20 import androidx.annotation.VisibleForTesting 21 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor 22 import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor 23 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor 24 import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel 25 import com.android.systemui.keyguard.shared.model.KeyguardState 26 import com.android.systemui.keyguard.shared.quickaffordance.ActivationState 27 import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition 28 import com.android.systemui.shade.domain.interactor.ShadeInteractor 29 import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots 30 import javax.inject.Inject 31 import kotlinx.coroutines.ExperimentalCoroutinesApi 32 import kotlinx.coroutines.flow.Flow 33 import kotlinx.coroutines.flow.MutableStateFlow 34 import kotlinx.coroutines.flow.combine 35 import kotlinx.coroutines.flow.distinctUntilChanged 36 import kotlinx.coroutines.flow.flatMapLatest 37 import kotlinx.coroutines.flow.map 38 import kotlinx.coroutines.flow.merge 39 40 @OptIn(ExperimentalCoroutinesApi::class) 41 class KeyguardQuickAffordancesCombinedViewModel 42 @Inject 43 constructor( 44 private val quickAffordanceInteractor: KeyguardQuickAffordanceInteractor, 45 private val keyguardInteractor: KeyguardInteractor, 46 shadeInteractor: ShadeInteractor, 47 aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel, 48 dozingToLockscreenTransitionViewModel: DozingToLockscreenTransitionViewModel, 49 dreamingHostedToLockscreenTransitionViewModel: DreamingHostedToLockscreenTransitionViewModel, 50 dreamingToLockscreenTransitionViewModel: DreamingToLockscreenTransitionViewModel, 51 goneToLockscreenTransitionViewModel: GoneToLockscreenTransitionViewModel, 52 occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel, 53 offToLockscreenTransitionViewModel: OffToLockscreenTransitionViewModel, 54 primaryBouncerToLockscreenTransitionViewModel: PrimaryBouncerToLockscreenTransitionViewModel, 55 glanceableHubToLockscreenTransitionViewModel: GlanceableHubToLockscreenTransitionViewModel, 56 lockscreenToAodTransitionViewModel: LockscreenToAodTransitionViewModel, 57 lockscreenToDozingTransitionViewModel: LockscreenToDozingTransitionViewModel, 58 lockscreenToDreamingHostedTransitionViewModel: LockscreenToDreamingHostedTransitionViewModel, 59 lockscreenToDreamingTransitionViewModel: LockscreenToDreamingTransitionViewModel, 60 lockscreenToGoneTransitionViewModel: LockscreenToGoneTransitionViewModel, 61 lockscreenToOccludedTransitionViewModel: LockscreenToOccludedTransitionViewModel, 62 lockscreenToPrimaryBouncerTransitionViewModel: LockscreenToPrimaryBouncerTransitionViewModel, 63 lockscreenToGlanceableHubTransitionViewModel: LockscreenToGlanceableHubTransitionViewModel, 64 transitionInteractor: KeyguardTransitionInteractor, 65 ) { 66 67 data class PreviewMode( 68 val isInPreviewMode: Boolean = false, 69 val shouldHighlightSelectedAffordance: Boolean = false, 70 ) 71 72 /** 73 * Whether this view-model instance is powering the preview experience that renders exclusively 74 * in the wallpaper picker application. This should _always_ be `false` for the real lock screen 75 * experience. 76 */ 77 private val previewMode = MutableStateFlow(PreviewMode()) 78 79 private val showingLockscreen: Flow<Boolean> = 80 transitionInteractor.finishedKeyguardState.map { keyguardState -> 81 keyguardState == KeyguardState.LOCKSCREEN 82 } 83 84 /** The only time the expansion is important is while lockscreen is actively displayed */ 85 private val shadeExpansionAlpha = 86 combine( 87 showingLockscreen, 88 shadeInteractor.anyExpansion, 89 ) { showingLockscreen, expansion -> 90 if (showingLockscreen) { 91 1 - expansion 92 } else { 93 0f 94 } 95 } 96 97 /** 98 * ID of the slot that's currently selected in the preview that renders exclusively in the 99 * wallpaper picker application. This is ignored for the actual, real lock screen experience. 100 */ 101 private val selectedPreviewSlotId = 102 MutableStateFlow(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START) 103 104 /** alpha while fading the quick affordances out */ 105 private val fadeInAlpha: Flow<Float> = 106 merge( 107 aodToLockscreenTransitionViewModel.shortcutsAlpha, 108 dozingToLockscreenTransitionViewModel.shortcutsAlpha, 109 dreamingHostedToLockscreenTransitionViewModel.shortcutsAlpha, 110 dreamingToLockscreenTransitionViewModel.shortcutsAlpha, 111 goneToLockscreenTransitionViewModel.shortcutsAlpha, 112 occludedToLockscreenTransitionViewModel.shortcutsAlpha, 113 offToLockscreenTransitionViewModel.shortcutsAlpha, 114 primaryBouncerToLockscreenTransitionViewModel.shortcutsAlpha, 115 glanceableHubToLockscreenTransitionViewModel.shortcutsAlpha, 116 ) 117 118 /** alpha while fading the quick affordances in */ 119 private val fadeOutAlpha: Flow<Float> = 120 merge( 121 lockscreenToAodTransitionViewModel.shortcutsAlpha, 122 lockscreenToDozingTransitionViewModel.shortcutsAlpha, 123 lockscreenToDreamingHostedTransitionViewModel.shortcutsAlpha, 124 lockscreenToDreamingTransitionViewModel.shortcutsAlpha, 125 lockscreenToGoneTransitionViewModel.shortcutsAlpha, 126 lockscreenToOccludedTransitionViewModel.shortcutsAlpha, 127 lockscreenToPrimaryBouncerTransitionViewModel.shortcutsAlpha, 128 lockscreenToGlanceableHubTransitionViewModel.shortcutsAlpha, 129 shadeExpansionAlpha, 130 ) 131 132 /** The source of truth of alpha for all of the quick affordances on lockscreen */ 133 val transitionAlpha: Flow<Float> = 134 merge( 135 fadeInAlpha, 136 fadeOutAlpha, 137 ) 138 139 /** 140 * Whether quick affordances are "opaque enough" to be considered visible to and interactive by 141 * the user. If they are not interactive, user input should not be allowed on them. 142 * 143 * Note that there is a margin of error, where we allow very, very slightly transparent views to 144 * be considered "fully opaque" for the purpose of being interactive. This is to accommodate the 145 * error margin of floating point arithmetic. 146 * 147 * A view that is visible but with an alpha of less than our threshold either means it's not 148 * fully done fading in or is fading/faded out. Either way, it should not be 149 * interactive/clickable unless "fully opaque" to avoid issues like in b/241830987. 150 */ 151 private val areQuickAffordancesFullyOpaque: Flow<Boolean> = 152 transitionAlpha 153 .map { alpha -> alpha >= AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD } 154 .distinctUntilChanged() 155 156 /** An observable for the view-model of the "start button" quick affordance. */ 157 val startButton: Flow<KeyguardQuickAffordanceViewModel> = 158 button(KeyguardQuickAffordancePosition.BOTTOM_START) 159 160 /** An observable for the view-model of the "end button" quick affordance. */ 161 val endButton: Flow<KeyguardQuickAffordanceViewModel> = 162 button(KeyguardQuickAffordancePosition.BOTTOM_END) 163 164 /** 165 * Notifies that a slot with the given ID has been selected in the preview experience that is 166 * rendering in the wallpaper picker. This is ignored for the real lock screen experience. 167 * 168 * @see [enablePreviewMode] 169 */ 170 fun onPreviewSlotSelected(slotId: String) { 171 selectedPreviewSlotId.value = slotId 172 } 173 174 /** 175 * Puts this view-model in "preview mode", which means it's being used for UI that is rendering 176 * the lock screen preview in wallpaper picker / settings and not the real experience on the 177 * lock screen. 178 * 179 * @param initiallySelectedSlotId The ID of the initial slot to render as the selected one. 180 * @param shouldHighlightSelectedAffordance Whether the selected quick affordance should be 181 * highlighted (while all others are dimmed to make the selected one stand out). 182 */ 183 fun enablePreviewMode( 184 initiallySelectedSlotId: String?, 185 shouldHighlightSelectedAffordance: Boolean, 186 ) { 187 val newPreviewMode = 188 PreviewMode( 189 isInPreviewMode = true, 190 shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance, 191 ) 192 onPreviewSlotSelected( 193 initiallySelectedSlotId ?: KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START 194 ) 195 previewMode.value = newPreviewMode 196 } 197 198 private fun button( 199 position: KeyguardQuickAffordancePosition 200 ): Flow<KeyguardQuickAffordanceViewModel> { 201 return previewMode.flatMapLatest { previewMode -> 202 combine( 203 if (previewMode.isInPreviewMode) { 204 quickAffordanceInteractor.quickAffordanceAlwaysVisible(position = position) 205 } else { 206 quickAffordanceInteractor.quickAffordance(position = position) 207 }, 208 keyguardInteractor.animateDozingTransitions.distinctUntilChanged(), 209 areQuickAffordancesFullyOpaque, 210 selectedPreviewSlotId, 211 quickAffordanceInteractor.useLongPress(), 212 ) { model, animateReveal, isFullyOpaque, selectedPreviewSlotId, useLongPress -> 213 val slotId = position.toSlotId() 214 val isSelected = selectedPreviewSlotId == slotId 215 model.toViewModel( 216 animateReveal = !previewMode.isInPreviewMode && animateReveal, 217 isClickable = isFullyOpaque && !previewMode.isInPreviewMode, 218 isSelected = 219 previewMode.isInPreviewMode && 220 previewMode.shouldHighlightSelectedAffordance && 221 isSelected, 222 isDimmed = 223 previewMode.isInPreviewMode && 224 previewMode.shouldHighlightSelectedAffordance && 225 !isSelected, 226 forceInactive = previewMode.isInPreviewMode, 227 slotId = slotId, 228 useLongPress = useLongPress, 229 ) 230 } 231 .distinctUntilChanged() 232 } 233 } 234 235 private fun KeyguardQuickAffordanceModel.toViewModel( 236 animateReveal: Boolean, 237 isClickable: Boolean, 238 isSelected: Boolean, 239 isDimmed: Boolean, 240 forceInactive: Boolean, 241 slotId: String, 242 useLongPress: Boolean, 243 ): KeyguardQuickAffordanceViewModel { 244 return when (this) { 245 is KeyguardQuickAffordanceModel.Visible -> 246 KeyguardQuickAffordanceViewModel( 247 configKey = configKey, 248 isVisible = true, 249 animateReveal = animateReveal, 250 icon = icon, 251 onClicked = { parameters -> 252 quickAffordanceInteractor.onQuickAffordanceTriggered( 253 configKey = parameters.configKey, 254 expandable = parameters.expandable, 255 slotId = parameters.slotId, 256 ) 257 }, 258 isClickable = isClickable, 259 isActivated = !forceInactive && activationState is ActivationState.Active, 260 isSelected = isSelected, 261 useLongPress = useLongPress, 262 isDimmed = isDimmed, 263 slotId = slotId, 264 ) 265 is KeyguardQuickAffordanceModel.Hidden -> 266 KeyguardQuickAffordanceViewModel( 267 slotId = slotId, 268 ) 269 } 270 } 271 272 companion object { 273 // We select a value that's less than 1.0 because we want floating point math precision to 274 // not be a factor in determining whether the affordance UI is fully opaque. The number we 275 // choose needs to be close enough 1.0 such that the user can't easily tell the difference 276 // between the UI with an alpha at the threshold and when the alpha is 1.0. At the same 277 // time, we don't want the number to be too close to 1.0 such that there is a chance that we 278 // never treat the affordance UI as "fully opaque" as that would risk making it forever not 279 // clickable. 280 @VisibleForTesting const val AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD = 0.95f 281 } 282 } 283