Content provider for quick affordances.
This is the content provider that wallpaper picker will be using to query for slots, affordances, and selections, and for setting or unsetting selections. The CL also includes a contract definition and "client" utility functions that any customer can use to query and update the content provider. Fix: 254857637 Test: end-to-end unit tests included. Manually verified the API by issuing adb shell content commands and seeing affordances appear and disappear on the actual lock screen. Change-Id: I08eefc931dcc7b5133c412f7b97715fedfe84f30
This commit is contained in:
committed by
Ale Nijamkin
parent
dd24109de0
commit
2836309851
@@ -195,6 +195,9 @@
|
||||
<permission android:name="com.android.systemui.permission.FLAGS"
|
||||
android:protectionLevel="signature" />
|
||||
|
||||
<permission android:name="android.permission.ACCESS_KEYGUARD_QUICK_AFFORDANCES"
|
||||
android:protectionLevel="signature|privileged" />
|
||||
|
||||
<!-- Adding Quick Settings tiles -->
|
||||
<uses-permission android:name="android.permission.BIND_QUICK_SETTINGS_TILE" />
|
||||
|
||||
@@ -993,5 +996,12 @@
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="false">
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
android:authorities="com.android.systemui.keyguard.quickaffordance"
|
||||
android:name="com.android.systemui.keyguard.KeyguardQuickAffordanceProvider"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.ACCESS_KEYGUARD_QUICK_AFFORDANCES"
|
||||
/>
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -0,0 +1,326 @@
|
||||
/*
|
||||
* 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.keyguard.data.content
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.ContentObserver
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.os.UserHandle
|
||||
import androidx.annotation.DrawableRes
|
||||
import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract as Contract
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
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
|
||||
|
||||
/** Collection of utility functions for using a content provider implementing the [Contract]. */
|
||||
object 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(
|
||||
context: Context,
|
||||
slotId: String,
|
||||
affordanceId: String,
|
||||
dispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||
) {
|
||||
withContext(dispatcher) {
|
||||
context.contentResolver.insert(
|
||||
Contract.SelectionTable.URI,
|
||||
ContentValues().apply {
|
||||
put(Contract.SelectionTable.Columns.SLOT_ID, slotId)
|
||||
put(Contract.SelectionTable.Columns.AFFORDANCE_ID, affordanceId)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns all available slots supported by the device. */
|
||||
suspend fun querySlots(
|
||||
context: Context,
|
||||
dispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||
): List<Slot> {
|
||||
return withContext(dispatcher) {
|
||||
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(
|
||||
Slot(
|
||||
id = cursor.getString(idColumnIndex),
|
||||
capacity = cursor.getInt(capacityColumnIndex),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
?: emptyList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [Flow] for observing the collection of slots.
|
||||
*
|
||||
* @see [querySlots]
|
||||
*/
|
||||
fun observeSlots(
|
||||
context: Context,
|
||||
dispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||
): Flow<List<Slot>> {
|
||||
return observeUri(
|
||||
context,
|
||||
Contract.SlotTable.URI,
|
||||
)
|
||||
.map { querySlots(context, dispatcher) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all available affordances supported by the device, regardless of current slot
|
||||
* placement.
|
||||
*/
|
||||
suspend fun queryAffordances(
|
||||
context: Context,
|
||||
dispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||
): List<Affordance> {
|
||||
return withContext(dispatcher) {
|
||||
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)
|
||||
if (idColumnIndex == -1 || nameColumnIndex == -1 || iconColumnIndex == -1) {
|
||||
return@buildList
|
||||
}
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
add(
|
||||
Affordance(
|
||||
id = cursor.getString(idColumnIndex),
|
||||
name = cursor.getString(nameColumnIndex),
|
||||
iconResourceId = cursor.getInt(iconColumnIndex),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
?: emptyList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [Flow] for observing the collection of affordances.
|
||||
*
|
||||
* @see [queryAffordances]
|
||||
*/
|
||||
fun observeAffordances(
|
||||
context: Context,
|
||||
dispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||
): Flow<List<Affordance>> {
|
||||
return observeUri(
|
||||
context,
|
||||
Contract.AffordanceTable.URI,
|
||||
)
|
||||
.map { queryAffordances(context, dispatcher) }
|
||||
}
|
||||
|
||||
/** Returns the current slot-affordance selections. */
|
||||
suspend fun querySelections(
|
||||
context: Context,
|
||||
dispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||
): List<Selection> {
|
||||
return withContext(dispatcher) {
|
||||
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)
|
||||
if (slotIdColumnIndex == -1 || affordanceIdColumnIndex == -1) {
|
||||
return@buildList
|
||||
}
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
add(
|
||||
Selection(
|
||||
slotId = cursor.getString(slotIdColumnIndex),
|
||||
affordanceId = cursor.getString(affordanceIdColumnIndex),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
?: emptyList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [Flow] for observing the collection of selections.
|
||||
*
|
||||
* @see [querySelections]
|
||||
*/
|
||||
fun observeSelections(
|
||||
context: Context,
|
||||
dispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||
): Flow<List<Selection>> {
|
||||
return observeUri(
|
||||
context,
|
||||
Contract.SelectionTable.URI,
|
||||
)
|
||||
.map { querySelections(context, dispatcher) }
|
||||
}
|
||||
|
||||
/** Unselects an affordance with the given ID from the slot with the given ID. */
|
||||
suspend fun deleteSelection(
|
||||
context: Context,
|
||||
slotId: String,
|
||||
affordanceId: String,
|
||||
dispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||
) {
|
||||
withContext(dispatcher) {
|
||||
context.contentResolver.delete(
|
||||
Contract.SelectionTable.URI,
|
||||
"${Contract.SelectionTable.Columns.SLOT_ID} = ? AND" +
|
||||
" ${Contract.SelectionTable.Columns.AFFORDANCE_ID} = ?",
|
||||
arrayOf(
|
||||
slotId,
|
||||
affordanceId,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Unselects all affordances from the slot with the given ID. */
|
||||
suspend fun deleteAllSelections(
|
||||
context: Context,
|
||||
slotId: String,
|
||||
dispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||
) {
|
||||
withContext(dispatcher) {
|
||||
context.contentResolver.delete(
|
||||
Contract.SelectionTable.URI,
|
||||
"${Contract.SelectionTable.Columns.SLOT_ID}",
|
||||
arrayOf(
|
||||
slotId,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeUri(
|
||||
context: Context,
|
||||
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,
|
||||
UserHandle.USER_CURRENT,
|
||||
)
|
||||
|
||||
awaitClose { context.contentResolver.unregisterContentObserver(observer) }
|
||||
}
|
||||
.onStart { emit(Unit) }
|
||||
}
|
||||
|
||||
@SuppressLint("UseCompatLoadingForDrawables")
|
||||
suspend fun getAffordanceIcon(
|
||||
context: Context,
|
||||
@DrawableRes iconResourceId: Int,
|
||||
dispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||
): Drawable {
|
||||
return withContext(dispatcher) {
|
||||
context.packageManager
|
||||
.getResourcesForApplication(SYSTEM_UI_PACKAGE_NAME)
|
||||
.getDrawable(iconResourceId)
|
||||
}
|
||||
}
|
||||
|
||||
data class Slot(
|
||||
val id: String,
|
||||
val capacity: Int,
|
||||
)
|
||||
|
||||
data class Affordance(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val iconResourceId: Int,
|
||||
)
|
||||
|
||||
data class Selection(
|
||||
val slotId: String,
|
||||
val affordanceId: String,
|
||||
)
|
||||
|
||||
private const val SYSTEM_UI_PACKAGE_NAME = "com.android.systemui"
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* 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.keyguard.data.content
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.net.Uri
|
||||
|
||||
/** Contract definitions for querying content about keyguard quick affordances. */
|
||||
object KeyguardQuickAffordanceProviderContract {
|
||||
|
||||
const val AUTHORITY = "com.android.systemui.keyguard.quickaffordance"
|
||||
const val PERMISSION = "android.permission.ACCESS_KEYGUARD_QUICK_AFFORDANCES"
|
||||
|
||||
private val BASE_URI: Uri =
|
||||
Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT).authority(AUTHORITY).build()
|
||||
|
||||
/**
|
||||
* Table for slots.
|
||||
*
|
||||
* Slots are positions where affordances can be placed on the lock screen. Affordances that are
|
||||
* placed on slots are said to be "selected". The system supports the idea of multiple
|
||||
* affordances per slot, though the implementation may limit the number of affordances on each
|
||||
* slot.
|
||||
*
|
||||
* Supported operations:
|
||||
* - Query - to know which slots are available, query the [SlotTable.URI] [Uri]. The result set
|
||||
* will contain rows with the [SlotTable.Columns] columns.
|
||||
*/
|
||||
object SlotTable {
|
||||
const val TABLE_NAME = "slots"
|
||||
val URI: Uri = BASE_URI.buildUpon().path(TABLE_NAME).build()
|
||||
|
||||
object Columns {
|
||||
/** String. Unique ID for this slot. */
|
||||
const val ID = "id"
|
||||
/** Integer. The maximum number of affordances that can be placed in the slot. */
|
||||
const val CAPACITY = "capacity"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Table for affordances.
|
||||
*
|
||||
* Affordances are actions/buttons that the user can execute. They are placed on slots on the
|
||||
* lock screen.
|
||||
*
|
||||
* Supported operations:
|
||||
* - Query - to know about all the affordances that are available on the device, regardless of
|
||||
* which ones are currently selected, query the [AffordanceTable.URI] [Uri]. The result set will
|
||||
* contain rows, each with the columns specified in [AffordanceTable.Columns].
|
||||
*/
|
||||
object AffordanceTable {
|
||||
const val TABLE_NAME = "affordances"
|
||||
val URI: Uri = BASE_URI.buildUpon().path(TABLE_NAME).build()
|
||||
|
||||
object Columns {
|
||||
/** String. Unique ID for this affordance. */
|
||||
const val ID = "id"
|
||||
/** String. User-visible name for this affordance. */
|
||||
const val NAME = "name"
|
||||
/**
|
||||
* Integer. Resource ID for the drawable to load for this affordance. This is a resource
|
||||
* ID from the system UI package.
|
||||
*/
|
||||
const val ICON = "icon"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Table for selections.
|
||||
*
|
||||
* Selections are pairs of slot and affordance IDs.
|
||||
*
|
||||
* Supported operations:
|
||||
* - Insert - to insert an affordance and place it in a slot, insert values for the columns into
|
||||
* the [SelectionTable.URI] [Uri]. The maximum capacity rule is enforced by the system.
|
||||
* Selecting a new affordance for a slot that is already full will automatically remove the
|
||||
* oldest affordance from the slot.
|
||||
* - Query - to know which affordances are set on which slots, query the [SelectionTable.URI]
|
||||
* [Uri]. The result set will contain rows, each of which with the columns from
|
||||
* [SelectionTable.Columns].
|
||||
* - Delete - to unselect an affordance, removing it from a slot, delete from the
|
||||
* [SelectionTable.URI] [Uri], passing in values for each column.
|
||||
*/
|
||||
object SelectionTable {
|
||||
const val TABLE_NAME = "selections"
|
||||
val URI: Uri = BASE_URI.buildUpon().path(TABLE_NAME).build()
|
||||
|
||||
object Columns {
|
||||
/** String. Unique ID for the slot. */
|
||||
const val SLOT_ID = "slot_id"
|
||||
/** String. Unique ID for the selected affordance. */
|
||||
const val AFFORDANCE_ID = "affordance_id"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package com.android.systemui.dagger;
|
||||
|
||||
import com.android.systemui.keyguard.KeyguardQuickAffordanceProvider;
|
||||
import com.android.systemui.statusbar.QsFrameTranslateModule;
|
||||
|
||||
import dagger.Subcomponent;
|
||||
@@ -42,4 +43,9 @@ public interface ReferenceSysUIComponent extends SysUIComponent {
|
||||
interface Builder extends SysUIComponent.Builder {
|
||||
ReferenceSysUIComponent build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Member injection into the supplied argument.
|
||||
*/
|
||||
void inject(KeyguardQuickAffordanceProvider keyguardQuickAffordanceProvider);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import android.content.ContentProvider
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.UriMatcher
|
||||
import android.content.pm.ProviderInfo
|
||||
import android.database.Cursor
|
||||
import android.database.MatrixCursor
|
||||
import android.net.Uri
|
||||
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 javax.inject.Inject
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class KeyguardQuickAffordanceProvider :
|
||||
ContentProvider(), SystemUIAppComponentFactoryBase.ContextInitializer {
|
||||
|
||||
@Inject lateinit var interactor: KeyguardQuickAffordanceInteractor
|
||||
|
||||
private lateinit var contextAvailableCallback: ContextAvailableCallback
|
||||
|
||||
private val uriMatcher =
|
||||
UriMatcher(UriMatcher.NO_MATCH).apply {
|
||||
addURI(
|
||||
Contract.AUTHORITY,
|
||||
Contract.SlotTable.TABLE_NAME,
|
||||
MATCH_CODE_ALL_SLOTS,
|
||||
)
|
||||
addURI(
|
||||
Contract.AUTHORITY,
|
||||
Contract.AffordanceTable.TABLE_NAME,
|
||||
MATCH_CODE_ALL_AFFORDANCES,
|
||||
)
|
||||
addURI(
|
||||
Contract.AUTHORITY,
|
||||
Contract.SelectionTable.TABLE_NAME,
|
||||
MATCH_CODE_ALL_SELECTIONS,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreate(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun attachInfo(context: Context?, info: ProviderInfo?) {
|
||||
contextAvailableCallback.onContextAvailable(checkNotNull(context))
|
||||
super.attachInfo(context, info)
|
||||
}
|
||||
|
||||
override fun setContextAvailableCallback(callback: ContextAvailableCallback) {
|
||||
contextAvailableCallback = callback
|
||||
}
|
||||
|
||||
override fun getType(uri: Uri): String? {
|
||||
val prefix =
|
||||
when (uriMatcher.match(uri)) {
|
||||
MATCH_CODE_ALL_SLOTS,
|
||||
MATCH_CODE_ALL_AFFORDANCES,
|
||||
MATCH_CODE_ALL_SELECTIONS -> "vnd.android.cursor.dir/vnd."
|
||||
else -> null
|
||||
}
|
||||
|
||||
val tableName =
|
||||
when (uriMatcher.match(uri)) {
|
||||
MATCH_CODE_ALL_SLOTS -> Contract.SlotTable.TABLE_NAME
|
||||
MATCH_CODE_ALL_AFFORDANCES -> Contract.AffordanceTable.TABLE_NAME
|
||||
MATCH_CODE_ALL_SELECTIONS -> Contract.SelectionTable.TABLE_NAME
|
||||
else -> null
|
||||
}
|
||||
|
||||
if (prefix == null || tableName == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return "$prefix${Contract.AUTHORITY}.$tableName"
|
||||
}
|
||||
|
||||
override fun insert(uri: Uri, values: ContentValues?): Uri? {
|
||||
if (uriMatcher.match(uri) != MATCH_CODE_ALL_SELECTIONS) {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
return insertSelection(values)
|
||||
}
|
||||
|
||||
override fun query(
|
||||
uri: Uri,
|
||||
projection: Array<out String>?,
|
||||
selection: String?,
|
||||
selectionArgs: Array<out String>?,
|
||||
sortOrder: String?,
|
||||
): Cursor? {
|
||||
return when (uriMatcher.match(uri)) {
|
||||
MATCH_CODE_ALL_AFFORDANCES -> queryAffordances()
|
||||
MATCH_CODE_ALL_SLOTS -> querySlots()
|
||||
MATCH_CODE_ALL_SELECTIONS -> querySelections()
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
override fun update(
|
||||
uri: Uri,
|
||||
values: ContentValues?,
|
||||
selection: String?,
|
||||
selectionArgs: Array<out String>?,
|
||||
): Int {
|
||||
Log.e(TAG, "Update is not supported!")
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun delete(
|
||||
uri: Uri,
|
||||
selection: String?,
|
||||
selectionArgs: Array<out String>?,
|
||||
): Int {
|
||||
if (uriMatcher.match(uri) != MATCH_CODE_ALL_SELECTIONS) {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
return deleteSelection(uri, selectionArgs)
|
||||
}
|
||||
|
||||
private fun insertSelection(values: ContentValues?): Uri? {
|
||||
if (values == null) {
|
||||
throw IllegalArgumentException("Cannot insert selection, no values passed in!")
|
||||
}
|
||||
|
||||
if (!values.containsKey(Contract.SelectionTable.Columns.SLOT_ID)) {
|
||||
throw IllegalArgumentException(
|
||||
"Cannot insert selection, " +
|
||||
"\"${Contract.SelectionTable.Columns.SLOT_ID}\" not specified!"
|
||||
)
|
||||
}
|
||||
|
||||
if (!values.containsKey(Contract.SelectionTable.Columns.AFFORDANCE_ID)) {
|
||||
throw IllegalArgumentException(
|
||||
"Cannot insert selection, " +
|
||||
"\"${Contract.SelectionTable.Columns.AFFORDANCE_ID}\" not specified!"
|
||||
)
|
||||
}
|
||||
|
||||
val slotId = values.getAsString(Contract.SelectionTable.Columns.SLOT_ID)
|
||||
val affordanceId = values.getAsString(Contract.SelectionTable.Columns.AFFORDANCE_ID)
|
||||
|
||||
if (slotId.isNullOrEmpty()) {
|
||||
throw IllegalArgumentException("Cannot insert selection, slot ID was empty!")
|
||||
}
|
||||
|
||||
if (affordanceId.isNullOrEmpty()) {
|
||||
throw IllegalArgumentException("Cannot insert selection, affordance ID was empty!")
|
||||
}
|
||||
|
||||
val success = runBlocking {
|
||||
interactor.select(
|
||||
slotId = slotId,
|
||||
affordanceId = affordanceId,
|
||||
)
|
||||
}
|
||||
|
||||
return if (success) {
|
||||
Log.d(TAG, "Successfully selected $affordanceId for slot $slotId")
|
||||
context?.contentResolver?.notifyChange(Contract.SelectionTable.URI, null)
|
||||
Contract.SelectionTable.URI
|
||||
} else {
|
||||
Log.d(TAG, "Failed to select $affordanceId for slot $slotId")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun querySelections(): Cursor {
|
||||
return MatrixCursor(
|
||||
arrayOf(
|
||||
Contract.SelectionTable.Columns.SLOT_ID,
|
||||
Contract.SelectionTable.Columns.AFFORDANCE_ID,
|
||||
)
|
||||
)
|
||||
.apply {
|
||||
val affordanceIdsBySlotId = runBlocking { interactor.getSelections() }
|
||||
affordanceIdsBySlotId.entries.forEach { (slotId, affordanceIds) ->
|
||||
affordanceIds.forEach { affordanceId ->
|
||||
addRow(
|
||||
arrayOf(
|
||||
slotId,
|
||||
affordanceId,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun queryAffordances(): Cursor {
|
||||
return MatrixCursor(
|
||||
arrayOf(
|
||||
Contract.AffordanceTable.Columns.ID,
|
||||
Contract.AffordanceTable.Columns.NAME,
|
||||
Contract.AffordanceTable.Columns.ICON,
|
||||
)
|
||||
)
|
||||
.apply {
|
||||
interactor.getAffordancePickerRepresentations().forEach { representation ->
|
||||
addRow(
|
||||
arrayOf(
|
||||
representation.id,
|
||||
representation.name,
|
||||
representation.iconResourceId,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun querySlots(): Cursor {
|
||||
return MatrixCursor(
|
||||
arrayOf(
|
||||
Contract.SlotTable.Columns.ID,
|
||||
Contract.SlotTable.Columns.CAPACITY,
|
||||
)
|
||||
)
|
||||
.apply {
|
||||
interactor.getSlotPickerRepresentations().forEach { representation ->
|
||||
addRow(
|
||||
arrayOf(
|
||||
representation.id,
|
||||
representation.maxSelectedAffordances,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteSelection(
|
||||
uri: Uri,
|
||||
selectionArgs: Array<out String>?,
|
||||
): Int {
|
||||
if (selectionArgs == null) {
|
||||
throw IllegalArgumentException(
|
||||
"Cannot delete selection, selection arguments not included!"
|
||||
)
|
||||
}
|
||||
|
||||
val (slotId, affordanceId) =
|
||||
when (selectionArgs.size) {
|
||||
1 -> Pair(selectionArgs[0], null)
|
||||
2 -> Pair(selectionArgs[0], selectionArgs[1])
|
||||
else ->
|
||||
throw IllegalArgumentException(
|
||||
"Cannot delete selection, selection arguments has wrong size, expected to" +
|
||||
" have 1 or 2 arguments, had ${selectionArgs.size} instead!"
|
||||
)
|
||||
}
|
||||
|
||||
val deleted = runBlocking {
|
||||
interactor.unselect(
|
||||
slotId = slotId,
|
||||
affordanceId = affordanceId,
|
||||
)
|
||||
}
|
||||
|
||||
return if (deleted) {
|
||||
Log.d(TAG, "Successfully unselected $affordanceId for slot $slotId")
|
||||
context?.contentResolver?.notifyChange(uri, null)
|
||||
1
|
||||
} else {
|
||||
Log.d(TAG, "Failed to unselect $affordanceId for slot $slotId")
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "KeyguardQuickAffordanceProvider"
|
||||
private const val MATCH_CODE_ALL_SLOTS = 1
|
||||
private const val MATCH_CODE_ALL_AFFORDANCES = 2
|
||||
private const val MATCH_CODE_ALL_SELECTIONS = 3
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.ProviderInfo
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.internal.widget.LockPatternUtils
|
||||
import com.android.systemui.SystemUIAppComponentFactoryBase
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.flags.FakeFeatureFlags
|
||||
import com.android.systemui.flags.Flags
|
||||
import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig
|
||||
import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager
|
||||
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
|
||||
import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
|
||||
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
|
||||
import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
|
||||
import com.android.systemui.plugins.ActivityStarter
|
||||
import com.android.systemui.settings.UserTracker
|
||||
import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderClient as Client
|
||||
import com.android.systemui.shared.keyguard.data.content.KeyguardQuickAffordanceProviderContract as Contract
|
||||
import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
|
||||
import com.android.systemui.statusbar.policy.KeyguardStateController
|
||||
import com.android.systemui.util.mockito.mock
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.JUnit4
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.verify
|
||||
import org.mockito.MockitoAnnotations
|
||||
|
||||
@SmallTest
|
||||
@RunWith(JUnit4::class)
|
||||
class KeyguardQuickAffordanceProviderTest : SysuiTestCase() {
|
||||
|
||||
@Mock private lateinit var lockPatternUtils: LockPatternUtils
|
||||
@Mock private lateinit var keyguardStateController: KeyguardStateController
|
||||
@Mock private lateinit var userTracker: UserTracker
|
||||
@Mock private lateinit var activityStarter: ActivityStarter
|
||||
|
||||
private lateinit var underTest: KeyguardQuickAffordanceProvider
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
|
||||
underTest = KeyguardQuickAffordanceProvider()
|
||||
val quickAffordanceRepository =
|
||||
KeyguardQuickAffordanceRepository(
|
||||
scope = CoroutineScope(IMMEDIATE),
|
||||
backgroundDispatcher = IMMEDIATE,
|
||||
selectionManager = KeyguardQuickAffordanceSelectionManager(),
|
||||
configs =
|
||||
setOf(
|
||||
FakeKeyguardQuickAffordanceConfig(
|
||||
key = AFFORDANCE_1,
|
||||
pickerIconResourceId = 1,
|
||||
),
|
||||
FakeKeyguardQuickAffordanceConfig(
|
||||
key = AFFORDANCE_2,
|
||||
pickerIconResourceId = 2,
|
||||
),
|
||||
),
|
||||
)
|
||||
underTest.interactor =
|
||||
KeyguardQuickAffordanceInteractor(
|
||||
keyguardInteractor =
|
||||
KeyguardInteractor(
|
||||
repository = FakeKeyguardRepository(),
|
||||
),
|
||||
registry = mock(),
|
||||
lockPatternUtils = lockPatternUtils,
|
||||
keyguardStateController = keyguardStateController,
|
||||
userTracker = userTracker,
|
||||
activityStarter = activityStarter,
|
||||
featureFlags =
|
||||
FakeFeatureFlags().apply {
|
||||
set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, true)
|
||||
},
|
||||
repository = { quickAffordanceRepository },
|
||||
)
|
||||
|
||||
underTest.attachInfoForTesting(
|
||||
context,
|
||||
ProviderInfo().apply { authority = Contract.AUTHORITY },
|
||||
)
|
||||
context.contentResolver.addProvider(Contract.AUTHORITY, underTest)
|
||||
context.testablePermissions.setPermission(
|
||||
Contract.PERMISSION,
|
||||
PackageManager.PERMISSION_GRANTED,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onAttachInfo - reportsContext`() {
|
||||
val callback: SystemUIAppComponentFactoryBase.ContextAvailableCallback = mock()
|
||||
underTest.setContextAvailableCallback(callback)
|
||||
|
||||
underTest.attachInfo(context, null)
|
||||
|
||||
verify(callback).onContextAvailable(context)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getType() {
|
||||
assertThat(underTest.getType(Contract.AffordanceTable.URI))
|
||||
.isEqualTo(
|
||||
"vnd.android.cursor.dir/vnd." +
|
||||
"${Contract.AUTHORITY}.${Contract.AffordanceTable.TABLE_NAME}"
|
||||
)
|
||||
assertThat(underTest.getType(Contract.SlotTable.URI))
|
||||
.isEqualTo(
|
||||
"vnd.android.cursor.dir/vnd.${Contract.AUTHORITY}.${Contract.SlotTable.TABLE_NAME}"
|
||||
)
|
||||
assertThat(underTest.getType(Contract.SelectionTable.URI))
|
||||
.isEqualTo(
|
||||
"vnd.android.cursor.dir/vnd." +
|
||||
"${Contract.AUTHORITY}.${Contract.SelectionTable.TABLE_NAME}"
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `insert and query selection`() =
|
||||
runBlocking(IMMEDIATE) {
|
||||
val slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START
|
||||
val affordanceId = AFFORDANCE_2
|
||||
|
||||
Client.insertSelection(
|
||||
context = context,
|
||||
slotId = slotId,
|
||||
affordanceId = affordanceId,
|
||||
dispatcher = IMMEDIATE,
|
||||
)
|
||||
|
||||
assertThat(
|
||||
Client.querySelections(
|
||||
context = context,
|
||||
dispatcher = IMMEDIATE,
|
||||
)
|
||||
)
|
||||
.isEqualTo(
|
||||
listOf(
|
||||
Client.Selection(
|
||||
slotId = slotId,
|
||||
affordanceId = affordanceId,
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `query slots`() =
|
||||
runBlocking(IMMEDIATE) {
|
||||
assertThat(
|
||||
Client.querySlots(
|
||||
context = context,
|
||||
dispatcher = IMMEDIATE,
|
||||
)
|
||||
)
|
||||
.isEqualTo(
|
||||
listOf(
|
||||
Client.Slot(
|
||||
id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
|
||||
capacity = 1,
|
||||
),
|
||||
Client.Slot(
|
||||
id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
|
||||
capacity = 1,
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `query affordances`() =
|
||||
runBlocking(IMMEDIATE) {
|
||||
assertThat(
|
||||
Client.queryAffordances(
|
||||
context = context,
|
||||
dispatcher = IMMEDIATE,
|
||||
)
|
||||
)
|
||||
.isEqualTo(
|
||||
listOf(
|
||||
Client.Affordance(
|
||||
id = AFFORDANCE_1,
|
||||
name = AFFORDANCE_1,
|
||||
iconResourceId = 1,
|
||||
),
|
||||
Client.Affordance(
|
||||
id = AFFORDANCE_2,
|
||||
name = AFFORDANCE_2,
|
||||
iconResourceId = 2,
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `delete and query selection`() =
|
||||
runBlocking(IMMEDIATE) {
|
||||
Client.insertSelection(
|
||||
context = context,
|
||||
slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
|
||||
affordanceId = AFFORDANCE_1,
|
||||
dispatcher = IMMEDIATE,
|
||||
)
|
||||
Client.insertSelection(
|
||||
context = context,
|
||||
slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
|
||||
affordanceId = AFFORDANCE_2,
|
||||
dispatcher = IMMEDIATE,
|
||||
)
|
||||
|
||||
Client.deleteSelection(
|
||||
context = context,
|
||||
slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
|
||||
affordanceId = AFFORDANCE_2,
|
||||
dispatcher = IMMEDIATE,
|
||||
)
|
||||
|
||||
assertThat(
|
||||
Client.querySelections(
|
||||
context = context,
|
||||
dispatcher = IMMEDIATE,
|
||||
)
|
||||
)
|
||||
.isEqualTo(
|
||||
listOf(
|
||||
Client.Selection(
|
||||
slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
|
||||
affordanceId = AFFORDANCE_1,
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `delete all selections in a slot`() =
|
||||
runBlocking(IMMEDIATE) {
|
||||
Client.insertSelection(
|
||||
context = context,
|
||||
slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
|
||||
affordanceId = AFFORDANCE_1,
|
||||
dispatcher = IMMEDIATE,
|
||||
)
|
||||
Client.insertSelection(
|
||||
context = context,
|
||||
slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
|
||||
affordanceId = AFFORDANCE_2,
|
||||
dispatcher = IMMEDIATE,
|
||||
)
|
||||
|
||||
Client.deleteAllSelections(
|
||||
context = context,
|
||||
slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
|
||||
dispatcher = IMMEDIATE,
|
||||
)
|
||||
|
||||
assertThat(
|
||||
Client.querySelections(
|
||||
context = context,
|
||||
dispatcher = IMMEDIATE,
|
||||
)
|
||||
)
|
||||
.isEqualTo(
|
||||
listOf(
|
||||
Client.Selection(
|
||||
slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
|
||||
affordanceId = AFFORDANCE_1,
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val IMMEDIATE = Dispatchers.Main.immediate
|
||||
private const val AFFORDANCE_1 = "affordance_1"
|
||||
private const val AFFORDANCE_2 = "affordance_2"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user