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

Commit 27ef2a2d authored by Fabián Kozynski's avatar Fabián Kozynski
Browse files

Remove qs refactor flag from tile request dialog [3/N]

Test: atest TileServiceRequestControllerTest
Test: manual, add tile through dialog
Flag: EXEMPT FLAG_REMOVAL
Bug: 444180543
Change-Id: I51a1741803aaee121b3716ed00ccec7dd43dbe48
parent 56c7337d
Loading
Loading
Loading
Loading
+7 −10
Original line number Diff line number Diff line
@@ -21,7 +21,6 @@ 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
@@ -30,10 +29,9 @@ 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.FakeTileRequestDialogDelegateFactory
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.external.ui.dialog.tileRequestDialogDelegateFactory
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
@@ -48,8 +46,7 @@ import org.mockito.kotlin.mock

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

    private val userId: Int
@@ -189,7 +186,7 @@ class TileServiceRequestControllerTestComposeOn : SysuiTestCase() {
    fun positiveAction_tileAdded() =
        kosmos.runTest {
            // Not using a real dialog
            tileRequestDialogComposeDelegateFactory = FakeTileRequestDialogComposeDelegateFactory()
            tileRequestDialogDelegateFactory = FakeTileRequestDialogDelegateFactory()

            val callback = Callback()
            val dialog =
@@ -202,7 +199,7 @@ class TileServiceRequestControllerTestComposeOn : SysuiTestCase() {
                    callback,
                )

            tileRequestDialogComposeDelegateFactory.fake.clickListener.onClick(
            tileRequestDialogDelegateFactory.fake.clickListener.onClick(
                dialog,
                DialogInterface.BUTTON_POSITIVE,
            )
@@ -217,7 +214,7 @@ class TileServiceRequestControllerTestComposeOn : SysuiTestCase() {
    fun negativeAction_tileNotAdded() =
        kosmos.runTest {
            // Not using a real dialog
            tileRequestDialogComposeDelegateFactory = FakeTileRequestDialogComposeDelegateFactory()
            tileRequestDialogDelegateFactory = FakeTileRequestDialogDelegateFactory()

            val callback = Callback()
            val dialog =
@@ -230,7 +227,7 @@ class TileServiceRequestControllerTestComposeOn : SysuiTestCase() {
                    callback,
                )

            tileRequestDialogComposeDelegateFactory.fake.clickListener.onClick(
            tileRequestDialogDelegateFactory.fake.clickListener.onClick(
                dialog,
                DialogInterface.BUTTON_NEGATIVE,
            )
+0 −96
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.IUriGrantsManager
import android.content.Context
import android.view.ContextThemeWrapper
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.TextView
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) {

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

    /** 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 {
                    requireViewById<TextView>(R.id.text).apply {
                        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),
                    )
                    isSelected = true
                }
        val spacing = 0
        setView(ll, spacing, spacing, spacing, spacing / 2)
    }

    private fun createTileView(
        tileData: TileData,
        iUriGrantsManager: IUriGrantsManager,
    ): QSTileView {
        val themedContext = ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings)
        val tile = QSTileViewImpl(themedContext, true)
        val state =
            QSTile.BooleanState().apply {
                label = tileData.label
                handlesLongClick = false
                icon =
                    tileData.icon
                        ?.loadDrawableCheckingUriGrant(
                            context,
                            iUriGrantsManager,
                            tileData.callingUid,
                            tileData.packageName,
                        )
                        ?.let { QSTileImpl.DrawableIcon(it) }
                        ?: ResourceIcon.get(R.drawable.android)
                contentDescription = label
            }
        tile.onStateChanged(state)
        tile.post {
            tile.stateDescription = ""
            tile.isClickable = false
            tile.isSelected = true
        }
        return tile
    }
}
+13 −53
Original line number Diff line number Diff line
@@ -17,7 +17,6 @@
package com.android.systemui.qs.external

import android.app.Dialog
import android.app.IUriGrantsManager
import android.app.StatusBarManager
import android.content.ComponentName
import android.content.DialogInterface
@@ -28,9 +27,7 @@ import androidx.annotation.VisibleForTesting
import com.android.internal.statusbar.IAddTileResultCallback
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.qs.QSHost
import com.android.systemui.qs.external.ui.dialog.TileRequestDialogComposeDelegate
import com.android.systemui.qs.flags.QsInCompose
import com.android.systemui.res.R
import com.android.systemui.qs.external.ui.dialog.TileRequestDialogDelegate
import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.commandline.Command
import com.android.systemui.statusbar.commandline.CommandRegistry
@@ -42,22 +39,19 @@ import javax.inject.Inject

private const val TAG = "TileServiceRequestController"

/** Controller to interface between [TileRequestDialog] and [QSHost]. */
/** Controller to interface between [TileRequestDialogDelegate] and [QSHost]. */
class TileServiceRequestController(
    private val qsHost: QSHost,
    private val commandQueue: CommandQueue,
    private val commandRegistry: CommandRegistry,
    private val eventLogger: TileRequestDialogEventLogger,
    private val iUriGrantsManager: IUriGrantsManager,
    private val tileRequestDialogComposeDelegateFactory: TileRequestDialogComposeDelegate.Factory,
    private val dialogCreator: () -> TileRequestDialog = { TileRequestDialog(qsHost.context) },
    private val tileRequestDialogComposeDelegateFactory: TileRequestDialogDelegate.Factory,
) {

    companion object {
        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 TILE_ALREADY_ADDED = StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ALREADY_ADDED
        const val DISMISSED = StatusBarManager.TILE_ADD_REQUEST_RESULT_DIALOG_DISMISSED
    }

@@ -127,12 +121,7 @@ class TileServiceRequestController(
                callback.accept(response)
            }
        val tileData = TileData(callingUid, appName, label, icon, componentName.packageName)
        return if (QsInCompose.isEnabled) {
                createComposeDialog(tileData, dialogResponse)
            } else {
                createDialog(tileData, dialogResponse)
            }
            .also { dialog ->
        return createComposeDialog(tileData, dialogResponse).also { dialog ->
            dialogCanceller = {
                if (packageName == it) {
                    dialog.cancel()
@@ -171,32 +160,6 @@ class TileServiceRequestController(
            }
    }

    private fun createDialog(
        tileData: TileData,
        responseHandler: SingleShotConsumer<Int>,
    ): SystemUIDialog {
        val dialogClickListener =
            DialogInterface.OnClickListener { _, which ->
                if (which == Dialog.BUTTON_POSITIVE) {
                    responseHandler.accept(ADD_TILE)
                } else {
                    responseHandler.accept(DONT_ADD_TILE)
                }
            }
        return dialogCreator().apply {
            setTileData(tileData, iUriGrantsManager)
            setShowForAllUsers(true)
            setCanceledOnTouchOutside(true)
            setOnCancelListener { responseHandler.accept(DISMISSED) }
            // We want this in case the dialog is dismissed without it being cancelled (for example
            // by going home or locking the device). We use a SingleShotConsumer so the response
            // is only sent once, with the first value.
            setOnDismissListener { responseHandler.accept(DISMISSED) }
            setPositiveButton(R.string.qs_tile_request_dialog_add, dialogClickListener)
            setNegativeButton(R.string.qs_tile_request_dialog_not_add, dialogClickListener)
        }
    }

    private fun isTileAlreadyAdded(componentName: ComponentName): Boolean {
        val spec = CustomTile.toSpec(componentName)
        return qsHost.indexOf(spec) != -1
@@ -237,9 +200,7 @@ class TileServiceRequestController(
    constructor(
        private val commandQueue: CommandQueue,
        private val commandRegistry: CommandRegistry,
        private val iUriGrantsManager: IUriGrantsManager,
        private val tileRequestDialogComposeDelegateFactory:
            TileRequestDialogComposeDelegate.Factory,
        private val tileRequestDialogComposeDelegateFactory: TileRequestDialogDelegate.Factory,
    ) {
        fun create(qsHost: QSHost): TileServiceRequestController {
            return TileServiceRequestController(
@@ -247,7 +208,6 @@ class TileServiceRequestController(
                commandQueue,
                commandRegistry,
                TileRequestDialogEventLogger(),
                iUriGrantsManager,
                tileRequestDialogComposeDelegateFactory,
            )
        }
+2 −5
Original line number Diff line number Diff line
@@ -46,7 +46,7 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject

class TileRequestDialogComposeDelegate
class TileRequestDialogDelegate
@AssistedInject
constructor(
    private val sysuiDialogFactory: SystemUIDialogFactory,
@@ -121,9 +121,6 @@ constructor(

    @AssistedFactory
    interface Factory {
        fun create(
            tiledata: TileData,
            dialogListener: OnClickListener,
        ): TileRequestDialogComposeDelegate
        fun create(tiledata: TileData, dialogListener: OnClickListener): TileRequestDialogDelegate
    }
}
+0 −289
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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.IUriGrantsManager
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.graphics.drawable.Icon
import android.testing.TestableLooper
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.plugins.qs.QSTileView
import com.android.systemui.res.R
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import java.util.Arrays
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.anyString
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations

@SmallTest
@RunWith(AndroidJUnit4::class)
@TestableLooper.RunWithLooper
class TileRequestDialogTest : SysuiTestCase() {

    companion object {
        private const val APP_NAME = "App name"
        private const val LABEL = "Label"
        private const val PACKAGE = "package"
        private const val UID = 12345
        private val DEFAULT_ICON = R.drawable.android
    }

    private lateinit var dialog: TileRequestDialog

    @Mock private lateinit var ugm: IUriGrantsManager

    @Before
    fun setUp() {
        MockitoAnnotations.initMocks(this)
        // Create in looper so we can make sure that the tile is fully updated
        TestableLooper.get(this).runWithLooper { dialog = TileRequestDialog(mContext) }
    }

    @After
    fun teardown() {
        if (this::dialog.isInitialized) {
            dialog.dismiss()
        }
    }

    @Test
    fun setTileData_hasCorrectViews() {
        val icon = Icon.createWithResource(mContext, R.drawable.cloud)
        val tileData = TileData(UID, APP_NAME, LABEL, icon, PACKAGE)

        dialog.setTileData(tileData, ugm)
        dialog.show()

        val content = dialog.requireViewById<ViewGroup>(TileRequestDialog.CONTENT_ID)

        assertThat(content.childCount).isEqualTo(2)
        assertThat(content.getChildAt(0)).isInstanceOf(TextView::class.java)
        assertThat(content.getChildAt(1)).isInstanceOf(QSTileView::class.java)
    }

    @Test
    fun setTileData_hasCorrectAppName() {
        val icon = Icon.createWithResource(mContext, R.drawable.cloud)
        val tileData = TileData(UID, APP_NAME, LABEL, icon, PACKAGE)

        dialog.setTileData(tileData, ugm)
        dialog.show()

        val content = dialog.requireViewById<ViewGroup>(TileRequestDialog.CONTENT_ID)
        val text = content.getChildAt(0) as TextView
        assertThat(text.text.toString()).contains(APP_NAME)
    }

    @Test
    fun setTileData_hasCorrectLabel() {
        val icon = Icon.createWithResource(mContext, R.drawable.cloud)
        val tileData = TileData(UID, APP_NAME, LABEL, icon, PACKAGE)

        dialog.setTileData(tileData, ugm)
        dialog.show()

        TestableLooper.get(this).processAllMessages()

        val content = dialog.requireViewById<ViewGroup>(TileRequestDialog.CONTENT_ID)
        val tile = content.getChildAt(1) as QSTileView
        assertThat((tile.label as TextView).text.toString()).isEqualTo(LABEL)
    }

    @Test
    fun setTileData_hasIcon() {
        val icon = Icon.createWithResource(mContext, R.drawable.cloud)
        val tileData = TileData(UID, APP_NAME, LABEL, icon, PACKAGE)

        dialog.setTileData(tileData, ugm)
        dialog.show()

        TestableLooper.get(this).processAllMessages()

        val content = dialog.requireViewById<ViewGroup>(TileRequestDialog.CONTENT_ID)
        val tile = content.getChildAt(1) as QSTileView
        assertThat((tile.icon.iconView as ImageView).drawable).isNotNull()
    }

    @Test
    fun setTileData_nullIcon_hasIcon() {
        val tileData = TileData(UID, APP_NAME, LABEL, null, PACKAGE)

        dialog.setTileData(tileData, ugm)
        dialog.show()

        TestableLooper.get(this).processAllMessages()

        val content = dialog.requireViewById<ViewGroup>(TileRequestDialog.CONTENT_ID)
        val tile = content.getChildAt(1) as QSTileView
        assertThat((tile.icon.iconView as ImageView).drawable).isNotNull()
    }

    @Test
    fun setTileData_hasNoStateDescription() {
        val icon = Icon.createWithResource(mContext, R.drawable.cloud)
        val tileData = TileData(UID, APP_NAME, LABEL, icon, PACKAGE)

        dialog.setTileData(tileData, ugm)
        dialog.show()

        TestableLooper.get(this).processAllMessages()

        val content = dialog.requireViewById<ViewGroup>(TileRequestDialog.CONTENT_ID)
        val tile = content.getChildAt(1) as QSTileView

        assertThat(tile.stateDescription).isEqualTo("")
    }

    @Test
    fun setTileData_tileNotClickable() {
        val icon = Icon.createWithResource(mContext, R.drawable.cloud)
        val tileData = TileData(UID, APP_NAME, LABEL, icon, PACKAGE)

        dialog.setTileData(tileData, ugm)
        dialog.show()

        TestableLooper.get(this).processAllMessages()

        val content = dialog.requireViewById<ViewGroup>(TileRequestDialog.CONTENT_ID)
        val tile = content.getChildAt(1) as QSTileView

        assertThat(tile.isClickable).isFalse()
        assertThat(tile.isLongClickable).isFalse()
    }

    @Test
    fun setTileData_tileHasCorrectContentDescription() {
        val icon = Icon.createWithResource(mContext, R.drawable.cloud)
        val tileData = TileData(UID, APP_NAME, LABEL, icon, PACKAGE)

        dialog.setTileData(tileData, ugm)
        dialog.show()

        TestableLooper.get(this).processAllMessages()

        val content = dialog.requireViewById<ViewGroup>(TileRequestDialog.CONTENT_ID)
        val tile = content.getChildAt(1) as QSTileView

        assertThat(tile.contentDescription).isEqualTo(LABEL)
    }

    @Test
    fun uriIconLoadSuccess_correctIcon() {
        val tintColor = Color.BLACK
        val icon = Mockito.mock(Icon::class.java)
        val drawable = context.getDrawable(R.drawable.cloud)!!.apply { setTint(tintColor) }
        whenever(icon.loadDrawable(any())).thenReturn(drawable)
        whenever(icon.loadDrawableCheckingUriGrant(any(), eq(ugm), anyInt(), anyString()))
            .thenReturn(drawable)

        val size = 100

        val tileData = TileData(UID, APP_NAME, LABEL, icon, PACKAGE)

        dialog.setTileData(tileData, ugm)
        dialog.show()

        TestableLooper.get(this).processAllMessages()

        verify(icon).loadDrawableCheckingUriGrant(any(), eq(ugm), eq(UID), eq(PACKAGE))

        val content = dialog.requireViewById<ViewGroup>(TileRequestDialog.CONTENT_ID)
        val tile = content.getChildAt(1) as QSTileView

        val iconDrawable = (tile.icon.iconView as ImageView).drawable.apply { setTint(tintColor) }

        assertThat(areDrawablesEqual(iconDrawable, drawable, size)).isTrue()
    }

    @Test
    fun uriIconLoadFail_defaultIcon() {
        val tintColor = Color.BLACK
        val icon = Mockito.mock(Icon::class.java)
        val drawable = context.getDrawable(R.drawable.cloud)!!.apply { setTint(tintColor) }
        whenever(icon.loadDrawable(any())).thenReturn(drawable)
        whenever(icon.loadDrawableCheckingUriGrant(any(), eq(ugm), anyInt(), anyString()))
            .thenReturn(null)

        val size = 100

        val tileData = TileData(UID, APP_NAME, LABEL, icon, PACKAGE)

        dialog.setTileData(tileData, ugm)
        dialog.show()

        TestableLooper.get(this).processAllMessages()

        verify(icon).loadDrawableCheckingUriGrant(any(), eq(ugm), eq(UID), eq(PACKAGE))

        val content = dialog.requireViewById<ViewGroup>(TileRequestDialog.CONTENT_ID)
        val tile = content.getChildAt(1) as QSTileView

        val iconDrawable = (tile.icon.iconView as ImageView).drawable.apply { setTint(tintColor) }

        val defaultIcon = context.getDrawable(DEFAULT_ICON)!!.apply { setTint(tintColor) }

        assertThat(areDrawablesEqual(iconDrawable, defaultIcon, size)).isTrue()
    }
}

private fun areDrawablesEqual(drawable1: Drawable, drawable2: Drawable, size: Int = 24): Boolean {
    val bm1 = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
    val bm2 = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)

    val canvas1 = Canvas(bm1)
    val canvas2 = Canvas(bm2)

    drawable1.setBounds(0, 0, size, size)
    drawable2.setBounds(0, 0, size, size)

    drawable1.draw(canvas1)
    drawable2.draw(canvas2)

    return equalBitmaps(bm1, bm2).also {
        bm1.recycle()
        bm2.recycle()
    }
}

private fun equalBitmaps(a: Bitmap, b: Bitmap): Boolean {
    if (a.width != b.width || a.height != b.height) return false
    val w = a.width
    val h = a.height
    val aPix = IntArray(w * h)
    val bPix = IntArray(w * h)
    a.getPixels(aPix, 0, w, 0, 0, w, h)
    b.getPixels(bPix, 0, w, 0, 0, w, h)
    return Arrays.equals(aPix, bPix)
}
Loading