Merge "Added Backup & Restore for SystemUI and controls" into rvc-dev am: 30c2f30f83
Change-Id: I2f417e5b34df6a37095a6c36b09f68f72f123877
This commit is contained in:
@@ -263,7 +263,8 @@
|
|||||||
android:name=".SystemUIApplication"
|
android:name=".SystemUIApplication"
|
||||||
android:persistent="true"
|
android:persistent="true"
|
||||||
android:allowClearUserData="false"
|
android:allowClearUserData="false"
|
||||||
android:allowBackup="false"
|
android:backupAgent=".backup.BackupHelper"
|
||||||
|
android:killAfterRestore="false"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:label="@string/app_label"
|
android:label="@string/app_label"
|
||||||
android:icon="@drawable/icon"
|
android:icon="@drawable/icon"
|
||||||
@@ -277,7 +278,7 @@
|
|||||||
<!-- Keep theme in sync with SystemUIApplication.onCreate().
|
<!-- Keep theme in sync with SystemUIApplication.onCreate().
|
||||||
Setting the theme on the application does not affect views inflated by services.
|
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. -->
|
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
|
<!-- Broadcast receiver that gets the broadcast at boot time and starts
|
||||||
up everything else.
|
up everything else.
|
||||||
TODO: Should have an android:permission attribute
|
TODO: Should have an android:permission attribute
|
||||||
@@ -690,6 +691,9 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
|
<service android:name=".controls.controller.AuxiliaryPersistenceWrapper$DeletionJobService"
|
||||||
|
android:permission="android.permission.BIND_JOB_SERVICE"/>
|
||||||
|
|
||||||
<!-- started from ControlsFavoritingActivity -->
|
<!-- started from ControlsFavoritingActivity -->
|
||||||
<activity
|
<activity
|
||||||
android:name=".controls.management.ControlsRequestDialog"
|
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.ActivityManager
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
|
import android.app.backup.BackupManager
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
@@ -35,6 +36,7 @@ import android.util.ArrayMap
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.android.internal.annotations.VisibleForTesting
|
import com.android.internal.annotations.VisibleForTesting
|
||||||
import com.android.systemui.Dumpable
|
import com.android.systemui.Dumpable
|
||||||
|
import com.android.systemui.backup.BackupHelper
|
||||||
import com.android.systemui.broadcast.BroadcastDispatcher
|
import com.android.systemui.broadcast.BroadcastDispatcher
|
||||||
import com.android.systemui.controls.ControlStatus
|
import com.android.systemui.controls.ControlStatus
|
||||||
import com.android.systemui.controls.ControlsServiceInfo
|
import com.android.systemui.controls.ControlsServiceInfo
|
||||||
@@ -69,6 +71,7 @@ class ControlsControllerImpl @Inject constructor (
|
|||||||
internal val URI = Settings.Secure.getUriFor(CONTROLS_AVAILABLE)
|
internal val URI = Settings.Secure.getUriFor(CONTROLS_AVAILABLE)
|
||||||
private const val USER_CHANGE_RETRY_DELAY = 500L // ms
|
private const val USER_CHANGE_RETRY_DELAY = 500L // ms
|
||||||
private const val DEFAULT_ENABLED = 1
|
private const val DEFAULT_ENABLED = 1
|
||||||
|
private const val PERMISSION_SELF = "com.android.systemui.permission.SELF"
|
||||||
}
|
}
|
||||||
|
|
||||||
private var userChanging: Boolean = true
|
private var userChanging: Boolean = true
|
||||||
@@ -88,23 +91,35 @@ class ControlsControllerImpl @Inject constructor (
|
|||||||
contentResolver, CONTROLS_AVAILABLE, DEFAULT_ENABLED, currentUserId) != 0
|
contentResolver, CONTROLS_AVAILABLE, DEFAULT_ENABLED, currentUserId) != 0
|
||||||
private set
|
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 {
|
private val persistenceWrapper = optionalWrapper.orElseGet {
|
||||||
ControlsFavoritePersistenceWrapper(
|
ControlsFavoritePersistenceWrapper(
|
||||||
Environment.buildPath(
|
file,
|
||||||
context.filesDir,
|
executor,
|
||||||
ControlsFavoritePersistenceWrapper.FILE_NAME
|
BackupManager(context)
|
||||||
),
|
|
||||||
executor
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal var auxiliaryPersistenceWrapper = AuxiliaryPersistenceWrapper(auxiliaryFile, executor)
|
||||||
|
|
||||||
private fun setValuesForUser(newUser: UserHandle) {
|
private fun setValuesForUser(newUser: UserHandle) {
|
||||||
Log.d(TAG, "Changing to user: $newUser")
|
Log.d(TAG, "Changing to user: $newUser")
|
||||||
currentUser = newUser
|
currentUser = newUser
|
||||||
val userContext = context.createContextAsUser(currentUser, 0)
|
val userContext = context.createContextAsUser(currentUser, 0)
|
||||||
val fileName = Environment.buildPath(
|
file = Environment.buildPath(
|
||||||
userContext.filesDir, ControlsFavoritePersistenceWrapper.FILE_NAME)
|
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,
|
available = Settings.Secure.getIntForUser(contentResolver, CONTROLS_AVAILABLE,
|
||||||
DEFAULT_ENABLED, newUser.identifier) != 0
|
DEFAULT_ENABLED, newUser.identifier) != 0
|
||||||
resetFavorites(available)
|
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
|
@VisibleForTesting
|
||||||
internal val settingObserver = object : ContentObserver(null) {
|
internal val settingObserver = object : ContentObserver(null) {
|
||||||
override fun onChange(
|
override fun onChange(
|
||||||
@@ -170,7 +200,25 @@ class ControlsControllerImpl @Inject constructor (
|
|||||||
bindingController.onComponentRemoved(it)
|
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) {
|
if (changed) {
|
||||||
persistenceWrapper.storeFavorites(Favorites.getAllStructures())
|
persistenceWrapper.storeFavorites(Favorites.getAllStructures())
|
||||||
}
|
}
|
||||||
@@ -188,9 +236,22 @@ class ControlsControllerImpl @Inject constructor (
|
|||||||
executor,
|
executor,
|
||||||
UserHandle.ALL
|
UserHandle.ALL
|
||||||
)
|
)
|
||||||
|
context.registerReceiver(
|
||||||
|
restoreFinishedReceiver,
|
||||||
|
IntentFilter(BackupHelper.ACTION_RESTORE_FINISHED),
|
||||||
|
PERMISSION_SELF,
|
||||||
|
null
|
||||||
|
)
|
||||||
contentResolver.registerContentObserver(URI, false, settingObserver, UserHandle.USER_ALL)
|
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) {
|
private fun resetFavorites(shouldLoad: Boolean) {
|
||||||
Favorites.clear()
|
Favorites.clear()
|
||||||
|
|
||||||
|
|||||||
@@ -16,10 +16,12 @@
|
|||||||
|
|
||||||
package com.android.systemui.controls.controller
|
package com.android.systemui.controls.controller
|
||||||
|
|
||||||
|
import android.app.backup.BackupManager
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.util.AtomicFile
|
import android.util.AtomicFile
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.Xml
|
import android.util.Xml
|
||||||
|
import com.android.systemui.backup.BackupHelper
|
||||||
import libcore.io.IoUtils
|
import libcore.io.IoUtils
|
||||||
import org.xmlpull.v1.XmlPullParser
|
import org.xmlpull.v1.XmlPullParser
|
||||||
import org.xmlpull.v1.XmlPullParserException
|
import org.xmlpull.v1.XmlPullParserException
|
||||||
@@ -38,7 +40,8 @@ import java.util.concurrent.Executor
|
|||||||
*/
|
*/
|
||||||
class ControlsFavoritePersistenceWrapper(
|
class ControlsFavoritePersistenceWrapper(
|
||||||
private var file: File,
|
private var file: File,
|
||||||
private val executor: Executor
|
private val executor: Executor,
|
||||||
|
private var backupManager: BackupManager? = null
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
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 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
|
file = fileName
|
||||||
|
backupManager = newBackupManager
|
||||||
|
}
|
||||||
|
|
||||||
|
val fileExists: Boolean
|
||||||
|
get() = file.exists()
|
||||||
|
|
||||||
|
fun deleteFile() {
|
||||||
|
file.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -77,49 +89,54 @@ class ControlsFavoritePersistenceWrapper(
|
|||||||
executor.execute {
|
executor.execute {
|
||||||
Log.d(TAG, "Saving data to file: $file")
|
Log.d(TAG, "Saving data to file: $file")
|
||||||
val atomicFile = AtomicFile(file)
|
val atomicFile = AtomicFile(file)
|
||||||
val writer = try {
|
val dataWritten = synchronized(BackupHelper.controlsDataLock) {
|
||||||
atomicFile.startWrite()
|
val writer = try {
|
||||||
} catch (e: IOException) {
|
atomicFile.startWrite()
|
||||||
Log.e(TAG, "Failed to start write file", e)
|
} catch (e: IOException) {
|
||||||
return@execute
|
Log.e(TAG, "Failed to start write file", e)
|
||||||
}
|
return@execute
|
||||||
try {
|
}
|
||||||
Xml.newSerializer().apply {
|
try {
|
||||||
setOutput(writer, "utf-8")
|
Xml.newSerializer().apply {
|
||||||
setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true)
|
setOutput(writer, "utf-8")
|
||||||
startDocument(null, true)
|
setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true)
|
||||||
startTag(null, TAG_VERSION)
|
startDocument(null, true)
|
||||||
text("$VERSION")
|
startTag(null, TAG_VERSION)
|
||||||
endTag(null, TAG_VERSION)
|
text("$VERSION")
|
||||||
|
endTag(null, TAG_VERSION)
|
||||||
startTag(null, TAG_STRUCTURES)
|
|
||||||
structures.forEach { s ->
|
startTag(null, TAG_STRUCTURES)
|
||||||
startTag(null, TAG_STRUCTURE)
|
structures.forEach { s ->
|
||||||
attribute(null, TAG_COMPONENT, s.componentName.flattenToString())
|
startTag(null, TAG_STRUCTURE)
|
||||||
attribute(null, TAG_STRUCTURE, s.structure.toString())
|
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_CONTROLS)
|
||||||
startTag(null, TAG_CONTROL)
|
s.controls.forEach { c ->
|
||||||
attribute(null, TAG_ID, c.controlId)
|
startTag(null, TAG_CONTROL)
|
||||||
attribute(null, TAG_TITLE, c.controlTitle.toString())
|
attribute(null, TAG_ID, c.controlId)
|
||||||
attribute(null, TAG_SUBTITLE, c.controlSubtitle.toString())
|
attribute(null, TAG_TITLE, c.controlTitle.toString())
|
||||||
attribute(null, TAG_TYPE, c.deviceType.toString())
|
attribute(null, TAG_SUBTITLE, c.controlSubtitle.toString())
|
||||||
endTag(null, TAG_CONTROL)
|
attribute(null, TAG_TYPE, c.deviceType.toString())
|
||||||
}
|
endTag(null, TAG_CONTROL)
|
||||||
endTag(null, TAG_CONTROLS)
|
}
|
||||||
endTag(null, TAG_STRUCTURE)
|
endTag(null, TAG_CONTROLS)
|
||||||
}
|
endTag(null, TAG_STRUCTURE)
|
||||||
endTag(null, TAG_STRUCTURES)
|
}
|
||||||
endDocument()
|
endTag(null, TAG_STRUCTURES)
|
||||||
atomicFile.finishWrite(writer)
|
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 {
|
try {
|
||||||
Log.d(TAG, "Reading data from file: $file")
|
Log.d(TAG, "Reading data from file: $file")
|
||||||
val parser = Xml.newPullParser()
|
synchronized(BackupHelper.controlsDataLock) {
|
||||||
parser.setInput(reader, null)
|
val parser = Xml.newPullParser()
|
||||||
return parseXml(parser)
|
parser.setInput(reader, null)
|
||||||
|
return parseXml(parser)
|
||||||
|
}
|
||||||
} catch (e: XmlPullParserException) {
|
} catch (e: XmlPullParserException) {
|
||||||
throw IllegalStateException("Failed parsing favorites file: $file", e)
|
throw IllegalStateException("Failed parsing favorites file: $file", e)
|
||||||
} catch (e: IOException) {
|
} 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 android.testing.AndroidTestingRunner
|
||||||
import androidx.test.filters.SmallTest
|
import androidx.test.filters.SmallTest
|
||||||
import com.android.systemui.SysuiTestCase
|
import com.android.systemui.SysuiTestCase
|
||||||
|
import com.android.systemui.backup.BackupHelper
|
||||||
import com.android.systemui.broadcast.BroadcastDispatcher
|
import com.android.systemui.broadcast.BroadcastDispatcher
|
||||||
import com.android.systemui.controls.ControlStatus
|
import com.android.systemui.controls.ControlStatus
|
||||||
import com.android.systemui.controls.ControlsServiceInfo
|
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.dump.DumpManager
|
||||||
import com.android.systemui.util.concurrency.FakeExecutor
|
import com.android.systemui.util.concurrency.FakeExecutor
|
||||||
import com.android.systemui.util.time.FakeSystemClock
|
import com.android.systemui.util.time.FakeSystemClock
|
||||||
|
import org.junit.After
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertFalse
|
import org.junit.Assert.assertFalse
|
||||||
import org.junit.Assert.assertNotEquals
|
import org.junit.Assert.assertNotEquals
|
||||||
@@ -57,6 +59,7 @@ import org.mockito.Mockito.mock
|
|||||||
import org.mockito.Mockito.never
|
import org.mockito.Mockito.never
|
||||||
import org.mockito.Mockito.reset
|
import org.mockito.Mockito.reset
|
||||||
import org.mockito.Mockito.verify
|
import org.mockito.Mockito.verify
|
||||||
|
import org.mockito.Mockito.verifyNoMoreInteractions
|
||||||
import org.mockito.MockitoAnnotations
|
import org.mockito.MockitoAnnotations
|
||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
import java.util.function.Consumer
|
import java.util.function.Consumer
|
||||||
@@ -74,6 +77,8 @@ class ControlsControllerImplTest : SysuiTestCase() {
|
|||||||
@Mock
|
@Mock
|
||||||
private lateinit var persistenceWrapper: ControlsFavoritePersistenceWrapper
|
private lateinit var persistenceWrapper: ControlsFavoritePersistenceWrapper
|
||||||
@Mock
|
@Mock
|
||||||
|
private lateinit var auxiliaryPersistenceWrapper: AuxiliaryPersistenceWrapper
|
||||||
|
@Mock
|
||||||
private lateinit var broadcastDispatcher: BroadcastDispatcher
|
private lateinit var broadcastDispatcher: BroadcastDispatcher
|
||||||
@Mock
|
@Mock
|
||||||
private lateinit var listingController: ControlsListingController
|
private lateinit var listingController: ControlsListingController
|
||||||
@@ -154,6 +159,8 @@ class ControlsControllerImplTest : SysuiTestCase() {
|
|||||||
Optional.of(persistenceWrapper),
|
Optional.of(persistenceWrapper),
|
||||||
mock(DumpManager::class.java)
|
mock(DumpManager::class.java)
|
||||||
)
|
)
|
||||||
|
controller.auxiliaryPersistenceWrapper = auxiliaryPersistenceWrapper
|
||||||
|
|
||||||
assertTrue(controller.available)
|
assertTrue(controller.available)
|
||||||
verify(broadcastDispatcher).registerReceiver(
|
verify(broadcastDispatcher).registerReceiver(
|
||||||
capture(broadcastReceiverCaptor), any(), any(), eq(UserHandle.ALL))
|
capture(broadcastReceiverCaptor), any(), any(), eq(UserHandle.ALL))
|
||||||
@@ -161,6 +168,11 @@ class ControlsControllerImplTest : SysuiTestCase() {
|
|||||||
verify(listingController).addCallback(capture(listingCallbackCaptor))
|
verify(listingController).addCallback(capture(listingCallbackCaptor))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
controller.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
private fun statelessBuilderFromInfo(
|
private fun statelessBuilderFromInfo(
|
||||||
controlInfo: ControlInfo,
|
controlInfo: ControlInfo,
|
||||||
structure: CharSequence = ""
|
structure: CharSequence = ""
|
||||||
@@ -517,8 +529,9 @@ class ControlsControllerImplTest : SysuiTestCase() {
|
|||||||
|
|
||||||
broadcastReceiverCaptor.value.onReceive(mContext, intent)
|
broadcastReceiverCaptor.value.onReceive(mContext, intent)
|
||||||
|
|
||||||
verify(persistenceWrapper).changeFile(any())
|
verify(persistenceWrapper).changeFileAndBackupManager(any(), any())
|
||||||
verify(persistenceWrapper).readFavorites()
|
verify(persistenceWrapper).readFavorites()
|
||||||
|
verify(auxiliaryPersistenceWrapper).changeFile(any())
|
||||||
verify(bindingController).changeUser(UserHandle.of(otherUser))
|
verify(bindingController).changeUser(UserHandle.of(otherUser))
|
||||||
verify(listingController).changeUser(UserHandle.of(otherUser))
|
verify(listingController).changeUser(UserHandle.of(otherUser))
|
||||||
assertTrue(controller.getFavorites().isEmpty())
|
assertTrue(controller.getFavorites().isEmpty())
|
||||||
@@ -767,6 +780,41 @@ class ControlsControllerImplTest : SysuiTestCase() {
|
|||||||
verify(persistenceWrapper).storeFavorites(ArgumentMatchers.anyList())
|
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
|
@Test
|
||||||
fun testListingCallbackNotListeningWhileReadingFavorites() {
|
fun testListingCallbackNotListeningWhileReadingFavorites() {
|
||||||
val intent = Intent(Intent.ACTION_USER_SWITCHED).apply {
|
val intent = Intent(Intent.ACTION_USER_SWITCHED).apply {
|
||||||
@@ -852,4 +900,40 @@ class ControlsControllerImplTest : SysuiTestCase() {
|
|||||||
assertTrue(succeeded)
|
assertTrue(succeeded)
|
||||||
assertTrue(seeded)
|
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
|
@After
|
||||||
fun tearDown() {
|
fun tearDown() {
|
||||||
if (file.exists() ?: false) {
|
if (file.exists()) {
|
||||||
file.delete()
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user