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>
This commit is contained in:
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.content.ContentResolver
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
@@ -29,13 +29,15 @@ import android.os.UserHandle
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.android.systemui.controls.controller.AuxiliaryPersistenceWrapper
|
import com.android.systemui.controls.controller.AuxiliaryPersistenceWrapper
|
||||||
import com.android.systemui.controls.controller.ControlsFavoritePersistenceWrapper
|
import com.android.systemui.controls.controller.ControlsFavoritePersistenceWrapper
|
||||||
|
import com.android.systemui.keyguard.domain.backup.KeyguardQuickAffordanceBackupHelper
|
||||||
import com.android.systemui.people.widget.PeopleBackupHelper
|
import com.android.systemui.people.widget.PeopleBackupHelper
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper for backing up elements in SystemUI
|
* Helper for backing up elements in SystemUI
|
||||||
*
|
*
|
||||||
* This helper is invoked by BackupManager whenever a backup or restore is required in SystemUI.
|
* This helper is invoked by BackupManager whenever a backup or restore is required in SystemUI. The
|
||||||
* The helper can be used to back up any element that is stored in [Context.getFilesDir].
|
* 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,
|
* 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.
|
* indicating that restoring is finished for a given user.
|
||||||
@@ -47,9 +49,11 @@ open class BackupHelper : BackupAgentHelper() {
|
|||||||
internal const val CONTROLS = ControlsFavoritePersistenceWrapper.FILE_NAME
|
internal const val CONTROLS = ControlsFavoritePersistenceWrapper.FILE_NAME
|
||||||
private const val NO_OVERWRITE_FILES_BACKUP_KEY = "systemui.files_no_overwrite"
|
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 PEOPLE_TILES_BACKUP_KEY = "systemui.people.shared_preferences"
|
||||||
|
private const val KEYGUARD_QUICK_AFFORDANCES_BACKUP_KEY =
|
||||||
|
"systemui.keyguard.quickaffordance.shared_preferences"
|
||||||
val controlsDataLock = Any()
|
val controlsDataLock = Any()
|
||||||
const val ACTION_RESTORE_FINISHED = "com.android.systemui.backup.RESTORE_FINISHED"
|
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) {
|
override fun onCreate(userHandle: UserHandle, operationType: Int) {
|
||||||
@@ -67,17 +71,27 @@ open class BackupHelper : BackupAgentHelper() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val keys = PeopleBackupHelper.getFilesToBackup()
|
val keys = PeopleBackupHelper.getFilesToBackup()
|
||||||
addHelper(PEOPLE_TILES_BACKUP_KEY, PeopleBackupHelper(
|
addHelper(
|
||||||
this, userHandle, keys.toTypedArray()))
|
PEOPLE_TILES_BACKUP_KEY,
|
||||||
|
PeopleBackupHelper(this, userHandle, keys.toTypedArray())
|
||||||
|
)
|
||||||
|
addHelper(
|
||||||
|
KEYGUARD_QUICK_AFFORDANCES_BACKUP_KEY,
|
||||||
|
KeyguardQuickAffordanceBackupHelper(
|
||||||
|
context = this,
|
||||||
|
userId = userHandle.identifier,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRestoreFinished() {
|
override fun onRestoreFinished() {
|
||||||
super.onRestoreFinished()
|
super.onRestoreFinished()
|
||||||
val intent = Intent(ACTION_RESTORE_FINISHED).apply {
|
val intent =
|
||||||
`package` = packageName
|
Intent(ACTION_RESTORE_FINISHED).apply {
|
||||||
putExtra(Intent.EXTRA_USER_ID, userId)
|
`package` = packageName
|
||||||
flags = Intent.FLAG_RECEIVER_REGISTERED_ONLY
|
putExtra(Intent.EXTRA_USER_ID, userId)
|
||||||
}
|
flags = Intent.FLAG_RECEIVER_REGISTERED_ONLY
|
||||||
|
}
|
||||||
sendBroadcastAsUser(intent, UserHandle.SYSTEM, PERMISSION_SELF)
|
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 lock a lock to hold while backing up and restoring the files.
|
||||||
* @property context the context of the [BackupAgent]
|
* @property context the context of the [BackupAgent]
|
||||||
* @property fileNamesAndPostProcess a map from the filenames to back up and the post processing
|
* @property fileNamesAndPostProcess a map from the filenames to back up and the post processing
|
||||||
|
* ```
|
||||||
* actions to take
|
* actions to take
|
||||||
|
* ```
|
||||||
*/
|
*/
|
||||||
private class NoOverwriteFileBackupHelper(
|
private class NoOverwriteFileBackupHelper(
|
||||||
val lock: Any,
|
val lock: Any,
|
||||||
@@ -115,23 +131,23 @@ open class BackupHelper : BackupAgentHelper() {
|
|||||||
data: BackupDataOutput?,
|
data: BackupDataOutput?,
|
||||||
newState: ParcelFileDescriptor?
|
newState: ParcelFileDescriptor?
|
||||||
) {
|
) {
|
||||||
synchronized(lock) {
|
synchronized(lock) { super.performBackup(oldState, data, newState) }
|
||||||
super.performBackup(oldState, data, newState)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPPControlsFile(context: Context): () -> Unit {
|
private fun getPPControlsFile(context: Context): () -> Unit {
|
||||||
return {
|
return {
|
||||||
val filesDir = context.filesDir
|
val filesDir = context.filesDir
|
||||||
val file = Environment.buildPath(filesDir, BackupHelper.CONTROLS)
|
val file = Environment.buildPath(filesDir, BackupHelper.CONTROLS)
|
||||||
if (file.exists()) {
|
if (file.exists()) {
|
||||||
val dest = Environment.buildPath(filesDir,
|
val dest =
|
||||||
AuxiliaryPersistenceWrapper.AUXILIARY_FILE_NAME)
|
Environment.buildPath(filesDir, AuxiliaryPersistenceWrapper.AUXILIARY_FILE_NAME)
|
||||||
file.copyTo(dest)
|
file.copyTo(dest)
|
||||||
val jobScheduler = context.getSystemService(JobScheduler::class.java)
|
val jobScheduler = context.getSystemService(JobScheduler::class.java)
|
||||||
jobScheduler?.schedule(
|
jobScheduler?.schedule(
|
||||||
AuxiliaryPersistenceWrapper.DeletionJobService.getJobForContext(context))
|
AuxiliaryPersistenceWrapper.DeletionJobService.getJobForContext(context)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,8 +64,9 @@ private const val TAG = "BroadcastDispatcher"
|
|||||||
* from SystemUI. That way the number of calls to [BroadcastReceiver.onReceive] can be reduced for
|
* from SystemUI. That way the number of calls to [BroadcastReceiver.onReceive] can be reduced for
|
||||||
* a given broadcast.
|
* a given broadcast.
|
||||||
*
|
*
|
||||||
* Use only for IntentFilters with actions and optionally categories. It does not support,
|
* Use only for IntentFilters with actions and optionally categories. It does not support schemes,
|
||||||
* permissions, schemes, data types, data authorities or priority different than 0.
|
* 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).
|
* 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
|
* Broadcast handling may be asynchronous *without* calling goAsync(), as it's running within sysui
|
||||||
* and doesn't need to worry about being killed.
|
* and doesn't need to worry about being killed.
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import android.util.Log
|
|||||||
import com.android.systemui.SystemUIAppComponentFactoryBase
|
import com.android.systemui.SystemUIAppComponentFactoryBase
|
||||||
import com.android.systemui.SystemUIAppComponentFactoryBase.ContextAvailableCallback
|
import com.android.systemui.SystemUIAppComponentFactoryBase.ContextAvailableCallback
|
||||||
import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
|
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 javax.inject.Inject
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
|
|||||||
@@ -26,14 +26,17 @@ import com.android.systemui.common.shared.model.ContentDescription
|
|||||||
import com.android.systemui.common.shared.model.Icon
|
import com.android.systemui.common.shared.model.Icon
|
||||||
import com.android.systemui.dagger.SysUISingleton
|
import com.android.systemui.dagger.SysUISingleton
|
||||||
import com.android.systemui.dagger.qualifiers.Application
|
import com.android.systemui.dagger.qualifiers.Application
|
||||||
|
import dagger.Lazy
|
||||||
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@SysUISingleton
|
@SysUISingleton
|
||||||
class CameraQuickAffordanceConfig @Inject constructor(
|
class CameraQuickAffordanceConfig
|
||||||
@Application private val context: Context,
|
@Inject
|
||||||
private val cameraGestureHelper: CameraGestureHelper,
|
constructor(
|
||||||
|
@Application private val context: Context,
|
||||||
|
private val cameraGestureHelper: Lazy<CameraGestureHelper>,
|
||||||
) : KeyguardQuickAffordanceConfig {
|
) : KeyguardQuickAffordanceConfig {
|
||||||
|
|
||||||
override val key: String
|
override val key: String
|
||||||
@@ -46,17 +49,23 @@ class CameraQuickAffordanceConfig @Inject constructor(
|
|||||||
get() = com.android.internal.R.drawable.perm_group_camera
|
get() = com.android.internal.R.drawable.perm_group_camera
|
||||||
|
|
||||||
override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState>
|
override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState>
|
||||||
get() = flowOf(
|
get() =
|
||||||
KeyguardQuickAffordanceConfig.LockScreenState.Visible(
|
flowOf(
|
||||||
icon = Icon.Resource(
|
KeyguardQuickAffordanceConfig.LockScreenState.Visible(
|
||||||
|
icon =
|
||||||
|
Icon.Resource(
|
||||||
com.android.internal.R.drawable.perm_group_camera,
|
com.android.internal.R.drawable.perm_group_camera,
|
||||||
ContentDescription.Resource(R.string.accessibility_camera_button)
|
ContentDescription.Resource(R.string.accessibility_camera_button)
|
||||||
)
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
override fun onTriggered(expandable: Expandable?): KeyguardQuickAffordanceConfig.OnTriggeredResult {
|
override fun onTriggered(
|
||||||
cameraGestureHelper.launchCamera(StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE)
|
expandable: Expandable?
|
||||||
|
): KeyguardQuickAffordanceConfig.OnTriggeredResult {
|
||||||
|
cameraGestureHelper
|
||||||
|
.get()
|
||||||
|
.launchCamera(StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE)
|
||||||
return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
|
return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import android.content.Intent
|
|||||||
import com.android.systemui.animation.Expandable
|
import com.android.systemui.animation.Expandable
|
||||||
import com.android.systemui.common.shared.model.Icon
|
import com.android.systemui.common.shared.model.Icon
|
||||||
import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
|
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
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
/** Defines interface that can act as data source for a single quick affordance model. */
|
/** Defines interface that can act as data source for a single quick affordance model. */
|
||||||
|
|||||||
@@ -18,9 +18,11 @@
|
|||||||
package com.android.systemui.keyguard.data.quickaffordance
|
package com.android.systemui.keyguard.data.quickaffordance
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.IntentFilter
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import androidx.annotation.VisibleForTesting
|
|
||||||
import com.android.systemui.R
|
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.ChannelExt.trySendWithFailureLogging
|
||||||
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
|
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
|
||||||
import com.android.systemui.dagger.SysUISingleton
|
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.UserFileManager
|
||||||
import com.android.systemui.settings.UserTracker
|
import com.android.systemui.settings.UserTracker
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.onStart
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages and provides access to the current "selections" of keyguard quick affordances, answering
|
* Manages and provides access to the current "selections" of keyguard quick affordances, answering
|
||||||
* the question "which affordances should the keyguard show?".
|
* the question "which affordances should the keyguard show?".
|
||||||
*/
|
*/
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@SysUISingleton
|
@SysUISingleton
|
||||||
class KeyguardQuickAffordanceSelectionManager
|
class KeyguardQuickAffordanceSelectionManager
|
||||||
@Inject
|
@Inject
|
||||||
@@ -43,15 +49,10 @@ constructor(
|
|||||||
@Application context: Context,
|
@Application context: Context,
|
||||||
private val userFileManager: UserFileManager,
|
private val userFileManager: UserFileManager,
|
||||||
private val userTracker: UserTracker,
|
private val userTracker: UserTracker,
|
||||||
|
broadcastDispatcher: BroadcastDispatcher,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val sharedPrefs: SharedPreferences
|
private var sharedPrefs: SharedPreferences = instantiateSharedPrefs()
|
||||||
get() =
|
|
||||||
userFileManager.getSharedPreferences(
|
|
||||||
FILE_NAME,
|
|
||||||
Context.MODE_PRIVATE,
|
|
||||||
userTracker.userId,
|
|
||||||
)
|
|
||||||
|
|
||||||
private val userId: Flow<Int> = conflatedCallbackFlow {
|
private val userId: Flow<Int> = conflatedCallbackFlow {
|
||||||
val callback =
|
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. */
|
/** IDs of affordances to show, indexed by slot ID, and sorted in descending priority order. */
|
||||||
val selections: Flow<Map<String, List<String>>> =
|
val selections: Flow<Map<String, List<String>>> =
|
||||||
userId.flatMapLatest {
|
combine(
|
||||||
conflatedCallbackFlow {
|
userId,
|
||||||
val listener =
|
backupRestorationEvents.onStart {
|
||||||
SharedPreferences.OnSharedPreferenceChangeListener { _, _ ->
|
// We emit an initial event to make sure that the combine emits at least once,
|
||||||
trySend(getSelections())
|
// even
|
||||||
}
|
// if we never get a Backup & Restore restoration event (which is the most
|
||||||
|
// common
|
||||||
sharedPrefs.registerOnSharedPreferenceChangeListener(listener)
|
// case anyway as restoration really only happens on initial device setup).
|
||||||
send(getSelections())
|
emit(Unit)
|
||||||
|
}
|
||||||
awaitClose { sharedPrefs.unregisterOnSharedPreferenceChangeListener(listener) }
|
) { _, _ ->
|
||||||
|
}
|
||||||
|
.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
|
* 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()
|
sharedPrefs.edit().putString(key, value).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun instantiateSharedPrefs(): SharedPreferences {
|
||||||
|
return userFileManager.getSharedPreferences(
|
||||||
|
FILE_NAME,
|
||||||
|
Context.MODE_PRIVATE,
|
||||||
|
userTracker.userId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "KeyguardQuickAffordanceSelectionManager"
|
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 KEY_PREFIX_SLOT = "slot_"
|
||||||
private const val SLOT_AFFORDANCES_DELIMITER = ":"
|
private const val SLOT_AFFORDANCES_DELIMITER = ":"
|
||||||
private const val AFFORDANCE_DELIMITER = ","
|
private const val AFFORDANCE_DELIMITER = ","
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -34,8 +34,8 @@ import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentati
|
|||||||
import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
|
import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
|
||||||
import com.android.systemui.plugins.ActivityStarter
|
import com.android.systemui.plugins.ActivityStarter
|
||||||
import com.android.systemui.settings.UserTracker
|
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.keyguard.shared.model.KeyguardQuickAffordanceSlots
|
||||||
|
import com.android.systemui.shared.quickaffordance.data.content.KeyguardQuickAffordanceProviderContract
|
||||||
import com.android.systemui.statusbar.policy.KeyguardStateController
|
import com.android.systemui.statusbar.policy.KeyguardStateController
|
||||||
import dagger.Lazy
|
import dagger.Lazy
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|||||||
@@ -35,12 +35,14 @@ import java.io.File
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implementation for retrieving file paths for file storage of system and secondary users.
|
* Implementation for retrieving file paths for file storage of system and secondary users. Files
|
||||||
* Files lie in {File Directory}/UserFileManager/{User Id} for secondary user.
|
* lie in {File Directory}/UserFileManager/{User Id} for secondary user. For system user, we use the
|
||||||
* For system user, we use the conventional {File Directory}
|
* conventional {File Directory}
|
||||||
*/
|
*/
|
||||||
@SysUISingleton
|
@SysUISingleton
|
||||||
class UserFileManagerImpl @Inject constructor(
|
class UserFileManagerImpl
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
// Context of system process and system user.
|
// Context of system process and system user.
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
val userManager: UserManager,
|
val userManager: UserManager,
|
||||||
@@ -49,80 +51,114 @@ class UserFileManagerImpl @Inject constructor(
|
|||||||
) : UserFileManager, CoreStartable {
|
) : UserFileManager, CoreStartable {
|
||||||
companion object {
|
companion object {
|
||||||
private const val FILES = "files"
|
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"
|
@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) {
|
fun secondaryUserFile(
|
||||||
if (intent.action == Intent.ACTION_USER_REMOVED) {
|
context: Context,
|
||||||
clearDeletedUserData()
|
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}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private val broadcastReceiver =
|
||||||
* Poll for user-specific directories to delete upon start up.
|
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() {
|
override fun start() {
|
||||||
clearDeletedUserData()
|
clearDeletedUserData()
|
||||||
val filter = IntentFilter().apply {
|
val filter = IntentFilter().apply { addAction(Intent.ACTION_USER_REMOVED) }
|
||||||
addAction(Intent.ACTION_USER_REMOVED)
|
|
||||||
}
|
|
||||||
broadcastDispatcher.registerReceiver(broadcastReceiver, filter, backgroundExecutor)
|
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 {
|
override fun getFile(fileName: String, userId: Int): File {
|
||||||
return if (UserHandle(userId).isSystem) {
|
return if (isPrimaryUser(userId)) {
|
||||||
Environment.buildPath(
|
Environment.buildPath(context.filesDir, fileName)
|
||||||
context.filesDir,
|
|
||||||
fileName
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
val secondaryFile = Environment.buildPath(
|
val secondaryFile =
|
||||||
context.filesDir,
|
secondaryUserFile(
|
||||||
ID,
|
context = context,
|
||||||
userId.toString(),
|
userId = userId,
|
||||||
FILES,
|
directoryName = FILES,
|
||||||
fileName
|
fileName = fileName,
|
||||||
)
|
)
|
||||||
ensureParentDirExists(secondaryFile)
|
ensureParentDirExists(secondaryFile)
|
||||||
secondaryFile
|
secondaryFile
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Get shared preferences from user. */
|
||||||
* Get shared preferences from user.
|
|
||||||
*/
|
|
||||||
override fun getSharedPreferences(
|
override fun getSharedPreferences(
|
||||||
fileName: String,
|
fileName: String,
|
||||||
@Context.PreferencesMode mode: Int,
|
@Context.PreferencesMode mode: Int,
|
||||||
userId: Int
|
userId: Int
|
||||||
): SharedPreferences {
|
): SharedPreferences {
|
||||||
if (UserHandle(userId).isSystem) {
|
if (isPrimaryUser(userId)) {
|
||||||
return context.getSharedPreferences(fileName, mode)
|
return context.getSharedPreferences(fileName, mode)
|
||||||
}
|
}
|
||||||
val secondaryUserDir = Environment.buildPath(
|
|
||||||
context.filesDir,
|
val secondaryUserDir =
|
||||||
ID,
|
secondaryUserFile(
|
||||||
userId.toString(),
|
context = context,
|
||||||
SHARED_PREFS,
|
fileName = fileName,
|
||||||
fileName
|
directoryName = SHARED_PREFS,
|
||||||
)
|
userId = userId,
|
||||||
|
)
|
||||||
|
|
||||||
ensureParentDirExists(secondaryUserDir)
|
ensureParentDirExists(secondaryUserDir)
|
||||||
return context.getSharedPreferences(secondaryUserDir, mode)
|
return context.getSharedPreferences(secondaryUserDir, mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Remove dirs for deleted users. */
|
||||||
* Remove dirs for deleted users.
|
|
||||||
*/
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
internal fun clearDeletedUserData() {
|
internal fun clearDeletedUserData() {
|
||||||
backgroundExecutor.execute {
|
backgroundExecutor.execute {
|
||||||
@@ -133,10 +169,11 @@ class UserFileManagerImpl @Inject constructor(
|
|||||||
|
|
||||||
dirsToDelete.forEach { dir ->
|
dirsToDelete.forEach { dir ->
|
||||||
try {
|
try {
|
||||||
val dirToDelete = Environment.buildPath(
|
val dirToDelete =
|
||||||
file,
|
Environment.buildPath(
|
||||||
dir,
|
file,
|
||||||
)
|
dir,
|
||||||
|
)
|
||||||
dirToDelete.deleteRecursively()
|
dirToDelete.deleteRecursively()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(ID, "Deletion failed.", e)
|
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}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceIn
|
|||||||
import com.android.systemui.plugins.ActivityStarter
|
import com.android.systemui.plugins.ActivityStarter
|
||||||
import com.android.systemui.settings.UserFileManager
|
import com.android.systemui.settings.UserFileManager
|
||||||
import com.android.systemui.settings.UserTracker
|
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.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.statusbar.policy.KeyguardStateController
|
||||||
import com.android.systemui.util.FakeSharedPreferences
|
import com.android.systemui.util.FakeSharedPreferences
|
||||||
import com.android.systemui.util.mockito.mock
|
import com.android.systemui.util.mockito.mock
|
||||||
@@ -89,6 +89,7 @@ class KeyguardQuickAffordanceProviderTest : SysuiTestCase() {
|
|||||||
.thenReturn(FakeSharedPreferences())
|
.thenReturn(FakeSharedPreferences())
|
||||||
},
|
},
|
||||||
userTracker = userTracker,
|
userTracker = userTracker,
|
||||||
|
broadcastDispatcher = fakeBroadcastDispatcher,
|
||||||
)
|
)
|
||||||
val quickAffordanceRepository =
|
val quickAffordanceRepository =
|
||||||
KeyguardQuickAffordanceRepository(
|
KeyguardQuickAffordanceRepository(
|
||||||
|
|||||||
@@ -37,25 +37,29 @@ class CameraQuickAffordanceConfigTest : SysuiTestCase() {
|
|||||||
|
|
||||||
@Mock private lateinit var cameraGestureHelper: CameraGestureHelper
|
@Mock private lateinit var cameraGestureHelper: CameraGestureHelper
|
||||||
@Mock private lateinit var context: Context
|
@Mock private lateinit var context: Context
|
||||||
|
|
||||||
private lateinit var underTest: CameraQuickAffordanceConfig
|
private lateinit var underTest: CameraQuickAffordanceConfig
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
MockitoAnnotations.initMocks(this)
|
MockitoAnnotations.initMocks(this)
|
||||||
underTest = CameraQuickAffordanceConfig(
|
|
||||||
|
underTest =
|
||||||
|
CameraQuickAffordanceConfig(
|
||||||
context,
|
context,
|
||||||
cameraGestureHelper,
|
) {
|
||||||
)
|
cameraGestureHelper
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `affordance triggered -- camera launch called`() {
|
fun `affordance triggered -- camera launch called`() {
|
||||||
//when
|
// When
|
||||||
val result = underTest.onTriggered(null)
|
val result = underTest.onTriggered(null)
|
||||||
|
|
||||||
//then
|
// Then
|
||||||
verify(cameraGestureHelper)
|
verify(cameraGestureHelper)
|
||||||
.launchCamera(StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE)
|
.launchCamera(StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE)
|
||||||
assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result)
|
assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ class KeyguardQuickAffordanceLegacySettingSyncerTest : SysuiTestCase() {
|
|||||||
.thenReturn(FakeSharedPreferences())
|
.thenReturn(FakeSharedPreferences())
|
||||||
},
|
},
|
||||||
userTracker = FakeUserTracker(),
|
userTracker = FakeUserTracker(),
|
||||||
|
broadcastDispatcher = fakeBroadcastDispatcher,
|
||||||
)
|
)
|
||||||
settings = FakeSettings()
|
settings = FakeSettings()
|
||||||
settings.putInt(Settings.Secure.LOCKSCREEN_SHOW_CONTROLS, 0)
|
settings.putInt(Settings.Secure.LOCKSCREEN_SHOW_CONTROLS, 0)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
package com.android.systemui.keyguard.data.quickaffordance
|
package com.android.systemui.keyguard.data.quickaffordance
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.content.pm.UserInfo
|
import android.content.pm.UserInfo
|
||||||
import androidx.test.filters.SmallTest
|
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.FakeSharedPreferences
|
||||||
import com.android.systemui.util.mockito.whenever
|
import com.android.systemui.util.mockito.whenever
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.toList
|
import kotlinx.coroutines.flow.toList
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||||
|
import kotlinx.coroutines.test.resetMain
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import kotlinx.coroutines.test.setMain
|
||||||
|
import org.junit.After
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
@@ -38,8 +44,12 @@ import org.junit.runners.JUnit4
|
|||||||
import org.mockito.ArgumentMatchers.anyInt
|
import org.mockito.ArgumentMatchers.anyInt
|
||||||
import org.mockito.ArgumentMatchers.anyString
|
import org.mockito.ArgumentMatchers.anyString
|
||||||
import org.mockito.Mock
|
import org.mockito.Mock
|
||||||
|
import org.mockito.Mockito.atLeastOnce
|
||||||
|
import org.mockito.Mockito.clearInvocations
|
||||||
|
import org.mockito.Mockito.verify
|
||||||
import org.mockito.MockitoAnnotations
|
import org.mockito.MockitoAnnotations
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@SmallTest
|
@SmallTest
|
||||||
@RunWith(JUnit4::class)
|
@RunWith(JUnit4::class)
|
||||||
class KeyguardQuickAffordanceSelectionManagerTest : SysuiTestCase() {
|
class KeyguardQuickAffordanceSelectionManagerTest : SysuiTestCase() {
|
||||||
@@ -60,15 +70,23 @@ class KeyguardQuickAffordanceSelectionManagerTest : SysuiTestCase() {
|
|||||||
sharedPrefs.getOrPut(userId) { FakeSharedPreferences() }
|
sharedPrefs.getOrPut(userId) { FakeSharedPreferences() }
|
||||||
}
|
}
|
||||||
userTracker = FakeUserTracker()
|
userTracker = FakeUserTracker()
|
||||||
|
val dispatcher = UnconfinedTestDispatcher()
|
||||||
|
Dispatchers.setMain(dispatcher)
|
||||||
|
|
||||||
underTest =
|
underTest =
|
||||||
KeyguardQuickAffordanceSelectionManager(
|
KeyguardQuickAffordanceSelectionManager(
|
||||||
context = context,
|
context = context,
|
||||||
userFileManager = userFileManager,
|
userFileManager = userFileManager,
|
||||||
userTracker = userTracker,
|
userTracker = userTracker,
|
||||||
|
broadcastDispatcher = fakeBroadcastDispatcher,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
Dispatchers.resetMain()
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun setSelections() = runTest {
|
fun setSelections() = runTest {
|
||||||
overrideResource(R.array.config_keyguardQuickAffordanceDefaults, arrayOf<String>())
|
overrideResource(R.array.config_keyguardQuickAffordanceDefaults, arrayOf<String>())
|
||||||
@@ -318,6 +336,22 @@ class KeyguardQuickAffordanceSelectionManagerTest : SysuiTestCase() {
|
|||||||
job.cancel()
|
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(
|
private fun assertSelections(
|
||||||
observed: Map<String, List<String>>?,
|
observed: Map<String, List<String>>?,
|
||||||
expected: Map<String, List<String>>,
|
expected: Map<String, List<String>>,
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ class KeyguardQuickAffordanceRepositoryTest : SysuiTestCase() {
|
|||||||
.thenReturn(FakeSharedPreferences())
|
.thenReturn(FakeSharedPreferences())
|
||||||
},
|
},
|
||||||
userTracker = FakeUserTracker(),
|
userTracker = FakeUserTracker(),
|
||||||
|
broadcastDispatcher = fakeBroadcastDispatcher,
|
||||||
)
|
)
|
||||||
|
|
||||||
underTest =
|
underTest =
|
||||||
|
|||||||
@@ -252,6 +252,7 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() {
|
|||||||
.thenReturn(FakeSharedPreferences())
|
.thenReturn(FakeSharedPreferences())
|
||||||
},
|
},
|
||||||
userTracker = userTracker,
|
userTracker = userTracker,
|
||||||
|
broadcastDispatcher = fakeBroadcastDispatcher,
|
||||||
)
|
)
|
||||||
val quickAffordanceRepository =
|
val quickAffordanceRepository =
|
||||||
KeyguardQuickAffordanceRepository(
|
KeyguardQuickAffordanceRepository(
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
|
|||||||
.thenReturn(FakeSharedPreferences())
|
.thenReturn(FakeSharedPreferences())
|
||||||
},
|
},
|
||||||
userTracker = userTracker,
|
userTracker = userTracker,
|
||||||
|
broadcastDispatcher = fakeBroadcastDispatcher,
|
||||||
)
|
)
|
||||||
val quickAffordanceRepository =
|
val quickAffordanceRepository =
|
||||||
KeyguardQuickAffordanceRepository(
|
KeyguardQuickAffordanceRepository(
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
|
|||||||
.thenReturn(FakeSharedPreferences())
|
.thenReturn(FakeSharedPreferences())
|
||||||
},
|
},
|
||||||
userTracker = userTracker,
|
userTracker = userTracker,
|
||||||
|
broadcastDispatcher = fakeBroadcastDispatcher,
|
||||||
)
|
)
|
||||||
val quickAffordanceRepository =
|
val quickAffordanceRepository =
|
||||||
KeyguardQuickAffordanceRepository(
|
KeyguardQuickAffordanceRepository(
|
||||||
|
|||||||
@@ -50,24 +50,20 @@ class UserFileManagerImplTest : SysuiTestCase() {
|
|||||||
|
|
||||||
lateinit var userFileManager: UserFileManagerImpl
|
lateinit var userFileManager: UserFileManagerImpl
|
||||||
lateinit var backgroundExecutor: FakeExecutor
|
lateinit var backgroundExecutor: FakeExecutor
|
||||||
@Mock
|
@Mock lateinit var userManager: UserManager
|
||||||
lateinit var userManager: UserManager
|
@Mock lateinit var broadcastDispatcher: BroadcastDispatcher
|
||||||
@Mock
|
|
||||||
lateinit var broadcastDispatcher: BroadcastDispatcher
|
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
MockitoAnnotations.initMocks(this)
|
MockitoAnnotations.initMocks(this)
|
||||||
backgroundExecutor = FakeExecutor(FakeSystemClock())
|
backgroundExecutor = FakeExecutor(FakeSystemClock())
|
||||||
userFileManager = UserFileManagerImpl(context, userManager,
|
userFileManager =
|
||||||
broadcastDispatcher, backgroundExecutor)
|
UserFileManagerImpl(context, userManager, broadcastDispatcher, backgroundExecutor)
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
fun end() {
|
fun end() {
|
||||||
val dir = Environment.buildPath(
|
val dir = Environment.buildPath(context.filesDir, UserFileManagerImpl.ID)
|
||||||
context.filesDir,
|
|
||||||
UserFileManagerImpl.ID)
|
|
||||||
dir.deleteRecursively()
|
dir.deleteRecursively()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,13 +78,14 @@ class UserFileManagerImplTest : SysuiTestCase() {
|
|||||||
@Test
|
@Test
|
||||||
fun testGetSharedPreferences() {
|
fun testGetSharedPreferences() {
|
||||||
val secondarySharedPref = userFileManager.getSharedPreferences(TEST_FILE_NAME, 0, 11)
|
val secondarySharedPref = userFileManager.getSharedPreferences(TEST_FILE_NAME, 0, 11)
|
||||||
val secondaryUserDir = Environment.buildPath(
|
val secondaryUserDir =
|
||||||
context.filesDir,
|
Environment.buildPath(
|
||||||
UserFileManagerImpl.ID,
|
context.filesDir,
|
||||||
"11",
|
UserFileManagerImpl.ID,
|
||||||
UserFileManagerImpl.SHARED_PREFS,
|
"11",
|
||||||
TEST_FILE_NAME
|
UserFileManagerImpl.SHARED_PREFS,
|
||||||
)
|
TEST_FILE_NAME
|
||||||
|
)
|
||||||
|
|
||||||
assertThat(secondarySharedPref).isNotNull()
|
assertThat(secondarySharedPref).isNotNull()
|
||||||
assertThat(secondaryUserDir.exists())
|
assertThat(secondaryUserDir.exists())
|
||||||
@@ -101,32 +98,35 @@ class UserFileManagerImplTest : SysuiTestCase() {
|
|||||||
val userFileManager = spy(userFileManager)
|
val userFileManager = spy(userFileManager)
|
||||||
userFileManager.start()
|
userFileManager.start()
|
||||||
verify(userFileManager).clearDeletedUserData()
|
verify(userFileManager).clearDeletedUserData()
|
||||||
verify(broadcastDispatcher).registerReceiver(any(BroadcastReceiver::class.java),
|
verify(broadcastDispatcher)
|
||||||
any(IntentFilter::class.java),
|
.registerReceiver(
|
||||||
any(Executor::class.java), isNull(), eq(Context.RECEIVER_EXPORTED), isNull())
|
any(BroadcastReceiver::class.java),
|
||||||
|
any(IntentFilter::class.java),
|
||||||
|
any(Executor::class.java),
|
||||||
|
isNull(),
|
||||||
|
eq(Context.RECEIVER_EXPORTED),
|
||||||
|
isNull()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testClearDeletedUserData() {
|
fun testClearDeletedUserData() {
|
||||||
val dir = Environment.buildPath(
|
val dir = Environment.buildPath(context.filesDir, UserFileManagerImpl.ID, "11", "files")
|
||||||
context.filesDir,
|
|
||||||
UserFileManagerImpl.ID,
|
|
||||||
"11",
|
|
||||||
"files"
|
|
||||||
)
|
|
||||||
dir.mkdirs()
|
dir.mkdirs()
|
||||||
val file = Environment.buildPath(
|
val file =
|
||||||
context.filesDir,
|
Environment.buildPath(
|
||||||
UserFileManagerImpl.ID,
|
context.filesDir,
|
||||||
"11",
|
UserFileManagerImpl.ID,
|
||||||
"files",
|
"11",
|
||||||
TEST_FILE_NAME
|
"files",
|
||||||
)
|
TEST_FILE_NAME
|
||||||
val secondaryUserDir = Environment.buildPath(
|
)
|
||||||
context.filesDir,
|
val secondaryUserDir =
|
||||||
UserFileManagerImpl.ID,
|
Environment.buildPath(
|
||||||
"11",
|
context.filesDir,
|
||||||
)
|
UserFileManagerImpl.ID,
|
||||||
|
"11",
|
||||||
|
)
|
||||||
file.createNewFile()
|
file.createNewFile()
|
||||||
assertThat(secondaryUserDir.exists()).isTrue()
|
assertThat(secondaryUserDir.exists()).isTrue()
|
||||||
assertThat(file.exists()).isTrue()
|
assertThat(file.exists()).isTrue()
|
||||||
@@ -139,15 +139,16 @@ class UserFileManagerImplTest : SysuiTestCase() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testEnsureParentDirExists() {
|
fun testEnsureParentDirExists() {
|
||||||
val file = Environment.buildPath(
|
val file =
|
||||||
context.filesDir,
|
Environment.buildPath(
|
||||||
UserFileManagerImpl.ID,
|
context.filesDir,
|
||||||
"11",
|
UserFileManagerImpl.ID,
|
||||||
"files",
|
"11",
|
||||||
TEST_FILE_NAME
|
"files",
|
||||||
)
|
TEST_FILE_NAME
|
||||||
|
)
|
||||||
assertThat(file.parentFile.exists()).isFalse()
|
assertThat(file.parentFile.exists()).isFalse()
|
||||||
userFileManager.ensureParentDirExists(file)
|
UserFileManagerImpl.ensureParentDirExists(file)
|
||||||
assertThat(file.parentFile.exists()).isTrue()
|
assertThat(file.parentFile.exists()).isTrue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user