diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index f2d40cef83c7a..791b832775715 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -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 @@
-
+
Unit>
+ ) : FileBackupHelper(context, *fileNamesAndPostProcess.keys.toTypedArray()) {
+
+ override fun restoreEntity(data: BackupDataInputStream) {
+ val file = Environment.buildPath(context.filesDir, data.key)
+ if (file.exists()) {
+ Log.w(TAG, "File " + data.key + " already exists. Skipping restore.")
+ return
+ }
+ synchronized(lock) {
+ super.restoreEntity(data)
+ fileNamesAndPostProcess.get(data.key)?.invoke()
+ }
+ }
+
+ override fun performBackup(
+ oldState: ParcelFileDescriptor?,
+ data: BackupDataOutput?,
+ newState: ParcelFileDescriptor?
+ ) {
+ synchronized(lock) {
+ super.performBackup(oldState, data, newState)
+ }
+ }
+ }
+}
+private fun getPPControlsFile(context: Context): () -> Unit {
+ return {
+ val filesDir = context.filesDir
+ val file = Environment.buildPath(filesDir, BackupHelper.CONTROLS)
+ if (file.exists()) {
+ val dest = Environment.buildPath(filesDir,
+ AuxiliaryPersistenceWrapper.AUXILIARY_FILE_NAME)
+ file.copyTo(dest)
+ val jobScheduler = context.getSystemService(JobScheduler::class.java)
+ jobScheduler?.schedule(
+ AuxiliaryPersistenceWrapper.DeletionJobService.getJobForContext(context))
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/AuxiliaryPersistenceWrapper.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/AuxiliaryPersistenceWrapper.kt
new file mode 100644
index 0000000000000..0a6335e01f9f1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/AuxiliaryPersistenceWrapper.kt
@@ -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 = 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 {
+ if (!persistenceWrapper.fileExists) {
+ return emptyList()
+ }
+ val (comp, noComp) = favorites.partition { it.componentName == componentName }
+ return comp.also {
+ favorites = noComp
+ if (favorites.isNotEmpty()) {
+ persistenceWrapper.storeFavorites(noComp)
+ } else {
+ persistenceWrapper.deleteFile()
+ }
+ }
+ }
+
+ /**
+ * [JobService] to delete the auxiliary file after a week.
+ */
+ class DeletionJobService : JobService() {
+ companion object {
+ @VisibleForTesting
+ internal val DELETE_FILE_JOB_ID = 1000
+ private val WEEK_IN_MILLIS = TimeUnit.DAYS.toMillis(7)
+ fun getJobForContext(context: Context): JobInfo {
+ val jobId = DELETE_FILE_JOB_ID + context.userId
+ val componentName = ComponentName(context, DeletionJobService::class.java)
+ return JobInfo.Builder(jobId, componentName)
+ .setMinimumLatency(WEEK_IN_MILLIS)
+ .setPersisted(true)
+ .build()
+ }
+ }
+
+ @VisibleForTesting
+ fun attachContext(context: Context) {
+ attachBaseContext(context)
+ }
+
+ override fun onStartJob(params: JobParameters): Boolean {
+ synchronized(BackupHelper.controlsDataLock) {
+ baseContext.deleteFile(AUXILIARY_FILE_NAME)
+ }
+ return false
+ }
+
+ override fun onStopJob(params: JobParameters?): Boolean {
+ return true // reschedule and try again if the job was stopped without completing
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
index fdb0e4c95bed0..34833396acef3 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
@@ -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()
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsFavoritePersistenceWrapper.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsFavoritePersistenceWrapper.kt
index afd82e7fcf78b..cde258a056dbe 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsFavoritePersistenceWrapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsFavoritePersistenceWrapper.kt
@@ -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) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/AuxiliaryPersistenceWrapperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/AuxiliaryPersistenceWrapperTest.kt
new file mode 100644
index 0000000000000..129fe9a36a0db
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/AuxiliaryPersistenceWrapperTest.kt
@@ -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 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(),
+ auxiliaryFileWrapper.getCachedFavoritesAndRemoveFor(TEST_COMPONENT))
+ assertEquals(emptyList(),
+ auxiliaryFileWrapper.getCachedFavoritesAndRemoveFor(TEST_COMPONENT_OTHER))
+
+ verify(persistenceWrapper, never()).storeFavorites(ArgumentMatchers.anyList())
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt
index 93aee33cc1c89..8630570c4e708 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt
@@ -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)
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsFavoritePersistenceWrapperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsFavoritePersistenceWrapperTest.kt
index 4f6cbe11149a7..861c6207f5b01 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsFavoritePersistenceWrapperTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsFavoritePersistenceWrapperTest.kt
@@ -48,7 +48,7 @@ class ControlsFavoritePersistenceWrapperTest : SysuiTestCase() {
@After
fun tearDown() {
- if (file.exists() ?: false) {
+ if (file.exists()) {
file.delete()
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/DeletionJobServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/DeletionJobServiceTest.kt
new file mode 100644
index 0000000000000..4439586497ffb
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/DeletionJobServiceTest.kt
@@ -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)
+ }
+}
\ No newline at end of file