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