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

Commit c6c5ae95 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.

The CL also includes a contract definition and "client" utility
functions that any customer can use to query and update the content
provider.

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

Change-Id: Ia47a8ee322028042bb3e118ddb82a92a27eae191
parent 83a9678c
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" />

@@ -993,5 +996,12 @@
                  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>
+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"
+326 −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.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"
}
+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);
}
Loading