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

Commit 99be1de5 authored by Ale Nijamkin's avatar Ale Nijamkin Committed by Android (Google) Code Review
Browse files

Merge "Content provider for quick affordances." into tm-qpr-dev

parents 1f17bf7e 04777dba
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