diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 4267ba2ff0b77..68ff116be4b05 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -195,6 +195,9 @@
+
+
@@ -976,5 +979,12 @@
+
+
diff --git a/packages/SystemUI/compose/features/AndroidManifest.xml b/packages/SystemUI/compose/features/AndroidManifest.xml
index eada40e6a40dc..278a89f7dba33 100644
--- a/packages/SystemUI/compose/features/AndroidManifest.xml
+++ b/packages/SystemUI/compose/features/AndroidManifest.xml
@@ -34,6 +34,11 @@
android:enabled="false"
tools:replace="android:authorities"
tools:node="remove" />
+
"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?,
+ selection: String?,
+ selectionArgs: Array?,
+ 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?,
+ ): Int {
+ Log.e(TAG, "Update is not supported!")
+ return 0
+ }
+
+ override fun delete(
+ uri: Uri,
+ selection: String?,
+ selectionArgs: Array?,
+ ): 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?,
+ ): 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
+ }
+}
diff --git a/packages/SystemUI/tests/AndroidManifest.xml b/packages/SystemUI/tests/AndroidManifest.xml
index ea0b03d1ebfd0..78c28ea23be05 100644
--- a/packages/SystemUI/tests/AndroidManifest.xml
+++ b/packages/SystemUI/tests/AndroidManifest.xml
@@ -140,6 +140,12 @@
tools:replace="android:authorities"
tools:node="remove" />
+
+