diff options
9 files changed, 698 insertions, 55 deletions
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index f2d40cef83c7..791b83277571 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 @@ <!-- 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" diff --git a/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt b/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt new file mode 100644 index 000000000000..fe31a7b75c06 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt @@ -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)) + } + } +}
\ 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 000000000000..0a6335e01f9f --- /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<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 + } + } +}
\ 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 fdb0e4c95bed..34833396acef 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) @@ -130,6 +145,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( selfChange: Boolean, @@ -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 afd82e7fcf78..cde258a056db 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) + 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_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) + 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_CONTROLS) - endTag(null, TAG_STRUCTURE) + endTag(null, TAG_STRUCTURES) + endDocument() + atomicFile.finishWrite(writer) } - 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 000000000000..129fe9a36a0d --- /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 <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()) + } +}
\ 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 93aee33cc1c8..8630570c4e70 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()) @@ -768,6 +781,41 @@ class ControlsControllerImplTest : SysuiTestCase() { } @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 { putExtra(Intent.EXTRA_USER_HANDLE, otherUser) @@ -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 4f6cbe11149a..861c6207f5b0 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 000000000000..4439586497ff --- /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 |