Revert "Content provider for quick affordances."

Revert submission 20425400-cherrypick-clsqa-wppg-content-provider-w0hk7thrjq

Reason for revert: b/258619039
Reverted Changes:
I207e4acfa:Adds gradle dependencies for viewmodel.
I08eefc931:Content provider for quick affordances.

Change-Id: I493e44e5acdb463b72de861eb36c4acf71fd4da2
This commit is contained in:
Ale Nijamkin
2022-11-10 15:46:52 +00:00
committed by Android (Google) Code Review
parent 2836309851
commit 5d94227f66
6 changed files with 0 additions and 1052 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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