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

Commit cb5228cf authored by Matt Pietal's avatar Matt Pietal Committed by Android (Google) Code Review
Browse files

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

parents e2a2e37c 7a8c125e
Loading
Loading
Loading
Loading
+24 −16
Original line number 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.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)
            )
        }
    }
+29 −22
Original line number Diff line number Diff line
@@ -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,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,
@@ -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,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()
@@ -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
     */
@@ -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
        }
+5 −12
Original line number Diff line number Diff line
@@ -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()
    )
+98 −87
Original line number 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.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
        }
    }

@@ -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)
                }
            }
        }
+18 −9
Original line number Diff line number Diff line
@@ -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)
    }
}
Loading