1 /* <lambda>null2 * Copyright (C) 2022 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 androidx.annotation.VisibleForTesting 20 import com.android.systemui.doze.util.BurnInHelperWrapper 21 import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor 22 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor 23 import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor 24 import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel 25 import com.android.systemui.keyguard.shared.quickaffordance.ActivationState 26 import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition 27 import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots 28 import javax.inject.Inject 29 import kotlinx.coroutines.ExperimentalCoroutinesApi 30 import kotlinx.coroutines.flow.Flow 31 import kotlinx.coroutines.flow.MutableStateFlow 32 import kotlinx.coroutines.flow.combine 33 import kotlinx.coroutines.flow.distinctUntilChanged 34 import kotlinx.coroutines.flow.flatMapLatest 35 import kotlinx.coroutines.flow.flowOf 36 import kotlinx.coroutines.flow.map 37 38 /** View-model for the keyguard bottom area view */ 39 @OptIn(ExperimentalCoroutinesApi::class) 40 class KeyguardBottomAreaViewModel 41 @Inject 42 constructor( 43 private val keyguardInteractor: KeyguardInteractor, 44 private val quickAffordanceInteractor: KeyguardQuickAffordanceInteractor, 45 private val bottomAreaInteractor: KeyguardBottomAreaInteractor, 46 private val burnInHelperWrapper: BurnInHelperWrapper, 47 private val longPressViewModel: KeyguardLongPressViewModel, 48 val settingsMenuViewModel: KeyguardSettingsMenuViewModel, 49 ) { 50 data class PreviewMode( 51 val isInPreviewMode: Boolean = false, 52 val shouldHighlightSelectedAffordance: Boolean = false, 53 ) 54 55 /** 56 * Whether this view-model instance is powering the preview experience that renders exclusively 57 * in the wallpaper picker application. This should _always_ be `false` for the real lock screen 58 * experience. 59 */ 60 val previewMode = MutableStateFlow(PreviewMode()) 61 62 /** 63 * ID of the slot that's currently selected in the preview that renders exclusively in the 64 * wallpaper picker application. This is ignored for the actual, real lock screen experience. 65 */ 66 private val selectedPreviewSlotId = 67 MutableStateFlow(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START) 68 69 /** 70 * Whether quick affordances are "opaque enough" to be considered visible to and interactive by 71 * the user. If they are not interactive, user input should not be allowed on them. 72 * 73 * Note that there is a margin of error, where we allow very, very slightly transparent views to 74 * be considered "fully opaque" for the purpose of being interactive. This is to accommodate the 75 * error margin of floating point arithmetic. 76 * 77 * A view that is visible but with an alpha of less than our threshold either means it's not 78 * fully done fading in or is fading/faded out. Either way, it should not be 79 * interactive/clickable unless "fully opaque" to avoid issues like in b/241830987. 80 */ 81 private val areQuickAffordancesFullyOpaque: Flow<Boolean> = 82 bottomAreaInteractor.alpha 83 .map { alpha -> alpha >= AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD } 84 .distinctUntilChanged() 85 86 /** An observable for the view-model of the "start button" quick affordance. */ 87 val startButton: Flow<KeyguardQuickAffordanceViewModel> = 88 button(KeyguardQuickAffordancePosition.BOTTOM_START) 89 /** An observable for the view-model of the "end button" quick affordance. */ 90 val endButton: Flow<KeyguardQuickAffordanceViewModel> = 91 button(KeyguardQuickAffordancePosition.BOTTOM_END) 92 /** An observable for whether the overlay container should be visible. */ 93 val isOverlayContainerVisible: Flow<Boolean> = 94 keyguardInteractor.isDozing.map { !it }.distinctUntilChanged() 95 /** An observable for the alpha level for the entire bottom area. */ 96 val alpha: Flow<Float> = 97 previewMode.flatMapLatest { 98 if (it.isInPreviewMode) { 99 flowOf(1f) 100 } else { 101 bottomAreaInteractor.alpha.distinctUntilChanged() 102 } 103 } 104 /** An observable for the x-offset by which the indication area should be translated. */ 105 val indicationAreaTranslationX: Flow<Float> = 106 bottomAreaInteractor.clockPosition.map { it.x.toFloat() }.distinctUntilChanged() 107 108 /** Returns an observable for the y-offset by which the indication area should be translated. */ 109 fun indicationAreaTranslationY(defaultBurnInOffset: Int): Flow<Float> { 110 return keyguardInteractor.dozeAmount 111 .map { dozeAmount -> 112 dozeAmount * 113 (burnInHelperWrapper.burnInOffset( 114 /* amplitude = */ defaultBurnInOffset * 2, 115 /* xAxis= */ false, 116 ) - defaultBurnInOffset) 117 } 118 .distinctUntilChanged() 119 } 120 121 /** 122 * Returns whether the keyguard bottom area should be constrained to the top of the lock icon 123 */ 124 fun shouldConstrainToTopOfLockIcon(): Boolean = 125 bottomAreaInteractor.shouldConstrainToTopOfLockIcon() 126 127 /** 128 * Puts this view-model in "preview mode", which means it's being used for UI that is rendering 129 * the lock screen preview in wallpaper picker / settings and not the real experience on the 130 * lock screen. 131 * 132 * @param initiallySelectedSlotId The ID of the initial slot to render as the selected one. 133 * @param shouldHighlightSelectedAffordance Whether the selected quick affordance should be 134 * highlighted (while all others are dimmed to make the selected one stand out). 135 */ 136 fun enablePreviewMode( 137 initiallySelectedSlotId: String?, 138 shouldHighlightSelectedAffordance: Boolean, 139 ) { 140 previewMode.value = 141 PreviewMode( 142 isInPreviewMode = true, 143 shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance, 144 ) 145 onPreviewSlotSelected( 146 initiallySelectedSlotId ?: KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START 147 ) 148 } 149 150 /** 151 * Notifies that a slot with the given ID has been selected in the preview experience that is 152 * rendering in the wallpaper picker. This is ignored for the real lock screen experience. 153 * 154 * @see enablePreviewMode 155 */ 156 fun onPreviewSlotSelected(slotId: String) { 157 selectedPreviewSlotId.value = slotId 158 } 159 160 /** 161 * Notifies that some input gesture has started somewhere in the bottom area that's outside of 162 * the lock screen settings menu item pop-up. 163 */ 164 fun onTouchedOutsideLockScreenSettingsMenu() { 165 longPressViewModel.onTouchedOutside() 166 } 167 168 private fun button( 169 position: KeyguardQuickAffordancePosition 170 ): Flow<KeyguardQuickAffordanceViewModel> { 171 return previewMode.flatMapLatest { previewMode -> 172 combine( 173 if (previewMode.isInPreviewMode) { 174 quickAffordanceInteractor.quickAffordanceAlwaysVisible(position = position) 175 } else { 176 quickAffordanceInteractor.quickAffordance(position = position) 177 }, 178 bottomAreaInteractor.animateDozingTransitions.distinctUntilChanged(), 179 areQuickAffordancesFullyOpaque, 180 selectedPreviewSlotId, 181 quickAffordanceInteractor.useLongPress(), 182 ) { model, animateReveal, isFullyOpaque, selectedPreviewSlotId, useLongPress -> 183 val slotId = position.toSlotId() 184 val isSelected = selectedPreviewSlotId == slotId 185 model.toViewModel( 186 animateReveal = !previewMode.isInPreviewMode && animateReveal, 187 isClickable = isFullyOpaque && !previewMode.isInPreviewMode, 188 isSelected = 189 previewMode.isInPreviewMode && 190 previewMode.shouldHighlightSelectedAffordance && 191 isSelected, 192 isDimmed = 193 previewMode.isInPreviewMode && 194 previewMode.shouldHighlightSelectedAffordance && 195 !isSelected, 196 forceInactive = previewMode.isInPreviewMode, 197 slotId = slotId, 198 useLongPress = useLongPress, 199 ) 200 } 201 .distinctUntilChanged() 202 } 203 } 204 205 private fun KeyguardQuickAffordanceModel.toViewModel( 206 animateReveal: Boolean, 207 isClickable: Boolean, 208 isSelected: Boolean, 209 isDimmed: Boolean, 210 forceInactive: Boolean, 211 slotId: String, 212 useLongPress: Boolean, 213 ): KeyguardQuickAffordanceViewModel { 214 return when (this) { 215 is KeyguardQuickAffordanceModel.Visible -> 216 KeyguardQuickAffordanceViewModel( 217 configKey = configKey, 218 isVisible = true, 219 animateReveal = animateReveal, 220 icon = icon, 221 onClicked = { parameters -> 222 quickAffordanceInteractor.onQuickAffordanceTriggered( 223 configKey = parameters.configKey, 224 expandable = parameters.expandable, 225 slotId = parameters.slotId, 226 ) 227 }, 228 isClickable = isClickable, 229 isActivated = !forceInactive && activationState is ActivationState.Active, 230 isSelected = isSelected, 231 useLongPress = useLongPress, 232 isDimmed = isDimmed, 233 slotId = slotId, 234 ) 235 is KeyguardQuickAffordanceModel.Hidden -> 236 KeyguardQuickAffordanceViewModel( 237 slotId = slotId, 238 ) 239 } 240 } 241 242 companion object { 243 // We select a value that's less than 1.0 because we want floating point math precision to 244 // not be a factor in determining whether the affordance UI is fully opaque. The number we 245 // choose needs to be close enough 1.0 such that the user can't easily tell the difference 246 // between the UI with an alpha at the threshold and when the alpha is 1.0. At the same 247 // time, we don't want the number to be too close to 1.0 such that there is a chance that we 248 // never treat the affordance UI as "fully opaque" as that would risk making it forever not 249 // clickable. 250 @VisibleForTesting const val AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD = 0.95f 251 } 252 } 253