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:
Fabian Kozynski 2020-03-11 15:49:19 -04:00
parent dce5a57376
commit 66e975404c
9 changed files with 702 additions and 59 deletions

View File

@ -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"

View File

@ -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))
}
}
}

View File

@ -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
}
}
}

View File

@ -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()

View File

@ -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) {

View File

@ -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())
}
}

View File

@ -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)
}
}

View File

@ -48,7 +48,7 @@ class ControlsFavoritePersistenceWrapperTest : SysuiTestCase() {
@After
fun tearDown() {
if (file.exists() ?: false) {
if (file.exists()) {
file.delete()
}
}

View File

@ -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)
}
}