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

Commit 84adfd9b authored by Alejandro Nijamkin's avatar Alejandro Nijamkin
Browse files

Removing client and test from the master branch.

On tm-qpr-dev, we want to define these in different ways and in
different CLs. Therefore, we need to remove them from master, to allow
CLs like ag/20470331 and ag/20336783 to be submitted without automerging
conflicts on master.

Bug: 254857637
Test: N/A
Change-Id: Iadad9499145368acec8e3ed64111a8e1d8c6fc86
parent 2bbc8623
Loading
Loading
Loading
Loading
+0 −326
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"
}
+0 −302
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.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"
    }
}