• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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