Loading packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt +24 −16 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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( Loading @@ -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. * Loading Loading @@ -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) ) } } Loading packages/SystemUI/src/com/android/systemui/controls/controller/AuxiliaryPersistenceWrapper.kt +29 −22 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -33,9 +35,9 @@ 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, Loading @@ -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 Loading @@ -73,7 +73,8 @@ class AuxiliaryPersistenceWrapper @VisibleForTesting internal constructor( * exist, it will be initialized to an empty list. */ fun initialize() { favorites = if (persistenceWrapper.fileExists) { favorites = if (persistenceWrapper.fileExists) { persistenceWrapper.readFavorites() } else { emptyList() Loading @@ -83,8 +84,8 @@ class AuxiliaryPersistenceWrapper @VisibleForTesting internal constructor( /** * 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 */ Loading @@ -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() } } Loading @@ -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 } Loading packages/SystemUI/src/com/android/systemui/keyguard/domain/backup/KeyguardQuickAffordanceBackupHelper.kt +5 −12 Original line number Diff line number Diff line Loading @@ -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, UserFileManagerImpl.createFile( userId = userId, fileName = KeyguardQuickAffordanceSelectionManager.FILE_NAME, ) .also { UserFileManagerImpl.ensureParentDirExists(it) } .toString() } .getPath() ) packages/SystemUI/src/com/android/systemui/settings/UserFileManagerImpl.kt +98 −87 Original line number Diff line number Diff line Loading @@ -32,73 +32,61 @@ 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 { 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, ID, ROOT_DIR, userId.toString(), directoryName, fileName, dir, 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 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 } } Loading @@ -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 dirToDelete = Environment.buildPath( file, dir, 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() } ) dirToDelete.deleteRecursively() // 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 { file.delete() } catch (e: Exception) { Log.e(ID, "Deletion failed.", e) Log.e(TAG, "Deletion failed.", e) } } } Loading packages/SystemUI/tests/src/com/android/systemui/controls/controller/DeletionJobServiceTest.kt +18 −9 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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) Loading @@ -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) } } Loading
packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt +24 −16 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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( Loading @@ -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. * Loading Loading @@ -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) ) } } Loading
packages/SystemUI/src/com/android/systemui/controls/controller/AuxiliaryPersistenceWrapper.kt +29 −22 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -33,9 +35,9 @@ 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, Loading @@ -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 Loading @@ -73,7 +73,8 @@ class AuxiliaryPersistenceWrapper @VisibleForTesting internal constructor( * exist, it will be initialized to an empty list. */ fun initialize() { favorites = if (persistenceWrapper.fileExists) { favorites = if (persistenceWrapper.fileExists) { persistenceWrapper.readFavorites() } else { emptyList() Loading @@ -83,8 +84,8 @@ class AuxiliaryPersistenceWrapper @VisibleForTesting internal constructor( /** * 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 */ Loading @@ -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() } } Loading @@ -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 } Loading
packages/SystemUI/src/com/android/systemui/keyguard/domain/backup/KeyguardQuickAffordanceBackupHelper.kt +5 −12 Original line number Diff line number Diff line Loading @@ -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, UserFileManagerImpl.createFile( userId = userId, fileName = KeyguardQuickAffordanceSelectionManager.FILE_NAME, ) .also { UserFileManagerImpl.ensureParentDirExists(it) } .toString() } .getPath() )
packages/SystemUI/src/com/android/systemui/settings/UserFileManagerImpl.kt +98 −87 Original line number Diff line number Diff line Loading @@ -32,73 +32,61 @@ 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 { 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, ID, ROOT_DIR, userId.toString(), directoryName, fileName, dir, 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 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 } } Loading @@ -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 dirToDelete = Environment.buildPath( file, dir, 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() } ) dirToDelete.deleteRecursively() // 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 { file.delete() } catch (e: Exception) { Log.e(ID, "Deletion failed.", e) Log.e(TAG, "Deletion failed.", e) } } } Loading
packages/SystemUI/tests/src/com/android/systemui/controls/controller/DeletionJobServiceTest.kt +18 −9 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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) Loading @@ -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) } }