1 /* 2 * 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.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 kotlinx.coroutines.ExperimentalCoroutinesApi 29 import kotlinx.coroutines.flow.Flow 30 import kotlinx.coroutines.flow.MutableStateFlow 31 import kotlinx.coroutines.flow.combine 32 import kotlinx.coroutines.flow.distinctUntilChanged 33 import kotlinx.coroutines.flow.flatMapLatest 34 import kotlinx.coroutines.flow.flowOf 35 import kotlinx.coroutines.flow.map 36 import javax.inject.Inject 37 38 @OptIn(ExperimentalCoroutinesApi::class) 39 class KeyguardQuickAffordancesCombinedViewModel 40 @Inject 41 constructor( 42 private val quickAffordanceInteractor: KeyguardQuickAffordanceInteractor, 43 private val keyguardInteractor: KeyguardInteractor, 44 ) { 45 46 /** 47 * ID of the slot that's currently selected in the preview that renders exclusively in the 48 * wallpaper picker application. This is ignored for the actual, real lock screen experience. 49 */ 50 private val selectedPreviewSlotId = 51 MutableStateFlow(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START) 52 53 /** 54 * Whether quick affordances are "opaque enough" to be considered visible to and interactive by 55 * the user. If they are not interactive, user input should not be allowed on them. 56 * 57 * Note that there is a margin of error, where we allow very, very slightly transparent views to 58 * be considered "fully opaque" for the purpose of being interactive. This is to accommodate the 59 * error margin of floating point arithmetic. 60 * 61 * A view that is visible but with an alpha of less than our threshold either means it's not 62 * fully done fading in or is fading/faded out. Either way, it should not be 63 * interactive/clickable unless "fully opaque" to avoid issues like in b/241830987. 64 */ 65 private val areQuickAffordancesFullyOpaque: Flow<Boolean> = 66 keyguardInteractor.keyguardAlpha 67 .map { alpha -> alpha >= AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD } 68 .distinctUntilChanged() 69 70 /** An observable for the view-model of the "start button" quick affordance. */ 71 val startButton: Flow<KeyguardQuickAffordanceViewModel> = 72 button(KeyguardQuickAffordancePosition.BOTTOM_START) 73 74 /** An observable for the view-model of the "end button" quick affordance. */ 75 val endButton: Flow<KeyguardQuickAffordanceViewModel> = 76 button(KeyguardQuickAffordancePosition.BOTTOM_END) 77 78 /** 79 * Notifies that a slot with the given ID has been selected in the preview experience that is 80 * rendering in the wallpaper picker. This is ignored for the real lock screen experience. 81 * 82 * @see [KeyguardRootViewModel.enablePreviewMode] 83 */ 84 fun onPreviewSlotSelected(slotId: String) { 85 selectedPreviewSlotId.value = slotId 86 } 87 88 private fun button( 89 position: KeyguardQuickAffordancePosition 90 ): Flow<KeyguardQuickAffordanceViewModel> { 91 return keyguardInteractor.previewMode.flatMapLatest { previewMode -> 92 combine( 93 if (previewMode.isInPreviewMode) { 94 quickAffordanceInteractor.quickAffordanceAlwaysVisible(position = position) 95 } else { 96 quickAffordanceInteractor.quickAffordance(position = position) 97 }, 98 keyguardInteractor.animateDozingTransitions.distinctUntilChanged(), 99 areQuickAffordancesFullyOpaque, 100 selectedPreviewSlotId, 101 quickAffordanceInteractor.useLongPress(), 102 ) { model, animateReveal, isFullyOpaque, selectedPreviewSlotId, useLongPress -> 103 val slotId = position.toSlotId() 104 val isSelected = selectedPreviewSlotId == slotId 105 model.toViewModel( 106 animateReveal = !previewMode.isInPreviewMode && animateReveal, 107 isClickable = isFullyOpaque && !previewMode.isInPreviewMode, 108 isSelected = 109 previewMode.isInPreviewMode && 110 previewMode.shouldHighlightSelectedAffordance && 111 isSelected, 112 isDimmed = 113 previewMode.isInPreviewMode && 114 previewMode.shouldHighlightSelectedAffordance && 115 !isSelected, 116 forceInactive = previewMode.isInPreviewMode, 117 slotId = slotId, 118 useLongPress = useLongPress, 119 ) 120 } 121 .distinctUntilChanged() 122 } 123 } 124 125 private fun KeyguardQuickAffordanceModel.toViewModel( 126 animateReveal: Boolean, 127 isClickable: Boolean, 128 isSelected: Boolean, 129 isDimmed: Boolean, 130 forceInactive: Boolean, 131 slotId: String, 132 useLongPress: Boolean, 133 ): KeyguardQuickAffordanceViewModel { 134 return when (this) { 135 is KeyguardQuickAffordanceModel.Visible -> 136 KeyguardQuickAffordanceViewModel( 137 configKey = configKey, 138 isVisible = true, 139 animateReveal = animateReveal, 140 icon = icon, 141 onClicked = { parameters -> 142 quickAffordanceInteractor.onQuickAffordanceTriggered( 143 configKey = parameters.configKey, 144 expandable = parameters.expandable, 145 slotId = parameters.slotId, 146 ) 147 }, 148 isClickable = isClickable, 149 isActivated = !forceInactive && activationState is ActivationState.Active, 150 isSelected = isSelected, 151 useLongPress = useLongPress, 152 isDimmed = isDimmed, 153 slotId = slotId, 154 ) 155 is KeyguardQuickAffordanceModel.Hidden -> 156 KeyguardQuickAffordanceViewModel( 157 slotId = slotId, 158 ) 159 } 160 } 161 162 companion object { 163 // We select a value that's less than 1.0 because we want floating point math precision to 164 // not be a factor in determining whether the affordance UI is fully opaque. The number we 165 // choose needs to be close enough 1.0 such that the user can't easily tell the difference 166 // between the UI with an alpha at the threshold and when the alpha is 1.0. At the same 167 // time, we don't want the number to be too close to 1.0 such that there is a chance that we 168 // never treat the affordance UI as "fully opaque" as that would risk making it forever not 169 // clickable. 170 @VisibleForTesting 171 const val AFFORDANCE_FULLY_OPAQUE_ALPHA_THRESHOLD = 0.95f 172 } 173 174 }