• 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 
18 package com.android.systemui.keyguard.data.repository
19 
20 import android.content.Context
21 import android.os.UserHandle
22 import com.android.systemui.Dumpable
23 import com.android.systemui.R
24 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
25 import com.android.systemui.common.coroutine.ConflatedCallbackFlow
26 import com.android.systemui.dagger.SysUISingleton
27 import com.android.systemui.dagger.qualifiers.Application
28 import com.android.systemui.dump.DumpManager
29 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
30 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer
31 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLocalUserSelectionManager
32 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceRemoteUserSelectionManager
33 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager
34 import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePickerRepresentation
35 import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentation
36 import com.android.systemui.settings.UserTracker
37 import java.io.PrintWriter
38 import javax.inject.Inject
39 import kotlinx.coroutines.CoroutineScope
40 import kotlinx.coroutines.ExperimentalCoroutinesApi
41 import kotlinx.coroutines.channels.awaitClose
42 import kotlinx.coroutines.flow.Flow
43 import kotlinx.coroutines.flow.SharingStarted
44 import kotlinx.coroutines.flow.StateFlow
45 import kotlinx.coroutines.flow.distinctUntilChanged
46 import kotlinx.coroutines.flow.flatMapLatest
47 import kotlinx.coroutines.flow.map
48 import kotlinx.coroutines.flow.stateIn
49 
50 /** Abstracts access to application state related to keyguard quick affordances. */
51 @OptIn(ExperimentalCoroutinesApi::class)
52 @SysUISingleton
53 class KeyguardQuickAffordanceRepository
54 @Inject
55 constructor(
56     @Application private val appContext: Context,
57     @Application private val scope: CoroutineScope,
58     private val localUserSelectionManager: KeyguardQuickAffordanceLocalUserSelectionManager,
59     private val remoteUserSelectionManager: KeyguardQuickAffordanceRemoteUserSelectionManager,
60     private val userTracker: UserTracker,
61     legacySettingSyncer: KeyguardQuickAffordanceLegacySettingSyncer,
62     private val configs: Set<@JvmSuppressWildcards KeyguardQuickAffordanceConfig>,
63     dumpManager: DumpManager,
64     userHandle: UserHandle,
65 ) {
66     private val userId: Flow<Int> =
67         ConflatedCallbackFlow.conflatedCallbackFlow {
68             val callback =
69                 object : UserTracker.Callback {
70                     override fun onUserChanged(newUser: Int, userContext: Context) {
71                         trySendWithFailureLogging(newUser, TAG)
72                     }
73                 }
74 
75             userTracker.addCallback(callback) { it.run() }
76             trySendWithFailureLogging(userTracker.userId, TAG)
77 
78             awaitClose { userTracker.removeCallback(callback) }
79         }
80 
81     private val selectionManager: StateFlow<KeyguardQuickAffordanceSelectionManager> =
82         userId
83             .distinctUntilChanged()
84             .map { selectedUserId ->
85                 if (userHandle.identifier == selectedUserId) {
86                     localUserSelectionManager
87                 } else {
88                     remoteUserSelectionManager
89                 }
90             }
91             .stateIn(
92                 scope = scope,
93                 started = SharingStarted.Eagerly,
94                 initialValue = localUserSelectionManager,
95             )
96 
97     /**
98      * List of [KeyguardQuickAffordanceConfig] instances of the affordances at the slot with the
99      * given ID. The configs are sorted in descending priority order.
100      */
101     val selections: StateFlow<Map<String, List<KeyguardQuickAffordanceConfig>>> =
102         selectionManager
103             .flatMapLatest { selectionManager ->
104                 selectionManager.selections.map { selectionsBySlotId ->
105                     selectionsBySlotId.mapValues { (_, selections) ->
106                         configs.filter { selections.contains(it.key) }
107                     }
108                 }
109             }
110             .stateIn(
111                 scope = scope,
112                 started = SharingStarted.Eagerly,
113                 initialValue = emptyMap(),
114             )
115 
116     private val _slotPickerRepresentations: List<KeyguardSlotPickerRepresentation> by lazy {
117         fun parseSlot(unparsedSlot: String): Pair<String, Int> {
118             val split = unparsedSlot.split(SLOT_CONFIG_DELIMITER)
119             check(split.size == 2)
120             val slotId = split[0]
121             val slotCapacity = split[1].toInt()
122             return slotId to slotCapacity
123         }
124 
125         val unparsedSlots =
126             appContext.resources.getStringArray(R.array.config_keyguardQuickAffordanceSlots)
127 
128         val seenSlotIds = mutableSetOf<String>()
129         unparsedSlots.mapNotNull { unparsedSlot ->
130             val (slotId, slotCapacity) = parseSlot(unparsedSlot)
131             check(!seenSlotIds.contains(slotId)) { "Duplicate slot \"$slotId\"!" }
132             seenSlotIds.add(slotId)
133             KeyguardSlotPickerRepresentation(
134                 id = slotId,
135                 maxSelectedAffordances = slotCapacity,
136             )
137         }
138     }
139 
140     init {
141         legacySettingSyncer.startSyncing()
142         dumpManager.registerDumpable("KeyguardQuickAffordances", Dumpster())
143     }
144 
145     /**
146      * Returns a snapshot of the [KeyguardQuickAffordanceConfig] instances of the affordances at the
147      * slot with the given ID. The configs are sorted in descending priority order.
148      */
149     fun getCurrentSelections(slotId: String): List<KeyguardQuickAffordanceConfig> {
150         val selections = selectionManager.value.getSelections().getOrDefault(slotId, emptyList())
151         return configs.filter { selections.contains(it.key) }
152     }
153 
154     /**
155      * Returns a snapshot of the IDs of the selected affordances, indexed by slot ID. The configs
156      * are sorted in descending priority order.
157      */
158     fun getCurrentSelections(): Map<String, List<String>> {
159         return selectionManager.value.getSelections()
160     }
161 
162     /**
163      * Updates the IDs of affordances to show at the slot with the given ID. The order of affordance
164      * IDs should be descending priority order.
165      */
166     fun setSelections(
167         slotId: String,
168         affordanceIds: List<String>,
169     ) {
170         selectionManager.value.setSelections(
171             slotId = slotId,
172             affordanceIds = affordanceIds,
173         )
174     }
175 
176     /**
177      * Returns the list of representation objects for all known, device-available affordances,
178      * regardless of what is selected. This is useful for building experiences like the
179      * picker/selector or user settings so the user can see everything that can be selected in a
180      * menu.
181      */
182     suspend fun getAffordancePickerRepresentations():
183         List<KeyguardQuickAffordancePickerRepresentation> {
184         return configs
185             .associateWith { config -> config.getPickerScreenState() }
186             .filterNot { (_, pickerState) ->
187                 pickerState is KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice
188             }
189             .map { (config, pickerState) ->
190                 val defaultPickerState =
191                     pickerState as? KeyguardQuickAffordanceConfig.PickerScreenState.Default
192                 val disabledPickerState =
193                     pickerState as? KeyguardQuickAffordanceConfig.PickerScreenState.Disabled
194                 KeyguardQuickAffordancePickerRepresentation(
195                     id = config.key,
196                     name = config.pickerName,
197                     iconResourceId = config.pickerIconResourceId,
198                     isEnabled =
199                         pickerState is KeyguardQuickAffordanceConfig.PickerScreenState.Default,
200                     instructions = disabledPickerState?.instructions,
201                     actionText = disabledPickerState?.actionText,
202                     actionComponentName = disabledPickerState?.actionComponentName,
203                     configureIntent = defaultPickerState?.configureIntent,
204                 )
205             }
206     }
207 
208     /**
209      * Returns the list of representation objects for all available slots on the keyguard. This is
210      * useful for building experiences like the picker/selector or user settings so the user can see
211      * each slot and select which affordance(s) is/are installed in each slot on the keyguard.
212      */
213     fun getSlotPickerRepresentations(): List<KeyguardSlotPickerRepresentation> {
214         return _slotPickerRepresentations
215     }
216 
217     private inner class Dumpster : Dumpable {
218         override fun dump(pw: PrintWriter, args: Array<out String>) {
219             val slotPickerRepresentations = getSlotPickerRepresentations()
220             val selectionsBySlotId = getCurrentSelections()
221             pw.println("Slots & selections:")
222             slotPickerRepresentations.forEach { slotPickerRepresentation ->
223                 val slotId = slotPickerRepresentation.id
224                 val capacity = slotPickerRepresentation.maxSelectedAffordances
225                 val affordanceIds = selectionsBySlotId[slotId]
226 
227                 val selectionText =
228                     if (!affordanceIds.isNullOrEmpty()) {
229                         ": ${affordanceIds.joinToString(", ")}"
230                     } else {
231                         " is empty"
232                     }
233 
234                 pw.println("    $slotId$selectionText (capacity = $capacity)")
235             }
236             pw.println("Available affordances on device:")
237             configs.forEach { config -> pw.println("    ${config.key} (\"${config.pickerName}\")") }
238         }
239     }
240 
241     companion object {
242         private const val TAG = "KeyguardQuickAffordanceRepository"
243         private const val SLOT_CONFIG_DELIMITER = ":"
244     }
245 }
246