Merge "Backup and restore - Fix non-system users" into tm-qpr-dev

This commit is contained in:
Matt Pietal
2023-02-15 15:06:08 +00:00
committed by Android (Google) Code Review
6 changed files with 249 additions and 202 deletions

View File

@@ -31,6 +31,7 @@ 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
import com.android.systemui.settings.UserFileManagerImpl
/**
* Helper for backing up elements in SystemUI
@@ -58,17 +59,8 @@ open class BackupHelper : BackupAgentHelper() {
override fun onCreate(userHandle: UserHandle, operationType: Int) {
super.onCreate()
// The map in mapOf is guaranteed to be order preserving
val controlsMap = mapOf(CONTROLS to getPPControlsFile(this))
NoOverwriteFileBackupHelper(controlsDataLock, this, controlsMap).also {
addHelper(NO_OVERWRITE_FILES_BACKUP_KEY, it)
}
// Conversations widgets backup only works for system user, because widgets' information is
// stored in system user's SharedPreferences files and we can't open those from other users.
if (!userHandle.isSystem) {
return
}
addControlsHelper(userHandle.identifier)
val keys = PeopleBackupHelper.getFilesToBackup()
addHelper(
@@ -95,6 +87,18 @@ open class BackupHelper : BackupAgentHelper() {
sendBroadcastAsUser(intent, UserHandle.SYSTEM, PERMISSION_SELF)
}
private fun addControlsHelper(userId: Int) {
val file = UserFileManagerImpl.createFile(
userId = userId,
fileName = CONTROLS,
)
// The map in mapOf is guaranteed to be order preserving
val controlsMap = mapOf(file.getPath() to getPPControlsFile(this, userId))
NoOverwriteFileBackupHelper(controlsDataLock, this, controlsMap).also {
addHelper(NO_OVERWRITE_FILES_BACKUP_KEY, it)
}
}
/**
* Helper class for restoring files ONLY if they are not present.
*
@@ -136,17 +140,21 @@ open class BackupHelper : BackupAgentHelper() {
}
}
private fun getPPControlsFile(context: Context): () -> Unit {
private fun getPPControlsFile(context: Context, userId: Int): () -> Unit {
return {
val filesDir = context.filesDir
val file = Environment.buildPath(filesDir, BackupHelper.CONTROLS)
val file = UserFileManagerImpl.createFile(
userId = userId,
fileName = BackupHelper.CONTROLS,
)
if (file.exists()) {
val dest =
Environment.buildPath(filesDir, AuxiliaryPersistenceWrapper.AUXILIARY_FILE_NAME)
val dest = UserFileManagerImpl.createFile(
userId = userId,
fileName = AuxiliaryPersistenceWrapper.AUXILIARY_FILE_NAME,
)
file.copyTo(dest)
val jobScheduler = context.getSystemService(JobScheduler::class.java)
jobScheduler?.schedule(
AuxiliaryPersistenceWrapper.DeletionJobService.getJobForContext(context)
AuxiliaryPersistenceWrapper.DeletionJobService.getJobForContext(context, userId)
)
}
}

View File

@@ -21,8 +21,10 @@ import android.app.job.JobParameters
import android.app.job.JobService
import android.content.ComponentName
import android.content.Context
import android.os.PersistableBundle
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.backup.BackupHelper
import com.android.systemui.settings.UserFileManagerImpl
import java.io.File
import java.util.concurrent.Executor
import java.util.concurrent.TimeUnit
@@ -33,14 +35,14 @@ import java.util.concurrent.TimeUnit
* This file is a copy of the `controls_favorites.xml` file restored from a back up. It is used to
* keep track of controls that were restored but its corresponding app has not been installed yet.
*/
class AuxiliaryPersistenceWrapper @VisibleForTesting internal constructor(
wrapper: ControlsFavoritePersistenceWrapper
) {
class AuxiliaryPersistenceWrapper
@VisibleForTesting
internal constructor(wrapper: ControlsFavoritePersistenceWrapper) {
constructor(
file: File,
executor: Executor
): this(ControlsFavoritePersistenceWrapper(file, executor))
) : this(ControlsFavoritePersistenceWrapper(file, executor))
companion object {
const val AUXILIARY_FILE_NAME = "aux_controls_favorites.xml"
@@ -48,9 +50,7 @@ class AuxiliaryPersistenceWrapper @VisibleForTesting internal constructor(
private var persistenceWrapper: ControlsFavoritePersistenceWrapper = wrapper
/**
* Access the current list of favorites as tracked by the auxiliary file
*/
/** Access the current list of favorites as tracked by the auxiliary file */
var favorites: List<StructureInfo> = emptyList()
private set
@@ -73,18 +73,19 @@ class AuxiliaryPersistenceWrapper @VisibleForTesting internal constructor(
* exist, it will be initialized to an empty list.
*/
fun initialize() {
favorites = if (persistenceWrapper.fileExists) {
persistenceWrapper.readFavorites()
} else {
emptyList()
}
favorites =
if (persistenceWrapper.fileExists) {
persistenceWrapper.readFavorites()
} else {
emptyList()
}
}
/**
* Gets the list of favorite controls as persisted in the auxiliary file for a given component.
*
* When the favorites for that application are returned, they will be removed from the
* auxiliary file immediately, so they won't be retrieved again.
* When the favorites for that application are returned, they will be removed from the auxiliary
* file immediately, so they won't be retrieved again.
* @param componentName the name of the service that provided the controls
* @return a list of structures with favorites
*/
@@ -103,20 +104,20 @@ class AuxiliaryPersistenceWrapper @VisibleForTesting internal constructor(
}
}
/**
* [JobService] to delete the auxiliary file after a week.
*/
/** [JobService] to delete the auxiliary file after a week. */
class DeletionJobService : JobService() {
companion object {
@VisibleForTesting
internal val DELETE_FILE_JOB_ID = 1000
@VisibleForTesting internal val DELETE_FILE_JOB_ID = 1000
@VisibleForTesting internal val USER = "USER"
private val WEEK_IN_MILLIS = TimeUnit.DAYS.toMillis(7)
fun getJobForContext(context: Context): JobInfo {
fun getJobForContext(context: Context, targetUserId: Int): JobInfo {
val jobId = DELETE_FILE_JOB_ID + context.userId
val componentName = ComponentName(context, DeletionJobService::class.java)
val bundle = PersistableBundle().also { it.putInt(USER, targetUserId) }
return JobInfo.Builder(jobId, componentName)
.setMinimumLatency(WEEK_IN_MILLIS)
.setPersisted(true)
.setExtras(bundle)
.build()
}
}
@@ -127,8 +128,14 @@ class AuxiliaryPersistenceWrapper @VisibleForTesting internal constructor(
}
override fun onStartJob(params: JobParameters): Boolean {
val userId = params.getExtras()?.getInt(USER, 0) ?: 0
synchronized(BackupHelper.controlsDataLock) {
baseContext.deleteFile(AUXILIARY_FILE_NAME)
val file =
UserFileManagerImpl.createFile(
userId = userId,
fileName = AUXILIARY_FILE_NAME,
)
baseContext.deleteFile(file.getPath())
}
return false
}
@@ -137,4 +144,4 @@ class AuxiliaryPersistenceWrapper @VisibleForTesting internal constructor(
return true // reschedule and try again if the job was stopped without completing
}
}
}
}

View File

@@ -29,16 +29,9 @@ class KeyguardQuickAffordanceBackupHelper(
) :
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()
}
UserFileManagerImpl.createFile(
userId = userId,
fileName = KeyguardQuickAffordanceSelectionManager.FILE_NAME,
)
.getPath()
)

View File

@@ -32,74 +32,62 @@ import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.util.concurrency.DelayableExecutor
import java.io.File
import java.io.FilenameFilter
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. For
* non-system users, files will be prepended by a special prefix containing the user id.
*/
@SysUISingleton
class UserFileManagerImpl
@Inject
constructor(
// Context of system process and system user.
private val context: Context,
val userManager: UserManager,
val broadcastDispatcher: BroadcastDispatcher,
@Background val backgroundExecutor: DelayableExecutor
) : UserFileManager, CoreStartable {
companion object {
private const val FILES = "files"
private const val PREFIX = "__USER_"
private const val TAG = "UserFileManagerImpl"
const val ROOT_DIR = "UserFileManager"
const val FILES = "files"
const val SHARED_PREFS = "shared_prefs"
@VisibleForTesting internal const val ID = "UserFileManager"
/** Returns `true` if the given user ID is that for the primary/system user. */
fun isPrimaryUser(userId: Int): Boolean {
return UserHandle(userId).isSystem
}
/**
* 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
* Returns a File object with a relative path, built from the userId for non-system users
*/
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}")
}
fun createFile(fileName: String, userId: Int): File {
return if (isSystemUser(userId)) {
File(fileName)
} else {
File(getFilePrefix(userId) + fileName)
}
}
fun createLegacyFile(context: Context, dir: String, fileName: String, userId: Int): File? {
return if (isSystemUser(userId)) {
null
} else {
return Environment.buildPath(
context.filesDir,
ROOT_DIR,
userId.toString(),
dir,
fileName
)
}
}
fun getFilePrefix(userId: Int): String {
return PREFIX + userId.toString() + "_"
}
/** Returns `true` if the given user ID is that for the system user. */
private fun isSystemUser(userId: Int): Boolean {
return UserHandle(userId).isSystem
}
}
private val broadcastReceiver =
@@ -119,64 +107,87 @@ constructor(
broadcastDispatcher.registerReceiver(broadcastReceiver, filter, backgroundExecutor)
}
/** Return the file based on current user. */
/**
* Return the file based on current user. Files for all users will exist in [context.filesDir],
* but non system user files will be prepended with [getFilePrefix].
*/
override fun getFile(fileName: String, userId: Int): File {
return if (isPrimaryUser(userId)) {
Environment.buildPath(context.filesDir, fileName)
} else {
val secondaryFile =
secondaryUserFile(
context = context,
userId = userId,
directoryName = FILES,
fileName = fileName,
)
ensureParentDirExists(secondaryFile)
secondaryFile
}
val file = File(context.filesDir, createFile(fileName, userId).path)
createLegacyFile(context, FILES, fileName, userId)?.run { migrate(file, this) }
return file
}
/** Get shared preferences from user. */
/**
* Get shared preferences from user. Files for all users will exist in the shared_prefs dir, but
* non system user files will be prepended with [getFilePrefix].
*/
override fun getSharedPreferences(
fileName: String,
@Context.PreferencesMode mode: Int,
userId: Int
): SharedPreferences {
if (isPrimaryUser(userId)) {
return context.getSharedPreferences(fileName, mode)
val file = createFile(fileName, userId)
createLegacyFile(context, SHARED_PREFS, "$fileName.xml", userId)?.run {
val path = Environment.buildPath(context.dataDir, SHARED_PREFS, "${file.path}.xml")
migrate(path, this)
}
val secondaryUserDir =
secondaryUserFile(
context = context,
fileName = fileName,
directoryName = SHARED_PREFS,
userId = userId,
)
ensureParentDirExists(secondaryUserDir)
return context.getSharedPreferences(secondaryUserDir, mode)
return context.getSharedPreferences(file.path, mode)
}
/** Remove dirs for deleted users. */
/** Remove files for deleted users. */
@VisibleForTesting
internal fun clearDeletedUserData() {
backgroundExecutor.execute {
val file = Environment.buildPath(context.filesDir, ID)
if (!file.exists()) return@execute
val aliveUsers = userManager.aliveUsers.map { it.id.toString() }
val dirsToDelete = file.list().filter { !aliveUsers.contains(it) }
deleteFiles(context.filesDir)
deleteFiles(File(context.dataDir, SHARED_PREFS))
}
}
dirsToDelete.forEach { dir ->
private fun migrate(dest: File, source: File) {
if (source.exists()) {
try {
val parent = source.getParentFile()
source.renameTo(dest)
deleteParentDirsIfEmpty(parent)
} catch (e: Exception) {
Log.e(TAG, "Failed to rename and delete ${source.path}", e)
}
}
}
private fun deleteParentDirsIfEmpty(dir: File?) {
if (dir != null && dir.listFiles().size == 0) {
val priorParent = dir.parentFile
val isRoot = dir.name == ROOT_DIR
dir.delete()
if (!isRoot) {
deleteParentDirsIfEmpty(priorParent)
}
}
}
private fun deleteFiles(parent: File) {
val aliveUserFilePrefix = userManager.aliveUsers.map { getFilePrefix(it.id) }
val filesToDelete =
parent.listFiles(
FilenameFilter { _, name ->
name.startsWith(PREFIX) &&
aliveUserFilePrefix.filter { name.startsWith(it) }.isEmpty()
}
)
// This can happen in test environments
if (filesToDelete == null) {
Log.i(TAG, "Empty directory: ${parent.path}")
} else {
filesToDelete.forEach { file ->
Log.i(TAG, "Deleting file: ${file.path}")
try {
val dirToDelete =
Environment.buildPath(
file,
dir,
)
dirToDelete.deleteRecursively()
file.delete()
} catch (e: Exception) {
Log.e(ID, "Deletion failed.", e)
Log.e(TAG, "Deletion failed.", e)
}
}
}

View File

@@ -18,9 +18,13 @@ package com.android.systemui.controls.controller
import android.app.job.JobParameters
import android.content.Context
import android.os.PersistableBundle
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.controls.controller.AuxiliaryPersistenceWrapper.DeletionJobService.Companion.USER
import com.android.systemui.util.mockito.whenever
import java.util.concurrent.TimeUnit
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
@@ -28,18 +32,15 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.`when`
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
import java.util.concurrent.TimeUnit
@SmallTest
@RunWith(AndroidTestingRunner::class)
class DeletionJobServiceTest : SysuiTestCase() {
@Mock
private lateinit var context: Context
@Mock private lateinit var context: Context
private lateinit var service: AuxiliaryPersistenceWrapper.DeletionJobService
@@ -53,6 +54,10 @@ class DeletionJobServiceTest : SysuiTestCase() {
@Test
fun testOnStartJob() {
val bundle = PersistableBundle().also { it.putInt(USER, 0) }
val params = mock(JobParameters::class.java)
whenever(params.getExtras()).thenReturn(bundle)
// false means job is terminated
assertFalse(service.onStartJob(mock(JobParameters::class.java)))
verify(context).deleteFile(AuxiliaryPersistenceWrapper.AUXILIARY_FILE_NAME)
@@ -67,13 +72,17 @@ class DeletionJobServiceTest : SysuiTestCase() {
@Test
fun testJobHasRightParameters() {
val userId = 10
`when`(context.userId).thenReturn(userId)
`when`(context.packageName).thenReturn(mContext.packageName)
whenever(context.userId).thenReturn(userId)
whenever(context.packageName).thenReturn(mContext.packageName)
val jobInfo = AuxiliaryPersistenceWrapper.DeletionJobService.getJobForContext(context)
val jobInfo =
AuxiliaryPersistenceWrapper.DeletionJobService.getJobForContext(context, userId)
assertEquals(
AuxiliaryPersistenceWrapper.DeletionJobService.DELETE_FILE_JOB_ID + userId, jobInfo.id)
AuxiliaryPersistenceWrapper.DeletionJobService.DELETE_FILE_JOB_ID + userId,
jobInfo.id
)
assertTrue(jobInfo.isPersisted)
assertEquals(userId, jobInfo.getExtras().getInt(USER))
assertEquals(TimeUnit.DAYS.toMillis(7), jobInfo.minLatencyMillis)
}
}
}

View File

@@ -30,12 +30,14 @@ import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import java.io.File
import java.nio.file.Files
import java.util.concurrent.Executor
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.atLeastOnce
import org.mockito.Mockito.isNull
import org.mockito.Mockito.spy
import org.mockito.Mockito.verify
@@ -61,36 +63,83 @@ class UserFileManagerImplTest : SysuiTestCase() {
UserFileManagerImpl(context, userManager, broadcastDispatcher, backgroundExecutor)
}
@After
fun end() {
val dir = Environment.buildPath(context.filesDir, UserFileManagerImpl.ID)
dir.deleteRecursively()
}
@Test
fun testGetFile() {
assertThat(userFileManager.getFile(TEST_FILE_NAME, 0).path)
.isEqualTo("${context.filesDir}/$TEST_FILE_NAME")
assertThat(userFileManager.getFile(TEST_FILE_NAME, 11).path)
.isEqualTo("${context.filesDir}/${UserFileManagerImpl.ID}/11/files/$TEST_FILE_NAME")
.isEqualTo("${context.filesDir}/__USER_11_$TEST_FILE_NAME")
}
@Test
fun testGetSharedPreferences() {
val primarySharedPref = userFileManager.getSharedPreferences(TEST_FILE_NAME, 0, 0)
val secondarySharedPref = userFileManager.getSharedPreferences(TEST_FILE_NAME, 0, 11)
val secondaryUserDir =
Environment.buildPath(
context.filesDir,
UserFileManagerImpl.ID,
"11",
UserFileManagerImpl.SHARED_PREFS,
TEST_FILE_NAME
)
assertThat(secondarySharedPref).isNotNull()
assertThat(secondaryUserDir.exists())
assertThat(userFileManager.getSharedPreferences(TEST_FILE_NAME, 0, 0))
.isNotEqualTo(secondarySharedPref)
assertThat(primarySharedPref).isNotEqualTo(secondarySharedPref)
// Make sure these are different files
primarySharedPref.edit().putString("TEST", "ABC").commit()
assertThat(secondarySharedPref.getString("TEST", null)).isNull()
context.deleteSharedPreferences("TEST")
context.deleteSharedPreferences("__USER_11_TEST")
}
@Test
fun testMigrateFile() {
val userId = 12
val fileName = "myFile.txt"
val fileContents = "TestingFile"
val legacyFile =
UserFileManagerImpl.createLegacyFile(
context,
UserFileManagerImpl.FILES,
fileName,
userId
)!!
// Write file to legacy area
Files.createDirectories(legacyFile.getParentFile().toPath())
Files.write(legacyFile.toPath(), fileContents.toByteArray())
assertThat(legacyFile.exists()).isTrue()
// getFile() should migrate the legacy file to the new location
val file = userFileManager.getFile(fileName, userId)
val newContents = String(Files.readAllBytes(file.toPath()))
assertThat(newContents).isEqualTo(fileContents)
assertThat(legacyFile.exists()).isFalse()
assertThat(File(context.filesDir, UserFileManagerImpl.ROOT_DIR).exists()).isFalse()
}
@Test
fun testMigrateSharedPrefs() {
val userId = 13
val fileName = "myFile"
val contents = "TestingSharedPrefs"
val legacyFile =
UserFileManagerImpl.createLegacyFile(
context,
UserFileManagerImpl.SHARED_PREFS,
"$fileName.xml",
userId
)!!
// Write a valid shared prefs xml file to legacy area
val tmpPrefs = context.getSharedPreferences("tmp", Context.MODE_PRIVATE)
tmpPrefs.edit().putString(contents, contents).commit()
Files.createDirectories(legacyFile.getParentFile().toPath())
val tmpFile =
Environment.buildPath(context.dataDir, UserFileManagerImpl.SHARED_PREFS, "tmp.xml")
tmpFile.renameTo(legacyFile)
assertThat(legacyFile.exists()).isTrue()
// getSharedpreferences() should migrate the legacy file to the new location
val prefs = userFileManager.getSharedPreferences(fileName, Context.MODE_PRIVATE, userId)
assertThat(prefs.getString(contents, "")).isEqualTo(contents)
assertThat(legacyFile.exists()).isFalse()
assertThat(File(context.filesDir, UserFileManagerImpl.ROOT_DIR).exists()).isFalse()
}
@Test
@@ -111,44 +160,14 @@ class UserFileManagerImplTest : SysuiTestCase() {
@Test
fun testClearDeletedUserData() {
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 = userFileManager.getFile(TEST_FILE_NAME, 11)
file.createNewFile()
assertThat(secondaryUserDir.exists()).isTrue()
assertThat(file.exists()).isTrue()
userFileManager.clearDeletedUserData()
assertThat(backgroundExecutor.runAllReady()).isGreaterThan(0)
verify(userManager).aliveUsers
assertThat(secondaryUserDir.exists()).isFalse()
verify(userManager, atLeastOnce()).aliveUsers
assertThat(file.exists()).isFalse()
}
@Test
fun testEnsureParentDirExists() {
val file =
Environment.buildPath(
context.filesDir,
UserFileManagerImpl.ID,
"11",
"files",
TEST_FILE_NAME
)
assertThat(file.parentFile.exists()).isFalse()
UserFileManagerImpl.ensureParentDirExists(file)
assertThat(file.parentFile.exists()).isTrue()
}
}