Loading packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt +25 −17 Original line number Diff line number Diff line Loading @@ -17,12 +17,19 @@ package com.android.systemui.keyguard.data.quickaffordance import dagger.Binds import dagger.Module import dagger.Provides import dagger.multibindings.ElementsIntoSet @Module object KeyguardDataQuickAffordanceModule { interface KeyguardDataQuickAffordanceModule { @Binds fun providerClientFactory( impl: KeyguardQuickAffordanceProviderClientFactoryImpl, ): KeyguardQuickAffordanceProviderClientFactory companion object { @Provides @ElementsIntoSet fun quickAffordanceConfigs( Loading @@ -41,3 +48,4 @@ object KeyguardDataQuickAffordanceModule { ) } } } packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncer.kt +1 −1 Original line number Diff line number Diff line Loading @@ -67,7 +67,7 @@ constructor( @Application private val scope: CoroutineScope, @Background private val backgroundDispatcher: CoroutineDispatcher, private val secureSettings: SecureSettings, private val selectionsManager: KeyguardQuickAffordanceSelectionManager, private val selectionsManager: KeyguardQuickAffordanceLocalUserSelectionManager, ) { companion object { private val BINDINGS = Loading packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManager.kt 0 → 100644 +184 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.android.systemui.keyguard.data.quickaffordance import android.content.Context import android.content.IntentFilter import android.content.SharedPreferences import com.android.systemui.R import com.android.systemui.backup.BackupHelper import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.onStart /** * Manages and provides access to the current "selections" of keyguard quick affordances, answering * the question "which affordances should the keyguard show?" for the user associated with the * System UI process. */ @OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class KeyguardQuickAffordanceLocalUserSelectionManager @Inject constructor( @Application context: Context, private val userFileManager: UserFileManager, private val userTracker: UserTracker, broadcastDispatcher: BroadcastDispatcher, ) : KeyguardQuickAffordanceSelectionManager { private var sharedPrefs: SharedPreferences = instantiateSharedPrefs() private val userId: Flow<Int> = conflatedCallbackFlow { val callback = object : UserTracker.Callback { override fun onUserChanged(newUser: Int, userContext: Context) { trySendWithFailureLogging(newUser, TAG) } } userTracker.addCallback(callback) { it.run() } trySendWithFailureLogging(userTracker.userId, TAG) awaitClose { userTracker.removeCallback(callback) } } private val defaults: Map<String, List<String>> by lazy { context.resources .getStringArray(R.array.config_keyguardQuickAffordanceDefaults) .associate { item -> val splitUp = item.split(SLOT_AFFORDANCES_DELIMITER) check(splitUp.size == 2) val slotId = splitUp[0] val affordanceIds = splitUp[1].split(AFFORDANCE_DELIMITER) slotId to affordanceIds } } /** * Emits an event each time a Backup & Restore restoration job is completed. Does not emit an * initial value. */ private val backupRestorationEvents: Flow<Unit> = broadcastDispatcher.broadcastFlow( filter = IntentFilter(BackupHelper.ACTION_RESTORE_FINISHED), flags = Context.RECEIVER_NOT_EXPORTED, permission = BackupHelper.PERMISSION_SELF, ) override val selections: Flow<Map<String, List<String>>> = combine( userId, backupRestorationEvents.onStart { // We emit an initial event to make sure that the combine emits at least once, // even if we never get a Backup & Restore restoration event (which is the most // common case anyway as restoration really only happens on initial device // setup). emit(Unit) } ) { _, _ -> } .flatMapLatest { conflatedCallbackFlow { // We want to instantiate a new SharedPreferences instance each time either the // user ID changes or we have a backup & restore restoration event. The reason // is that our sharedPrefs instance needs to be replaced with a new one as it // depends on the user ID and when the B&R job completes, the backing file is // replaced but the existing instance still has a stale in-memory cache. sharedPrefs = instantiateSharedPrefs() val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, _ -> trySend(getSelections()) } sharedPrefs.registerOnSharedPreferenceChangeListener(listener) send(getSelections()) awaitClose { sharedPrefs.unregisterOnSharedPreferenceChangeListener(listener) } } } override fun getSelections(): Map<String, List<String>> { val slotKeys = sharedPrefs.all.keys.filter { it.startsWith(KEY_PREFIX_SLOT) } val result = slotKeys .associate { key -> val slotId = key.substring(KEY_PREFIX_SLOT.length) val value = sharedPrefs.getString(key, null) val affordanceIds = if (!value.isNullOrEmpty()) { value.split(AFFORDANCE_DELIMITER) } else { emptyList() } slotId to affordanceIds } .toMutableMap() // If the result map is missing keys, it means that the system has never set anything for // those slots. This is where we need examine our defaults and see if there should be a // default value for the affordances in the slot IDs that are missing from the result. // // Once the user makes any selection for a slot, even when they select "None", this class // will persist a key for that slot ID. In the case of "None", it will have a value of the // empty string. This is why this system works. defaults.forEach { (slotId, affordanceIds) -> if (!result.containsKey(slotId)) { result[slotId] = affordanceIds } } return result } override fun setSelections( slotId: String, affordanceIds: List<String>, ) { val key = "$KEY_PREFIX_SLOT$slotId" val value = affordanceIds.joinToString(AFFORDANCE_DELIMITER) sharedPrefs.edit().putString(key, value).apply() } private fun instantiateSharedPrefs(): SharedPreferences { return userFileManager.getSharedPreferences( FILE_NAME, Context.MODE_PRIVATE, userTracker.userId, ) } companion object { private const val TAG = "KeyguardQuickAffordancePrimaryUserSelectionManager" const val FILE_NAME = "quick_affordance_selections" private const val KEY_PREFIX_SLOT = "slot_" private const val SLOT_AFFORDANCES_DELIMITER = ":" private const val AFFORDANCE_DELIMITER = "," } } packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceProviderClientFactory.kt 0 → 100644 +43 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.android.systemui.keyguard.data.quickaffordance import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.settings.UserTracker import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderClient import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderClientImpl import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher interface KeyguardQuickAffordanceProviderClientFactory { fun create(): KeyguardQuickAffordanceProviderClient } class KeyguardQuickAffordanceProviderClientFactoryImpl @Inject constructor( private val userTracker: UserTracker, @Background private val backgroundDispatcher: CoroutineDispatcher, ) : KeyguardQuickAffordanceProviderClientFactory { override fun create(): KeyguardQuickAffordanceProviderClient { return KeyguardQuickAffordanceProviderClientImpl( context = userTracker.userContext, backgroundDispatcher = backgroundDispatcher, ) } } packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceRemoteUserSelectionManager.kt 0 → 100644 +129 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.android.systemui.keyguard.data.quickaffordance import android.content.Context import android.os.UserHandle import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.settings.UserTracker import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderClient import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch /** * Manages and provides access to the current "selections" of keyguard quick affordances, answering * the question "which affordances should the keyguard show?" for users associated with other System * UI processes. */ @OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class KeyguardQuickAffordanceRemoteUserSelectionManager @Inject constructor( @Application private val scope: CoroutineScope, private val userTracker: UserTracker, private val clientFactory: KeyguardQuickAffordanceProviderClientFactory, private val userHandle: UserHandle, ) : KeyguardQuickAffordanceSelectionManager { private val userId: Flow<Int> = conflatedCallbackFlow { val callback = object : UserTracker.Callback { override fun onUserChanged(newUser: Int, userContext: Context) { trySendWithFailureLogging(newUser, TAG) } } userTracker.addCallback(callback) { it.run() } trySendWithFailureLogging(userTracker.userId, TAG) awaitClose { userTracker.removeCallback(callback) } } private val clientOrNull: StateFlow<KeyguardQuickAffordanceProviderClient?> = userId .distinctUntilChanged() .map { selectedUserId -> if (userHandle.isSystem && userHandle.identifier != selectedUserId) { clientFactory.create() } else { null } } .stateIn( scope = scope, started = SharingStarted.Eagerly, initialValue = null, ) private val _selections: StateFlow<Map<String, List<String>>> = clientOrNull .flatMapLatest { client -> client?.observeSelections()?.map { selections -> buildMap<String, List<String>> { selections.forEach { selection -> val slotId = selection.slotId val affordanceIds = (get(slotId) ?: emptyList()).toMutableList() affordanceIds.add(selection.affordanceId) put(slotId, affordanceIds) } } } ?: emptyFlow() } .stateIn( scope = scope, started = SharingStarted.Eagerly, initialValue = emptyMap(), ) override val selections: Flow<Map<String, List<String>>> = _selections override fun getSelections(): Map<String, List<String>> { return _selections.value } override fun setSelections(slotId: String, affordanceIds: List<String>) { clientOrNull.value?.let { client -> scope.launch { client.deleteAllSelections(slotId = slotId) affordanceIds.forEach { affordanceId -> client.insertSelection(slotId = slotId, affordanceId = affordanceId) } } } } companion object { private const val TAG = "KeyguardQuickAffordanceMultiUserSelectionManager" } } Loading
packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt +25 −17 Original line number Diff line number Diff line Loading @@ -17,12 +17,19 @@ package com.android.systemui.keyguard.data.quickaffordance import dagger.Binds import dagger.Module import dagger.Provides import dagger.multibindings.ElementsIntoSet @Module object KeyguardDataQuickAffordanceModule { interface KeyguardDataQuickAffordanceModule { @Binds fun providerClientFactory( impl: KeyguardQuickAffordanceProviderClientFactoryImpl, ): KeyguardQuickAffordanceProviderClientFactory companion object { @Provides @ElementsIntoSet fun quickAffordanceConfigs( Loading @@ -41,3 +48,4 @@ object KeyguardDataQuickAffordanceModule { ) } } }
packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncer.kt +1 −1 Original line number Diff line number Diff line Loading @@ -67,7 +67,7 @@ constructor( @Application private val scope: CoroutineScope, @Background private val backgroundDispatcher: CoroutineDispatcher, private val secureSettings: SecureSettings, private val selectionsManager: KeyguardQuickAffordanceSelectionManager, private val selectionsManager: KeyguardQuickAffordanceLocalUserSelectionManager, ) { companion object { private val BINDINGS = Loading
packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManager.kt 0 → 100644 +184 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.android.systemui.keyguard.data.quickaffordance import android.content.Context import android.content.IntentFilter import android.content.SharedPreferences import com.android.systemui.R import com.android.systemui.backup.BackupHelper import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.onStart /** * Manages and provides access to the current "selections" of keyguard quick affordances, answering * the question "which affordances should the keyguard show?" for the user associated with the * System UI process. */ @OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class KeyguardQuickAffordanceLocalUserSelectionManager @Inject constructor( @Application context: Context, private val userFileManager: UserFileManager, private val userTracker: UserTracker, broadcastDispatcher: BroadcastDispatcher, ) : KeyguardQuickAffordanceSelectionManager { private var sharedPrefs: SharedPreferences = instantiateSharedPrefs() private val userId: Flow<Int> = conflatedCallbackFlow { val callback = object : UserTracker.Callback { override fun onUserChanged(newUser: Int, userContext: Context) { trySendWithFailureLogging(newUser, TAG) } } userTracker.addCallback(callback) { it.run() } trySendWithFailureLogging(userTracker.userId, TAG) awaitClose { userTracker.removeCallback(callback) } } private val defaults: Map<String, List<String>> by lazy { context.resources .getStringArray(R.array.config_keyguardQuickAffordanceDefaults) .associate { item -> val splitUp = item.split(SLOT_AFFORDANCES_DELIMITER) check(splitUp.size == 2) val slotId = splitUp[0] val affordanceIds = splitUp[1].split(AFFORDANCE_DELIMITER) slotId to affordanceIds } } /** * Emits an event each time a Backup & Restore restoration job is completed. Does not emit an * initial value. */ private val backupRestorationEvents: Flow<Unit> = broadcastDispatcher.broadcastFlow( filter = IntentFilter(BackupHelper.ACTION_RESTORE_FINISHED), flags = Context.RECEIVER_NOT_EXPORTED, permission = BackupHelper.PERMISSION_SELF, ) override val selections: Flow<Map<String, List<String>>> = combine( userId, backupRestorationEvents.onStart { // We emit an initial event to make sure that the combine emits at least once, // even if we never get a Backup & Restore restoration event (which is the most // common case anyway as restoration really only happens on initial device // setup). emit(Unit) } ) { _, _ -> } .flatMapLatest { conflatedCallbackFlow { // We want to instantiate a new SharedPreferences instance each time either the // user ID changes or we have a backup & restore restoration event. The reason // is that our sharedPrefs instance needs to be replaced with a new one as it // depends on the user ID and when the B&R job completes, the backing file is // replaced but the existing instance still has a stale in-memory cache. sharedPrefs = instantiateSharedPrefs() val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, _ -> trySend(getSelections()) } sharedPrefs.registerOnSharedPreferenceChangeListener(listener) send(getSelections()) awaitClose { sharedPrefs.unregisterOnSharedPreferenceChangeListener(listener) } } } override fun getSelections(): Map<String, List<String>> { val slotKeys = sharedPrefs.all.keys.filter { it.startsWith(KEY_PREFIX_SLOT) } val result = slotKeys .associate { key -> val slotId = key.substring(KEY_PREFIX_SLOT.length) val value = sharedPrefs.getString(key, null) val affordanceIds = if (!value.isNullOrEmpty()) { value.split(AFFORDANCE_DELIMITER) } else { emptyList() } slotId to affordanceIds } .toMutableMap() // If the result map is missing keys, it means that the system has never set anything for // those slots. This is where we need examine our defaults and see if there should be a // default value for the affordances in the slot IDs that are missing from the result. // // Once the user makes any selection for a slot, even when they select "None", this class // will persist a key for that slot ID. In the case of "None", it will have a value of the // empty string. This is why this system works. defaults.forEach { (slotId, affordanceIds) -> if (!result.containsKey(slotId)) { result[slotId] = affordanceIds } } return result } override fun setSelections( slotId: String, affordanceIds: List<String>, ) { val key = "$KEY_PREFIX_SLOT$slotId" val value = affordanceIds.joinToString(AFFORDANCE_DELIMITER) sharedPrefs.edit().putString(key, value).apply() } private fun instantiateSharedPrefs(): SharedPreferences { return userFileManager.getSharedPreferences( FILE_NAME, Context.MODE_PRIVATE, userTracker.userId, ) } companion object { private const val TAG = "KeyguardQuickAffordancePrimaryUserSelectionManager" const val FILE_NAME = "quick_affordance_selections" private const val KEY_PREFIX_SLOT = "slot_" private const val SLOT_AFFORDANCES_DELIMITER = ":" private const val AFFORDANCE_DELIMITER = "," } }
packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceProviderClientFactory.kt 0 → 100644 +43 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.android.systemui.keyguard.data.quickaffordance import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.settings.UserTracker import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderClient import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderClientImpl import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher interface KeyguardQuickAffordanceProviderClientFactory { fun create(): KeyguardQuickAffordanceProviderClient } class KeyguardQuickAffordanceProviderClientFactoryImpl @Inject constructor( private val userTracker: UserTracker, @Background private val backgroundDispatcher: CoroutineDispatcher, ) : KeyguardQuickAffordanceProviderClientFactory { override fun create(): KeyguardQuickAffordanceProviderClient { return KeyguardQuickAffordanceProviderClientImpl( context = userTracker.userContext, backgroundDispatcher = backgroundDispatcher, ) } }
packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceRemoteUserSelectionManager.kt 0 → 100644 +129 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.android.systemui.keyguard.data.quickaffordance import android.content.Context import android.os.UserHandle import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.settings.UserTracker import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderClient import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch /** * Manages and provides access to the current "selections" of keyguard quick affordances, answering * the question "which affordances should the keyguard show?" for users associated with other System * UI processes. */ @OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class KeyguardQuickAffordanceRemoteUserSelectionManager @Inject constructor( @Application private val scope: CoroutineScope, private val userTracker: UserTracker, private val clientFactory: KeyguardQuickAffordanceProviderClientFactory, private val userHandle: UserHandle, ) : KeyguardQuickAffordanceSelectionManager { private val userId: Flow<Int> = conflatedCallbackFlow { val callback = object : UserTracker.Callback { override fun onUserChanged(newUser: Int, userContext: Context) { trySendWithFailureLogging(newUser, TAG) } } userTracker.addCallback(callback) { it.run() } trySendWithFailureLogging(userTracker.userId, TAG) awaitClose { userTracker.removeCallback(callback) } } private val clientOrNull: StateFlow<KeyguardQuickAffordanceProviderClient?> = userId .distinctUntilChanged() .map { selectedUserId -> if (userHandle.isSystem && userHandle.identifier != selectedUserId) { clientFactory.create() } else { null } } .stateIn( scope = scope, started = SharingStarted.Eagerly, initialValue = null, ) private val _selections: StateFlow<Map<String, List<String>>> = clientOrNull .flatMapLatest { client -> client?.observeSelections()?.map { selections -> buildMap<String, List<String>> { selections.forEach { selection -> val slotId = selection.slotId val affordanceIds = (get(slotId) ?: emptyList()).toMutableList() affordanceIds.add(selection.affordanceId) put(slotId, affordanceIds) } } } ?: emptyFlow() } .stateIn( scope = scope, started = SharingStarted.Eagerly, initialValue = emptyMap(), ) override val selections: Flow<Map<String, List<String>>> = _selections override fun getSelections(): Map<String, List<String>> { return _selections.value } override fun setSelections(slotId: String, affordanceIds: List<String>) { clientOrNull.value?.let { client -> scope.launch { client.deleteAllSelections(slotId = slotId) affordanceIds.forEach { affordanceId -> client.insertSelection(slotId = slotId, affordanceId = affordanceId) } } } } companion object { private const val TAG = "KeyguardQuickAffordanceMultiUserSelectionManager" } }