Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 04777dba authored by Alejandro Nijamkin's avatar Alejandro Nijamkin Committed by Ale Nijamkin
Browse files

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.

Fix: 254857637
Test: Manually verified the API by
issuing adb shell content commands and seeing affordances appear and
disappear on the actual lock screen.

Change-Id: Ia47a8ee322028042bb3e118ddb82a92a27eae191
Merged-In: Ia47a8ee322028042bb3e118ddb82a92a27eae191
parent 463531ee
Loading
Loading
Loading
Loading
+10 −0
Original line number Diff line number Diff line
@@ -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" />

@@ -976,5 +979,12 @@
                <action android:name="com.android.systemui.action.DISMISS_VOLUME_PANEL_DIALOG" />
            </intent-filter>
        </receiver>

        <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>
+5 −0
Original line number Diff line number Diff line
@@ -34,6 +34,11 @@
            android:enabled="false"
            tools:replace="android:authorities"
            tools:node="remove" />
        <provider android:name="com.android.systemui.keyguard.KeyguardQuickAffordanceProvider"
            android:authorities="com.android.systemui.test.keyguard.quickaffordance.disabled"
            android:enabled="false"
            tools:replace="android:authorities"
            tools:node="remove" />
        <provider android:name="com.android.keyguard.clock.ClockOptionsProvider"
            android:authorities="com.android.systemui.test.keyguard.clock.disabled"
            android:enabled="false"
+111 −0
Original line number Diff line number Diff line
/*
 * 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"
        }
    }
}
+6 −0
Original line number Diff line number Diff line
@@ -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);
}
+297 −0
Original line number Diff line number Diff line
/*
 * 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
    }
}
Loading