Merge "Multi-user customizable quick affordances." into tm-qpr-dev
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt
index 3013227c..072cfb1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt
@@ -17,27 +17,35 @@
 
 package com.android.systemui.keyguard.data.quickaffordance
 
+import dagger.Binds
 import dagger.Module
 import dagger.Provides
 import dagger.multibindings.ElementsIntoSet
 
 @Module
-object KeyguardDataQuickAffordanceModule {
-    @Provides
-    @ElementsIntoSet
-    fun quickAffordanceConfigs(
-        flashlight: FlashlightQuickAffordanceConfig,
-        home: HomeControlsKeyguardQuickAffordanceConfig,
-        quickAccessWallet: QuickAccessWalletKeyguardQuickAffordanceConfig,
-        qrCodeScanner: QrCodeScannerKeyguardQuickAffordanceConfig,
-        camera: CameraQuickAffordanceConfig,
-    ): Set<KeyguardQuickAffordanceConfig> {
-        return setOf(
-            camera,
-            flashlight,
-            home,
-            quickAccessWallet,
-            qrCodeScanner,
-        )
+interface KeyguardDataQuickAffordanceModule {
+    @Binds
+    fun providerClientFactory(
+        impl: KeyguardQuickAffordanceProviderClientFactoryImpl,
+    ): KeyguardQuickAffordanceProviderClientFactory
+
+    companion object {
+        @Provides
+        @ElementsIntoSet
+        fun quickAffordanceConfigs(
+            flashlight: FlashlightQuickAffordanceConfig,
+            home: HomeControlsKeyguardQuickAffordanceConfig,
+            quickAccessWallet: QuickAccessWalletKeyguardQuickAffordanceConfig,
+            qrCodeScanner: QrCodeScannerKeyguardQuickAffordanceConfig,
+            camera: CameraQuickAffordanceConfig,
+        ): Set<KeyguardQuickAffordanceConfig> {
+            return setOf(
+                camera,
+                flashlight,
+                home,
+                quickAccessWallet,
+                qrCodeScanner,
+            )
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncer.kt
index 766096f..72747f6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncer.kt
@@ -67,7 +67,7 @@
     @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 =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManager.kt
new file mode 100644
index 0000000..0066785
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManager.kt
@@ -0,0 +1,184 @@
+/*
+ * 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 = ","
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceProviderClientFactory.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceProviderClientFactory.kt
new file mode 100644
index 0000000..727a813
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceProviderClientFactory.kt
@@ -0,0 +1,43 @@
+/*
+ * 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,
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceRemoteUserSelectionManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceRemoteUserSelectionManager.kt
new file mode 100644
index 0000000..8ffef25
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceRemoteUserSelectionManager.kt
@@ -0,0 +1,129 @@
+/*
+ * 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"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt
index 4f37e5f..21fffed 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt
@@ -17,153 +17,22 @@
 
 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?".
+ * Defines interface for classes that manage and provide access to the current "selections" of
+ * keyguard quick affordances, answering the question "which affordances should the keyguard show?".
  */
-@OptIn(ExperimentalCoroutinesApi::class)
-@SysUISingleton
-class KeyguardQuickAffordanceSelectionManager
-@Inject
-constructor(
-    @Application context: Context,
-    private val userFileManager: UserFileManager,
-    private val userTracker: UserTracker,
-    broadcastDispatcher: BroadcastDispatcher,
-) {
-
-    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,
-        )
+interface KeyguardQuickAffordanceSelectionManager {
 
     /** IDs of affordances to show, indexed by slot ID, and sorted in descending priority order. */
-    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) }
-                }
-            }
+    val selections: Flow<Map<String, List<String>>>
 
     /**
      * Returns a snapshot of the IDs of affordances to show, indexed by slot ID, and sorted in
      * descending priority order.
      */
-    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
-    }
+    fun getSelections(): Map<String, List<String>>
 
     /**
      * Updates the IDs of affordances to show at the slot with the given ID. The order of affordance
@@ -172,25 +41,9 @@
     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 = "KeyguardQuickAffordanceSelectionManager"
         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 = ","
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt
index d95a1a7..e3f5e90 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt
@@ -18,45 +18,93 @@
 package com.android.systemui.keyguard.data.repository
 
 import android.content.Context
+import android.os.UserHandle
 import com.android.systemui.Dumpable
 import com.android.systemui.R
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLocalUserSelectionManager
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceRemoteUserSelectionManager
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager
 import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePickerRepresentation
 import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentation
+import com.android.systemui.settings.UserTracker
 import java.io.PrintWriter
 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.flatMapLatest
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
 
 /** Abstracts access to application state related to keyguard quick affordances. */
+@OptIn(ExperimentalCoroutinesApi::class)
 @SysUISingleton
 class KeyguardQuickAffordanceRepository
 @Inject
 constructor(
     @Application private val appContext: Context,
     @Application private val scope: CoroutineScope,
-    private val selectionManager: KeyguardQuickAffordanceSelectionManager,
+    private val localUserSelectionManager: KeyguardQuickAffordanceLocalUserSelectionManager,
+    private val remoteUserSelectionManager: KeyguardQuickAffordanceRemoteUserSelectionManager,
+    private val userTracker: UserTracker,
     legacySettingSyncer: KeyguardQuickAffordanceLegacySettingSyncer,
     private val configs: Set<@JvmSuppressWildcards KeyguardQuickAffordanceConfig>,
     dumpManager: DumpManager,
+    userHandle: UserHandle,
 ) {
+    private val userId: Flow<Int> =
+        ConflatedCallbackFlow.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 selectionManager: StateFlow<KeyguardQuickAffordanceSelectionManager> =
+        userId
+            .distinctUntilChanged()
+            .map { selectedUserId ->
+                if (userHandle.identifier == selectedUserId) {
+                    localUserSelectionManager
+                } else {
+                    remoteUserSelectionManager
+                }
+            }
+            .stateIn(
+                scope = scope,
+                started = SharingStarted.Eagerly,
+                initialValue = localUserSelectionManager,
+            )
+
     /**
      * List of [KeyguardQuickAffordanceConfig] instances of the affordances at the slot with the
      * given ID. The configs are sorted in descending priority order.
      */
     val selections: StateFlow<Map<String, List<KeyguardQuickAffordanceConfig>>> =
-        selectionManager.selections
-            .map { selectionsBySlotId ->
-                selectionsBySlotId.mapValues { (_, selections) ->
-                    configs.filter { selections.contains(it.key) }
+        selectionManager
+            .flatMapLatest { selectionManager ->
+                selectionManager.selections.map { selectionsBySlotId ->
+                    selectionsBySlotId.mapValues { (_, selections) ->
+                        configs.filter { selections.contains(it.key) }
+                    }
                 }
             }
             .stateIn(
@@ -99,7 +147,7 @@
      * slot with the given ID. The configs are sorted in descending priority order.
      */
     fun getSelections(slotId: String): List<KeyguardQuickAffordanceConfig> {
-        val selections = selectionManager.getSelections().getOrDefault(slotId, emptyList())
+        val selections = selectionManager.value.getSelections().getOrDefault(slotId, emptyList())
         return configs.filter { selections.contains(it.key) }
     }
 
@@ -108,7 +156,7 @@
      * are sorted in descending priority order.
      */
     fun getSelections(): Map<String, List<String>> {
-        return selectionManager.getSelections()
+        return selectionManager.value.getSelections()
     }
 
     /**
@@ -119,7 +167,7 @@
         slotId: String,
         affordanceIds: List<String>,
     ) {
-        selectionManager.setSelections(
+        selectionManager.value.setSelections(
             slotId = slotId,
             affordanceIds = affordanceIds,
         )
@@ -188,6 +236,7 @@
     }
 
     companion object {
+        private const val TAG = "KeyguardQuickAffordanceRepository"
         private const val SLOT_CONFIG_DELIMITER = ":"
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
index ee7154f..748c6e8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
@@ -190,8 +190,6 @@
 
     /** Returns affordance IDs indexed by slot ID, for all known slots. */
     suspend fun getSelections(): Map<String, List<KeyguardQuickAffordancePickerRepresentation>> {
-        check(isUsingRepository)
-
         val slots = repository.get().getSlotPickerRepresentations()
         val selections = repository.get().getSelections()
         val affordanceById =
@@ -312,8 +310,6 @@
 
     suspend fun getAffordancePickerRepresentations():
         List<KeyguardQuickAffordancePickerRepresentation> {
-        check(isUsingRepository)
-
         return repository.get().getAffordancePickerRepresentations()
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/user/UserModule.java b/packages/SystemUI/src/com/android/systemui/user/UserModule.java
index 0c72b78..2b29885 100644
--- a/packages/SystemUI/src/com/android/systemui/user/UserModule.java
+++ b/packages/SystemUI/src/com/android/systemui/user/UserModule.java
@@ -17,6 +17,7 @@
 package com.android.systemui.user;
 
 import android.app.Activity;
+import android.os.UserHandle;
 
 import com.android.settingslib.users.EditUserInfoController;
 import com.android.systemui.user.data.repository.UserRepositoryModule;
@@ -51,4 +52,22 @@
     @IntoMap
     @ClassKey(UserSwitcherActivity.class)
     public abstract Activity provideUserSwitcherActivity(UserSwitcherActivity activity);
+
+    /**
+     * Provides the {@link UserHandle} for the user associated with this System UI process.
+     *
+     * <p>Note that this is static and unchanging for the life-time of the process we are running
+     * in. It can be <i>different</i> from the user that is the currently-selected user, which may
+     * be associated with a different System UI process.
+     *
+     * <p>For example, the System UI process which creates all the windows and renders UI is always
+     * the one associated with the primary user on the device. However, if the user is switched to
+     * another, non-primary user (for example user "X"), then a secondary System UI process will be
+     * spawned. While the original primary user process continues to be the only one rendering UI,
+     * the new system UI process may be used for things like file or content access.
+     */
+    @Provides
+    public static UserHandle provideUserHandle() {
+        return new UserHandle(UserHandle.myUserId());
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt
index 32c5b3f..cef452b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt
@@ -20,6 +20,7 @@
 import android.content.ContentValues
 import android.content.pm.PackageManager
 import android.content.pm.ProviderInfo
+import android.os.UserHandle
 import androidx.test.filters.SmallTest
 import com.android.internal.widget.LockPatternUtils
 import com.android.systemui.SystemUIAppComponentFactoryBase
@@ -27,8 +28,10 @@
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceProviderClientFactory
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer
-import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLocalUserSelectionManager
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceRemoteUserSelectionManager
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
@@ -74,8 +77,8 @@
 
         underTest = KeyguardQuickAffordanceProvider()
         val scope = CoroutineScope(IMMEDIATE)
-        val selectionManager =
-            KeyguardQuickAffordanceSelectionManager(
+        val localUserSelectionManager =
+            KeyguardQuickAffordanceLocalUserSelectionManager(
                 context = context,
                 userFileManager =
                     mock<UserFileManager>().apply {
@@ -91,11 +94,20 @@
                 userTracker = userTracker,
                 broadcastDispatcher = fakeBroadcastDispatcher,
             )
+        val remoteUserSelectionManager =
+            KeyguardQuickAffordanceRemoteUserSelectionManager(
+                scope = scope,
+                userTracker = userTracker,
+                clientFactory = FakeKeyguardQuickAffordanceProviderClientFactory(userTracker),
+                userHandle = UserHandle.SYSTEM,
+            )
         val quickAffordanceRepository =
             KeyguardQuickAffordanceRepository(
                 appContext = context,
                 scope = scope,
-                selectionManager = selectionManager,
+                localUserSelectionManager = localUserSelectionManager,
+                remoteUserSelectionManager = remoteUserSelectionManager,
+                userTracker = userTracker,
                 configs =
                     setOf(
                         FakeKeyguardQuickAffordanceConfig(
@@ -114,9 +126,10 @@
                         scope = scope,
                         backgroundDispatcher = IMMEDIATE,
                         secureSettings = FakeSettings(),
-                        selectionsManager = selectionManager,
+                        selectionsManager = localUserSelectionManager,
                     ),
                 dumpManager = mock(),
+                userHandle = UserHandle.SYSTEM,
             )
         underTest.interactor =
             KeyguardQuickAffordanceInteractor(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt
index 552b8cb..3b0169d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt
@@ -57,7 +57,7 @@
 
     private lateinit var testScope: TestScope
     private lateinit var testDispatcher: TestDispatcher
-    private lateinit var selectionManager: KeyguardQuickAffordanceSelectionManager
+    private lateinit var selectionManager: KeyguardQuickAffordanceLocalUserSelectionManager
     private lateinit var settings: FakeSettings
 
     @Before
@@ -75,7 +75,7 @@
         testDispatcher = UnconfinedTestDispatcher()
         testScope = TestScope(testDispatcher)
         selectionManager =
-            KeyguardQuickAffordanceSelectionManager(
+            KeyguardQuickAffordanceLocalUserSelectionManager(
                 context = context,
                 userFileManager =
                     mock {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManagerTest.kt
similarity index 97%
rename from packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManagerTest.kt
index 6a2376b..67091a9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManagerTest.kt
@@ -52,11 +52,11 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(JUnit4::class)
-class KeyguardQuickAffordanceSelectionManagerTest : SysuiTestCase() {
+class KeyguardQuickAffordanceLocalUserSelectionManagerTest : SysuiTestCase() {
 
     @Mock private lateinit var userFileManager: UserFileManager
 
-    private lateinit var underTest: KeyguardQuickAffordanceSelectionManager
+    private lateinit var underTest: KeyguardQuickAffordanceLocalUserSelectionManager
 
     private lateinit var userTracker: FakeUserTracker
     private lateinit var sharedPrefs: MutableMap<Int, SharedPreferences>
@@ -74,7 +74,7 @@
         Dispatchers.setMain(dispatcher)
 
         underTest =
-            KeyguardQuickAffordanceSelectionManager(
+            KeyguardQuickAffordanceLocalUserSelectionManager(
                 context = context,
                 userFileManager = userFileManager,
                 userTracker = userTracker,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceRemoteUserSelectionManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceRemoteUserSelectionManagerTest.kt
new file mode 100644
index 0000000..d7e9cf1
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceRemoteUserSelectionManagerTest.kt
@@ -0,0 +1,219 @@
+/*
+ * 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.pm.UserInfo
+import android.os.UserHandle
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.settings.FakeUserTracker
+import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
+import com.android.systemui.shared.quickaffordance.data.content.FakeKeyguardQuickAffordanceProviderClient
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class KeyguardQuickAffordanceRemoteUserSelectionManagerTest : SysuiTestCase() {
+
+    @Mock private lateinit var userHandle: UserHandle
+
+    private lateinit var underTest: KeyguardQuickAffordanceRemoteUserSelectionManager
+
+    private lateinit var clientFactory: FakeKeyguardQuickAffordanceProviderClientFactory
+    private lateinit var testScope: TestScope
+    private lateinit var testDispatcher: TestDispatcher
+    private lateinit var userTracker: FakeUserTracker
+    private lateinit var client1: FakeKeyguardQuickAffordanceProviderClient
+    private lateinit var client2: FakeKeyguardQuickAffordanceProviderClient
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        whenever(userHandle.identifier).thenReturn(UserHandle.USER_SYSTEM)
+        whenever(userHandle.isSystem).thenReturn(true)
+        client1 = FakeKeyguardQuickAffordanceProviderClient()
+        client2 = FakeKeyguardQuickAffordanceProviderClient()
+
+        userTracker = FakeUserTracker()
+        userTracker.set(
+            userInfos =
+                listOf(
+                    UserInfo(
+                        UserHandle.USER_SYSTEM,
+                        "Primary",
+                        /* flags= */ 0,
+                    ),
+                    UserInfo(
+                        OTHER_USER_ID_1,
+                        "Secondary 1",
+                        /* flags= */ 0,
+                    ),
+                    UserInfo(
+                        OTHER_USER_ID_2,
+                        "Secondary 2",
+                        /* flags= */ 0,
+                    ),
+                ),
+            selectedUserIndex = 0,
+        )
+
+        clientFactory =
+            FakeKeyguardQuickAffordanceProviderClientFactory(
+                userTracker,
+            ) { selectedUserId ->
+                when (selectedUserId) {
+                    OTHER_USER_ID_1 -> client1
+                    OTHER_USER_ID_2 -> client2
+                    else -> error("No client set-up for user $selectedUserId!")
+                }
+            }
+
+        testDispatcher = StandardTestDispatcher()
+        testScope = TestScope(testDispatcher)
+
+        underTest =
+            KeyguardQuickAffordanceRemoteUserSelectionManager(
+                scope = testScope.backgroundScope,
+                userTracker = userTracker,
+                clientFactory = clientFactory,
+                userHandle = userHandle,
+            )
+    }
+
+    @Test
+    fun `selections - primary user process`() =
+        testScope.runTest {
+            val values = mutableListOf<Map<String, List<String>>>()
+            val job = launch { underTest.selections.toList(values) }
+
+            runCurrent()
+            assertThat(values.last()).isEmpty()
+
+            client1.insertSelection(
+                slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
+                affordanceId = FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_1,
+            )
+            client2.insertSelection(
+                slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
+                affordanceId = FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_2,
+            )
+
+            userTracker.set(
+                userInfos = userTracker.userProfiles,
+                selectedUserIndex = 1,
+            )
+            runCurrent()
+            assertThat(values.last())
+                .isEqualTo(
+                    mapOf(
+                        KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to
+                            listOf(
+                                FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_1,
+                            ),
+                    )
+                )
+
+            userTracker.set(
+                userInfos = userTracker.userProfiles,
+                selectedUserIndex = 2,
+            )
+            runCurrent()
+            assertThat(values.last())
+                .isEqualTo(
+                    mapOf(
+                        KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to
+                            listOf(
+                                FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_2,
+                            ),
+                    )
+                )
+
+            job.cancel()
+        }
+
+    @Test
+    fun `selections - secondary user process - always empty`() =
+        testScope.runTest {
+            whenever(userHandle.isSystem).thenReturn(false)
+            val values = mutableListOf<Map<String, List<String>>>()
+            val job = launch { underTest.selections.toList(values) }
+
+            runCurrent()
+            assertThat(values.last()).isEmpty()
+
+            client1.insertSelection(
+                slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
+                affordanceId = FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_1,
+            )
+            userTracker.set(
+                userInfos = userTracker.userProfiles,
+                selectedUserIndex = 1,
+            )
+            runCurrent()
+            assertThat(values.last()).isEmpty()
+
+            job.cancel()
+        }
+
+    @Test
+    fun setSelections() =
+        testScope.runTest {
+            userTracker.set(
+                userInfos = userTracker.userProfiles,
+                selectedUserIndex = 1,
+            )
+            runCurrent()
+
+            underTest.setSelections(
+                slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
+                affordanceIds = listOf(FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_1),
+            )
+            runCurrent()
+
+            assertThat(underTest.getSelections())
+                .isEqualTo(
+                    mapOf(
+                        KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to
+                            listOf(
+                                FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_1,
+                            ),
+                    )
+                )
+        }
+
+    companion object {
+        private const val OTHER_USER_ID_1 = UserHandle.MIN_SECONDARY_USER_ID + 1
+        private const val OTHER_USER_ID_2 = UserHandle.MIN_SECONDARY_USER_ID + 2
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt
index 652fae9..c40488a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt
@@ -17,17 +17,23 @@
 
 package com.android.systemui.keyguard.data.repository
 
+import android.content.pm.UserInfo
+import android.os.UserHandle
 import androidx.test.filters.SmallTest
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceProviderClientFactory
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer
-import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLocalUserSelectionManager
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceRemoteUserSelectionManager
 import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePickerRepresentation
 import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentation
 import com.android.systemui.settings.FakeUserTracker
 import com.android.systemui.settings.UserFileManager
+import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
+import com.android.systemui.shared.quickaffordance.data.content.FakeKeyguardQuickAffordanceProviderClient
 import com.android.systemui.util.FakeSharedPreferences
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
@@ -39,6 +45,7 @@
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.yield
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -55,14 +62,24 @@
 
     private lateinit var config1: FakeKeyguardQuickAffordanceConfig
     private lateinit var config2: FakeKeyguardQuickAffordanceConfig
+    private lateinit var userTracker: FakeUserTracker
+    private lateinit var client1: FakeKeyguardQuickAffordanceProviderClient
+    private lateinit var client2: FakeKeyguardQuickAffordanceProviderClient
 
     @Before
     fun setUp() {
-        config1 = FakeKeyguardQuickAffordanceConfig("built_in:1")
-        config2 = FakeKeyguardQuickAffordanceConfig("built_in:2")
+        config1 =
+            FakeKeyguardQuickAffordanceConfig(
+                FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_1
+            )
+        config2 =
+            FakeKeyguardQuickAffordanceConfig(
+                FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_2
+            )
         val scope = CoroutineScope(IMMEDIATE)
-        val selectionManager =
-            KeyguardQuickAffordanceSelectionManager(
+        userTracker = FakeUserTracker()
+        val localUserSelectionManager =
+            KeyguardQuickAffordanceLocalUserSelectionManager(
                 context = context,
                 userFileManager =
                     mock<UserFileManager>().apply {
@@ -75,24 +92,45 @@
                             )
                             .thenReturn(FakeSharedPreferences())
                     },
-                userTracker = FakeUserTracker(),
+                userTracker = userTracker,
                 broadcastDispatcher = fakeBroadcastDispatcher,
             )
+        client1 = FakeKeyguardQuickAffordanceProviderClient()
+        client2 = FakeKeyguardQuickAffordanceProviderClient()
+        val remoteUserSelectionManager =
+            KeyguardQuickAffordanceRemoteUserSelectionManager(
+                scope = scope,
+                userTracker = userTracker,
+                clientFactory =
+                    FakeKeyguardQuickAffordanceProviderClientFactory(
+                        userTracker,
+                    ) { selectedUserId ->
+                        when (selectedUserId) {
+                            SECONDARY_USER_1 -> client1
+                            SECONDARY_USER_2 -> client2
+                            else -> error("No set-up client for user $selectedUserId!")
+                        }
+                    },
+                userHandle = UserHandle.SYSTEM,
+            )
 
         underTest =
             KeyguardQuickAffordanceRepository(
                 appContext = context,
                 scope = scope,
-                selectionManager = selectionManager,
+                localUserSelectionManager = localUserSelectionManager,
+                remoteUserSelectionManager = remoteUserSelectionManager,
+                userTracker = userTracker,
                 legacySettingSyncer =
                     KeyguardQuickAffordanceLegacySettingSyncer(
                         scope = scope,
                         backgroundDispatcher = IMMEDIATE,
                         secureSettings = FakeSettings(),
-                        selectionsManager = selectionManager,
+                        selectionsManager = localUserSelectionManager,
                     ),
                 configs = setOf(config1, config2),
                 dumpManager = mock(),
+                userHandle = UserHandle.SYSTEM,
             )
     }
 
@@ -187,7 +225,53 @@
             )
     }
 
-    private suspend fun assertSelections(
+    @Test
+    fun `selections for secondary user`() =
+        runBlocking(IMMEDIATE) {
+            userTracker.set(
+                userInfos =
+                    listOf(
+                        UserInfo(
+                            UserHandle.USER_SYSTEM,
+                            "Primary",
+                            /* flags= */ 0,
+                        ),
+                        UserInfo(
+                            SECONDARY_USER_1,
+                            "Secondary 1",
+                            /* flags= */ 0,
+                        ),
+                        UserInfo(
+                            SECONDARY_USER_2,
+                            "Secondary 2",
+                            /* flags= */ 0,
+                        ),
+                    ),
+                selectedUserIndex = 2,
+            )
+            client2.insertSelection(
+                slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
+                affordanceId = FakeKeyguardQuickAffordanceProviderClient.AFFORDANCE_2,
+            )
+            val observed = mutableListOf<Map<String, List<KeyguardQuickAffordanceConfig>>>()
+            val job = underTest.selections.onEach { observed.add(it) }.launchIn(this)
+            yield()
+
+            assertSelections(
+                observed = observed.last(),
+                expected =
+                    mapOf(
+                        KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to
+                            listOf(
+                                config2,
+                            ),
+                    )
+            )
+
+            job.cancel()
+        }
+
+    private fun assertSelections(
         observed: Map<String, List<KeyguardQuickAffordanceConfig>>?,
         expected: Map<String, List<KeyguardQuickAffordanceConfig>>,
     ) {
@@ -201,5 +285,7 @@
 
     companion object {
         private val IMMEDIATE = Dispatchers.Main.immediate
+        private const val SECONDARY_USER_1 = UserHandle.MIN_SECONDARY_USER_ID + 1
+        private const val SECONDARY_USER_2 = UserHandle.MIN_SECONDARY_USER_ID + 2
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
index ba7c40b..1c1f039 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
@@ -18,6 +18,7 @@
 package com.android.systemui.keyguard.domain.interactor
 
 import android.content.Intent
+import android.os.UserHandle
 import androidx.test.filters.SmallTest
 import com.android.internal.widget.LockPatternUtils
 import com.android.systemui.SysuiTestCase
@@ -29,9 +30,11 @@
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys
 import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceProviderClientFactory
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer
-import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLocalUserSelectionManager
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceRemoteUserSelectionManager
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
 import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry
@@ -237,8 +240,8 @@
         val qrCodeScanner =
             FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER)
         val scope = CoroutineScope(IMMEDIATE)
-        val selectionManager =
-            KeyguardQuickAffordanceSelectionManager(
+        val localUserSelectionManager =
+            KeyguardQuickAffordanceLocalUserSelectionManager(
                 context = context,
                 userFileManager =
                     mock<UserFileManager>().apply {
@@ -254,20 +257,30 @@
                 userTracker = userTracker,
                 broadcastDispatcher = fakeBroadcastDispatcher,
             )
+        val remoteUserSelectionManager =
+            KeyguardQuickAffordanceRemoteUserSelectionManager(
+                scope = scope,
+                userTracker = userTracker,
+                clientFactory = FakeKeyguardQuickAffordanceProviderClientFactory(userTracker),
+                userHandle = UserHandle.SYSTEM,
+            )
         val quickAffordanceRepository =
             KeyguardQuickAffordanceRepository(
                 appContext = context,
                 scope = scope,
-                selectionManager = selectionManager,
+                localUserSelectionManager = localUserSelectionManager,
+                remoteUserSelectionManager = remoteUserSelectionManager,
+                userTracker = userTracker,
                 legacySettingSyncer =
                     KeyguardQuickAffordanceLegacySettingSyncer(
                         scope = scope,
                         backgroundDispatcher = IMMEDIATE,
                         secureSettings = FakeSettings(),
-                        selectionsManager = selectionManager,
+                        selectionsManager = localUserSelectionManager,
                     ),
                 configs = setOf(homeControls, quickAccessWallet, qrCodeScanner),
                 dumpManager = mock(),
+                userHandle = UserHandle.SYSTEM,
             )
         underTest =
             KeyguardQuickAffordanceInteractor(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
index 8d0c4ef..11fe905 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
@@ -17,6 +17,7 @@
 
 package com.android.systemui.keyguard.domain.interactor
 
+import android.os.UserHandle
 import androidx.test.filters.SmallTest
 import com.android.internal.widget.LockPatternUtils
 import com.android.systemui.SysuiTestCase
@@ -26,9 +27,11 @@
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys
 import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceProviderClientFactory
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer
-import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLocalUserSelectionManager
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceRemoteUserSelectionManager
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
 import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel
@@ -98,8 +101,8 @@
             FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER)
         val scope = CoroutineScope(IMMEDIATE)
 
-        val selectionManager =
-            KeyguardQuickAffordanceSelectionManager(
+        val localUserSelectionManager =
+            KeyguardQuickAffordanceLocalUserSelectionManager(
                 context = context,
                 userFileManager =
                     mock<UserFileManager>().apply {
@@ -115,20 +118,30 @@
                 userTracker = userTracker,
                 broadcastDispatcher = fakeBroadcastDispatcher,
             )
+        val remoteUserSelectionManager =
+            KeyguardQuickAffordanceRemoteUserSelectionManager(
+                scope = scope,
+                userTracker = userTracker,
+                clientFactory = FakeKeyguardQuickAffordanceProviderClientFactory(userTracker),
+                userHandle = UserHandle.SYSTEM,
+            )
         val quickAffordanceRepository =
             KeyguardQuickAffordanceRepository(
                 appContext = context,
                 scope = scope,
-                selectionManager = selectionManager,
+                localUserSelectionManager = localUserSelectionManager,
+                remoteUserSelectionManager = remoteUserSelectionManager,
+                userTracker = userTracker,
                 legacySettingSyncer =
                     KeyguardQuickAffordanceLegacySettingSyncer(
                         scope = scope,
                         backgroundDispatcher = IMMEDIATE,
                         secureSettings = FakeSettings(),
-                        selectionsManager = selectionManager,
+                        selectionsManager = localUserSelectionManager,
                     ),
                 configs = setOf(homeControls, quickAccessWallet, qrCodeScanner),
                 dumpManager = mock(),
+                userHandle = UserHandle.SYSTEM,
             )
         featureFlags =
             FakeFeatureFlags().apply {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
index 32849cd..83a5d0e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.keyguard.ui.viewmodel
 
 import android.content.Intent
+import android.os.UserHandle
 import androidx.test.filters.SmallTest
 import com.android.internal.widget.LockPatternUtils
 import com.android.systemui.SysuiTestCase
@@ -27,9 +28,11 @@
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys
 import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceProviderClientFactory
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLegacySettingSyncer
-import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceLocalUserSelectionManager
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceRemoteUserSelectionManager
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor
@@ -121,8 +124,8 @@
         whenever(lockPatternUtils.getStrongAuthForUser(anyInt()))
             .thenReturn(LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED)
         val scope = CoroutineScope(IMMEDIATE)
-        val selectionManager =
-            KeyguardQuickAffordanceSelectionManager(
+        val localUserSelectionManager =
+            KeyguardQuickAffordanceLocalUserSelectionManager(
                 context = context,
                 userFileManager =
                     mock<UserFileManager>().apply {
@@ -138,17 +141,26 @@
                 userTracker = userTracker,
                 broadcastDispatcher = fakeBroadcastDispatcher,
             )
+        val remoteUserSelectionManager =
+            KeyguardQuickAffordanceRemoteUserSelectionManager(
+                scope = scope,
+                userTracker = userTracker,
+                clientFactory = FakeKeyguardQuickAffordanceProviderClientFactory(userTracker),
+                userHandle = UserHandle.SYSTEM,
+            )
         val quickAffordanceRepository =
             KeyguardQuickAffordanceRepository(
                 appContext = context,
                 scope = scope,
-                selectionManager = selectionManager,
+                localUserSelectionManager = localUserSelectionManager,
+                remoteUserSelectionManager = remoteUserSelectionManager,
+                userTracker = userTracker,
                 legacySettingSyncer =
                     KeyguardQuickAffordanceLegacySettingSyncer(
                         scope = scope,
                         backgroundDispatcher = IMMEDIATE,
                         secureSettings = FakeSettings(),
-                        selectionsManager = selectionManager,
+                        selectionsManager = localUserSelectionManager,
                     ),
                 configs =
                     setOf(
@@ -157,6 +169,7 @@
                         qrCodeScannerAffordanceConfig,
                     ),
                 dumpManager = mock(),
+                userHandle = UserHandle.SYSTEM,
             )
         underTest =
             KeyguardBottomAreaViewModel(
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceProviderClientFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceProviderClientFactory.kt
new file mode 100644
index 0000000..d85dd2e
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceProviderClientFactory.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.settings.UserTracker
+import com.android.systemui.shared.quickaffordance.data.content.FakeKeyguardQuickAffordanceProviderClient
+import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderClient
+
+class FakeKeyguardQuickAffordanceProviderClientFactory(
+    private val userTracker: UserTracker,
+    private val callback: (Int) -> KeyguardQuickAffordanceProviderClient = {
+        FakeKeyguardQuickAffordanceProviderClient()
+    },
+) : KeyguardQuickAffordanceProviderClientFactory {
+
+    override fun create(): KeyguardQuickAffordanceProviderClient {
+        return callback(userTracker.userId)
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt
index a7eadba..0dd1fc7 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/FakeUserTracker.kt
@@ -66,7 +66,8 @@
         _userId = _userInfo.id
         _userHandle = UserHandle.of(_userId)
 
-        callbacks.forEach { it.onUserChanged(_userId, userContext) }
+        val copy = callbacks.toList()
+        copy.forEach { it.onUserChanged(_userId, userContext) }
     }
 
     fun onProfileChanged() {