diff options
| author | 2022-11-30 16:51:01 +0000 | |
|---|---|---|
| committer | 2022-11-30 16:51:01 +0000 | |
| commit | 729c55cd28079e21ac4edee8e6c6be7c236c163a (patch) | |
| tree | 93d1cd988bb96c87237a311bdd91956ac6e1e30a | |
| parent | f21cce1f478876347b5c427ed3e52e6e2984388b (diff) | |
| parent | c5e1cdf699f5340c10f776fc025361f80b6abe42 (diff) | |
Merge changes from topic "clsqa-in-themepicker" into tm-qpr-dev am: c5e1cdf699
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/20561075
Change-Id: I815833443b043f23f8aa5f33eb8805838d20fafd
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
22 files changed, 1020 insertions, 171 deletions
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/data/content/FakeKeyguardQuickAffordanceProviderClient.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/data/content/FakeKeyguardQuickAffordanceProviderClient.kt new file mode 100644 index 000000000000..f490c5459d46 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/data/content/FakeKeyguardQuickAffordanceProviderClient.kt @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2022 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.shared.quickaffordance.data.content + +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine + +class FakeKeyguardQuickAffordanceProviderClient( + slots: List<KeyguardQuickAffordanceProviderClient.Slot> = + listOf( + KeyguardQuickAffordanceProviderClient.Slot( + id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, + capacity = 1, + ), + KeyguardQuickAffordanceProviderClient.Slot( + id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, + capacity = 1, + ), + ), + affordances: List<KeyguardQuickAffordanceProviderClient.Affordance> = + listOf( + KeyguardQuickAffordanceProviderClient.Affordance( + id = AFFORDANCE_1, + name = AFFORDANCE_1, + iconResourceId = 0, + ), + KeyguardQuickAffordanceProviderClient.Affordance( + id = AFFORDANCE_2, + name = AFFORDANCE_2, + iconResourceId = 0, + ), + KeyguardQuickAffordanceProviderClient.Affordance( + id = AFFORDANCE_3, + name = AFFORDANCE_3, + iconResourceId = 0, + ), + ), + flags: List<KeyguardQuickAffordanceProviderClient.Flag> = + listOf( + KeyguardQuickAffordanceProviderClient.Flag( + name = KeyguardQuickAffordanceProviderContract.FlagsTable.FLAG_NAME_FEATURE_ENABLED, + value = true, + ) + ), +) : KeyguardQuickAffordanceProviderClient { + + private val slots = MutableStateFlow(slots) + private val affordances = MutableStateFlow(affordances) + private val flags = MutableStateFlow(flags) + + private val selections = MutableStateFlow<Map<String, List<String>>>(emptyMap()) + + override suspend fun insertSelection(slotId: String, affordanceId: String) { + val slotCapacity = + querySlots().find { it.id == slotId }?.capacity + ?: error("Slot with ID \"$slotId\" not found!") + val affordances = selections.value.getOrDefault(slotId, mutableListOf()).toMutableList() + while (affordances.size + 1 > slotCapacity) { + affordances.removeAt(0) + } + affordances.remove(affordanceId) + affordances.add(affordanceId) + selections.value = selections.value.toMutableMap().apply { this[slotId] = affordances } + } + + override suspend fun querySlots(): List<KeyguardQuickAffordanceProviderClient.Slot> { + return slots.value + } + + override suspend fun queryFlags(): List<KeyguardQuickAffordanceProviderClient.Flag> { + return flags.value + } + + override fun observeSlots(): Flow<List<KeyguardQuickAffordanceProviderClient.Slot>> { + return slots.asStateFlow() + } + + override fun observeFlags(): Flow<List<KeyguardQuickAffordanceProviderClient.Flag>> { + return flags.asStateFlow() + } + + override suspend fun queryAffordances(): + List<KeyguardQuickAffordanceProviderClient.Affordance> { + return affordances.value + } + + override fun observeAffordances(): + Flow<List<KeyguardQuickAffordanceProviderClient.Affordance>> { + return affordances.asStateFlow() + } + + override suspend fun querySelections(): List<KeyguardQuickAffordanceProviderClient.Selection> { + return toSelectionList(selections.value, affordances.value) + } + + override fun observeSelections(): Flow<List<KeyguardQuickAffordanceProviderClient.Selection>> { + return combine(selections, affordances) { selections, affordances -> + toSelectionList(selections, affordances) + } + } + + override suspend fun deleteSelection(slotId: String, affordanceId: String) { + val affordances = selections.value.getOrDefault(slotId, mutableListOf()).toMutableList() + affordances.remove(affordanceId) + + selections.value = selections.value.toMutableMap().apply { this[slotId] = affordances } + } + + override suspend fun deleteAllSelections(slotId: String) { + selections.value = selections.value.toMutableMap().apply { this[slotId] = emptyList() } + } + + override suspend fun getAffordanceIcon(iconResourceId: Int, tintColor: Int): Drawable { + return BitmapDrawable() + } + + fun setFlag( + name: String, + value: Boolean, + ) { + flags.value = + flags.value.toMutableList().apply { + removeIf { it.name == name } + add(KeyguardQuickAffordanceProviderClient.Flag(name = name, value = value)) + } + } + + fun setSlotCapacity(slotId: String, capacity: Int) { + slots.value = + slots.value.toMutableList().apply { + val index = indexOfFirst { it.id == slotId } + check(index != -1) { "Slot with ID \"$slotId\" doesn't exist!" } + set( + index, + KeyguardQuickAffordanceProviderClient.Slot(id = slotId, capacity = capacity) + ) + } + } + + fun addAffordance(affordance: KeyguardQuickAffordanceProviderClient.Affordance): Int { + affordances.value = affordances.value + listOf(affordance) + return affordances.value.size - 1 + } + + private fun toSelectionList( + selections: Map<String, List<String>>, + affordances: List<KeyguardQuickAffordanceProviderClient.Affordance>, + ): List<KeyguardQuickAffordanceProviderClient.Selection> { + return selections + .map { (slotId, affordanceIds) -> + affordanceIds.map { affordanceId -> + val affordanceName = + affordances.find { it.id == affordanceId }?.name + ?: error("No affordance with ID of \"$affordanceId\"!") + KeyguardQuickAffordanceProviderClient.Selection( + slotId = slotId, + affordanceId = affordanceId, + affordanceName = affordanceName, + ) + } + } + .flatten() + } + + companion object { + const val AFFORDANCE_1 = "affordance_1" + const val AFFORDANCE_2 = "affordance_2" + const val AFFORDANCE_3 = "affordance_3" + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/data/content/KeyguardQuickAffordanceProviderClient.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/data/content/KeyguardQuickAffordanceProviderClient.kt new file mode 100644 index 000000000000..3213b2e97ac9 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/data/content/KeyguardQuickAffordanceProviderClient.kt @@ -0,0 +1,479 @@ +/* + * Copyright (C) 2022 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.shared.quickaffordance.data.content + +import android.annotation.SuppressLint +import android.content.ContentValues +import android.content.Context +import android.database.ContentObserver +import android.graphics.Color +import android.graphics.drawable.Drawable +import android.net.Uri +import androidx.annotation.DrawableRes +import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderContract as Contract +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.withContext + +/** Client for using a content provider implementing the [Contract]. */ +interface KeyguardQuickAffordanceProviderClient { + + /** + * Selects an affordance with the given ID for a slot on the lock screen with the given ID. + * + * Note that the maximum number of selected affordances on this slot is automatically enforced. + * Selecting a slot that is already full (e.g. already has a number of selected affordances at + * its maximum capacity) will automatically remove the oldest selected affordance before adding + * the one passed in this call. Additionally, selecting an affordance that's already one of the + * selected affordances on the slot will move the selected affordance to the newest location in + * the slot. + */ + suspend fun insertSelection( + slotId: String, + affordanceId: String, + ) + + /** Returns all available slots supported by the device. */ + suspend fun querySlots(): List<Slot> + + /** Returns the list of flags. */ + suspend fun queryFlags(): List<Flag> + + /** + * Returns [Flow] for observing the collection of slots. + * + * @see [querySlots] + */ + fun observeSlots(): Flow<List<Slot>> + + /** + * Returns [Flow] for observing the collection of flags. + * + * @see [queryFlags] + */ + fun observeFlags(): Flow<List<Flag>> + + /** + * Returns all available affordances supported by the device, regardless of current slot + * placement. + */ + suspend fun queryAffordances(): List<Affordance> + + /** + * Returns [Flow] for observing the collection of affordances. + * + * @see [queryAffordances] + */ + fun observeAffordances(): Flow<List<Affordance>> + + /** Returns the current slot-affordance selections. */ + suspend fun querySelections(): List<Selection> + + /** + * Returns [Flow] for observing the collection of selections. + * + * @see [querySelections] + */ + fun observeSelections(): Flow<List<Selection>> + + /** Unselects an affordance with the given ID from the slot with the given ID. */ + suspend fun deleteSelection( + slotId: String, + affordanceId: String, + ) + + /** Unselects all affordances from the slot with the given ID. */ + suspend fun deleteAllSelections( + slotId: String, + ) + + /** Returns a [Drawable] with the given ID, loaded from the system UI package. */ + suspend fun getAffordanceIcon( + @DrawableRes iconResourceId: Int, + tintColor: Int = Color.WHITE, + ): Drawable + + /** Models a slot. A position that quick affordances can be positioned in. */ + data class Slot( + /** Unique ID of the slot. */ + val id: String, + /** + * The maximum number of quick affordances that are allowed to be positioned in this slot. + */ + val capacity: Int, + ) + + /** + * Models a quick affordance. An action that can be selected by the user to appear in one or + * more slots on the lock screen. + */ + data class Affordance( + /** Unique ID of the quick affordance. */ + val id: String, + /** User-facing label for this affordance. */ + val name: String, + /** + * Resource ID for the user-facing icon for this affordance. This resource is hosted by the + * System UI process so it must be used with + * `PackageManager.getResourcesForApplication(String)`. + */ + val iconResourceId: Int, + /** + * Whether the affordance is enabled. Disabled affordances should be shown on the picker but + * should be rendered as "disabled". When tapped, the enablement properties should be used + * to populate UI that would explain to the user what to do in order to re-enable this + * affordance. + */ + val isEnabled: Boolean = true, + /** + * If the affordance is disabled, this is a set of instruction messages to be shown to the + * user when the disabled affordance is selected. The instructions should help the user + * figure out what to do in order to re-neable this affordance. + */ + val enablementInstructions: List<String>? = null, + /** + * If the affordance is disabled, this is a label for a button shown together with the set + * of instruction messages when the disabled affordance is selected. The button should help + * send the user to a flow that would help them achieve the instructions and re-enable this + * affordance. + * + * If `null`, the button should not be shown. + */ + val enablementActionText: String? = null, + /** + * If the affordance is disabled, this is a "component name" of the format + * `packageName/action` to be used as an `Intent` for `startActivity` when the action button + * (shown together with the set of instruction messages when the disabled affordance is + * selected) is clicked by the user. The button should help send the user to a flow that + * would help them achieve the instructions and re-enable this affordance. + * + * If `null`, the button should not be shown. + */ + val enablementActionComponentName: String? = null, + ) + + /** Models a selection of a quick affordance on a slot. */ + data class Selection( + /** The unique ID of the slot. */ + val slotId: String, + /** The unique ID of the quick affordance. */ + val affordanceId: String, + /** The user-visible label for the quick affordance. */ + val affordanceName: String, + ) + + /** Models a System UI flag. */ + data class Flag( + /** The name of the flag. */ + val name: String, + /** The value of the flag. */ + val value: Boolean, + ) +} + +class KeyguardQuickAffordanceProviderClientImpl( + private val context: Context, + private val backgroundDispatcher: CoroutineDispatcher, +) : KeyguardQuickAffordanceProviderClient { + + override suspend fun insertSelection( + slotId: String, + affordanceId: String, + ) { + withContext(backgroundDispatcher) { + context.contentResolver.insert( + Contract.SelectionTable.URI, + ContentValues().apply { + put(Contract.SelectionTable.Columns.SLOT_ID, slotId) + put(Contract.SelectionTable.Columns.AFFORDANCE_ID, affordanceId) + } + ) + } + } + + override suspend fun querySlots(): List<KeyguardQuickAffordanceProviderClient.Slot> { + return withContext(backgroundDispatcher) { + context.contentResolver + .query( + Contract.SlotTable.URI, + null, + null, + null, + null, + ) + ?.use { cursor -> + buildList { + val idColumnIndex = cursor.getColumnIndex(Contract.SlotTable.Columns.ID) + val capacityColumnIndex = + cursor.getColumnIndex(Contract.SlotTable.Columns.CAPACITY) + if (idColumnIndex == -1 || capacityColumnIndex == -1) { + return@buildList + } + + while (cursor.moveToNext()) { + add( + KeyguardQuickAffordanceProviderClient.Slot( + id = cursor.getString(idColumnIndex), + capacity = cursor.getInt(capacityColumnIndex), + ) + ) + } + } + } + } + ?: emptyList() + } + + override suspend fun queryFlags(): List<KeyguardQuickAffordanceProviderClient.Flag> { + return withContext(backgroundDispatcher) { + context.contentResolver + .query( + Contract.FlagsTable.URI, + null, + null, + null, + null, + ) + ?.use { cursor -> + buildList { + val nameColumnIndex = + cursor.getColumnIndex(Contract.FlagsTable.Columns.NAME) + val valueColumnIndex = + cursor.getColumnIndex(Contract.FlagsTable.Columns.VALUE) + if (nameColumnIndex == -1 || valueColumnIndex == -1) { + return@buildList + } + + while (cursor.moveToNext()) { + add( + KeyguardQuickAffordanceProviderClient.Flag( + name = cursor.getString(nameColumnIndex), + value = cursor.getInt(valueColumnIndex) == 1, + ) + ) + } + } + } + } + ?: emptyList() + } + + override fun observeSlots(): Flow<List<KeyguardQuickAffordanceProviderClient.Slot>> { + return observeUri(Contract.SlotTable.URI).map { querySlots() } + } + + override fun observeFlags(): Flow<List<KeyguardQuickAffordanceProviderClient.Flag>> { + return observeUri(Contract.FlagsTable.URI).map { queryFlags() } + } + + override suspend fun queryAffordances(): + List<KeyguardQuickAffordanceProviderClient.Affordance> { + return withContext(backgroundDispatcher) { + context.contentResolver + .query( + Contract.AffordanceTable.URI, + null, + null, + null, + null, + ) + ?.use { cursor -> + buildList { + val idColumnIndex = + cursor.getColumnIndex(Contract.AffordanceTable.Columns.ID) + val nameColumnIndex = + cursor.getColumnIndex(Contract.AffordanceTable.Columns.NAME) + val iconColumnIndex = + cursor.getColumnIndex(Contract.AffordanceTable.Columns.ICON) + val isEnabledColumnIndex = + cursor.getColumnIndex(Contract.AffordanceTable.Columns.IS_ENABLED) + val enablementInstructionsColumnIndex = + cursor.getColumnIndex( + Contract.AffordanceTable.Columns.ENABLEMENT_INSTRUCTIONS + ) + val enablementActionTextColumnIndex = + cursor.getColumnIndex( + Contract.AffordanceTable.Columns.ENABLEMENT_ACTION_TEXT + ) + val enablementComponentNameColumnIndex = + cursor.getColumnIndex( + Contract.AffordanceTable.Columns.ENABLEMENT_COMPONENT_NAME + ) + if ( + idColumnIndex == -1 || + nameColumnIndex == -1 || + iconColumnIndex == -1 || + isEnabledColumnIndex == -1 || + enablementInstructionsColumnIndex == -1 || + enablementActionTextColumnIndex == -1 || + enablementComponentNameColumnIndex == -1 + ) { + return@buildList + } + + while (cursor.moveToNext()) { + add( + KeyguardQuickAffordanceProviderClient.Affordance( + id = cursor.getString(idColumnIndex), + name = cursor.getString(nameColumnIndex), + iconResourceId = cursor.getInt(iconColumnIndex), + isEnabled = cursor.getInt(isEnabledColumnIndex) == 1, + enablementInstructions = + cursor + .getString(enablementInstructionsColumnIndex) + ?.split( + Contract.AffordanceTable + .ENABLEMENT_INSTRUCTIONS_DELIMITER + ), + enablementActionText = + cursor.getString(enablementActionTextColumnIndex), + enablementActionComponentName = + cursor.getString(enablementComponentNameColumnIndex), + ) + ) + } + } + } + } + ?: emptyList() + } + + override fun observeAffordances(): + Flow<List<KeyguardQuickAffordanceProviderClient.Affordance>> { + return observeUri(Contract.AffordanceTable.URI).map { queryAffordances() } + } + + override suspend fun querySelections(): List<KeyguardQuickAffordanceProviderClient.Selection> { + return withContext(backgroundDispatcher) { + context.contentResolver + .query( + Contract.SelectionTable.URI, + null, + null, + null, + null, + ) + ?.use { cursor -> + buildList { + val slotIdColumnIndex = + cursor.getColumnIndex(Contract.SelectionTable.Columns.SLOT_ID) + val affordanceIdColumnIndex = + cursor.getColumnIndex(Contract.SelectionTable.Columns.AFFORDANCE_ID) + val affordanceNameColumnIndex = + cursor.getColumnIndex(Contract.SelectionTable.Columns.AFFORDANCE_NAME) + if ( + slotIdColumnIndex == -1 || + affordanceIdColumnIndex == -1 || + affordanceNameColumnIndex == -1 + ) { + return@buildList + } + + while (cursor.moveToNext()) { + add( + KeyguardQuickAffordanceProviderClient.Selection( + slotId = cursor.getString(slotIdColumnIndex), + affordanceId = cursor.getString(affordanceIdColumnIndex), + affordanceName = cursor.getString(affordanceNameColumnIndex), + ) + ) + } + } + } + } + ?: emptyList() + } + + override fun observeSelections(): Flow<List<KeyguardQuickAffordanceProviderClient.Selection>> { + return observeUri(Contract.SelectionTable.URI).map { querySelections() } + } + + override suspend fun deleteSelection( + slotId: String, + affordanceId: String, + ) { + withContext(backgroundDispatcher) { + context.contentResolver.delete( + Contract.SelectionTable.URI, + "${Contract.SelectionTable.Columns.SLOT_ID} = ? AND" + + " ${Contract.SelectionTable.Columns.AFFORDANCE_ID} = ?", + arrayOf( + slotId, + affordanceId, + ), + ) + } + } + + override suspend fun deleteAllSelections( + slotId: String, + ) { + withContext(backgroundDispatcher) { + context.contentResolver.delete( + Contract.SelectionTable.URI, + Contract.SelectionTable.Columns.SLOT_ID, + arrayOf( + slotId, + ), + ) + } + } + + @SuppressLint("UseCompatLoadingForDrawables") + override suspend fun getAffordanceIcon( + @DrawableRes iconResourceId: Int, + tintColor: Int, + ): Drawable { + return withContext(backgroundDispatcher) { + context.packageManager + .getResourcesForApplication(SYSTEM_UI_PACKAGE_NAME) + .getDrawable(iconResourceId, context.theme) + .apply { setTint(tintColor) } + } + } + + private fun observeUri( + uri: Uri, + ): Flow<Unit> { + return callbackFlow { + val observer = + object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + trySend(Unit) + } + } + + context.contentResolver.registerContentObserver( + uri, + /* notifyForDescendants= */ true, + observer, + ) + + awaitClose { context.contentResolver.unregisterContentObserver(observer) } + } + .onStart { emit(Unit) } + } + + companion object { + private const val SYSTEM_UI_PACKAGE_NAME = "com.android.systemui" + } +} diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderContract.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/data/content/KeyguardQuickAffordanceProviderContract.kt index 98d8d3eb9a4a..17be74b08690 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/data/content/KeyguardQuickAffordanceProviderContract.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/data/content/KeyguardQuickAffordanceProviderContract.kt @@ -15,7 +15,7 @@ * */ -package com.android.systemui.shared.keyguard.data.content +package com.android.systemui.shared.quickaffordance.data.content import android.content.ContentResolver import android.net.Uri diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/shared/model/KeyguardQuickAffordanceSlots.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/shared/model/KeyguardQuickAffordanceSlots.kt index 2dc7a280e423..2dc7a280e423 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/shared/model/KeyguardQuickAffordanceSlots.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/quickaffordance/shared/model/KeyguardQuickAffordanceSlots.kt diff --git a/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt b/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt index 5616a00592f2..621b99d6804a 100644 --- a/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt @@ -29,13 +29,15 @@ import android.os.UserHandle import android.util.Log import com.android.systemui.controls.controller.AuxiliaryPersistenceWrapper import com.android.systemui.controls.controller.ControlsFavoritePersistenceWrapper +import com.android.systemui.keyguard.domain.backup.KeyguardQuickAffordanceBackupHelper import com.android.systemui.people.widget.PeopleBackupHelper /** * 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]. + * 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] or + * [Context.getSharedPreferences]. * * 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. @@ -47,9 +49,11 @@ open class BackupHelper : BackupAgentHelper() { internal const val CONTROLS = ControlsFavoritePersistenceWrapper.FILE_NAME private const val NO_OVERWRITE_FILES_BACKUP_KEY = "systemui.files_no_overwrite" private const val PEOPLE_TILES_BACKUP_KEY = "systemui.people.shared_preferences" + private const val KEYGUARD_QUICK_AFFORDANCES_BACKUP_KEY = + "systemui.keyguard.quickaffordance.shared_preferences" val controlsDataLock = Any() const val ACTION_RESTORE_FINISHED = "com.android.systemui.backup.RESTORE_FINISHED" - private const val PERMISSION_SELF = "com.android.systemui.permission.SELF" + const val PERMISSION_SELF = "com.android.systemui.permission.SELF" } override fun onCreate(userHandle: UserHandle, operationType: Int) { @@ -67,17 +71,27 @@ open class BackupHelper : BackupAgentHelper() { } val keys = PeopleBackupHelper.getFilesToBackup() - addHelper(PEOPLE_TILES_BACKUP_KEY, PeopleBackupHelper( - this, userHandle, keys.toTypedArray())) + addHelper( + PEOPLE_TILES_BACKUP_KEY, + PeopleBackupHelper(this, userHandle, keys.toTypedArray()) + ) + addHelper( + KEYGUARD_QUICK_AFFORDANCES_BACKUP_KEY, + KeyguardQuickAffordanceBackupHelper( + context = this, + userId = userHandle.identifier, + ), + ) } 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 - } + 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) } @@ -90,7 +104,9 @@ open class BackupHelper : BackupAgentHelper() { * @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, @@ -115,23 +131,23 @@ open class BackupHelper : BackupAgentHelper() { data: BackupDataOutput?, newState: ParcelFileDescriptor? ) { - synchronized(lock) { - super.performBackup(oldState, data, newState) - } + 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) + 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)) + AuxiliaryPersistenceWrapper.DeletionJobService.getJobForContext(context) + ) } } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt b/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt index 537cbc5a267d..a0a892de0085 100644 --- a/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt +++ b/packages/SystemUI/src/com/android/systemui/broadcast/BroadcastDispatcher.kt @@ -64,8 +64,9 @@ private const val TAG = "BroadcastDispatcher" * from SystemUI. That way the number of calls to [BroadcastReceiver.onReceive] can be reduced for * a given broadcast. * - * Use only for IntentFilters with actions and optionally categories. It does not support, - * permissions, schemes, data types, data authorities or priority different than 0. + * Use only for IntentFilters with actions and optionally categories. It does not support schemes, + * data types, data authorities or priority different than 0. + * * Cannot be used for getting sticky broadcasts (either as return of registering or as re-delivery). * Broadcast handling may be asynchronous *without* calling goAsync(), as it's running within sysui * and doesn't need to worry about being killed. diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt index 29febb6dd0d9..4ae37c51f278 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProvider.kt @@ -29,7 +29,7 @@ import android.util.Log import com.android.systemui.SystemUIAppComponentFactoryBase import com.android.systemui.SystemUIAppComponentFactoryBase.ContextAvailableCallback import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor -import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract as Contract +import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderContract as Contract import javax.inject.Inject import kotlinx.coroutines.runBlocking diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfig.kt index 3c09aab60443..dbc376e62950 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfig.kt @@ -26,14 +26,17 @@ import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import dagger.Lazy +import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf -import javax.inject.Inject @SysUISingleton -class CameraQuickAffordanceConfig @Inject constructor( - @Application private val context: Context, - private val cameraGestureHelper: CameraGestureHelper, +class CameraQuickAffordanceConfig +@Inject +constructor( + @Application private val context: Context, + private val cameraGestureHelper: Lazy<CameraGestureHelper>, ) : KeyguardQuickAffordanceConfig { override val key: String @@ -46,17 +49,23 @@ class CameraQuickAffordanceConfig @Inject constructor( get() = com.android.internal.R.drawable.perm_group_camera override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> - get() = flowOf( - KeyguardQuickAffordanceConfig.LockScreenState.Visible( - icon = Icon.Resource( + get() = + flowOf( + KeyguardQuickAffordanceConfig.LockScreenState.Visible( + icon = + Icon.Resource( com.android.internal.R.drawable.perm_group_camera, ContentDescription.Resource(R.string.accessibility_camera_button) - ) + ) + ) ) - ) - override fun onTriggered(expandable: Expandable?): KeyguardQuickAffordanceConfig.OnTriggeredResult { - cameraGestureHelper.launchCamera(StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE) + override fun onTriggered( + expandable: Expandable? + ): KeyguardQuickAffordanceConfig.OnTriggeredResult { + cameraGestureHelper + .get() + .launchCamera(StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE) return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt index 4477310dca41..98b1a731b82c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt @@ -21,7 +21,7 @@ import android.content.Intent import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.Icon import com.android.systemui.keyguard.shared.quickaffordance.ActivationState -import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract as Contract +import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderContract as Contract import kotlinx.coroutines.flow.Flow /** Defines interface that can act as data source for a single quick affordance model. */ diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt index b29cf45cc709..4f37e5f389ee 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt @@ -18,9 +18,11 @@ package com.android.systemui.keyguard.data.quickaffordance import android.content.Context +import android.content.IntentFilter import android.content.SharedPreferences -import androidx.annotation.VisibleForTesting import com.android.systemui.R +import com.android.systemui.backup.BackupHelper +import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton @@ -28,14 +30,18 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.onStart /** * Manages and provides access to the current "selections" of keyguard quick affordances, answering * the question "which affordances should the keyguard show?". */ +@OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class KeyguardQuickAffordanceSelectionManager @Inject @@ -43,15 +49,10 @@ constructor( @Application context: Context, private val userFileManager: UserFileManager, private val userTracker: UserTracker, + broadcastDispatcher: BroadcastDispatcher, ) { - private val sharedPrefs: SharedPreferences - get() = - userFileManager.getSharedPreferences( - FILE_NAME, - Context.MODE_PRIVATE, - userTracker.userId, - ) + private var sharedPrefs: SharedPreferences = instantiateSharedPrefs() private val userId: Flow<Int> = conflatedCallbackFlow { val callback = @@ -78,21 +79,54 @@ constructor( } } + /** + * Emits an event each time a Backup & Restore restoration job is completed. Does not emit an + * initial value. + */ + private val backupRestorationEvents: Flow<Unit> = + broadcastDispatcher.broadcastFlow( + filter = IntentFilter(BackupHelper.ACTION_RESTORE_FINISHED), + flags = Context.RECEIVER_NOT_EXPORTED, + permission = BackupHelper.PERMISSION_SELF, + ) + /** IDs of affordances to show, indexed by slot ID, and sorted in descending priority order. */ val selections: Flow<Map<String, List<String>>> = - userId.flatMapLatest { - conflatedCallbackFlow { - val listener = - SharedPreferences.OnSharedPreferenceChangeListener { _, _ -> - trySend(getSelections()) - } + combine( + userId, + backupRestorationEvents.onStart { + // We emit an initial event to make sure that the combine emits at least once, + // even + // if we never get a Backup & Restore restoration event (which is the most + // common + // case anyway as restoration really only happens on initial device setup). + emit(Unit) + } + ) { _, _ -> + } + .flatMapLatest { + conflatedCallbackFlow { + // We want to instantiate a new SharedPreferences instance each time either the + // user + // ID changes or we have a backup & restore restoration event. The reason is + // that + // our sharedPrefs instance needs to be replaced with a new one as it depends on + // the + // user ID and when the B&R job completes, the backing file is replaced but the + // existing instance still has a stale in-memory cache. + sharedPrefs = instantiateSharedPrefs() - sharedPrefs.registerOnSharedPreferenceChangeListener(listener) - send(getSelections()) + val listener = + SharedPreferences.OnSharedPreferenceChangeListener { _, _ -> + trySend(getSelections()) + } - awaitClose { sharedPrefs.unregisterOnSharedPreferenceChangeListener(listener) } + sharedPrefs.registerOnSharedPreferenceChangeListener(listener) + send(getSelections()) + + awaitClose { sharedPrefs.unregisterOnSharedPreferenceChangeListener(listener) } + } } - } /** * Returns a snapshot of the IDs of affordances to show, indexed by slot ID, and sorted in @@ -144,9 +178,17 @@ constructor( sharedPrefs.edit().putString(key, value).apply() } + private fun instantiateSharedPrefs(): SharedPreferences { + return userFileManager.getSharedPreferences( + FILE_NAME, + Context.MODE_PRIVATE, + userTracker.userId, + ) + } + companion object { private const val TAG = "KeyguardQuickAffordanceSelectionManager" - @VisibleForTesting const val FILE_NAME = "quick_affordance_selections" + const val FILE_NAME = "quick_affordance_selections" private const val KEY_PREFIX_SLOT = "slot_" private const val SLOT_AFFORDANCES_DELIMITER = ":" private const val AFFORDANCE_DELIMITER = "," diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/backup/KeyguardQuickAffordanceBackupHelper.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/backup/KeyguardQuickAffordanceBackupHelper.kt new file mode 100644 index 000000000000..0e865cee0b76 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/backup/KeyguardQuickAffordanceBackupHelper.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2022 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.keyguard.domain.backup + +import android.app.backup.SharedPreferencesBackupHelper +import android.content.Context +import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager +import com.android.systemui.settings.UserFileManagerImpl + +/** Handles backup & restore for keyguard quick affordances. */ +class KeyguardQuickAffordanceBackupHelper( + context: Context, + userId: Int, +) : + SharedPreferencesBackupHelper( + context, + if (UserFileManagerImpl.isPrimaryUser(userId)) { + KeyguardQuickAffordanceSelectionManager.FILE_NAME + } else { + UserFileManagerImpl.secondaryUserFile( + context = context, + fileName = KeyguardQuickAffordanceSelectionManager.FILE_NAME, + directoryName = UserFileManagerImpl.SHARED_PREFS, + userId = userId, + ) + .also { UserFileManagerImpl.ensureParentDirExists(it) } + .toString() + } + ) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt index 2d94d760cb54..ee7154ff7219 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt @@ -34,8 +34,8 @@ import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentati import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition import com.android.systemui.plugins.ActivityStarter import com.android.systemui.settings.UserTracker -import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots +import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderContract import com.android.systemui.statusbar.policy.KeyguardStateController import dagger.Lazy import javax.inject.Inject diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerImpl.kt index d450afa59c7d..bfba6dfddfac 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerImpl.kt @@ -35,12 +35,14 @@ import java.io.File import javax.inject.Inject /** - * Implementation for retrieving file paths for file storage of system and secondary users. - * Files lie in {File Directory}/UserFileManager/{User Id} for secondary user. - * For system user, we use the conventional {File Directory} + * Implementation for retrieving file paths for file storage of system and secondary users. Files + * lie in {File Directory}/UserFileManager/{User Id} for secondary user. For system user, we use the + * conventional {File Directory} */ @SysUISingleton -class UserFileManagerImpl @Inject constructor( +class UserFileManagerImpl +@Inject +constructor( // Context of system process and system user. private val context: Context, val userManager: UserManager, @@ -49,80 +51,114 @@ class UserFileManagerImpl @Inject constructor( ) : UserFileManager, CoreStartable { companion object { private const val FILES = "files" - @VisibleForTesting internal const val SHARED_PREFS = "shared_prefs" + const val SHARED_PREFS = "shared_prefs" @VisibleForTesting internal const val ID = "UserFileManager" - } - private val broadcastReceiver = object : BroadcastReceiver() { + /** Returns `true` if the given user ID is that for the primary/system user. */ + fun isPrimaryUser(userId: Int): Boolean { + return UserHandle(userId).isSystem + } + /** - * Listen to Intent.ACTION_USER_REMOVED to clear user data. + * Returns a [File] pointing to the correct path for a secondary user ID. + * + * Note that there is no check for the type of user. This should only be called for + * secondary users, never for the system user. For that, make sure to call [isPrimaryUser]. + * + * Note also that there is no guarantee that the parent directory structure for the file + * exists on disk. For that, call [ensureParentDirExists]. + * + * @param context The context + * @param fileName The name of the file + * @param directoryName The name of the directory that would contain the file + * @param userId The ID of the user to build a file path for */ - override fun onReceive(context: Context, intent: Intent) { - if (intent.action == Intent.ACTION_USER_REMOVED) { - clearDeletedUserData() + fun secondaryUserFile( + context: Context, + fileName: String, + directoryName: String, + userId: Int, + ): File { + return Environment.buildPath( + context.filesDir, + ID, + userId.toString(), + directoryName, + fileName, + ) + } + + /** + * Checks to see if parent dir of the file exists. If it does not, we create the parent dirs + * recursively. + */ + fun ensureParentDirExists(file: File) { + val parent = file.parentFile + if (!parent.exists()) { + if (!parent.mkdirs()) { + Log.e(ID, "Could not create parent directory for file: ${file.absolutePath}") + } } } } - /** - * Poll for user-specific directories to delete upon start up. - */ + private val broadcastReceiver = + object : BroadcastReceiver() { + /** Listen to Intent.ACTION_USER_REMOVED to clear user data. */ + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == Intent.ACTION_USER_REMOVED) { + clearDeletedUserData() + } + } + } + + /** Poll for user-specific directories to delete upon start up. */ override fun start() { clearDeletedUserData() - val filter = IntentFilter().apply { - addAction(Intent.ACTION_USER_REMOVED) - } + val filter = IntentFilter().apply { addAction(Intent.ACTION_USER_REMOVED) } broadcastDispatcher.registerReceiver(broadcastReceiver, filter, backgroundExecutor) } - /** - * Return the file based on current user. - */ + /** Return the file based on current user. */ override fun getFile(fileName: String, userId: Int): File { - return if (UserHandle(userId).isSystem) { - Environment.buildPath( - context.filesDir, - fileName - ) + return if (isPrimaryUser(userId)) { + Environment.buildPath(context.filesDir, fileName) } else { - val secondaryFile = Environment.buildPath( - context.filesDir, - ID, - userId.toString(), - FILES, - fileName - ) + val secondaryFile = + secondaryUserFile( + context = context, + userId = userId, + directoryName = FILES, + fileName = fileName, + ) ensureParentDirExists(secondaryFile) secondaryFile } } - /** - * Get shared preferences from user. - */ + /** Get shared preferences from user. */ override fun getSharedPreferences( fileName: String, @Context.PreferencesMode mode: Int, userId: Int ): SharedPreferences { - if (UserHandle(userId).isSystem) { + if (isPrimaryUser(userId)) { return context.getSharedPreferences(fileName, mode) } - val secondaryUserDir = Environment.buildPath( - context.filesDir, - ID, - userId.toString(), - SHARED_PREFS, - fileName - ) + + val secondaryUserDir = + secondaryUserFile( + context = context, + fileName = fileName, + directoryName = SHARED_PREFS, + userId = userId, + ) ensureParentDirExists(secondaryUserDir) return context.getSharedPreferences(secondaryUserDir, mode) } - /** - * Remove dirs for deleted users. - */ + /** Remove dirs for deleted users. */ @VisibleForTesting internal fun clearDeletedUserData() { backgroundExecutor.execute { @@ -133,10 +169,11 @@ class UserFileManagerImpl @Inject constructor( dirsToDelete.forEach { dir -> try { - val dirToDelete = Environment.buildPath( - file, - dir, - ) + val dirToDelete = + Environment.buildPath( + file, + dir, + ) dirToDelete.deleteRecursively() } catch (e: Exception) { Log.e(ID, "Deletion failed.", e) @@ -144,18 +181,4 @@ class UserFileManagerImpl @Inject constructor( } } } - - /** - * Checks to see if parent dir of the file exists. If it does not, we create the parent dirs - * recursively. - */ - @VisibleForTesting - internal fun ensureParentDirExists(file: File) { - val parent = file.parentFile - if (!parent.exists()) { - if (!parent.mkdirs()) { - Log.e(ID, "Could not create parent directory for file: ${file.absolutePath}") - } - } - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt index cedde58746d2..32c5b3f99d41 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardQuickAffordanceProviderTest.kt @@ -36,8 +36,8 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceIn import com.android.systemui.plugins.ActivityStarter import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker -import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract as Contract import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots +import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderContract as Contract import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.FakeSharedPreferences import com.android.systemui.util.mockito.mock @@ -89,6 +89,7 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() { .thenReturn(FakeSharedPreferences()) }, userTracker = userTracker, + broadcastDispatcher = fakeBroadcastDispatcher, ) val quickAffordanceRepository = KeyguardQuickAffordanceRepository( diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt index 623becf166d3..7205f3068abb 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt @@ -37,25 +37,29 @@ class CameraQuickAffordanceConfigTest : SysuiTestCase() { @Mock private lateinit var cameraGestureHelper: CameraGestureHelper @Mock private lateinit var context: Context + private lateinit var underTest: CameraQuickAffordanceConfig @Before fun setUp() { MockitoAnnotations.initMocks(this) - underTest = CameraQuickAffordanceConfig( + + underTest = + CameraQuickAffordanceConfig( context, - cameraGestureHelper, - ) + ) { + cameraGestureHelper + } } @Test fun `affordance triggered -- camera launch called`() { - //when + // When val result = underTest.onTriggered(null) - //then + // Then verify(cameraGestureHelper) - .launchCamera(StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE) + .launchCamera(StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE) assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result) } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt index 8ef921eaa50a..552b8cb96525 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt @@ -89,6 +89,7 @@ class KeyguardQuickAffordanceLegacySettingSyncerTest : SysuiTestCase() { .thenReturn(FakeSharedPreferences()) }, userTracker = FakeUserTracker(), + broadcastDispatcher = fakeBroadcastDispatcher, ) settings = FakeSettings() settings.putInt(Settings.Secure.LOCKSCREEN_SHOW_CONTROLS, 0) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt index d8ee9f113d33..6a2376b5bc4e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.data.quickaffordance +import android.content.Intent import android.content.SharedPreferences import android.content.pm.UserInfo import androidx.test.filters.SmallTest @@ -27,10 +28,15 @@ import com.android.systemui.settings.UserFileManager import com.android.systemui.util.FakeSharedPreferences import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -38,8 +44,12 @@ import org.junit.runners.JUnit4 import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.anyString import org.mockito.Mock +import org.mockito.Mockito.atLeastOnce +import org.mockito.Mockito.clearInvocations +import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(JUnit4::class) class KeyguardQuickAffordanceSelectionManagerTest : SysuiTestCase() { @@ -60,15 +70,23 @@ class KeyguardQuickAffordanceSelectionManagerTest : SysuiTestCase() { sharedPrefs.getOrPut(userId) { FakeSharedPreferences() } } userTracker = FakeUserTracker() + val dispatcher = UnconfinedTestDispatcher() + Dispatchers.setMain(dispatcher) underTest = KeyguardQuickAffordanceSelectionManager( context = context, userFileManager = userFileManager, userTracker = userTracker, + broadcastDispatcher = fakeBroadcastDispatcher, ) } + @After + fun tearDown() { + Dispatchers.resetMain() + } + @Test fun setSelections() = runTest { overrideResource(R.array.config_keyguardQuickAffordanceDefaults, arrayOf<String>()) @@ -318,6 +336,22 @@ class KeyguardQuickAffordanceSelectionManagerTest : SysuiTestCase() { job.cancel() } + @Test + fun `responds to backup and restore by reloading the selections from disk`() = runTest { + overrideResource(R.array.config_keyguardQuickAffordanceDefaults, arrayOf<String>()) + val affordanceIdsBySlotId = mutableListOf<Map<String, List<String>>>() + val job = + launch(UnconfinedTestDispatcher()) { + underTest.selections.toList(affordanceIdsBySlotId) + } + clearInvocations(userFileManager) + + fakeBroadcastDispatcher.registeredReceivers.firstOrNull()?.onReceive(context, Intent()) + + verify(userFileManager, atLeastOnce()).getSharedPreferences(anyString(), anyInt(), anyInt()) + job.cancel() + } + private fun assertSelections( observed: Map<String, List<String>>?, expected: Map<String, List<String>>, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt index 5c75417c3473..652fae968744 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt @@ -76,6 +76,7 @@ class KeyguardQuickAffordanceRepositoryTest : SysuiTestCase() { .thenReturn(FakeSharedPreferences()) }, userTracker = FakeUserTracker(), + broadcastDispatcher = fakeBroadcastDispatcher, ) underTest = diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt index c2650ec455d8..ba7c40b6b381 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt @@ -252,6 +252,7 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { .thenReturn(FakeSharedPreferences()) }, userTracker = userTracker, + broadcastDispatcher = fakeBroadcastDispatcher, ) val quickAffordanceRepository = KeyguardQuickAffordanceRepository( diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt index b79030602368..8d0c4ef4b3da 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt @@ -113,6 +113,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { .thenReturn(FakeSharedPreferences()) }, userTracker = userTracker, + broadcastDispatcher = fakeBroadcastDispatcher, ) val quickAffordanceRepository = KeyguardQuickAffordanceRepository( diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt index 8b166bd89426..32849cdce02e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt @@ -136,6 +136,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() { .thenReturn(FakeSharedPreferences()) }, userTracker = userTracker, + broadcastDispatcher = fakeBroadcastDispatcher, ) val quickAffordanceRepository = KeyguardQuickAffordanceRepository( diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/UserFileManagerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/UserFileManagerImplTest.kt index 6d9b01e28aa4..020a86611552 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/settings/UserFileManagerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/settings/UserFileManagerImplTest.kt @@ -50,24 +50,20 @@ class UserFileManagerImplTest : SysuiTestCase() { lateinit var userFileManager: UserFileManagerImpl lateinit var backgroundExecutor: FakeExecutor - @Mock - lateinit var userManager: UserManager - @Mock - lateinit var broadcastDispatcher: BroadcastDispatcher + @Mock lateinit var userManager: UserManager + @Mock lateinit var broadcastDispatcher: BroadcastDispatcher @Before fun setUp() { MockitoAnnotations.initMocks(this) backgroundExecutor = FakeExecutor(FakeSystemClock()) - userFileManager = UserFileManagerImpl(context, userManager, - broadcastDispatcher, backgroundExecutor) + userFileManager = + UserFileManagerImpl(context, userManager, broadcastDispatcher, backgroundExecutor) } @After fun end() { - val dir = Environment.buildPath( - context.filesDir, - UserFileManagerImpl.ID) + val dir = Environment.buildPath(context.filesDir, UserFileManagerImpl.ID) dir.deleteRecursively() } @@ -82,13 +78,14 @@ class UserFileManagerImplTest : SysuiTestCase() { @Test fun testGetSharedPreferences() { val secondarySharedPref = userFileManager.getSharedPreferences(TEST_FILE_NAME, 0, 11) - val secondaryUserDir = Environment.buildPath( - context.filesDir, - UserFileManagerImpl.ID, - "11", - UserFileManagerImpl.SHARED_PREFS, - TEST_FILE_NAME - ) + val secondaryUserDir = + Environment.buildPath( + context.filesDir, + UserFileManagerImpl.ID, + "11", + UserFileManagerImpl.SHARED_PREFS, + TEST_FILE_NAME + ) assertThat(secondarySharedPref).isNotNull() assertThat(secondaryUserDir.exists()) @@ -101,32 +98,35 @@ class UserFileManagerImplTest : SysuiTestCase() { val userFileManager = spy(userFileManager) userFileManager.start() verify(userFileManager).clearDeletedUserData() - verify(broadcastDispatcher).registerReceiver(any(BroadcastReceiver::class.java), - any(IntentFilter::class.java), - any(Executor::class.java), isNull(), eq(Context.RECEIVER_EXPORTED), isNull()) + verify(broadcastDispatcher) + .registerReceiver( + any(BroadcastReceiver::class.java), + any(IntentFilter::class.java), + any(Executor::class.java), + isNull(), + eq(Context.RECEIVER_EXPORTED), + isNull() + ) } @Test fun testClearDeletedUserData() { - val dir = Environment.buildPath( - context.filesDir, - UserFileManagerImpl.ID, - "11", - "files" - ) + val dir = Environment.buildPath(context.filesDir, UserFileManagerImpl.ID, "11", "files") dir.mkdirs() - val file = Environment.buildPath( - context.filesDir, - UserFileManagerImpl.ID, - "11", - "files", - TEST_FILE_NAME - ) - val secondaryUserDir = Environment.buildPath( - context.filesDir, - UserFileManagerImpl.ID, - "11", - ) + val file = + Environment.buildPath( + context.filesDir, + UserFileManagerImpl.ID, + "11", + "files", + TEST_FILE_NAME + ) + val secondaryUserDir = + Environment.buildPath( + context.filesDir, + UserFileManagerImpl.ID, + "11", + ) file.createNewFile() assertThat(secondaryUserDir.exists()).isTrue() assertThat(file.exists()).isTrue() @@ -139,15 +139,16 @@ class UserFileManagerImplTest : SysuiTestCase() { @Test fun testEnsureParentDirExists() { - val file = Environment.buildPath( - context.filesDir, - UserFileManagerImpl.ID, - "11", - "files", - TEST_FILE_NAME - ) + val file = + Environment.buildPath( + context.filesDir, + UserFileManagerImpl.ID, + "11", + "files", + TEST_FILE_NAME + ) assertThat(file.parentFile.exists()).isFalse() - userFileManager.ensureParentDirExists(file) + UserFileManagerImpl.ensureParentDirExists(file) assertThat(file.parentFile.exists()).isTrue() } } |