Merge "Revert "Content provider for quick affordances.""
This commit is contained in:
@@ -195,9 +195,6 @@
|
||||
<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" />
|
||||
|
||||
@@ -996,12 +993,5 @@
|
||||
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>
|
||||
|
||||
@@ -1,326 +0,0 @@
|
||||
/*
|
||||
* 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"
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
/*
|
||||
* 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,7 +16,6 @@
|
||||
|
||||
package com.android.systemui.dagger;
|
||||
|
||||
import com.android.systemui.keyguard.KeyguardQuickAffordanceProvider;
|
||||
import com.android.systemui.statusbar.QsFrameTranslateModule;
|
||||
|
||||
import dagger.Subcomponent;
|
||||
@@ -43,9 +42,4 @@ public interface ReferenceSysUIComponent extends SysUIComponent {
|
||||
interface Builder extends SysUIComponent.Builder {
|
||||
ReferenceSysUIComponent build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Member injection into the supplied argument.
|
||||
*/
|
||||
void inject(KeyguardQuickAffordanceProvider keyguardQuickAffordanceProvider);
|
||||
}
|
||||
|
||||
@@ -1,297 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
}
|
||||
@@ -1,302 +0,0 @@
|
||||
/*
|
||||
* 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