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

Commit 94f82c67 authored by Alejandro Nijamkin's avatar Alejandro Nijamkin
Browse files

Backup & Restore support for quick affordances.

Adds support for backup & restore of the currently-selected quick
affordances on the lock screen.

Only supports user 0 for now. A future CL will add support for additional users since the feature is broken for multi-user anyway (see b/260251307).

Fix: 258278136
Test: added test case. Manually verified, using the instructions from
go/localtransport that selecting some affordances, then triggering a
backup, then changing the selections and triggering a restore properly
restores the initial set of selections.

Change-Id: I9f2a51287a7467af95e7824f2b1804baba962099
parent 96f5e432
Loading
Loading
Loading
Loading
+33 −17
Original line number Diff line number Diff line
@@ -29,13 +29,15 @@ import android.os.UserHandle
import android.util.Log
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

/**
 * 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].
 * 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] or
 * [Context.getSharedPreferences].
 *
 * 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.
@@ -47,9 +49,11 @@ open class BackupHelper : BackupAgentHelper() {
        internal const val CONTROLS = ControlsFavoritePersistenceWrapper.FILE_NAME
        private const val NO_OVERWRITE_FILES_BACKUP_KEY = "systemui.files_no_overwrite"
        private const val PEOPLE_TILES_BACKUP_KEY = "systemui.people.shared_preferences"
        private const val KEYGUARD_QUICK_AFFORDANCES_BACKUP_KEY =
            "systemui.keyguard.quickaffordance.shared_preferences"
        val controlsDataLock = Any()
        const val ACTION_RESTORE_FINISHED = "com.android.systemui.backup.RESTORE_FINISHED"
        private const val PERMISSION_SELF = "com.android.systemui.permission.SELF"
        const val PERMISSION_SELF = "com.android.systemui.permission.SELF"
    }

    override fun onCreate(userHandle: UserHandle, operationType: Int) {
@@ -67,13 +71,23 @@ open class BackupHelper : BackupAgentHelper() {
        }

        val keys = PeopleBackupHelper.getFilesToBackup()
        addHelper(PEOPLE_TILES_BACKUP_KEY, PeopleBackupHelper(
                this, userHandle, keys.toTypedArray()))
        addHelper(
            PEOPLE_TILES_BACKUP_KEY,
            PeopleBackupHelper(this, userHandle, keys.toTypedArray())
        )
        addHelper(
            KEYGUARD_QUICK_AFFORDANCES_BACKUP_KEY,
            KeyguardQuickAffordanceBackupHelper(
                context = this,
                userId = userHandle.identifier,
            ),
        )
    }

    override fun onRestoreFinished() {
        super.onRestoreFinished()
        val intent = Intent(ACTION_RESTORE_FINISHED).apply {
        val intent =
            Intent(ACTION_RESTORE_FINISHED).apply {
                `package` = packageName
                putExtra(Intent.EXTRA_USER_ID, userId)
                flags = Intent.FLAG_RECEIVER_REGISTERED_ONLY
@@ -90,7 +104,9 @@ open class BackupHelper : BackupAgentHelper() {
     * @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,
@@ -115,23 +131,23 @@ open class BackupHelper : BackupAgentHelper() {
            data: BackupDataOutput?,
            newState: ParcelFileDescriptor?
        ) {
            synchronized(lock) {
                super.performBackup(oldState, data, newState)
            }
            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)
            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))
                AuxiliaryPersistenceWrapper.DeletionJobService.getJobForContext(context)
            )
        }
    }
}
+3 −2
Original line number Diff line number Diff line
@@ -64,8 +64,9 @@ private const val TAG = "BroadcastDispatcher"
 * from SystemUI. That way the number of calls to [BroadcastReceiver.onReceive] can be reduced for
 * a given broadcast.
 *
 * Use only for IntentFilters with actions and optionally categories. It does not support,
 * permissions, schemes, data types, data authorities or priority different than 0.
 * Use only for IntentFilters with actions and optionally categories. It does not support schemes,
 * data types, data authorities or priority different than 0.
 *
 * Cannot be used for getting sticky broadcasts (either as return of registering or as re-delivery).
 * Broadcast handling may be asynchronous *without* calling goAsync(), as it's running within sysui
 * and doesn't need to worry about being killed.
+61 −19
Original line number Diff line number Diff line
@@ -18,9 +18,11 @@
package com.android.systemui.keyguard.data.quickaffordance

import android.content.Context
import android.content.IntentFilter
import android.content.SharedPreferences
import androidx.annotation.VisibleForTesting
import com.android.systemui.R
import com.android.systemui.backup.BackupHelper
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
@@ -28,14 +30,18 @@ import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.settings.UserFileManager
import com.android.systemui.settings.UserTracker
import javax.inject.Inject
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.onStart

/**
 * Manages and provides access to the current "selections" of keyguard quick affordances, answering
 * the question "which affordances should the keyguard show?".
 */
@OptIn(ExperimentalCoroutinesApi::class)
@SysUISingleton
class KeyguardQuickAffordanceSelectionManager
@Inject
@@ -43,15 +49,10 @@ constructor(
    @Application context: Context,
    private val userFileManager: UserFileManager,
    private val userTracker: UserTracker,
    broadcastDispatcher: BroadcastDispatcher,
) {

    private val sharedPrefs: SharedPreferences
        get() =
            userFileManager.getSharedPreferences(
                FILE_NAME,
                Context.MODE_PRIVATE,
                userTracker.userId,
            )
    private var sharedPrefs: SharedPreferences = instantiateSharedPrefs()

    private val userId: Flow<Int> = conflatedCallbackFlow {
        val callback =
@@ -78,10 +79,43 @@ constructor(
            }
    }

    /**
     * Emits an event each time a Backup & Restore restoration job is completed. Does not emit an
     * initial value.
     */
    private val backupRestorationEvents: Flow<Unit> =
        broadcastDispatcher.broadcastFlow(
            filter = IntentFilter(BackupHelper.ACTION_RESTORE_FINISHED),
            flags = Context.RECEIVER_NOT_EXPORTED,
            permission = BackupHelper.PERMISSION_SELF,
        )

    /** IDs of affordances to show, indexed by slot ID, and sorted in descending priority order. */
    val selections: Flow<Map<String, List<String>>> =
        userId.flatMapLatest {
        combine(
                userId,
                backupRestorationEvents.onStart {
                    // We emit an initial event to make sure that the combine emits at least once,
                    // even
                    // if we never get a Backup & Restore restoration event (which is the most
                    // common
                    // case anyway as restoration really only happens on initial device setup).
                    emit(Unit)
                }
            ) { _, _ ->
            }
            .flatMapLatest {
                conflatedCallbackFlow {
                    // We want to instantiate a new SharedPreferences instance each time either the
                    // user
                    // ID changes or we have a backup & restore restoration event. The reason is
                    // that
                    // our sharedPrefs instance needs to be replaced with a new one as it depends on
                    // the
                    // user ID and when the B&R job completes, the backing file is replaced but the
                    // existing instance still has a stale in-memory cache.
                    sharedPrefs = instantiateSharedPrefs()

                    val listener =
                        SharedPreferences.OnSharedPreferenceChangeListener { _, _ ->
                            trySend(getSelections())
@@ -144,9 +178,17 @@ constructor(
        sharedPrefs.edit().putString(key, value).apply()
    }

    private fun instantiateSharedPrefs(): SharedPreferences {
        return userFileManager.getSharedPreferences(
            FILE_NAME,
            Context.MODE_PRIVATE,
            userTracker.userId,
        )
    }

    companion object {
        private const val TAG = "KeyguardQuickAffordanceSelectionManager"
        @VisibleForTesting const val FILE_NAME = "quick_affordance_selections"
        const val FILE_NAME = "quick_affordance_selections"
        private const val KEY_PREFIX_SLOT = "slot_"
        private const val SLOT_AFFORDANCES_DELIMITER = ":"
        private const val AFFORDANCE_DELIMITER = ","
+44 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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.keyguard.domain.backup

import android.app.backup.SharedPreferencesBackupHelper
import android.content.Context
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager
import com.android.systemui.settings.UserFileManagerImpl

/** Handles backup & restore for keyguard quick affordances. */
class KeyguardQuickAffordanceBackupHelper(
    context: Context,
    userId: Int,
) :
    SharedPreferencesBackupHelper(
        context,
        if (UserFileManagerImpl.isPrimaryUser(userId)) {
            KeyguardQuickAffordanceSelectionManager.FILE_NAME
        } else {
            UserFileManagerImpl.secondaryUserFile(
                    context = context,
                    fileName = KeyguardQuickAffordanceSelectionManager.FILE_NAME,
                    directoryName = UserFileManagerImpl.SHARED_PREFS,
                    userId = userId,
                )
                .also { UserFileManagerImpl.ensureParentDirExists(it) }
                .toString()
        }
    )
+87 −64
Original line number Diff line number Diff line
@@ -35,12 +35,14 @@ import java.io.File
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. Files
 * lie in {File Directory}/UserFileManager/{User Id} for secondary user. For system user, we use the
 * conventional {File Directory}
 */
@SysUISingleton
class UserFileManagerImpl @Inject constructor(
class UserFileManagerImpl
@Inject
constructor(
    // Context of system process and system user.
    private val context: Context,
    val userManager: UserManager,
@@ -49,14 +51,60 @@ class UserFileManagerImpl @Inject constructor(
) : UserFileManager, CoreStartable {
    companion object {
        private const val FILES = "files"
        @VisibleForTesting internal 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
        }

   private val broadcastReceiver = object : BroadcastReceiver() {
        /**
         * Listen to Intent.ACTION_USER_REMOVED to clear user data.
         * 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
         */
        fun secondaryUserFile(
            context: Context,
            fileName: String,
            directoryName: String,
            userId: Int,
        ): File {
            return Environment.buildPath(
                context.filesDir,
                ID,
                userId.toString(),
                directoryName,
                fileName,
            )
        }

        /**
         * Checks to see if parent dir of the file exists. If it does not, we create the parent dirs
         * recursively.
         */
        fun ensureParentDirExists(file: File) {
            val parent = file.parentFile
            if (!parent.exists()) {
                if (!parent.mkdirs()) {
                    Log.e(ID, "Could not create parent directory for file: ${file.absolutePath}")
                }
            }
        }
    }

    private val broadcastReceiver =
        object : BroadcastReceiver() {
            /** Listen to Intent.ACTION_USER_REMOVED to clear user data. */
            override fun onReceive(context: Context, intent: Intent) {
                if (intent.action == Intent.ACTION_USER_REMOVED) {
                    clearDeletedUserData()
@@ -64,65 +112,53 @@ class UserFileManagerImpl @Inject constructor(
            }
        }

    /**
     * Poll for user-specific directories to delete upon start up.
     */
    /** Poll for user-specific directories to delete upon start up. */
    override fun start() {
        clearDeletedUserData()
        val filter = IntentFilter().apply {
            addAction(Intent.ACTION_USER_REMOVED)
        }
        val filter = IntentFilter().apply { addAction(Intent.ACTION_USER_REMOVED) }
        broadcastDispatcher.registerReceiver(broadcastReceiver, filter, backgroundExecutor)
    }

    /**
     * Return the file based on current user.
     */
    /** Return the file based on current user. */
    override fun getFile(fileName: String, userId: Int): File {
        return if (UserHandle(userId).isSystem) {
            Environment.buildPath(
                context.filesDir,
                fileName
            )
        return if (isPrimaryUser(userId)) {
            Environment.buildPath(context.filesDir, fileName)
        } else {
            val secondaryFile = Environment.buildPath(
                context.filesDir,
                ID,
                userId.toString(),
                FILES,
                fileName
            val secondaryFile =
                secondaryUserFile(
                    context = context,
                    userId = userId,
                    directoryName = FILES,
                    fileName = fileName,
                )
            ensureParentDirExists(secondaryFile)
            secondaryFile
        }
    }

    /**
     * Get shared preferences from user.
     */
    /** Get shared preferences from user. */
    override fun getSharedPreferences(
        fileName: String,
        @Context.PreferencesMode mode: Int,
        userId: Int
    ): SharedPreferences {
        if (UserHandle(userId).isSystem) {
        if (isPrimaryUser(userId)) {
            return context.getSharedPreferences(fileName, mode)
        }
        val secondaryUserDir = Environment.buildPath(
            context.filesDir,
            ID,
            userId.toString(),
            SHARED_PREFS,
            fileName

        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 dirs for deleted users. */
    @VisibleForTesting
    internal fun clearDeletedUserData() {
        backgroundExecutor.execute {
@@ -133,7 +169,8 @@ class UserFileManagerImpl @Inject constructor(

            dirsToDelete.forEach { dir ->
                try {
                    val dirToDelete = Environment.buildPath(
                    val dirToDelete =
                        Environment.buildPath(
                            file,
                            dir,
                        )
@@ -144,18 +181,4 @@ class UserFileManagerImpl @Inject constructor(
            }
        }
    }

    /**
     * Checks to see if parent dir of the file exists. If it does not, we create the parent dirs
     * recursively.
     */
    @VisibleForTesting
    internal 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}")
            }
        }
    }
}
Loading