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