Backup & Restore support for quick affordances.

Adds support for backup & restore of the currently-selected quick
affordances on the lock screen.

Only supports user 0 for now. A future CL will add support for additional users since the feature is broken for multi-user anyway (see b/260251307).

Fix: 258278136
Test: added test case. Manually verified, using the instructions from
go/localtransport that selecting some affordances, then triggering a
backup, then changing the selections and triggering a restore properly
restores the initial set of selections.

Change-Id: I9f2a51287a7467af95e7824f2b1804baba962099
diff --git a/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt b/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt
index 5616a00..621b99d 100644
--- a/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt
@@ -29,13 +29,15 @@
 import android.util.Log
 import com.android.systemui.controls.controller.AuxiliaryPersistenceWrapper
 import com.android.systemui.controls.controller.ControlsFavoritePersistenceWrapper
+import com.android.systemui.keyguard.domain.backup.KeyguardQuickAffordanceBackupHelper
 import com.android.systemui.people.widget.PeopleBackupHelper
 
 /**
  * Helper for backing up elements in SystemUI
  *
- * This helper is invoked by BackupManager whenever a backup or restore is required in SystemUI.
- * The helper can be used to back up any element that is stored in [Context.getFilesDir].
+ * This helper is invoked by BackupManager whenever a backup or restore is required in SystemUI. The
+ * helper can be used to back up any element that is stored in [Context.getFilesDir] or
+ * [Context.getSharedPreferences].
  *
  * After restoring is done, a [ACTION_RESTORE_FINISHED] intent will be send to SystemUI user 0,
  * indicating that restoring is finished for a given user.
@@ -47,9 +49,11 @@
         internal const val CONTROLS = ControlsFavoritePersistenceWrapper.FILE_NAME
         private const val NO_OVERWRITE_FILES_BACKUP_KEY = "systemui.files_no_overwrite"
         private const val PEOPLE_TILES_BACKUP_KEY = "systemui.people.shared_preferences"
+        private const val KEYGUARD_QUICK_AFFORDANCES_BACKUP_KEY =
+            "systemui.keyguard.quickaffordance.shared_preferences"
         val controlsDataLock = Any()
         const val ACTION_RESTORE_FINISHED = "com.android.systemui.backup.RESTORE_FINISHED"
-        private const val PERMISSION_SELF = "com.android.systemui.permission.SELF"
+        const val PERMISSION_SELF = "com.android.systemui.permission.SELF"
     }
 
     override fun onCreate(userHandle: UserHandle, operationType: Int) {
@@ -67,17 +71,27 @@
         }
 
         val keys = PeopleBackupHelper.getFilesToBackup()
-        addHelper(PEOPLE_TILES_BACKUP_KEY, PeopleBackupHelper(
-                this, userHandle, keys.toTypedArray()))
+        addHelper(
+            PEOPLE_TILES_BACKUP_KEY,
+            PeopleBackupHelper(this, userHandle, keys.toTypedArray())
+        )
+        addHelper(
+            KEYGUARD_QUICK_AFFORDANCES_BACKUP_KEY,
+            KeyguardQuickAffordanceBackupHelper(
+                context = this,
+                userId = userHandle.identifier,
+            ),
+        )
     }
 
     override fun onRestoreFinished() {
         super.onRestoreFinished()
-        val intent = Intent(ACTION_RESTORE_FINISHED).apply {
-            `package` = packageName
-            putExtra(Intent.EXTRA_USER_ID, userId)
-            flags = Intent.FLAG_RECEIVER_REGISTERED_ONLY
-        }
+        val intent =
+            Intent(ACTION_RESTORE_FINISHED).apply {
+                `package` = packageName
+                putExtra(Intent.EXTRA_USER_ID, userId)
+                flags = Intent.FLAG_RECEIVER_REGISTERED_ONLY
+            }
         sendBroadcastAsUser(intent, UserHandle.SYSTEM, PERMISSION_SELF)
     }
 
@@ -90,7 +104,9 @@
      * @property lock a lock to hold while backing up and restoring the files.
      * @property context the context of the [BackupAgent]
      * @property fileNamesAndPostProcess a map from the filenames to back up and the post processing
+     * ```
      *                                   actions to take
+     * ```
      */
     private class NoOverwriteFileBackupHelper(
         val lock: Any,
@@ -115,23 +131,23 @@
             data: BackupDataOutput?,
             newState: ParcelFileDescriptor?
         ) {
-            synchronized(lock) {
-                super.performBackup(oldState, data, newState)
-            }
+            synchronized(lock) { super.performBackup(oldState, data, newState) }
         }
     }
 }
+
 private fun getPPControlsFile(context: Context): () -> Unit {
     return {
         val filesDir = context.filesDir
         val file = Environment.buildPath(filesDir, BackupHelper.CONTROLS)
         if (file.exists()) {
-            val dest = Environment.buildPath(filesDir,
-                AuxiliaryPersistenceWrapper.AUXILIARY_FILE_NAME)
+            val dest =
+                Environment.buildPath(filesDir, AuxiliaryPersistenceWrapper.AUXILIARY_FILE_NAME)
             file.copyTo(dest)
             val jobScheduler = context.getSystemService(JobScheduler::class.java)
             jobScheduler?.schedule(
-                AuxiliaryPersistenceWrapper.DeletionJobService.getJobForContext(context))
+                AuxiliaryPersistenceWrapper.DeletionJobService.getJobForContext(context)
+            )
         }
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt b/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt
index 537cbc5..a0a892d 100644
--- a/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt
+++ b/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt
@@ -64,8 +64,9 @@
  * from SystemUI. That way the number of calls to [BroadcastReceiver.onReceive] can be reduced for
  * a given broadcast.
  *
- * Use only for IntentFilters with actions and optionally categories. It does not support,
- * permissions, schemes, data types, data authorities or priority different than 0.
+ * Use only for IntentFilters with actions and optionally categories. It does not support schemes,
+ * data types, data authorities or priority different than 0.
+ *
  * Cannot be used for getting sticky broadcasts (either as return of registering or as re-delivery).
  * Broadcast handling may be asynchronous *without* calling goAsync(), as it's running within sysui
  * and doesn't need to worry about being killed.
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 b29cf45..4f37e5f 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
@@ -18,9 +18,11 @@
 package com.android.systemui.keyguard.data.quickaffordance
 
 import android.content.Context
+import android.content.IntentFilter
 import android.content.SharedPreferences
-import androidx.annotation.VisibleForTesting
 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
@@ -28,14 +30,18 @@
 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?".
  */
+@OptIn(ExperimentalCoroutinesApi::class)
 @SysUISingleton
 class KeyguardQuickAffordanceSelectionManager
 @Inject
@@ -43,15 +49,10 @@
     @Application context: Context,
     private val userFileManager: UserFileManager,
     private val userTracker: UserTracker,
+    broadcastDispatcher: BroadcastDispatcher,
 ) {
 
-    private val sharedPrefs: SharedPreferences
-        get() =
-            userFileManager.getSharedPreferences(
-                FILE_NAME,
-                Context.MODE_PRIVATE,
-                userTracker.userId,
-            )
+    private var sharedPrefs: SharedPreferences = instantiateSharedPrefs()
 
     private val userId: Flow<Int> = conflatedCallbackFlow {
         val callback =
@@ -78,21 +79,54 @@
             }
     }
 
+    /**
+     * 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,
+        )
+
     /** IDs of affordances to show, indexed by slot ID, and sorted in descending priority order. */
     val selections: Flow<Map<String, List<String>>> =
-        userId.flatMapLatest {
-            conflatedCallbackFlow {
-                val listener =
-                    SharedPreferences.OnSharedPreferenceChangeListener { _, _ ->
-                        trySend(getSelections())
-                    }
-
-                sharedPrefs.registerOnSharedPreferenceChangeListener(listener)
-                send(getSelections())
-
-                awaitClose { sharedPrefs.unregisterOnSharedPreferenceChangeListener(listener) }
+        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) }
+                }
+            }
 
     /**
      * Returns a snapshot of the IDs of affordances to show, indexed by slot ID, and sorted in
@@ -144,9 +178,17 @@
         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"
-        @VisibleForTesting const val FILE_NAME = "quick_affordance_selections"
+        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/domain/backup/KeyguardQuickAffordanceBackupHelper.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/backup/KeyguardQuickAffordanceBackupHelper.kt
new file mode 100644
index 0000000..0e865ce
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/backup/KeyguardQuickAffordanceBackupHelper.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.domain.backup
+
+import android.app.backup.SharedPreferencesBackupHelper
+import android.content.Context
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager
+import com.android.systemui.settings.UserFileManagerImpl
+
+/** Handles backup & restore for keyguard quick affordances. */
+class KeyguardQuickAffordanceBackupHelper(
+    context: Context,
+    userId: Int,
+) :
+    SharedPreferencesBackupHelper(
+        context,
+        if (UserFileManagerImpl.isPrimaryUser(userId)) {
+            KeyguardQuickAffordanceSelectionManager.FILE_NAME
+        } else {
+            UserFileManagerImpl.secondaryUserFile(
+                    context = context,
+                    fileName = KeyguardQuickAffordanceSelectionManager.FILE_NAME,
+                    directoryName = UserFileManagerImpl.SHARED_PREFS,
+                    userId = userId,
+                )
+                .also { UserFileManagerImpl.ensureParentDirExists(it) }
+                .toString()
+        }
+    )
diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerImpl.kt
index d450afa..bfba6df 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerImpl.kt
@@ -35,12 +35,14 @@
 import javax.inject.Inject
 
 /**
- * Implementation for retrieving file paths for file storage of system and secondary users.
- * Files lie in {File Directory}/UserFileManager/{User Id} for secondary user.
- * For system user, we use the conventional {File Directory}
+ * Implementation for retrieving file paths for file storage of system and secondary users. Files
+ * lie in {File Directory}/UserFileManager/{User Id} for secondary user. For system user, we use the
+ * conventional {File Directory}
  */
 @SysUISingleton
-class UserFileManagerImpl @Inject constructor(
+class UserFileManagerImpl
+@Inject
+constructor(
     // Context of system process and system user.
     private val context: Context,
     val userManager: UserManager,
@@ -49,80 +51,114 @@
 ) : UserFileManager, CoreStartable {
     companion object {
         private const val FILES = "files"
-        @VisibleForTesting internal const val SHARED_PREFS = "shared_prefs"
+        const val SHARED_PREFS = "shared_prefs"
         @VisibleForTesting internal const val ID = "UserFileManager"
-    }
 
-   private val broadcastReceiver = object : BroadcastReceiver() {
+        /** Returns `true` if the given user ID is that for the primary/system user. */
+        fun isPrimaryUser(userId: Int): Boolean {
+            return UserHandle(userId).isSystem
+        }
+
         /**
-         * Listen to Intent.ACTION_USER_REMOVED to clear user data.
+         * Returns a [File] pointing to the correct path for a secondary user ID.
+         *
+         * Note that there is no check for the type of user. This should only be called for
+         * secondary users, never for the system user. For that, make sure to call [isPrimaryUser].
+         *
+         * Note also that there is no guarantee that the parent directory structure for the file
+         * exists on disk. For that, call [ensureParentDirExists].
+         *
+         * @param context The context
+         * @param fileName The name of the file
+         * @param directoryName The name of the directory that would contain the file
+         * @param userId The ID of the user to build a file path for
          */
-        override fun onReceive(context: Context, intent: Intent) {
-            if (intent.action == Intent.ACTION_USER_REMOVED) {
-                clearDeletedUserData()
+        fun secondaryUserFile(
+            context: Context,
+            fileName: String,
+            directoryName: String,
+            userId: Int,
+        ): File {
+            return Environment.buildPath(
+                context.filesDir,
+                ID,
+                userId.toString(),
+                directoryName,
+                fileName,
+            )
+        }
+
+        /**
+         * Checks to see if parent dir of the file exists. If it does not, we create the parent dirs
+         * recursively.
+         */
+        fun ensureParentDirExists(file: File) {
+            val parent = file.parentFile
+            if (!parent.exists()) {
+                if (!parent.mkdirs()) {
+                    Log.e(ID, "Could not create parent directory for file: ${file.absolutePath}")
+                }
             }
         }
     }
 
-    /**
-     * Poll for user-specific directories to delete upon start up.
-     */
+    private val broadcastReceiver =
+        object : BroadcastReceiver() {
+            /** Listen to Intent.ACTION_USER_REMOVED to clear user data. */
+            override fun onReceive(context: Context, intent: Intent) {
+                if (intent.action == Intent.ACTION_USER_REMOVED) {
+                    clearDeletedUserData()
+                }
+            }
+        }
+
+    /** Poll for user-specific directories to delete upon start up. */
     override fun start() {
         clearDeletedUserData()
-        val filter = IntentFilter().apply {
-            addAction(Intent.ACTION_USER_REMOVED)
-        }
+        val filter = IntentFilter().apply { addAction(Intent.ACTION_USER_REMOVED) }
         broadcastDispatcher.registerReceiver(broadcastReceiver, filter, backgroundExecutor)
     }
 
-    /**
-     * Return the file based on current user.
-     */
+    /** Return the file based on current user. */
     override fun getFile(fileName: String, userId: Int): File {
-        return if (UserHandle(userId).isSystem) {
-            Environment.buildPath(
-                context.filesDir,
-                fileName
-            )
+        return if (isPrimaryUser(userId)) {
+            Environment.buildPath(context.filesDir, fileName)
         } else {
-            val secondaryFile = Environment.buildPath(
-                context.filesDir,
-                ID,
-                userId.toString(),
-                FILES,
-                fileName
-            )
+            val secondaryFile =
+                secondaryUserFile(
+                    context = context,
+                    userId = userId,
+                    directoryName = FILES,
+                    fileName = fileName,
+                )
             ensureParentDirExists(secondaryFile)
             secondaryFile
         }
     }
 
-    /**
-     * Get shared preferences from user.
-     */
+    /** Get shared preferences from user. */
     override fun getSharedPreferences(
         fileName: String,
         @Context.PreferencesMode mode: Int,
         userId: Int
     ): SharedPreferences {
-        if (UserHandle(userId).isSystem) {
+        if (isPrimaryUser(userId)) {
             return context.getSharedPreferences(fileName, mode)
         }
-        val secondaryUserDir = Environment.buildPath(
-            context.filesDir,
-            ID,
-            userId.toString(),
-            SHARED_PREFS,
-            fileName
-        )
+
+        val secondaryUserDir =
+            secondaryUserFile(
+                context = context,
+                fileName = fileName,
+                directoryName = SHARED_PREFS,
+                userId = userId,
+            )
 
         ensureParentDirExists(secondaryUserDir)
         return context.getSharedPreferences(secondaryUserDir, mode)
     }
 
-    /**
-     * Remove dirs for deleted users.
-     */
+    /** Remove dirs for deleted users. */
     @VisibleForTesting
     internal fun clearDeletedUserData() {
         backgroundExecutor.execute {
@@ -133,10 +169,11 @@
 
             dirsToDelete.forEach { dir ->
                 try {
-                    val dirToDelete = Environment.buildPath(
-                        file,
-                        dir,
-                    )
+                    val dirToDelete =
+                        Environment.buildPath(
+                            file,
+                            dir,
+                        )
                     dirToDelete.deleteRecursively()
                 } catch (e: Exception) {
                     Log.e(ID, "Deletion failed.", e)
@@ -144,18 +181,4 @@
             }
         }
     }
-
-    /**
-     * Checks to see if parent dir of the file exists. If it does not, we create the parent dirs
-     * recursively.
-     */
-    @VisibleForTesting
-    internal fun ensureParentDirExists(file: File) {
-        val parent = file.parentFile
-        if (!parent.exists()) {
-            if (!parent.mkdirs()) {
-                Log.e(ID, "Could not create parent directory for file: ${file.absolutePath}")
-            }
-        }
-    }
 }
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 cedde58..f451b3c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt
@@ -89,6 +89,7 @@
                             .thenReturn(FakeSharedPreferences())
                     },
                 userTracker = userTracker,
+                broadcastDispatcher = fakeBroadcastDispatcher,
             )
         val quickAffordanceRepository =
             KeyguardQuickAffordanceRepository(
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 8ef921e..552b8cb 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
@@ -89,6 +89,7 @@
                             .thenReturn(FakeSharedPreferences())
                     },
                 userTracker = FakeUserTracker(),
+                broadcastDispatcher = fakeBroadcastDispatcher,
             )
         settings = FakeSettings()
         settings.putInt(Settings.Secure.LOCKSCREEN_SHOW_CONTROLS, 0)
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/KeyguardQuickAffordanceSelectionManagerTest.kt
index d8ee9f1..6a2376b 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/KeyguardQuickAffordanceSelectionManagerTest.kt
@@ -17,6 +17,7 @@
 
 package com.android.systemui.keyguard.data.quickaffordance
 
+import android.content.Intent
 import android.content.SharedPreferences
 import android.content.pm.UserInfo
 import androidx.test.filters.SmallTest
@@ -27,10 +28,15 @@
 import com.android.systemui.util.FakeSharedPreferences
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.toList
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.resetMain
 import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.junit.After
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -38,8 +44,12 @@
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.ArgumentMatchers.anyString
 import org.mockito.Mock
+import org.mockito.Mockito.atLeastOnce
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(JUnit4::class)
 class KeyguardQuickAffordanceSelectionManagerTest : SysuiTestCase() {
@@ -60,15 +70,23 @@
             sharedPrefs.getOrPut(userId) { FakeSharedPreferences() }
         }
         userTracker = FakeUserTracker()
+        val dispatcher = UnconfinedTestDispatcher()
+        Dispatchers.setMain(dispatcher)
 
         underTest =
             KeyguardQuickAffordanceSelectionManager(
                 context = context,
                 userFileManager = userFileManager,
                 userTracker = userTracker,
+                broadcastDispatcher = fakeBroadcastDispatcher,
             )
     }
 
+    @After
+    fun tearDown() {
+        Dispatchers.resetMain()
+    }
+
     @Test
     fun setSelections() = runTest {
         overrideResource(R.array.config_keyguardQuickAffordanceDefaults, arrayOf<String>())
@@ -318,6 +336,22 @@
         job.cancel()
     }
 
+    @Test
+    fun `responds to backup and restore by reloading the selections from disk`() = runTest {
+        overrideResource(R.array.config_keyguardQuickAffordanceDefaults, arrayOf<String>())
+        val affordanceIdsBySlotId = mutableListOf<Map<String, List<String>>>()
+        val job =
+            launch(UnconfinedTestDispatcher()) {
+                underTest.selections.toList(affordanceIdsBySlotId)
+            }
+        clearInvocations(userFileManager)
+
+        fakeBroadcastDispatcher.registeredReceivers.firstOrNull()?.onReceive(context, Intent())
+
+        verify(userFileManager, atLeastOnce()).getSharedPreferences(anyString(), anyInt(), anyInt())
+        job.cancel()
+    }
+
     private fun assertSelections(
         observed: Map<String, List<String>>?,
         expected: Map<String, List<String>>,
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 5c75417..652fae9 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
@@ -76,6 +76,7 @@
                             .thenReturn(FakeSharedPreferences())
                     },
                 userTracker = FakeUserTracker(),
+                broadcastDispatcher = fakeBroadcastDispatcher,
             )
 
         underTest =
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 c2650ec..ba7c40b 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
@@ -252,6 +252,7 @@
                             .thenReturn(FakeSharedPreferences())
                     },
                 userTracker = userTracker,
+                broadcastDispatcher = fakeBroadcastDispatcher,
             )
         val quickAffordanceRepository =
             KeyguardQuickAffordanceRepository(
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 b790306..8d0c4ef 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
@@ -113,6 +113,7 @@
                             .thenReturn(FakeSharedPreferences())
                     },
                 userTracker = userTracker,
+                broadcastDispatcher = fakeBroadcastDispatcher,
             )
         val quickAffordanceRepository =
             KeyguardQuickAffordanceRepository(
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 8b166bd..32849cd 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
@@ -136,6 +136,7 @@
                             .thenReturn(FakeSharedPreferences())
                     },
                 userTracker = userTracker,
+                broadcastDispatcher = fakeBroadcastDispatcher,
             )
         val quickAffordanceRepository =
             KeyguardQuickAffordanceRepository(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/UserFileManagerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/UserFileManagerImplTest.kt
index 6d9b01e..020a866 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/settings/UserFileManagerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/settings/UserFileManagerImplTest.kt
@@ -50,24 +50,20 @@
 
     lateinit var userFileManager: UserFileManagerImpl
     lateinit var backgroundExecutor: FakeExecutor
-    @Mock
-    lateinit var userManager: UserManager
-    @Mock
-    lateinit var broadcastDispatcher: BroadcastDispatcher
+    @Mock lateinit var userManager: UserManager
+    @Mock lateinit var broadcastDispatcher: BroadcastDispatcher
 
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
         backgroundExecutor = FakeExecutor(FakeSystemClock())
-        userFileManager = UserFileManagerImpl(context, userManager,
-            broadcastDispatcher, backgroundExecutor)
+        userFileManager =
+            UserFileManagerImpl(context, userManager, broadcastDispatcher, backgroundExecutor)
     }
 
     @After
     fun end() {
-        val dir = Environment.buildPath(
-            context.filesDir,
-            UserFileManagerImpl.ID)
+        val dir = Environment.buildPath(context.filesDir, UserFileManagerImpl.ID)
         dir.deleteRecursively()
     }
 
@@ -82,13 +78,14 @@
     @Test
     fun testGetSharedPreferences() {
         val secondarySharedPref = userFileManager.getSharedPreferences(TEST_FILE_NAME, 0, 11)
-        val secondaryUserDir = Environment.buildPath(
-            context.filesDir,
-            UserFileManagerImpl.ID,
-            "11",
-            UserFileManagerImpl.SHARED_PREFS,
-            TEST_FILE_NAME
-        )
+        val secondaryUserDir =
+            Environment.buildPath(
+                context.filesDir,
+                UserFileManagerImpl.ID,
+                "11",
+                UserFileManagerImpl.SHARED_PREFS,
+                TEST_FILE_NAME
+            )
 
         assertThat(secondarySharedPref).isNotNull()
         assertThat(secondaryUserDir.exists())
@@ -101,32 +98,35 @@
         val userFileManager = spy(userFileManager)
         userFileManager.start()
         verify(userFileManager).clearDeletedUserData()
-        verify(broadcastDispatcher).registerReceiver(any(BroadcastReceiver::class.java),
-            any(IntentFilter::class.java),
-            any(Executor::class.java), isNull(), eq(Context.RECEIVER_EXPORTED), isNull())
+        verify(broadcastDispatcher)
+            .registerReceiver(
+                any(BroadcastReceiver::class.java),
+                any(IntentFilter::class.java),
+                any(Executor::class.java),
+                isNull(),
+                eq(Context.RECEIVER_EXPORTED),
+                isNull()
+            )
     }
 
     @Test
     fun testClearDeletedUserData() {
-        val dir = Environment.buildPath(
-            context.filesDir,
-            UserFileManagerImpl.ID,
-            "11",
-            "files"
-        )
+        val dir = Environment.buildPath(context.filesDir, UserFileManagerImpl.ID, "11", "files")
         dir.mkdirs()
-        val file = Environment.buildPath(
-            context.filesDir,
-            UserFileManagerImpl.ID,
-            "11",
-            "files",
-            TEST_FILE_NAME
-        )
-        val secondaryUserDir = Environment.buildPath(
-            context.filesDir,
-            UserFileManagerImpl.ID,
-            "11",
-        )
+        val file =
+            Environment.buildPath(
+                context.filesDir,
+                UserFileManagerImpl.ID,
+                "11",
+                "files",
+                TEST_FILE_NAME
+            )
+        val secondaryUserDir =
+            Environment.buildPath(
+                context.filesDir,
+                UserFileManagerImpl.ID,
+                "11",
+            )
         file.createNewFile()
         assertThat(secondaryUserDir.exists()).isTrue()
         assertThat(file.exists()).isTrue()
@@ -139,15 +139,16 @@
 
     @Test
     fun testEnsureParentDirExists() {
-        val file = Environment.buildPath(
-            context.filesDir,
-            UserFileManagerImpl.ID,
-            "11",
-            "files",
-            TEST_FILE_NAME
-        )
+        val file =
+            Environment.buildPath(
+                context.filesDir,
+                UserFileManagerImpl.ID,
+                "11",
+                "files",
+                TEST_FILE_NAME
+            )
         assertThat(file.parentFile.exists()).isFalse()
-        userFileManager.ensureParentDirExists(file)
+        UserFileManagerImpl.ensureParentDirExists(file)
         assertThat(file.parentFile.exists()).isTrue()
     }
 }