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

Commit ecf7ff29 authored by Fabian Kozynski's avatar Fabian Kozynski Committed by Automerger Merge Worker
Browse files

Merge "Added Backup & Restore for SystemUI and controls" into rvc-dev am: 30c2f30f am: 8fdca993

Change-Id: Idd8c3baed174c6c9f415968566a3f37ac59157fd
parents 02e1c314 8fdca993
Loading
Loading
Loading
Loading
+6 −2
Original line number Diff line number Diff line
@@ -263,7 +263,8 @@
        android:name=".SystemUIApplication"
        android:persistent="true"
        android:allowClearUserData="false"
        android:allowBackup="false"
        android:backupAgent=".backup.BackupHelper"
        android:killAfterRestore="false"
        android:hardwareAccelerated="true"
        android:label="@string/app_label"
        android:icon="@drawable/icon"
@@ -277,7 +278,7 @@
        <!-- Keep theme in sync with SystemUIApplication.onCreate().
             Setting the theme on the application does not affect views inflated by services.
             The application theme is set again from onCreate to take effect for those views. -->

        <meta-data android:name="com.google.android.backup.api_key" android:value="AEdPqrEAAAAIWTZsUG100coeb3xbEoTWKd3ZL3R79JshRDZfYQ" />
        <!-- Broadcast receiver that gets the broadcast at boot time and starts
             up everything else.
             TODO: Should have an android:permission attribute
@@ -690,6 +691,9 @@
            </intent-filter>
        </receiver>

        <service android:name=".controls.controller.AuxiliaryPersistenceWrapper$DeletionJobService"
                 android:permission="android.permission.BIND_JOB_SERVICE"/>

        <!-- started from ControlsFavoritingActivity -->
        <activity
            android:name=".controls.management.ControlsRequestDialog"
+125 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.backup

import android.app.backup.BackupAgentHelper
import android.app.backup.BackupDataInputStream
import android.app.backup.BackupDataOutput
import android.app.backup.FileBackupHelper
import android.app.job.JobScheduler
import android.content.Context
import android.content.Intent
import android.os.Environment
import android.os.ParcelFileDescriptor
import android.os.UserHandle
import android.util.Log
import com.android.systemui.controls.controller.AuxiliaryPersistenceWrapper
import com.android.systemui.controls.controller.ControlsFavoritePersistenceWrapper

/**
 * Helper for backing up elements in SystemUI
 *
 * This helper is invoked by BackupManager whenever a backup or restore is required in SystemUI.
 * The helper can be used to back up any element that is stored in [Context.getFilesDir].
 *
 * After restoring is done, a [ACTION_RESTORE_FINISHED] intent will be send to SystemUI user 0,
 * indicating that restoring is finished for a given user.
 */
class BackupHelper : BackupAgentHelper() {

    companion object {
        private const val TAG = "BackupHelper"
        internal const val CONTROLS = ControlsFavoritePersistenceWrapper.FILE_NAME
        private const val NO_OVERWRITE_FILES_BACKUP_KEY = "systemui.files_no_overwrite"
        val controlsDataLock = Any()
        const val ACTION_RESTORE_FINISHED = "com.android.systemui.backup.RESTORE_FINISHED"
        private const val PERMISSION_SELF = "com.android.systemui.permission.SELF"
    }

    override fun 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)
        }
    }

    override fun onRestoreFinished() {
        super.onRestoreFinished()
        val intent = Intent(ACTION_RESTORE_FINISHED).apply {
            `package` = packageName
            putExtra(Intent.EXTRA_USER_ID, userId)
            flags = Intent.FLAG_RECEIVER_REGISTERED_ONLY
        }
        sendBroadcastAsUser(intent, UserHandle.SYSTEM, PERMISSION_SELF)
    }

    /**
     * Helper class for restoring files ONLY if they are not present.
     *
     * A [Map] between filenames and actions (functions) is passed to indicate post processing
     * actions to be taken after each file is restored.
     *
     * @property lock a lock to hold while backing up and restoring the files.
     * @property context the context of the [BackupAgent]
     * @property fileNamesAndPostProcess a map from the filenames to back up and the post processing
     *                                   actions to take
     */
    private class NoOverwriteFileBackupHelper(
        val lock: Any,
        val context: Context,
        val fileNamesAndPostProcess: Map<String, () -> Unit>
    ) : FileBackupHelper(context, *fileNamesAndPostProcess.keys.toTypedArray()) {

        override fun restoreEntity(data: BackupDataInputStream) {
            val file = Environment.buildPath(context.filesDir, data.key)
            if (file.exists()) {
                Log.w(TAG, "File " + data.key + " already exists. Skipping restore.")
                return
            }
            synchronized(lock) {
                super.restoreEntity(data)
                fileNamesAndPostProcess.get(data.key)?.invoke()
            }
        }

        override fun performBackup(
            oldState: ParcelFileDescriptor?,
            data: BackupDataOutput?,
            newState: ParcelFileDescriptor?
        ) {
            synchronized(lock) {
                super.performBackup(oldState, data, newState)
            }
        }
    }
}
private fun getPPControlsFile(context: Context): () -> Unit {
    return {
        val filesDir = context.filesDir
        val file = Environment.buildPath(filesDir, BackupHelper.CONTROLS)
        if (file.exists()) {
            val dest = Environment.buildPath(filesDir,
                AuxiliaryPersistenceWrapper.AUXILIARY_FILE_NAME)
            file.copyTo(dest)
            val jobScheduler = context.getSystemService(JobScheduler::class.java)
            jobScheduler?.schedule(
                AuxiliaryPersistenceWrapper.DeletionJobService.getJobForContext(context))
        }
    }
}
 No newline at end of file
+140 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.controls.controller

import android.app.job.JobInfo
import android.app.job.JobParameters
import android.app.job.JobService
import android.content.ComponentName
import android.content.Context
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.backup.BackupHelper
import java.io.File
import java.util.concurrent.Executor
import java.util.concurrent.TimeUnit

/**
 * Class to track the auxiliary persistence of controls.
 *
 * 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
) {

    constructor(
        file: File,
        executor: Executor
    ): this(ControlsFavoritePersistenceWrapper(file, executor))

    companion object {
        const val AUXILIARY_FILE_NAME = "aux_controls_favorites.xml"
    }

    private var persistenceWrapper: ControlsFavoritePersistenceWrapper = wrapper

    /**
     * Access the current list of favorites as tracked by the auxiliary file
     */
    var favorites: List<StructureInfo> = emptyList()
        private set

    init {
        initialize()
    }

    /**
     * Change the file that this class is tracking.
     *
     * This will reset [favorites].
     */
    fun changeFile(file: File) {
        persistenceWrapper.changeFileAndBackupManager(file, null)
        initialize()
    }

    /**
     * Initialize the list of favorites to the content of the auxiliary file. If the file does not
     * exist, it will be initialized to an empty list.
     */
    fun initialize() {
        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.
     * @param componentName the name of the service that provided the controls
     * @return a list of structures with favorites
     */
    fun getCachedFavoritesAndRemoveFor(componentName: ComponentName): List<StructureInfo> {
        if (!persistenceWrapper.fileExists) {
            return emptyList()
        }
        val (comp, noComp) = favorites.partition { it.componentName == componentName }
        return comp.also {
            favorites = noComp
            if (favorites.isNotEmpty()) {
                persistenceWrapper.storeFavorites(noComp)
            } else {
                persistenceWrapper.deleteFile()
            }
        }
    }

    /**
     * [JobService] to delete the auxiliary file after a week.
     */
    class DeletionJobService : JobService() {
        companion object {
            @VisibleForTesting
            internal val DELETE_FILE_JOB_ID = 1000
            private val WEEK_IN_MILLIS = TimeUnit.DAYS.toMillis(7)
            fun getJobForContext(context: Context): JobInfo {
                val jobId = DELETE_FILE_JOB_ID + context.userId
                val componentName = ComponentName(context, DeletionJobService::class.java)
                return JobInfo.Builder(jobId, componentName)
                    .setMinimumLatency(WEEK_IN_MILLIS)
                    .setPersisted(true)
                    .build()
            }
        }

        @VisibleForTesting
        fun attachContext(context: Context) {
            attachBaseContext(context)
        }

        override fun onStartJob(params: JobParameters): Boolean {
            synchronized(BackupHelper.controlsDataLock) {
                baseContext.deleteFile(AUXILIARY_FILE_NAME)
            }
            return false
        }

        override fun onStopJob(params: JobParameters?): Boolean {
            return true // reschedule and try again if the job was stopped without completing
        }
    }
}
 No newline at end of file
+69 −8
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.systemui.controls.controller

import android.app.ActivityManager
import android.app.PendingIntent
import android.app.backup.BackupManager
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.ContentResolver
@@ -35,6 +36,7 @@ import android.util.ArrayMap
import android.util.Log
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.Dumpable
import com.android.systemui.backup.BackupHelper
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.controls.ControlStatus
import com.android.systemui.controls.ControlsServiceInfo
@@ -69,6 +71,7 @@ class ControlsControllerImpl @Inject constructor (
        internal val URI = Settings.Secure.getUriFor(CONTROLS_AVAILABLE)
        private const val USER_CHANGE_RETRY_DELAY = 500L // ms
        private const val DEFAULT_ENABLED = 1
        private const val PERMISSION_SELF = "com.android.systemui.permission.SELF"
    }

    private var userChanging: Boolean = true
@@ -88,23 +91,35 @@ class ControlsControllerImpl @Inject constructor (
            contentResolver, CONTROLS_AVAILABLE, DEFAULT_ENABLED, currentUserId) != 0
        private set

    private val persistenceWrapper = optionalWrapper.orElseGet {
        ControlsFavoritePersistenceWrapper(
                Environment.buildPath(
    private var file = Environment.buildPath(
        context.filesDir,
        ControlsFavoritePersistenceWrapper.FILE_NAME
                ),
                executor
    )
    private var auxiliaryFile = Environment.buildPath(
        context.filesDir,
        AuxiliaryPersistenceWrapper.AUXILIARY_FILE_NAME
    )
    private val persistenceWrapper = optionalWrapper.orElseGet {
        ControlsFavoritePersistenceWrapper(
            file,
            executor,
            BackupManager(context)
        )
    }

    @VisibleForTesting
    internal var auxiliaryPersistenceWrapper = AuxiliaryPersistenceWrapper(auxiliaryFile, executor)

    private fun setValuesForUser(newUser: UserHandle) {
        Log.d(TAG, "Changing to user: $newUser")
        currentUser = newUser
        val userContext = context.createContextAsUser(currentUser, 0)
        val fileName = Environment.buildPath(
        file = Environment.buildPath(
                userContext.filesDir, ControlsFavoritePersistenceWrapper.FILE_NAME)
        persistenceWrapper.changeFile(fileName)
        auxiliaryFile = Environment.buildPath(
            userContext.filesDir, AuxiliaryPersistenceWrapper.AUXILIARY_FILE_NAME)
        persistenceWrapper.changeFileAndBackupManager(file, BackupManager(userContext))
        auxiliaryPersistenceWrapper.changeFile(auxiliaryFile)
        available = Settings.Secure.getIntForUser(contentResolver, CONTROLS_AVAILABLE,
                DEFAULT_ENABLED, newUser.identifier) != 0
        resetFavorites(available)
@@ -129,6 +144,21 @@ class ControlsControllerImpl @Inject constructor (
        }
    }

    @VisibleForTesting
    internal val restoreFinishedReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            val user = intent.getIntExtra(Intent.EXTRA_USER_ID, UserHandle.USER_NULL)
            if (user == currentUserId) {
                executor.execute {
                    auxiliaryPersistenceWrapper.initialize()
                    listingController.removeCallback(listingCallback)
                    persistenceWrapper.storeFavorites(auxiliaryPersistenceWrapper.favorites)
                    resetFavorites(available)
                }
            }
        }
    }

    @VisibleForTesting
    internal val settingObserver = object : ContentObserver(null) {
        override fun onChange(
@@ -170,7 +200,25 @@ class ControlsControllerImpl @Inject constructor (
                    bindingController.onComponentRemoved(it)
                }

                // Check if something has been removed, if so, store the new list
                if (auxiliaryPersistenceWrapper.favorites.isNotEmpty()) {
                    serviceInfoSet.subtract(favoriteComponentSet).forEach {
                        val toAdd = auxiliaryPersistenceWrapper.getCachedFavoritesAndRemoveFor(it)
                        if (toAdd.isNotEmpty()) {
                            changed = true
                            toAdd.forEach {
                                Favorites.replaceControls(it)
                            }
                        }
                    }
                    // Need to clear the ones that were restored immediately. This will delete
                    // them from the auxiliary file if they were not deleted. Should only do any
                    // work the first time after a restore.
                    serviceInfoSet.intersect(favoriteComponentSet).forEach {
                        auxiliaryPersistenceWrapper.getCachedFavoritesAndRemoveFor(it)
                    }
                }

                // Check if something has been added or removed, if so, store the new list
                if (changed) {
                    persistenceWrapper.storeFavorites(Favorites.getAllStructures())
                }
@@ -188,9 +236,22 @@ class ControlsControllerImpl @Inject constructor (
                executor,
                UserHandle.ALL
        )
        context.registerReceiver(
            restoreFinishedReceiver,
            IntentFilter(BackupHelper.ACTION_RESTORE_FINISHED),
            PERMISSION_SELF,
            null
        )
        contentResolver.registerContentObserver(URI, false, settingObserver, UserHandle.USER_ALL)
    }

    fun destroy() {
        broadcastDispatcher.unregisterReceiver(userSwitchReceiver)
        context.unregisterReceiver(restoreFinishedReceiver)
        contentResolver.unregisterContentObserver(settingObserver)
        listingController.removeCallback(listingCallback)
    }

    private fun resetFavorites(shouldLoad: Boolean) {
        Favorites.clear()

+62 −43
Original line number Diff line number Diff line
@@ -16,10 +16,12 @@

package com.android.systemui.controls.controller

import android.app.backup.BackupManager
import android.content.ComponentName
import android.util.AtomicFile
import android.util.Log
import android.util.Xml
import com.android.systemui.backup.BackupHelper
import libcore.io.IoUtils
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException
@@ -38,7 +40,8 @@ import java.util.concurrent.Executor
 */
class ControlsFavoritePersistenceWrapper(
    private var file: File,
    private val executor: Executor
    private val executor: Executor,
    private var backupManager: BackupManager? = null
) {

    companion object {
@@ -60,12 +63,21 @@ class ControlsFavoritePersistenceWrapper(
    }

    /**
     * Change the file location for storing/reading the favorites
     * Change the file location for storing/reading the favorites and the [BackupManager]
     *
     * @param fileName new location
     * @param newBackupManager new [BackupManager]. Pass null to not trigger backups.
     */
    fun changeFile(fileName: File) {
    fun changeFileAndBackupManager(fileName: File, newBackupManager: BackupManager?) {
        file = fileName
        backupManager = newBackupManager
    }

    val fileExists: Boolean
        get() = file.exists()

    fun deleteFile() {
        file.delete()
    }

    /**
@@ -77,6 +89,7 @@ class ControlsFavoritePersistenceWrapper(
        executor.execute {
            Log.d(TAG, "Saving data to file: $file")
            val atomicFile = AtomicFile(file)
            val dataWritten = synchronized(BackupHelper.controlsDataLock) {
                val writer = try {
                    atomicFile.startWrite()
                } catch (e: IOException) {
@@ -114,13 +127,17 @@ class ControlsFavoritePersistenceWrapper(
                        endDocument()
                        atomicFile.finishWrite(writer)
                    }
                    true
                } catch (t: Throwable) {
                    Log.e(TAG, "Failed to write file, reverting to previous version")
                    atomicFile.failWrite(writer)
                    false
                } finally {
                    IoUtils.closeQuietly(writer)
                }
            }
            if (dataWritten) backupManager?.dataChanged()
        }
    }

    /**
@@ -142,9 +159,11 @@ class ControlsFavoritePersistenceWrapper(
        }
        try {
            Log.d(TAG, "Reading data from file: $file")
            synchronized(BackupHelper.controlsDataLock) {
                val parser = Xml.newPullParser()
                parser.setInput(reader, null)
                return parseXml(parser)
            }
        } catch (e: XmlPullParserException) {
            throw IllegalStateException("Failed parsing favorites file: $file", e)
        } catch (e: IOException) {
Loading