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
This commit is contained in:
@@ -29,13 +29,15 @@ import android.os.UserHandle
|
||||
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 @@ open class BackupHelper : BackupAgentHelper() {
|
||||
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 @@ open class BackupHelper : BackupAgentHelper() {
|
||||
}
|
||||
|
||||
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 @@ open class BackupHelper : BackupAgentHelper() {
|
||||
* @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 @@ open class BackupHelper : BackupAgentHelper() {
|
||||
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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,8 +64,9 @@ private const val TAG = "BroadcastDispatcher"
|
||||
* 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.
|
||||
|
||||
@@ -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.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?".
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@SysUISingleton
|
||||
class KeyguardQuickAffordanceSelectionManager
|
||||
@Inject
|
||||
@@ -43,15 +49,10 @@ constructor(
|
||||
@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 @@ constructor(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 @@ constructor(
|
||||
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 = ","
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
)
|
||||
@@ -35,12 +35,14 @@ import java.io.File
|
||||
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 @@ class UserFileManagerImpl @Inject constructor(
|
||||
) : 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 @@ class UserFileManagerImpl @Inject constructor(
|
||||
|
||||
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 @@ class UserFileManagerImpl @Inject constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,7 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() {
|
||||
.thenReturn(FakeSharedPreferences())
|
||||
},
|
||||
userTracker = userTracker,
|
||||
broadcastDispatcher = fakeBroadcastDispatcher,
|
||||
)
|
||||
val quickAffordanceRepository =
|
||||
KeyguardQuickAffordanceRepository(
|
||||
|
||||
@@ -89,6 +89,7 @@ class KeyguardQuickAffordanceLegacySettingSyncerTest : SysuiTestCase() {
|
||||
.thenReturn(FakeSharedPreferences())
|
||||
},
|
||||
userTracker = FakeUserTracker(),
|
||||
broadcastDispatcher = fakeBroadcastDispatcher,
|
||||
)
|
||||
settings = FakeSettings()
|
||||
settings.putInt(Settings.Secure.LOCKSCREEN_SHOW_CONTROLS, 0)
|
||||
|
||||
@@ -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.settings.UserFileManager
|
||||
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.junit.runners.JUnit4
|
||||
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 @@ class KeyguardQuickAffordanceSelectionManagerTest : SysuiTestCase() {
|
||||
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 @@ class KeyguardQuickAffordanceSelectionManagerTest : SysuiTestCase() {
|
||||
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>>,
|
||||
|
||||
@@ -76,6 +76,7 @@ class KeyguardQuickAffordanceRepositoryTest : SysuiTestCase() {
|
||||
.thenReturn(FakeSharedPreferences())
|
||||
},
|
||||
userTracker = FakeUserTracker(),
|
||||
broadcastDispatcher = fakeBroadcastDispatcher,
|
||||
)
|
||||
|
||||
underTest =
|
||||
|
||||
@@ -252,6 +252,7 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() {
|
||||
.thenReturn(FakeSharedPreferences())
|
||||
},
|
||||
userTracker = userTracker,
|
||||
broadcastDispatcher = fakeBroadcastDispatcher,
|
||||
)
|
||||
val quickAffordanceRepository =
|
||||
KeyguardQuickAffordanceRepository(
|
||||
|
||||
@@ -113,6 +113,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
|
||||
.thenReturn(FakeSharedPreferences())
|
||||
},
|
||||
userTracker = userTracker,
|
||||
broadcastDispatcher = fakeBroadcastDispatcher,
|
||||
)
|
||||
val quickAffordanceRepository =
|
||||
KeyguardQuickAffordanceRepository(
|
||||
|
||||
@@ -136,6 +136,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
|
||||
.thenReturn(FakeSharedPreferences())
|
||||
},
|
||||
userTracker = userTracker,
|
||||
broadcastDispatcher = fakeBroadcastDispatcher,
|
||||
)
|
||||
val quickAffordanceRepository =
|
||||
KeyguardQuickAffordanceRepository(
|
||||
|
||||
@@ -50,24 +50,20 @@ class UserFileManagerImplTest : SysuiTestCase() {
|
||||
|
||||
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 @@ class UserFileManagerImplTest : SysuiTestCase() {
|
||||
@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 @@ class UserFileManagerImplTest : SysuiTestCase() {
|
||||
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 @@ class UserFileManagerImplTest : SysuiTestCase() {
|
||||
|
||||
@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()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user