Added Backup & Restore for SystemUI and controls
This CL adds support for Backup & Restore in SystemUI by adding Key-Value BR through the use of a BackupAgent (BackupHelper.kt). Right now, the only thing that is backed up in SystemUI is the controls_favorites.xml file. BackupAgent will send a broadcast to user 0 indicating that the restore has been completed for a particular user. Regarding controls BR: * The file is set as changed every time it is written and will participate in normal backups as determined by BackupManager. * On restore, a copy is made to serve as cache of controls restored but whose app is not installed yet. * This cache is persisted for a week and deleted with a JobService. Test: atest SystemUITests Test: manual using LocalTransport Fixes: 149210531 Change-Id: I6048f1ebd1d9021058778dff707c8c53c9989b35
This commit is contained in:
parent
dce5a57376
commit
66e975404c
@ -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"
|
||||
|
@ -0,0 +1,125 @@
|
||||
/*
|
||||
* 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))
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,140 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
}
|
||||
}
|
@ -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 var file = Environment.buildPath(
|
||||
context.filesDir,
|
||||
ControlsFavoritePersistenceWrapper.FILE_NAME
|
||||
)
|
||||
private var auxiliaryFile = Environment.buildPath(
|
||||
context.filesDir,
|
||||
AuxiliaryPersistenceWrapper.AUXILIARY_FILE_NAME
|
||||
)
|
||||
private val persistenceWrapper = optionalWrapper.orElseGet {
|
||||
ControlsFavoritePersistenceWrapper(
|
||||
Environment.buildPath(
|
||||
context.filesDir,
|
||||
ControlsFavoritePersistenceWrapper.FILE_NAME
|
||||
),
|
||||
executor
|
||||
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()
|
||||
|
||||
|
@ -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,49 +89,54 @@ class ControlsFavoritePersistenceWrapper(
|
||||
executor.execute {
|
||||
Log.d(TAG, "Saving data to file: $file")
|
||||
val atomicFile = AtomicFile(file)
|
||||
val writer = try {
|
||||
atomicFile.startWrite()
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Failed to start write file", e)
|
||||
return@execute
|
||||
}
|
||||
try {
|
||||
Xml.newSerializer().apply {
|
||||
setOutput(writer, "utf-8")
|
||||
setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true)
|
||||
startDocument(null, true)
|
||||
startTag(null, TAG_VERSION)
|
||||
text("$VERSION")
|
||||
endTag(null, TAG_VERSION)
|
||||
|
||||
startTag(null, TAG_STRUCTURES)
|
||||
structures.forEach { s ->
|
||||
startTag(null, TAG_STRUCTURE)
|
||||
attribute(null, TAG_COMPONENT, s.componentName.flattenToString())
|
||||
attribute(null, TAG_STRUCTURE, s.structure.toString())
|
||||
|
||||
startTag(null, TAG_CONTROLS)
|
||||
s.controls.forEach { c ->
|
||||
startTag(null, TAG_CONTROL)
|
||||
attribute(null, TAG_ID, c.controlId)
|
||||
attribute(null, TAG_TITLE, c.controlTitle.toString())
|
||||
attribute(null, TAG_SUBTITLE, c.controlSubtitle.toString())
|
||||
attribute(null, TAG_TYPE, c.deviceType.toString())
|
||||
endTag(null, TAG_CONTROL)
|
||||
}
|
||||
endTag(null, TAG_CONTROLS)
|
||||
endTag(null, TAG_STRUCTURE)
|
||||
}
|
||||
endTag(null, TAG_STRUCTURES)
|
||||
endDocument()
|
||||
atomicFile.finishWrite(writer)
|
||||
val dataWritten = synchronized(BackupHelper.controlsDataLock) {
|
||||
val writer = try {
|
||||
atomicFile.startWrite()
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Failed to start write file", e)
|
||||
return@execute
|
||||
}
|
||||
try {
|
||||
Xml.newSerializer().apply {
|
||||
setOutput(writer, "utf-8")
|
||||
setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true)
|
||||
startDocument(null, true)
|
||||
startTag(null, TAG_VERSION)
|
||||
text("$VERSION")
|
||||
endTag(null, TAG_VERSION)
|
||||
|
||||
startTag(null, TAG_STRUCTURES)
|
||||
structures.forEach { s ->
|
||||
startTag(null, TAG_STRUCTURE)
|
||||
attribute(null, TAG_COMPONENT, s.componentName.flattenToString())
|
||||
attribute(null, TAG_STRUCTURE, s.structure.toString())
|
||||
|
||||
startTag(null, TAG_CONTROLS)
|
||||
s.controls.forEach { c ->
|
||||
startTag(null, TAG_CONTROL)
|
||||
attribute(null, TAG_ID, c.controlId)
|
||||
attribute(null, TAG_TITLE, c.controlTitle.toString())
|
||||
attribute(null, TAG_SUBTITLE, c.controlSubtitle.toString())
|
||||
attribute(null, TAG_TYPE, c.deviceType.toString())
|
||||
endTag(null, TAG_CONTROL)
|
||||
}
|
||||
endTag(null, TAG_CONTROLS)
|
||||
endTag(null, TAG_STRUCTURE)
|
||||
}
|
||||
endTag(null, TAG_STRUCTURES)
|
||||
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)
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
Log.e(TAG, "Failed to write file, reverting to previous version")
|
||||
atomicFile.failWrite(writer)
|
||||
} finally {
|
||||
IoUtils.closeQuietly(writer)
|
||||
}
|
||||
if (dataWritten) backupManager?.dataChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@ -142,9 +159,11 @@ class ControlsFavoritePersistenceWrapper(
|
||||
}
|
||||
try {
|
||||
Log.d(TAG, "Reading data from file: $file")
|
||||
val parser = Xml.newPullParser()
|
||||
parser.setInput(reader, null)
|
||||
return parseXml(parser)
|
||||
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) {
|
||||
|
@ -0,0 +1,131 @@
|
||||
/*
|
||||
* 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.content.ComponentName
|
||||
import android.testing.AndroidTestingRunner
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.ArgumentMatchers
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito
|
||||
import org.mockito.Mockito.`when`
|
||||
import org.mockito.Mockito.inOrder
|
||||
import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.never
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.MockitoAnnotations
|
||||
import java.io.File
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidTestingRunner::class)
|
||||
class AuxiliaryPersistenceWrapperTest : SysuiTestCase() {
|
||||
|
||||
companion object {
|
||||
fun <T> any(): T = Mockito.any()
|
||||
private val TEST_COMPONENT = ComponentName.unflattenFromString("test_pkg/.test_cls")!!
|
||||
private val TEST_COMPONENT_OTHER =
|
||||
ComponentName.unflattenFromString("test_pkg/.test_other")!!
|
||||
}
|
||||
|
||||
@Mock
|
||||
private lateinit var persistenceWrapper: ControlsFavoritePersistenceWrapper
|
||||
@Mock
|
||||
private lateinit var structure1: StructureInfo
|
||||
@Mock
|
||||
private lateinit var structure2: StructureInfo
|
||||
@Mock
|
||||
private lateinit var structure3: StructureInfo
|
||||
|
||||
private lateinit var auxiliaryFileWrapper: AuxiliaryPersistenceWrapper
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
|
||||
`when`(structure1.componentName).thenReturn(TEST_COMPONENT)
|
||||
`when`(structure2.componentName).thenReturn(TEST_COMPONENT_OTHER)
|
||||
`when`(structure3.componentName).thenReturn(TEST_COMPONENT)
|
||||
|
||||
`when`(persistenceWrapper.fileExists).thenReturn(true)
|
||||
`when`(persistenceWrapper.readFavorites()).thenReturn(
|
||||
listOf(structure1, structure2, structure3))
|
||||
|
||||
auxiliaryFileWrapper = AuxiliaryPersistenceWrapper(persistenceWrapper)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInitialStructures() {
|
||||
val expected = listOf(structure1, structure2, structure3)
|
||||
assertEquals(expected, auxiliaryFileWrapper.favorites)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInitialize_fileDoesNotExist() {
|
||||
`when`(persistenceWrapper.fileExists).thenReturn(false)
|
||||
auxiliaryFileWrapper.initialize()
|
||||
assertTrue(auxiliaryFileWrapper.favorites.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetCachedValues_component() {
|
||||
val cached = auxiliaryFileWrapper.getCachedFavoritesAndRemoveFor(TEST_COMPONENT)
|
||||
val expected = listOf(structure1, structure3)
|
||||
|
||||
assertEquals(expected, cached)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetCachedValues_componentOther() {
|
||||
val cached = auxiliaryFileWrapper.getCachedFavoritesAndRemoveFor(TEST_COMPONENT_OTHER)
|
||||
val expected = listOf(structure2)
|
||||
|
||||
assertEquals(expected, cached)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetCachedValues_component_removed() {
|
||||
auxiliaryFileWrapper.getCachedFavoritesAndRemoveFor(TEST_COMPONENT)
|
||||
verify(persistenceWrapper).storeFavorites(listOf(structure2))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testChangeFile() {
|
||||
auxiliaryFileWrapper.changeFile(mock(File::class.java))
|
||||
val inOrder = inOrder(persistenceWrapper)
|
||||
inOrder.verify(persistenceWrapper).changeFileAndBackupManager(
|
||||
any(), ArgumentMatchers.isNull())
|
||||
inOrder.verify(persistenceWrapper).readFavorites()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFileRemoved() {
|
||||
`when`(persistenceWrapper.fileExists).thenReturn(false)
|
||||
|
||||
assertEquals(emptyList<StructureInfo>(),
|
||||
auxiliaryFileWrapper.getCachedFavoritesAndRemoveFor(TEST_COMPONENT))
|
||||
assertEquals(emptyList<StructureInfo>(),
|
||||
auxiliaryFileWrapper.getCachedFavoritesAndRemoveFor(TEST_COMPONENT_OTHER))
|
||||
|
||||
verify(persistenceWrapper, never()).storeFavorites(ArgumentMatchers.anyList())
|
||||
}
|
||||
}
|
@ -31,6 +31,7 @@ import android.service.controls.actions.ControlAction
|
||||
import android.testing.AndroidTestingRunner
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.backup.BackupHelper
|
||||
import com.android.systemui.broadcast.BroadcastDispatcher
|
||||
import com.android.systemui.controls.ControlStatus
|
||||
import com.android.systemui.controls.ControlsServiceInfo
|
||||
@ -39,6 +40,7 @@ import com.android.systemui.controls.ui.ControlsUiController
|
||||
import com.android.systemui.dump.DumpManager
|
||||
import com.android.systemui.util.concurrency.FakeExecutor
|
||||
import com.android.systemui.util.time.FakeSystemClock
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotEquals
|
||||
@ -57,6 +59,7 @@ import org.mockito.Mockito.mock
|
||||
import org.mockito.Mockito.never
|
||||
import org.mockito.Mockito.reset
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.Mockito.verifyNoMoreInteractions
|
||||
import org.mockito.MockitoAnnotations
|
||||
import java.util.Optional
|
||||
import java.util.function.Consumer
|
||||
@ -74,6 +77,8 @@ class ControlsControllerImplTest : SysuiTestCase() {
|
||||
@Mock
|
||||
private lateinit var persistenceWrapper: ControlsFavoritePersistenceWrapper
|
||||
@Mock
|
||||
private lateinit var auxiliaryPersistenceWrapper: AuxiliaryPersistenceWrapper
|
||||
@Mock
|
||||
private lateinit var broadcastDispatcher: BroadcastDispatcher
|
||||
@Mock
|
||||
private lateinit var listingController: ControlsListingController
|
||||
@ -154,6 +159,8 @@ class ControlsControllerImplTest : SysuiTestCase() {
|
||||
Optional.of(persistenceWrapper),
|
||||
mock(DumpManager::class.java)
|
||||
)
|
||||
controller.auxiliaryPersistenceWrapper = auxiliaryPersistenceWrapper
|
||||
|
||||
assertTrue(controller.available)
|
||||
verify(broadcastDispatcher).registerReceiver(
|
||||
capture(broadcastReceiverCaptor), any(), any(), eq(UserHandle.ALL))
|
||||
@ -161,6 +168,11 @@ class ControlsControllerImplTest : SysuiTestCase() {
|
||||
verify(listingController).addCallback(capture(listingCallbackCaptor))
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
controller.destroy()
|
||||
}
|
||||
|
||||
private fun statelessBuilderFromInfo(
|
||||
controlInfo: ControlInfo,
|
||||
structure: CharSequence = ""
|
||||
@ -517,8 +529,9 @@ class ControlsControllerImplTest : SysuiTestCase() {
|
||||
|
||||
broadcastReceiverCaptor.value.onReceive(mContext, intent)
|
||||
|
||||
verify(persistenceWrapper).changeFile(any())
|
||||
verify(persistenceWrapper).changeFileAndBackupManager(any(), any())
|
||||
verify(persistenceWrapper).readFavorites()
|
||||
verify(auxiliaryPersistenceWrapper).changeFile(any())
|
||||
verify(bindingController).changeUser(UserHandle.of(otherUser))
|
||||
verify(listingController).changeUser(UserHandle.of(otherUser))
|
||||
assertTrue(controller.getFavorites().isEmpty())
|
||||
@ -767,6 +780,41 @@ class ControlsControllerImplTest : SysuiTestCase() {
|
||||
verify(persistenceWrapper).storeFavorites(ArgumentMatchers.anyList())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testExistingPackage_removedFromCache() {
|
||||
`when`(auxiliaryPersistenceWrapper.favorites).thenReturn(
|
||||
listOf(TEST_STRUCTURE_INFO, TEST_STRUCTURE_INFO_2))
|
||||
|
||||
controller.replaceFavoritesForStructure(TEST_STRUCTURE_INFO)
|
||||
delayableExecutor.runAllReady()
|
||||
|
||||
val serviceInfo = mock(ServiceInfo::class.java)
|
||||
`when`(serviceInfo.componentName).thenReturn(TEST_COMPONENT)
|
||||
val info = ControlsServiceInfo(mContext, serviceInfo)
|
||||
|
||||
listingCallbackCaptor.value.onServicesUpdated(listOf(info))
|
||||
delayableExecutor.runAllReady()
|
||||
|
||||
verify(auxiliaryPersistenceWrapper).getCachedFavoritesAndRemoveFor(TEST_COMPONENT)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAddedPackage_requestedFromCache() {
|
||||
`when`(auxiliaryPersistenceWrapper.favorites).thenReturn(
|
||||
listOf(TEST_STRUCTURE_INFO, TEST_STRUCTURE_INFO_2))
|
||||
|
||||
val serviceInfo = mock(ServiceInfo::class.java)
|
||||
`when`(serviceInfo.componentName).thenReturn(TEST_COMPONENT)
|
||||
val info = ControlsServiceInfo(mContext, serviceInfo)
|
||||
|
||||
listingCallbackCaptor.value.onServicesUpdated(listOf(info))
|
||||
delayableExecutor.runAllReady()
|
||||
|
||||
verify(auxiliaryPersistenceWrapper).getCachedFavoritesAndRemoveFor(TEST_COMPONENT)
|
||||
verify(auxiliaryPersistenceWrapper, never())
|
||||
.getCachedFavoritesAndRemoveFor(TEST_COMPONENT_2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testListingCallbackNotListeningWhileReadingFavorites() {
|
||||
val intent = Intent(Intent.ACTION_USER_SWITCHED).apply {
|
||||
@ -852,4 +900,40 @@ class ControlsControllerImplTest : SysuiTestCase() {
|
||||
assertTrue(succeeded)
|
||||
assertTrue(seeded)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRestoreReceiver_loadsAuxiliaryData() {
|
||||
val receiver = controller.restoreFinishedReceiver
|
||||
|
||||
val structure1 = mock(StructureInfo::class.java)
|
||||
val structure2 = mock(StructureInfo::class.java)
|
||||
val listOfStructureInfo = listOf(structure1, structure2)
|
||||
`when`(auxiliaryPersistenceWrapper.favorites).thenReturn(listOfStructureInfo)
|
||||
|
||||
val intent = Intent(BackupHelper.ACTION_RESTORE_FINISHED)
|
||||
intent.putExtra(Intent.EXTRA_USER_ID, context.userId)
|
||||
receiver.onReceive(context, intent)
|
||||
delayableExecutor.runAllReady()
|
||||
|
||||
val inOrder = inOrder(auxiliaryPersistenceWrapper, persistenceWrapper)
|
||||
inOrder.verify(auxiliaryPersistenceWrapper).initialize()
|
||||
inOrder.verify(auxiliaryPersistenceWrapper).favorites
|
||||
inOrder.verify(persistenceWrapper).storeFavorites(listOfStructureInfo)
|
||||
inOrder.verify(persistenceWrapper).readFavorites()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRestoreReceiver_noActionOnWrongUser() {
|
||||
val receiver = controller.restoreFinishedReceiver
|
||||
|
||||
reset(persistenceWrapper)
|
||||
reset(auxiliaryPersistenceWrapper)
|
||||
val intent = Intent(BackupHelper.ACTION_RESTORE_FINISHED)
|
||||
intent.putExtra(Intent.EXTRA_USER_ID, context.userId + 1)
|
||||
receiver.onReceive(context, intent)
|
||||
delayableExecutor.runAllReady()
|
||||
|
||||
verifyNoMoreInteractions(persistenceWrapper)
|
||||
verifyNoMoreInteractions(auxiliaryPersistenceWrapper)
|
||||
}
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ class ControlsFavoritePersistenceWrapperTest : SysuiTestCase() {
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
if (file.exists() ?: false) {
|
||||
if (file.exists()) {
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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.JobParameters
|
||||
import android.content.Context
|
||||
import android.testing.AndroidTestingRunner
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
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
|
||||
|
||||
private lateinit var service: AuxiliaryPersistenceWrapper.DeletionJobService
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
|
||||
service = AuxiliaryPersistenceWrapper.DeletionJobService()
|
||||
service.attachContext(context)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnStartJob() {
|
||||
// false means job is terminated
|
||||
assertFalse(service.onStartJob(mock(JobParameters::class.java)))
|
||||
verify(context).deleteFile(AuxiliaryPersistenceWrapper.AUXILIARY_FILE_NAME)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnStopJob() {
|
||||
// true means run after backoff
|
||||
assertTrue(service.onStopJob(mock(JobParameters::class.java)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testJobHasRightParameters() {
|
||||
val userId = 10
|
||||
`when`(context.userId).thenReturn(userId)
|
||||
`when`(context.packageName).thenReturn(mContext.packageName)
|
||||
|
||||
val jobInfo = AuxiliaryPersistenceWrapper.DeletionJobService.getJobForContext(context)
|
||||
assertEquals(
|
||||
AuxiliaryPersistenceWrapper.DeletionJobService.DELETE_FILE_JOB_ID + userId, jobInfo.id)
|
||||
assertTrue(jobInfo.isPersisted)
|
||||
assertEquals(TimeUnit.DAYS.toMillis(7), jobInfo.minLatencyMillis)
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user