Merge changes from topic "clsqa-in-themepicker" into tm-qpr-dev am: c5e1cdf699 am: 729c55cd28

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/20561075

Change-Id: I3a9276d1be40db994993db5cf3f33edf5c786572
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Ale Nijamkin
2022-12-01 12:31:41 +00:00
committed by Automerger Merge Worker
22 changed files with 1022 additions and 173 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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. */

View File

@@ -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())
}
sharedPrefs.registerOnSharedPreferenceChangeListener(listener)
send(getSelections())
awaitClose { sharedPrefs.unregisterOnSharedPreferenceChangeListener(listener) }
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()
val listener =
SharedPreferences.OnSharedPreferenceChangeListener { _, _ ->
trySend(getSelections())
}
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 = ","

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -89,6 +89,7 @@ class KeyguardQuickAffordanceLegacySettingSyncerTest : SysuiTestCase() {
.thenReturn(FakeSharedPreferences())
},
userTracker = FakeUserTracker(),
broadcastDispatcher = fakeBroadcastDispatcher,
)
settings = FakeSettings()
settings.putInt(Settings.Secure.LOCKSCREEN_SHOW_CONTROLS, 0)

View File

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

View File

@@ -76,6 +76,7 @@ class KeyguardQuickAffordanceRepositoryTest : SysuiTestCase() {
.thenReturn(FakeSharedPreferences())
},
userTracker = FakeUserTracker(),
broadcastDispatcher = fakeBroadcastDispatcher,
)
underTest =

View File

@@ -252,6 +252,7 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() {
.thenReturn(FakeSharedPreferences())
},
userTracker = userTracker,
broadcastDispatcher = fakeBroadcastDispatcher,
)
val quickAffordanceRepository =
KeyguardQuickAffordanceRepository(

View File

@@ -113,6 +113,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
.thenReturn(FakeSharedPreferences())
},
userTracker = userTracker,
broadcastDispatcher = fakeBroadcastDispatcher,
)
val quickAffordanceRepository =
KeyguardQuickAffordanceRepository(

View File

@@ -136,6 +136,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
.thenReturn(FakeSharedPreferences())
},
userTracker = userTracker,
broadcastDispatcher = fakeBroadcastDispatcher,
)
val quickAffordanceRepository =
KeyguardQuickAffordanceRepository(

View File

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