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