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