Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 7a8c125e authored by Matt Pietal's avatar Matt Pietal
Browse files

Backup and restore - Fix non-system users

Use a flat format, rather than a directory structure, to store files
for non-primary users. Shared preferences does not explicitly support
directories. The code was relying on a soon to be removed API, as well
as it being unsupported by the backup helper.

Files will remain stored under "shared_prefs" and "files", but the
format will be __USER_{id}_{fileName}

Test: atest UserFileManagerImplTest AuxiliaryPersistenceWrapperTest
DeletionJobServiceTest
Test: manual run Backup and Restore from Settings
Fixes: 261168533
Fixes: 266918967

Change-Id: Ib9804a2ec46fc2f5d53995c50adc36ff712fc1c1
parent 3fb52a63
Loading
Loading
Loading
Loading
+24 −16
Original line number Original line Diff line number Diff line
@@ -31,6 +31,7 @@ import com.android.systemui.controls.controller.AuxiliaryPersistenceWrapper
import com.android.systemui.controls.controller.ControlsFavoritePersistenceWrapper
import com.android.systemui.controls.controller.ControlsFavoritePersistenceWrapper
import com.android.systemui.keyguard.domain.backup.KeyguardQuickAffordanceBackupHelper
import com.android.systemui.keyguard.domain.backup.KeyguardQuickAffordanceBackupHelper
import com.android.systemui.people.widget.PeopleBackupHelper
import com.android.systemui.people.widget.PeopleBackupHelper
import com.android.systemui.settings.UserFileManagerImpl


/**
/**
 * Helper for backing up elements in SystemUI
 * Helper for backing up elements in SystemUI
@@ -58,17 +59,8 @@ open class BackupHelper : BackupAgentHelper() {


    override fun onCreate(userHandle: UserHandle, operationType: Int) {
    override fun onCreate(userHandle: UserHandle, operationType: Int) {
        super.onCreate()
        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
        addControlsHelper(userHandle.identifier)
        // stored in system user's SharedPreferences files and we can't open those from other users.
        if (!userHandle.isSystem) {
            return
        }


        val keys = PeopleBackupHelper.getFilesToBackup()
        val keys = PeopleBackupHelper.getFilesToBackup()
        addHelper(
        addHelper(
@@ -95,6 +87,18 @@ open class BackupHelper : BackupAgentHelper() {
        sendBroadcastAsUser(intent, UserHandle.SYSTEM, PERMISSION_SELF)
        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.
     * 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 {
    return {
        val filesDir = context.filesDir
        val file = UserFileManagerImpl.createFile(
        val file = Environment.buildPath(filesDir, BackupHelper.CONTROLS)
            userId = userId,
            fileName = BackupHelper.CONTROLS,
        )
        if (file.exists()) {
        if (file.exists()) {
            val dest =
            val dest = UserFileManagerImpl.createFile(
                Environment.buildPath(filesDir, AuxiliaryPersistenceWrapper.AUXILIARY_FILE_NAME)
                userId = userId,
                fileName = AuxiliaryPersistenceWrapper.AUXILIARY_FILE_NAME,
            )
            file.copyTo(dest)
            file.copyTo(dest)
            val jobScheduler = context.getSystemService(JobScheduler::class.java)
            val jobScheduler = context.getSystemService(JobScheduler::class.java)
            jobScheduler?.schedule(
            jobScheduler?.schedule(
                AuxiliaryPersistenceWrapper.DeletionJobService.getJobForContext(context)
                AuxiliaryPersistenceWrapper.DeletionJobService.getJobForContext(context, userId)
            )
            )
        }
        }
    }
    }
+29 −22
Original line number Original line Diff line number Diff line
@@ -21,8 +21,10 @@ import android.app.job.JobParameters
import android.app.job.JobService
import android.app.job.JobService
import android.content.ComponentName
import android.content.ComponentName
import android.content.Context
import android.content.Context
import android.os.PersistableBundle
import com.android.internal.annotations.VisibleForTesting
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.backup.BackupHelper
import com.android.systemui.backup.BackupHelper
import com.android.systemui.settings.UserFileManagerImpl
import java.io.File
import java.io.File
import java.util.concurrent.Executor
import java.util.concurrent.Executor
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeUnit
@@ -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
 * 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.
 * keep track of controls that were restored but its corresponding app has not been installed yet.
 */
 */
class AuxiliaryPersistenceWrapper @VisibleForTesting internal constructor(
class AuxiliaryPersistenceWrapper
    wrapper: ControlsFavoritePersistenceWrapper
@VisibleForTesting
) {
internal constructor(wrapper: ControlsFavoritePersistenceWrapper) {


    constructor(
    constructor(
        file: File,
        file: File,
@@ -48,9 +50,7 @@ class AuxiliaryPersistenceWrapper @VisibleForTesting internal constructor(


    private var persistenceWrapper: ControlsFavoritePersistenceWrapper = wrapper
    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()
    var favorites: List<StructureInfo> = emptyList()
        private set
        private set


@@ -73,7 +73,8 @@ class AuxiliaryPersistenceWrapper @VisibleForTesting internal constructor(
     * exist, it will be initialized to an empty list.
     * exist, it will be initialized to an empty list.
     */
     */
    fun initialize() {
    fun initialize() {
        favorites = if (persistenceWrapper.fileExists) {
        favorites =
            if (persistenceWrapper.fileExists) {
                persistenceWrapper.readFavorites()
                persistenceWrapper.readFavorites()
            } else {
            } else {
                emptyList()
                emptyList()
@@ -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.
     * 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
     * When the favorites for that application are returned, they will be removed from the auxiliary
     * auxiliary file immediately, so they won't be retrieved again.
     * file immediately, so they won't be retrieved again.
     * @param componentName the name of the service that provided the controls
     * @param componentName the name of the service that provided the controls
     * @return a list of structures with favorites
     * @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() {
    class DeletionJobService : JobService() {
        companion object {
        companion object {
            @VisibleForTesting
            @VisibleForTesting internal val DELETE_FILE_JOB_ID = 1000
            internal val DELETE_FILE_JOB_ID = 1000
            @VisibleForTesting internal val USER = "USER"
            private val WEEK_IN_MILLIS = TimeUnit.DAYS.toMillis(7)
            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 jobId = DELETE_FILE_JOB_ID + context.userId
                val componentName = ComponentName(context, DeletionJobService::class.java)
                val componentName = ComponentName(context, DeletionJobService::class.java)
                val bundle = PersistableBundle().also { it.putInt(USER, targetUserId) }
                return JobInfo.Builder(jobId, componentName)
                return JobInfo.Builder(jobId, componentName)
                    .setMinimumLatency(WEEK_IN_MILLIS)
                    .setMinimumLatency(WEEK_IN_MILLIS)
                    .setPersisted(true)
                    .setPersisted(true)
                    .setExtras(bundle)
                    .build()
                    .build()
            }
            }
        }
        }
@@ -127,8 +128,14 @@ class AuxiliaryPersistenceWrapper @VisibleForTesting internal constructor(
        }
        }


        override fun onStartJob(params: JobParameters): Boolean {
        override fun onStartJob(params: JobParameters): Boolean {
            val userId = params.getExtras()?.getInt(USER, 0) ?: 0
            synchronized(BackupHelper.controlsDataLock) {
            synchronized(BackupHelper.controlsDataLock) {
                baseContext.deleteFile(AUXILIARY_FILE_NAME)
                val file =
                    UserFileManagerImpl.createFile(
                        userId = userId,
                        fileName = AUXILIARY_FILE_NAME,
                    )
                baseContext.deleteFile(file.getPath())
            }
            }
            return false
            return false
        }
        }
+5 −12
Original line number Original line Diff line number Diff line
@@ -29,16 +29,9 @@ class KeyguardQuickAffordanceBackupHelper(
) :
) :
    SharedPreferencesBackupHelper(
    SharedPreferencesBackupHelper(
        context,
        context,
        if (UserFileManagerImpl.isPrimaryUser(userId)) {
        UserFileManagerImpl.createFile(
            KeyguardQuickAffordanceSelectionManager.FILE_NAME
        } else {
            UserFileManagerImpl.secondaryUserFile(
                    context = context,
                    fileName = KeyguardQuickAffordanceSelectionManager.FILE_NAME,
                    directoryName = UserFileManagerImpl.SHARED_PREFS,
                userId = userId,
                userId = userId,
                fileName = KeyguardQuickAffordanceSelectionManager.FILE_NAME,
            )
            )
                .also { UserFileManagerImpl.ensureParentDirExists(it) }
            .getPath()
                .toString()
        }
    )
    )
+98 −87
Original line number Original line Diff line number Diff line
@@ -32,73 +32,61 @@ import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.util.concurrency.DelayableExecutor
import com.android.systemui.util.concurrency.DelayableExecutor
import java.io.File
import java.io.File
import java.io.FilenameFilter
import javax.inject.Inject
import javax.inject.Inject


/**
/**
 * Implementation for retrieving file paths for file storage of system and secondary users. Files
 * Implementation for retrieving file paths for file storage of system and secondary users. For
 * lie in {File Directory}/UserFileManager/{User Id} for secondary user. For system user, we use the
 * non-system users, files will be prepended by a special prefix containing the user id.
 * conventional {File Directory}
 */
 */
@SysUISingleton
@SysUISingleton
class UserFileManagerImpl
class UserFileManagerImpl
@Inject
@Inject
constructor(
constructor(
    // Context of system process and system user.
    private val context: Context,
    private val context: Context,
    val userManager: UserManager,
    val userManager: UserManager,
    val broadcastDispatcher: BroadcastDispatcher,
    val broadcastDispatcher: BroadcastDispatcher,
    @Background val backgroundExecutor: DelayableExecutor
    @Background val backgroundExecutor: DelayableExecutor
) : UserFileManager, CoreStartable {
) : UserFileManager, CoreStartable {
    companion object {
    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"
        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.
         * Returns a File object with a relative path, built from the userId for non-system users
         *
         * 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
         */
         */
        fun secondaryUserFile(
        fun createFile(fileName: String, userId: Int): File {
            context: Context,
            return if (isSystemUser(userId)) {
            fileName: String,
                File(fileName)
            directoryName: String,
            } else {
            userId: Int,
                File(getFilePrefix(userId) + fileName)
        ): File {
            }
        }

        fun createLegacyFile(context: Context, dir: String, fileName: String, userId: Int): File? {
            return if (isSystemUser(userId)) {
                null
            } else {
                return Environment.buildPath(
                return Environment.buildPath(
                    context.filesDir,
                    context.filesDir,
                ID,
                    ROOT_DIR,
                    userId.toString(),
                    userId.toString(),
                directoryName,
                    dir,
                fileName,
                    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
        }
        }
    }
    }


@@ -119,64 +107,87 @@ constructor(
        broadcastDispatcher.registerReceiver(broadcastReceiver, filter, backgroundExecutor)
        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 {
    override fun getFile(fileName: String, userId: Int): File {
        return if (isPrimaryUser(userId)) {
        val file = File(context.filesDir, createFile(fileName, userId).path)
            Environment.buildPath(context.filesDir, fileName)
        createLegacyFile(context, FILES, fileName, userId)?.run { migrate(file, this) }
        } else {
        return file
            val secondaryFile =
                secondaryUserFile(
                    context = context,
                    userId = userId,
                    directoryName = FILES,
                    fileName = fileName,
                )
            ensureParentDirExists(secondaryFile)
            secondaryFile
        }
    }
    }


    /** 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(
    override fun getSharedPreferences(
        fileName: String,
        fileName: String,
        @Context.PreferencesMode mode: Int,
        @Context.PreferencesMode mode: Int,
        userId: Int
        userId: Int
    ): SharedPreferences {
    ): SharedPreferences {
        if (isPrimaryUser(userId)) {
        val file = createFile(fileName, userId)
            return context.getSharedPreferences(fileName, mode)
        createLegacyFile(context, SHARED_PREFS, "$fileName.xml", userId)?.run {
            val path = Environment.buildPath(context.dataDir, SHARED_PREFS, "${file.path}.xml")
            migrate(path, this)
        }
        }

        return context.getSharedPreferences(file.path, mode)
        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 files for deleted users. */
    @VisibleForTesting
    @VisibleForTesting
    internal fun clearDeletedUserData() {
    internal fun clearDeletedUserData() {
        backgroundExecutor.execute {
        backgroundExecutor.execute {
            val file = Environment.buildPath(context.filesDir, ID)
            deleteFiles(context.filesDir)
            if (!file.exists()) return@execute
            deleteFiles(File(context.dataDir, SHARED_PREFS))
            val aliveUsers = userManager.aliveUsers.map { it.id.toString() }
        }
            val dirsToDelete = file.list().filter { !aliveUsers.contains(it) }
    }


            dirsToDelete.forEach { dir ->
    private fun migrate(dest: File, source: File) {
        if (source.exists()) {
            try {
            try {
                    val dirToDelete =
                val parent = source.getParentFile()
                        Environment.buildPath(
                source.renameTo(dest)
                            file,

                            dir,
                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) {
                } catch (e: Exception) {
                    Log.e(ID, "Deletion failed.", e)
                    Log.e(TAG, "Deletion failed.", e)
                }
                }
            }
            }
        }
        }
+18 −9
Original line number Original line Diff line number Diff line
@@ -18,9 +18,13 @@ package com.android.systemui.controls.controller


import android.app.job.JobParameters
import android.app.job.JobParameters
import android.content.Context
import android.content.Context
import android.os.PersistableBundle
import android.testing.AndroidTestingRunner
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
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.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Assert.assertTrue
@@ -28,18 +32,15 @@ import org.junit.Before
import org.junit.Test
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mock
import org.mockito.Mockito.`when`
import org.mockito.Mockito.mock
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
import org.mockito.MockitoAnnotations
import java.util.concurrent.TimeUnit


@SmallTest
@SmallTest
@RunWith(AndroidTestingRunner::class)
@RunWith(AndroidTestingRunner::class)
class DeletionJobServiceTest : SysuiTestCase() {
class DeletionJobServiceTest : SysuiTestCase() {


    @Mock
    @Mock private lateinit var context: Context
    private lateinit var context: Context


    private lateinit var service: AuxiliaryPersistenceWrapper.DeletionJobService
    private lateinit var service: AuxiliaryPersistenceWrapper.DeletionJobService


@@ -53,6 +54,10 @@ class DeletionJobServiceTest : SysuiTestCase() {


    @Test
    @Test
    fun testOnStartJob() {
    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
        // false means job is terminated
        assertFalse(service.onStartJob(mock(JobParameters::class.java)))
        assertFalse(service.onStartJob(mock(JobParameters::class.java)))
        verify(context).deleteFile(AuxiliaryPersistenceWrapper.AUXILIARY_FILE_NAME)
        verify(context).deleteFile(AuxiliaryPersistenceWrapper.AUXILIARY_FILE_NAME)
@@ -67,13 +72,17 @@ class DeletionJobServiceTest : SysuiTestCase() {
    @Test
    @Test
    fun testJobHasRightParameters() {
    fun testJobHasRightParameters() {
        val userId = 10
        val userId = 10
        `when`(context.userId).thenReturn(userId)
        whenever(context.userId).thenReturn(userId)
        `when`(context.packageName).thenReturn(mContext.packageName)
        whenever(context.packageName).thenReturn(mContext.packageName)


        val jobInfo = AuxiliaryPersistenceWrapper.DeletionJobService.getJobForContext(context)
        val jobInfo =
            AuxiliaryPersistenceWrapper.DeletionJobService.getJobForContext(context, userId)
        assertEquals(
        assertEquals(
            AuxiliaryPersistenceWrapper.DeletionJobService.DELETE_FILE_JOB_ID + userId, jobInfo.id)
            AuxiliaryPersistenceWrapper.DeletionJobService.DELETE_FILE_JOB_ID + userId,
            jobInfo.id
        )
        assertTrue(jobInfo.isPersisted)
        assertTrue(jobInfo.isPersisted)
        assertEquals(userId, jobInfo.getExtras().getInt(USER))
        assertEquals(TimeUnit.DAYS.toMillis(7), jobInfo.minLatencyMillis)
        assertEquals(TimeUnit.DAYS.toMillis(7), jobInfo.minLatencyMillis)
    }
    }
}
}
Loading