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