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 }