Moves client and contract into customization lib.

We want to share this code between System UI and ThemePicker.

Fix: 259728815
Test: System UI and WPPG both build and work end-to-end with
customizable lock screen quick affordances.

Change-Id: If2fe99c5d679da57e00c6fcece10a1d2fc9ca807
This commit is contained in:
Alejandro Nijamkin
2022-11-24 14:03:53 -08:00
parent 6d638a8956
commit e5f67447bc
8 changed files with 674 additions and 5 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,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

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

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

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