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

Commit c9b66b17 authored by Fabian Kozynski's avatar Fabian Kozynski Committed by Android (Google) Code Review
Browse files

Merge "Use new tiles in TileRequestDialog" into main

parents b95799a7 9eb44a88
Loading
Loading
Loading
Loading
+276 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.qs.external

import android.app.Dialog
import android.app.StatusBarManager
import android.content.ComponentName
import android.content.DialogInterface
import android.graphics.drawable.Icon
import android.platform.test.annotations.EnableFlags
import android.view.WindowManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.statusbar.IAddTileResultCallback
import com.android.systemui.SysuiTestCase
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.runCurrent
import com.android.systemui.kosmos.runTest
import com.android.systemui.qs.external.ui.dialog.FakeTileRequestDialogComposeDelegateFactory
import com.android.systemui.qs.external.ui.dialog.fake
import com.android.systemui.qs.external.ui.dialog.tileRequestDialogComposeDelegateFactory
import com.android.systemui.qs.flags.QSComposeFragment
import com.android.systemui.qs.pipeline.data.repository.fakeInstalledTilesRepository
import com.android.systemui.qs.pipeline.domain.interactor.currentTilesInteractor
import com.android.systemui.qs.pipeline.shared.TileSpec
import com.android.systemui.runOnMainThreadAndWaitForIdleSync
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import java.util.function.Consumer
import kotlin.test.Test
import org.junit.Before
import org.junit.runner.RunWith
import org.mockito.kotlin.mock

@SmallTest
@RunWith(AndroidJUnit4::class)
@EnableFlags(QSComposeFragment.FLAG_NAME)
class TileServiceRequestControllerTestComposeOn : SysuiTestCase() {
    private val kosmos = testKosmos()

    private val userId: Int
        get() = kosmos.currentTilesInteractor.userId.value

    private val mockIcon: Icon
        get() = mock()

    private val Kosmos.underTest by Kosmos.Fixture { tileServiceRequestController }

    @Before
    fun setup() {
        kosmos.fakeInstalledTilesRepository.setInstalledPackagesForUser(
            userId,
            setOf(TEST_COMPONENT),
        )
        // Start with some tiles, so adding tiles is possible (adding tiles waits until there's
        // at least one tile, to wait for setup).
        kosmos.currentTilesInteractor.setTiles(listOf(TileSpec.create("a")))
        kosmos.runCurrent()
    }

    @Test
    fun tileAlreadyAdded_correctResult() =
        kosmos.runTest {
            // An existing tile
            currentTilesInteractor.setTiles(listOf(TILE_SPEC))
            runCurrent()

            val callback = Callback()
            runOnMainThreadAndWaitForIdleSync {
                val dialog =
                    underTest.requestTileAdd(
                        TEST_UID,
                        TEST_COMPONENT,
                        TEST_APP_NAME,
                        TEST_LABEL,
                        mockIcon,
                        callback,
                    )
                assertThat(dialog).isNull()
            }

            assertThat(callback.lastAccepted).isEqualTo(TILE_ALREADY_ADDED)
            assertThat(currentTilesInteractor.currentTilesSpecs.count { it == TILE_SPEC })
                .isEqualTo(1)
        }

    @Test
    fun cancelDialog_dismissResult_tileNotAdded() =
        kosmos.runTest {
            val callback = Callback()
            val dialog = runOnMainThreadAndWaitForIdleSync {
                underTest.requestTileAdd(
                    TEST_UID,
                    TEST_COMPONENT,
                    TEST_APP_NAME,
                    TEST_LABEL,
                    mockIcon,
                    callback,
                )!!
            }

            runOnMainThreadAndWaitForIdleSync { dialog.cancel() }

            assertThat(callback.lastAccepted).isEqualTo(DISMISSED)
            assertThat(currentTilesInteractor.currentTilesSpecs).doesNotContain(TILE_SPEC)
        }

    @Test
    fun cancelAndThenDismissSendsOnlyOnce() =
        kosmos.runTest {
            // After cancelling, the dialog is dismissed. This tests that only one response
            // is sent.
            val callback = Callback()
            val dialog = runOnMainThreadAndWaitForIdleSync {
                underTest.requestTileAdd(
                    TEST_UID,
                    TEST_COMPONENT,
                    TEST_APP_NAME,
                    TEST_LABEL,
                    mockIcon,
                    callback,
                )!!
            }

            runOnMainThreadAndWaitForIdleSync {
                dialog.cancel()
                dialog.dismiss()
            }

            assertThat(callback.lastAccepted).isEqualTo(DISMISSED)
            assertThat(callback.timesCalled).isEqualTo(1)
        }

    @Test
    fun showAllUsers_set() =
        kosmos.runTest {
            val dialog = runOnMainThreadAndWaitForIdleSync {
                underTest.requestTileAdd(
                    TEST_UID,
                    TEST_COMPONENT,
                    TEST_APP_NAME,
                    TEST_LABEL,
                    mockIcon,
                    Callback(),
                )!!
            }
            onTeardown { dialog.cancel() }

            assertThat(dialog.isShowForAllUsers).isTrue()
        }

    @Test
    fun cancelOnTouchOutside_set() =
        kosmos.runTest {
            val dialog = runOnMainThreadAndWaitForIdleSync {
                underTest.requestTileAdd(
                    TEST_UID,
                    TEST_COMPONENT,
                    TEST_APP_NAME,
                    TEST_LABEL,
                    mockIcon,
                    Callback(),
                )!!
            }
            onTeardown { dialog.cancel() }

            assertThat(dialog.isCancelOnTouchOutside).isTrue()
        }

    @Test
    fun positiveAction_tileAdded() =
        kosmos.runTest {
            // Not using a real dialog
            tileRequestDialogComposeDelegateFactory = FakeTileRequestDialogComposeDelegateFactory()

            val callback = Callback()
            val dialog =
                underTest.requestTileAdd(
                    TEST_UID,
                    TEST_COMPONENT,
                    TEST_APP_NAME,
                    TEST_LABEL,
                    mockIcon,
                    callback,
                )

            tileRequestDialogComposeDelegateFactory.fake.clickListener.onClick(
                dialog,
                DialogInterface.BUTTON_POSITIVE,
            )
            runCurrent()

            assertThat(callback.lastAccepted).isEqualTo(ADD_TILE)
            assertThat(currentTilesInteractor.currentTilesSpecs).hasSize(2)
            assertThat(currentTilesInteractor.currentTilesSpecs.last()).isEqualTo(TILE_SPEC)
        }

    @Test
    fun negativeAction_tileNotAdded() =
        kosmos.runTest {
            // Not using a real dialog
            tileRequestDialogComposeDelegateFactory = FakeTileRequestDialogComposeDelegateFactory()

            val callback = Callback()
            val dialog =
                underTest.requestTileAdd(
                    TEST_UID,
                    TEST_COMPONENT,
                    TEST_APP_NAME,
                    TEST_LABEL,
                    mockIcon,
                    callback,
                )

            tileRequestDialogComposeDelegateFactory.fake.clickListener.onClick(
                dialog,
                DialogInterface.BUTTON_NEGATIVE,
            )
            runCurrent()

            assertThat(callback.lastAccepted).isEqualTo(DONT_ADD_TILE)
            assertThat(currentTilesInteractor.currentTilesSpecs).doesNotContain(TILE_SPEC)
        }

    companion object {
        private val TEST_COMPONENT = ComponentName("test_pkg", "test_cls")
        private val TILE_SPEC = TileSpec.create(TEST_COMPONENT)
        private const val TEST_APP_NAME = "App"
        private const val TEST_LABEL = "Label"
        private const val TEST_UID = 12345

        const val ADD_TILE = StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ADDED
        const val DONT_ADD_TILE = StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_NOT_ADDED
        const val TILE_ALREADY_ADDED = StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ALREADY_ADDED
        const val DISMISSED = StatusBarManager.TILE_ADD_REQUEST_RESULT_DIALOG_DISMISSED
    }

    private class Callback : IAddTileResultCallback.Stub(), Consumer<Int> {
        var lastAccepted: Int? = null
            private set

        var timesCalled = 0
            private set

        override fun accept(t: Int) {
            lastAccepted = t
            timesCalled++
        }

        override fun onTileRequest(r: Int) {
            accept(r)
        }
    }
}

private val Dialog.isShowForAllUsers: Boolean
    get() =
        window!!.attributes.privateFlags and
            WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS != 0

private val Dialog.isCancelOnTouchOutside: Boolean
    get() = window!!.shouldCloseOnTouchOutside()
+150 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.qs.external.ui.viewmodel

import android.content.applicationContext
import android.content.res.mainResources
import android.graphics.drawable.Icon
import android.graphics.drawable.TestStubDrawable
import android.service.quicksettings.Tile
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.app.iUriGrantsManager
import com.android.systemui.SysuiTestCase
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.runCurrent
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testScope
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.qs.external.TileData
import com.android.systemui.qs.panels.ui.viewmodel.toUiState
import com.android.systemui.qs.tileimpl.QSTileImpl
import com.android.systemui.qs.tileimpl.QSTileImpl.ResourceIcon
import com.android.systemui.res.R
import com.android.systemui.testKosmos
import com.google.common.truth.Expect
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.stub

@SmallTest
@RunWith(AndroidJUnit4::class)
class TileRequestDialogViewModelTest : SysuiTestCase() {

    @get:Rule val expect: Expect = Expect.create()

    private val kosmos = testKosmos()

    private val icon: Icon = mock {
        on {
            loadDrawableCheckingUriGrant(
                kosmos.applicationContext,
                kosmos.iUriGrantsManager,
                TEST_UID,
                TEST_PACKAGE,
            )
        } doReturn (loadedDrawable)
    }

    private val tileData = TileData(TEST_UID, TEST_APP_NAME, TEST_LABEL, icon, TEST_PACKAGE)

    private val Kosmos.underTest by
        Kosmos.Fixture { tileRequestDialogViewModelFactory.create(applicationContext, tileData) }

    private val baseResultLegacyState =
        QSTile.State().apply {
            label = TEST_LABEL
            state = Tile.STATE_ACTIVE
            handlesLongClick = false
        }

    @Test
    fun uiState_beforeActivation_hasDefaultIcon_andCorrectData() =
        kosmos.runTest {
            val expectedState =
                baseResultLegacyState.apply { icon = defaultIcon }.toUiState(mainResources)

            with(underTest.uiState) {
                expect.that(label).isEqualTo(TEST_LABEL)
                expect.that(secondaryLabel).isEmpty()
                expect.that(state).isEqualTo(expectedState.state)
                expect.that(handlesLongClick).isFalse()
                expect.that(handlesSecondaryClick).isFalse()
                expect.that(icon.get()).isEqualTo(defaultIcon)
                expect.that(sideDrawable).isNull()
                expect.that(accessibilityUiState).isEqualTo(expectedState.accessibilityUiState)
            }
        }

    @Test
    fun uiState_afterActivation_hasCorrectIcon_andCorrectData() =
        kosmos.runTest {
            val expectedState =
                baseResultLegacyState
                    .apply { icon = QSTileImpl.DrawableIcon(loadedDrawable) }
                    .toUiState(mainResources)

            underTest.activateIn(testScope)
            runCurrent()

            with(underTest.uiState) {
                expect.that(label).isEqualTo(TEST_LABEL)
                expect.that(secondaryLabel).isEmpty()
                expect.that(state).isEqualTo(expectedState.state)
                expect.that(handlesLongClick).isFalse()
                expect.that(handlesSecondaryClick).isFalse()
                expect.that(icon.get()).isEqualTo(QSTileImpl.DrawableIcon(loadedDrawable))
                expect.that(sideDrawable).isNull()
                expect.that(accessibilityUiState).isEqualTo(expectedState.accessibilityUiState)
            }
        }

    @Test
    fun uiState_afterActivation_iconNotLoaded_usesDefault() =
        kosmos.runTest {
            icon.stub {
                on {
                    loadDrawableCheckingUriGrant(
                        kosmos.applicationContext,
                        kosmos.iUriGrantsManager,
                        TEST_UID,
                        TEST_PACKAGE,
                    )
                } doReturn (null)
            }

            underTest.activateIn(testScope)
            runCurrent()

            assertThat(underTest.uiState.icon.get()).isEqualTo(defaultIcon)
        }

    companion object {
        private val defaultIcon: QSTile.Icon = ResourceIcon.get(R.drawable.android)
        private val loadedDrawable = TestStubDrawable("loaded")

        private const val TEST_PACKAGE = "test_pkg"
        private const val TEST_APP_NAME = "App"
        private const val TEST_LABEL = "Label"
        private const val TEST_UID = 12345
    }
}
+36 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 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.qs.external

import android.graphics.drawable.Icon
import androidx.compose.runtime.Immutable

/**
 * Data bundle of information to show the user when requesting to add a TileService
 *
 * @property appName Name of the app requesting their [TileService] to be added.
 * @property label Label of the tile.
 * @property icon Icon for the tile.
 */
@Immutable
data class TileData(
    val callingUid: Int,
    val appName: CharSequence,
    val label: CharSequence,
    val icon: Icon?,
    val packageName: String,
)
+37 −52
Original line number Diff line number Diff line
@@ -18,47 +18,44 @@ package com.android.systemui.qs.external

import android.app.IUriGrantsManager
import android.content.Context
import android.graphics.drawable.Icon
import android.view.ContextThemeWrapper
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.TextView
import com.android.systemui.res.R
import com.android.systemui.plugins.qs.QSTile
import com.android.systemui.plugins.qs.QSTileView
import com.android.systemui.qs.tileimpl.QSTileImpl
import com.android.systemui.qs.tileimpl.QSTileImpl.ResourceIcon
import com.android.systemui.qs.tileimpl.QSTileViewImpl
import com.android.systemui.res.R
import com.android.systemui.statusbar.phone.SystemUIDialog

/**
 * Dialog to present to the user to ask for authorization to add a [TileService].
 */
class TileRequestDialog(
    context: Context,
) : SystemUIDialog(context) {
/** Dialog to present to the user to ask for authorization to add a [TileService]. */
class TileRequestDialog(context: Context) : SystemUIDialog(context) {

    companion object {
        internal val CONTENT_ID = R.id.content
    }

    /**
     * Set the data of the tile to add, to show the user.
     */
    /** Set the data of the tile to add, to show the user. */
    fun setTileData(tileData: TileData, iUriGrantsManager: IUriGrantsManager) {
        val ll = (LayoutInflater
                        .from(context)
                        .inflate(R.layout.tile_service_request_dialog, null)
                        as ViewGroup).apply {
        val ll =
            (LayoutInflater.from(context).inflate(R.layout.tile_service_request_dialog, null)
                    as ViewGroup)
                .apply {
                    requireViewById<TextView>(R.id.text).apply {
                        text = context
                                .getString(R.string.qs_tile_request_dialog_text, tileData.appName)
                        text =
                            context.getString(
                                R.string.qs_tile_request_dialog_text,
                                tileData.appName,
                            )
                    }
                    addView(
                        createTileView(tileData, iUriGrantsManager),
                        context.resources.getDimensionPixelSize(
                                    R.dimen.qs_tile_service_request_tile_width),
                            context.resources.getDimensionPixelSize(R.dimen.qs_quick_tile_size)
                            R.dimen.qs_tile_service_request_tile_width
                        ),
                        context.resources.getDimensionPixelSize(R.dimen.qs_quick_tile_size),
                    )
                    isSelected = true
                }
@@ -72,17 +69,20 @@ class TileRequestDialog(
    ): QSTileView {
        val themedContext = ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings)
        val tile = QSTileViewImpl(themedContext, true)
        val state = QSTile.BooleanState().apply {
        val state =
            QSTile.BooleanState().apply {
                label = tileData.label
                handlesLongClick = false
            icon = tileData.icon?.loadDrawableCheckingUriGrant(
                icon =
                    tileData.icon
                        ?.loadDrawableCheckingUriGrant(
                            context,
                            iUriGrantsManager,
                            tileData.callingUid,
                            tileData.packageName,
            )?.let {
                QSTileImpl.DrawableIcon(it)
            } ?: ResourceIcon.get(R.drawable.android)
                        )
                        ?.let { QSTileImpl.DrawableIcon(it) }
                        ?: ResourceIcon.get(R.drawable.android)
                contentDescription = label
            }
        tile.onStateChanged(state)
@@ -93,19 +93,4 @@ class TileRequestDialog(
        }
        return tile
    }

    /**
     * Data bundle of information to show the user.
     *
     * @property appName Name of the app requesting their [TileService] to be added.
     * @property label Label of the tile.
     * @property icon Icon for the tile.
     */
    data class TileData(
        val callingUid: Int,
        val appName: CharSequence,
        val label: CharSequence,
        val icon: Icon?,
        val packageName: String,
    )
}
+115 −78

File changed.

Preview size limit exceeded, changes collapsed.

Loading