diff options
16 files changed, 1224 insertions, 304 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/external/TileServiceRequestControllerTestComposeOn.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/external/TileServiceRequestControllerTestComposeOn.kt new file mode 100644 index 000000000000..f02856c2f5ae --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/external/TileServiceRequestControllerTestComposeOn.kt @@ -0,0 +1,276 @@ +/* + * 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() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/external/ui/viewmodel/TileRequestDialogViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/external/ui/viewmodel/TileRequestDialogViewModelTest.kt new file mode 100644 index 000000000000..369975a95579 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/external/ui/viewmodel/TileRequestDialogViewModelTest.kt @@ -0,0 +1,150 @@ +/* + * 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 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileData.kt b/packages/SystemUI/src/com/android/systemui/qs/external/TileData.kt new file mode 100644 index 000000000000..de759687e012 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileData.kt @@ -0,0 +1,36 @@ +/* + * 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, +) diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileRequestDialog.kt b/packages/SystemUI/src/com/android/systemui/qs/external/TileRequestDialog.kt index c3c587de5a24..5597f288e122 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/TileRequestDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileRequestDialog.kt @@ -18,73 +18,73 @@ 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) + 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, + 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 - } + 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 = "" @@ -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, - ) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceRequestController.kt b/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceRequestController.kt index 08567afd729e..33e059074a81 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceRequestController.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceRequestController.kt @@ -26,9 +26,11 @@ import android.os.RemoteException import android.util.Log import androidx.annotation.VisibleForTesting import com.android.internal.statusbar.IAddTileResultCallback -import com.android.systemui.res.R 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.statusbar.CommandQueue import com.android.systemui.statusbar.commandline.Command import com.android.systemui.statusbar.commandline.CommandRegistry @@ -40,50 +42,50 @@ import javax.inject.Inject private const val TAG = "TileServiceRequestController" -/** - * Controller to interface between [TileRequestDialog] and [QSHost]. - */ +/** Controller to interface between [TileRequestDialog] 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 dialogCreator: () -> TileRequestDialog = { TileRequestDialog(qsHost.context) } + 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) }, ) { companion object { - internal const val ADD_TILE = StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ADDED - internal const val DONT_ADD_TILE = StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_NOT_ADDED - internal const val TILE_ALREADY_ADDED = - StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ALREADY_ADDED - internal const val DISMISSED = StatusBarManager.TILE_ADD_REQUEST_RESULT_DIALOG_DISMISSED + 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 var dialogCanceller: ((String) -> Unit)? = null - private val commandQueueCallback = object : CommandQueue.Callbacks { - override fun requestAddTile( - callingUid: Int, - componentName: ComponentName, - appName: CharSequence, - label: CharSequence, - icon: Icon, - callback: IAddTileResultCallback - ) { - requestTileAdd(callingUid, componentName, appName, label, icon) { - try { - callback.onTileRequest(it) - } catch (e: RemoteException) { - Log.e(TAG, "Couldn't respond to request", e) + private val commandQueueCallback = + object : CommandQueue.Callbacks { + override fun requestAddTile( + callingUid: Int, + componentName: ComponentName, + appName: CharSequence, + label: CharSequence, + icon: Icon, + callback: IAddTileResultCallback, + ) { + requestTileAdd(callingUid, componentName, appName, label, icon) { + try { + callback.onTileRequest(it) + } catch (e: RemoteException) { + Log.e(TAG, "Couldn't respond to request", e) + } } } - } - override fun cancelRequestAddTile(packageName: String) { - dialogCanceller?.invoke(packageName) + override fun cancelRequestAddTile(packageName: String) { + dialogCanceller?.invoke(packageName) + } } - } fun init() { commandRegistry.registerCommand("tile-service-add") { TileServiceRequestCommand() } @@ -100,58 +102,87 @@ class TileServiceRequestController( } @VisibleForTesting - internal fun requestTileAdd( + fun requestTileAdd( callingUid: Int, componentName: ComponentName, appName: CharSequence, label: CharSequence, icon: Icon?, - callback: Consumer<Int> - ) { + callback: Consumer<Int>, + ): SystemUIDialog? { val instanceId = eventLogger.newInstanceId() val packageName = componentName.packageName if (isTileAlreadyAdded(componentName)) { callback.accept(TILE_ALREADY_ADDED) eventLogger.logTileAlreadyAdded(packageName, instanceId) - return + return null } - val dialogResponse = SingleShotConsumer<Int> { response -> - if (response == ADD_TILE) { - addTile(componentName) - } - dialogCanceller = null - eventLogger.logUserResponse(response, packageName, instanceId) - callback.accept(response) - } - val tileData = TileRequestDialog.TileData( - callingUid, - appName, - label, - icon, - componentName.packageName, - ) - createDialog(tileData, dialogResponse).also { dialog -> - dialogCanceller = { - if (packageName == it) { - dialog.cancel() + val dialogResponse = + SingleShotConsumer<Int> { response -> + if (response == ADD_TILE) { + addTile(componentName) } dialogCanceller = null + eventLogger.logUserResponse(response, packageName, instanceId) + 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 -> + dialogCanceller = { + if (packageName == it) { + dialog.cancel() + } + dialogCanceller = null + } + dialog.show() + eventLogger.logDialogShown(packageName, instanceId) + } + } + + private fun createComposeDialog( + 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 tileRequestDialogComposeDelegateFactory + .create(dialogListener = dialogClickListener, tiledata = tileData) + .createDialog() + .apply { + 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) } } - }.show() - eventLogger.logDialogShown(packageName, instanceId) } private fun createDialog( - tileData: TileRequestDialog.TileData, - responseHandler: SingleShotConsumer<Int> + 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) + 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) @@ -173,19 +204,20 @@ class TileServiceRequestController( inner class TileServiceRequestCommand : Command { override fun execute(pw: PrintWriter, args: List<String>) { - val componentName: ComponentName = ComponentName.unflattenFromString(args[0]) + val componentName: ComponentName = + ComponentName.unflattenFromString(args[0]) ?: run { Log.w(TAG, "Malformed componentName ${args[0]}") return } - requestTileAdd(0, componentName, args[1], args[2], null) { - Log.d(TAG, "Response: $it") - } + requestTileAdd(0, componentName, args[1], args[2], null) { Log.d(TAG, "Response: $it") } } override fun help(pw: PrintWriter) { - pw.println("Usage: adb shell cmd statusbar tile-service-add " + - "<componentName> <appName> <label>") + pw.println( + "Usage: adb shell cmd statusbar tile-service-add " + + "<componentName> <appName> <label>" + ) } } @@ -200,18 +232,23 @@ class TileServiceRequestController( } @SysUISingleton - class Builder @Inject constructor( + class Builder + @Inject + constructor( private val commandQueue: CommandQueue, private val commandRegistry: CommandRegistry, private val iUriGrantsManager: IUriGrantsManager, + private val tileRequestDialogComposeDelegateFactory: + TileRequestDialogComposeDelegate.Factory, ) { fun create(qsHost: QSHost): TileServiceRequestController { return TileServiceRequestController( - qsHost, - commandQueue, - commandRegistry, - TileRequestDialogEventLogger(), - iUriGrantsManager, + qsHost, + commandQueue, + commandRegistry, + TileRequestDialogEventLogger(), + iUriGrantsManager, + tileRequestDialogComposeDelegateFactory, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/ui/dialog/TileRequestDialogComposeDelegate.kt b/packages/SystemUI/src/com/android/systemui/qs/external/ui/dialog/TileRequestDialogComposeDelegate.kt new file mode 100644 index 000000000000..446be9b9ebcb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/external/ui/dialog/TileRequestDialogComposeDelegate.kt @@ -0,0 +1,128 @@ +/* + * 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.dialog + +import android.content.DialogInterface.BUTTON_NEGATIVE +import android.content.DialogInterface.BUTTON_POSITIVE +import android.content.DialogInterface.OnClickListener +import androidx.compose.foundation.layout.Arrangement.spacedBy +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.android.compose.PlatformButton +import com.android.compose.PlatformOutlinedButton +import com.android.compose.theme.PlatformTheme +import com.android.systemui.dialog.ui.composable.AlertDialogContent +import com.android.systemui.lifecycle.rememberViewModel +import com.android.systemui.qs.external.TileData +import com.android.systemui.qs.external.ui.viewmodel.TileRequestDialogViewModel +import com.android.systemui.qs.panels.ui.compose.infinitegrid.LargeStaticTile +import com.android.systemui.res.R +import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.statusbar.phone.SystemUIDialogFactory +import com.android.systemui.statusbar.phone.create +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +class TileRequestDialogComposeDelegate +@AssistedInject +constructor( + private val sysuiDialogFactory: SystemUIDialogFactory, + private val tileRequestDialogViewModelFactory: TileRequestDialogViewModel.Factory, + @Assisted private val tileData: TileData, + @Assisted private val dialogListener: OnClickListener, +) : SystemUIDialog.Delegate { + + override fun createDialog(): SystemUIDialog { + return sysuiDialogFactory.create { TileRequestDialogContent(it) } + } + + @Composable + private fun TileRequestDialogContent(dialog: SystemUIDialog) { + PlatformTheme { + AlertDialogContent( + title = {}, + content = { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = spacedBy(16.dp), + ) { + val viewModel = + rememberViewModel(traceName = "TileRequestDialog", key = tileData) { + tileRequestDialogViewModelFactory.create(dialog.context, tileData) + } + + Text( + text = + stringResource( + R.string.qs_tile_request_dialog_text, + tileData.appName, + ), + textAlign = TextAlign.Start, + ) + + LargeStaticTile( + uiState = viewModel.uiState, + modifier = + Modifier.width( + dimensionResource( + id = R.dimen.qs_tile_service_request_tile_width + ) + ), + ) + } + }, + positiveButton = { + PlatformButton( + onClick = { + dialogListener.onClick(dialog, BUTTON_POSITIVE) + dialog.dismiss() + } + ) { + Text(stringResource(R.string.qs_tile_request_dialog_add)) + } + }, + negativeButton = { + PlatformOutlinedButton( + onClick = { + dialogListener.onClick(dialog, BUTTON_NEGATIVE) + dialog.dismiss() + } + ) { + Text(stringResource(R.string.qs_tile_request_dialog_not_add)) + } + }, + ) + } + } + + @AssistedFactory + interface Factory { + fun create( + tiledata: TileData, + dialogListener: OnClickListener, + ): TileRequestDialogComposeDelegate + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/ui/viewmodel/TileRequestDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/external/ui/viewmodel/TileRequestDialogViewModel.kt new file mode 100644 index 000000000000..c756adc07ba4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/external/ui/viewmodel/TileRequestDialogViewModel.kt @@ -0,0 +1,83 @@ +/* + * 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.app.IUriGrantsManager +import android.content.Context +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.lifecycle.ExclusiveActivatable +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.DrawableIcon +import com.android.systemui.qs.tileimpl.QSTileImpl.ResourceIcon +import com.android.systemui.res.R +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.withContext + +class TileRequestDialogViewModel +@AssistedInject +constructor( + private val iUriGrantsManager: IUriGrantsManager, + @Background private val backgroundDispatcher: CoroutineDispatcher, + @Assisted private val dialogContext: Context, + @Assisted private val tileData: TileData, +) : ExclusiveActivatable() { + + private var _icon by mutableStateOf(defaultIcon) + + private val state: QSTile.State + get() = + QSTile.State().apply { + label = tileData.label + handlesLongClick = false + this.icon = _icon + } + + val uiState by derivedStateOf { state.toUiState(dialogContext.resources) } + + override suspend fun onActivated(): Nothing { + withContext(backgroundDispatcher) { + tileData.icon + ?.loadDrawableCheckingUriGrant( + dialogContext, + iUriGrantsManager, + tileData.callingUid, + tileData.packageName, + ) + ?.run { _icon = DrawableIcon(this) } + } + awaitCancellation() + } + + @AssistedFactory + interface Factory { + fun create(dialogContext: Context, tileData: TileData): TileRequestDialogViewModel + } + + companion object { + private val defaultIcon: QSTile.Icon = ResourceIcon.get(R.drawable.android) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt index abdf923ebe73..c798e5bb6dc7 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt @@ -24,6 +24,7 @@ import android.service.quicksettings.Tile.STATE_INACTIVE import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.layout.Arrangement @@ -263,6 +264,28 @@ fun TileContainer( } @Composable +fun LargeStaticTile(uiState: TileUiState, modifier: Modifier = Modifier) { + val colors = TileDefaults.getColorForState(uiState = uiState, iconOnly = false) + + Box( + modifier + .clip(TileDefaults.animateTileShape(state = uiState.state)) + .background(colors.background) + .height(TileHeight) + .tilePadding() + ) { + LargeTileContent( + label = uiState.label, + secondaryLabel = "", + icon = getTileIcon(icon = uiState.icon), + sideDrawable = null, + colors = colors, + squishiness = { 1f }, + ) + } +} + +@Composable private fun getTileIcon(icon: Supplier<QSTile.Icon?>): Icon { val context = LocalContext.current return icon.get()?.let { diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileRequestDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileRequestDialogTest.kt index 2db5e83cf185..d058484de204 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileRequestDialogTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileRequestDialogTest.kt @@ -28,13 +28,14 @@ import android.widget.ImageView import android.widget.TextView import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.systemui.res.R 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 @@ -45,7 +46,6 @@ import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations -import java.util.Arrays @SmallTest @RunWith(AndroidJUnit4::class) @@ -62,16 +62,13 @@ class TileRequestDialogTest : SysuiTestCase() { private lateinit var dialog: TileRequestDialog - @Mock - private lateinit var ugm: IUriGrantsManager + @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) - } + TestableLooper.get(this).runWithLooper { dialog = TileRequestDialog(mContext) } } @After @@ -84,7 +81,7 @@ class TileRequestDialogTest : SysuiTestCase() { @Test fun setTileData_hasCorrectViews() { val icon = Icon.createWithResource(mContext, R.drawable.cloud) - val tileData = TileRequestDialog.TileData(UID, APP_NAME, LABEL, icon, PACKAGE) + val tileData = TileData(UID, APP_NAME, LABEL, icon, PACKAGE) dialog.setTileData(tileData, ugm) dialog.show() @@ -99,7 +96,7 @@ class TileRequestDialogTest : SysuiTestCase() { @Test fun setTileData_hasCorrectAppName() { val icon = Icon.createWithResource(mContext, R.drawable.cloud) - val tileData = TileRequestDialog.TileData(UID, APP_NAME, LABEL, icon, PACKAGE) + val tileData = TileData(UID, APP_NAME, LABEL, icon, PACKAGE) dialog.setTileData(tileData, ugm) dialog.show() @@ -112,7 +109,7 @@ class TileRequestDialogTest : SysuiTestCase() { @Test fun setTileData_hasCorrectLabel() { val icon = Icon.createWithResource(mContext, R.drawable.cloud) - val tileData = TileRequestDialog.TileData(UID, APP_NAME, LABEL, icon, PACKAGE) + val tileData = TileData(UID, APP_NAME, LABEL, icon, PACKAGE) dialog.setTileData(tileData, ugm) dialog.show() @@ -127,7 +124,7 @@ class TileRequestDialogTest : SysuiTestCase() { @Test fun setTileData_hasIcon() { val icon = Icon.createWithResource(mContext, R.drawable.cloud) - val tileData = TileRequestDialog.TileData(UID, APP_NAME, LABEL, icon, PACKAGE) + val tileData = TileData(UID, APP_NAME, LABEL, icon, PACKAGE) dialog.setTileData(tileData, ugm) dialog.show() @@ -141,7 +138,7 @@ class TileRequestDialogTest : SysuiTestCase() { @Test fun setTileData_nullIcon_hasIcon() { - val tileData = TileRequestDialog.TileData(UID, APP_NAME, LABEL, null, PACKAGE) + val tileData = TileData(UID, APP_NAME, LABEL, null, PACKAGE) dialog.setTileData(tileData, ugm) dialog.show() @@ -156,7 +153,7 @@ class TileRequestDialogTest : SysuiTestCase() { @Test fun setTileData_hasNoStateDescription() { val icon = Icon.createWithResource(mContext, R.drawable.cloud) - val tileData = TileRequestDialog.TileData(UID, APP_NAME, LABEL, icon, PACKAGE) + val tileData = TileData(UID, APP_NAME, LABEL, icon, PACKAGE) dialog.setTileData(tileData, ugm) dialog.show() @@ -172,7 +169,7 @@ class TileRequestDialogTest : SysuiTestCase() { @Test fun setTileData_tileNotClickable() { val icon = Icon.createWithResource(mContext, R.drawable.cloud) - val tileData = TileRequestDialog.TileData(UID, APP_NAME, LABEL, icon, PACKAGE) + val tileData = TileData(UID, APP_NAME, LABEL, icon, PACKAGE) dialog.setTileData(tileData, ugm) dialog.show() @@ -189,7 +186,7 @@ class TileRequestDialogTest : SysuiTestCase() { @Test fun setTileData_tileHasCorrectContentDescription() { val icon = Icon.createWithResource(mContext, R.drawable.cloud) - val tileData = TileRequestDialog.TileData(UID, APP_NAME, LABEL, icon, PACKAGE) + val tileData = TileData(UID, APP_NAME, LABEL, icon, PACKAGE) dialog.setTileData(tileData, ugm) dialog.show() @@ -206,20 +203,14 @@ class TileRequestDialogTest : SysuiTestCase() { fun uriIconLoadSuccess_correctIcon() { val tintColor = Color.BLACK val icon = Mockito.mock(Icon::class.java) - val drawable = context.getDrawable(R.drawable.cloud)!!.apply { - setTint(tintColor) - } + 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) + whenever(icon.loadDrawableCheckingUriGrant(any(), eq(ugm), anyInt(), anyString())) + .thenReturn(drawable) val size = 100 - val tileData = TileRequestDialog.TileData(UID, APP_NAME, LABEL, icon, PACKAGE) + val tileData = TileData(UID, APP_NAME, LABEL, icon, PACKAGE) dialog.setTileData(tileData, ugm) dialog.show() @@ -231,9 +222,7 @@ class TileRequestDialogTest : SysuiTestCase() { 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 iconDrawable = (tile.icon.iconView as ImageView).drawable.apply { setTint(tintColor) } assertThat(areDrawablesEqual(iconDrawable, drawable, size)).isTrue() } @@ -242,20 +231,14 @@ class TileRequestDialogTest : SysuiTestCase() { fun uriIconLoadFail_defaultIcon() { val tintColor = Color.BLACK val icon = Mockito.mock(Icon::class.java) - val drawable = context.getDrawable(R.drawable.cloud)!!.apply { - setTint(tintColor) - } + 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) + whenever(icon.loadDrawableCheckingUriGrant(any(), eq(ugm), anyInt(), anyString())) + .thenReturn(null) val size = 100 - val tileData = TileRequestDialog.TileData(UID, APP_NAME, LABEL, icon, PACKAGE) + val tileData = TileData(UID, APP_NAME, LABEL, icon, PACKAGE) dialog.setTileData(tileData, ugm) dialog.show() @@ -267,13 +250,9 @@ class TileRequestDialogTest : SysuiTestCase() { 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 iconDrawable = (tile.icon.iconView as ImageView).drawable.apply { setTint(tintColor) } - val defaultIcon = context.getDrawable(DEFAULT_ICON)!!.apply { - setTint(tintColor) - } + val defaultIcon = context.getDrawable(DEFAULT_ICON)!!.apply { setTint(tintColor) } assertThat(areDrawablesEqual(iconDrawable, defaultIcon, size)).isTrue() } @@ -308,4 +287,3 @@ private fun equalBitmaps(a: Bitmap, b: Bitmap): Boolean { b.getPixels(bPix, 0, w, 0, 0, w, h) return Arrays.equals(aPix, bPix) } - diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceRequestControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceRequestControllerTestComposeOff.kt index 89ec687ad123..82e247714794 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceRequestControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceRequestControllerTestComposeOff.kt @@ -22,6 +22,7 @@ import android.content.ComponentName import android.content.DialogInterface import android.graphics.drawable.Icon import android.os.RemoteException +import android.platform.test.annotations.DisableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.logging.InstanceId @@ -29,8 +30,12 @@ import com.android.internal.statusbar.IAddTileResultCallback import com.android.systemui.InstanceIdSequenceFake import com.android.systemui.SysuiTestCase import com.android.systemui.qs.QSHost +import com.android.systemui.qs.external.ui.dialog.tileRequestDialogComposeDelegateFactory +import com.android.systemui.qs.flags.QSComposeFragment +import com.android.systemui.shade.shared.flag.DualShade import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.commandline.CommandRegistry +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.eq @@ -52,7 +57,8 @@ import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidJUnit4::class) -class TileServiceRequestControllerTest : SysuiTestCase() { +@DisableFlags(value = [QSComposeFragment.FLAG_NAME, DualShade.FLAG_NAME]) +class TileServiceRequestControllerTestComposeOff : SysuiTestCase() { companion object { private val TEST_COMPONENT = ComponentName("test_pkg", "test_cls") @@ -61,20 +67,15 @@ class TileServiceRequestControllerTest : SysuiTestCase() { private const val TEST_UID = 12345 } - @Mock - private lateinit var tileRequestDialog: TileRequestDialog - @Mock - private lateinit var qsHost: QSHost - @Mock - private lateinit var commandRegistry: CommandRegistry - @Mock - private lateinit var commandQueue: CommandQueue - @Mock - private lateinit var logger: TileRequestDialogEventLogger - @Mock - private lateinit var icon: Icon - @Mock - private lateinit var ugm: IUriGrantsManager + private val kosmos = testKosmos() + + @Mock private lateinit var tileRequestDialog: TileRequestDialog + @Mock private lateinit var qsHost: QSHost + @Mock private lateinit var commandRegistry: CommandRegistry + @Mock private lateinit var commandQueue: CommandQueue + @Mock private lateinit var logger: TileRequestDialogEventLogger + @Mock private lateinit var icon: Icon + @Mock private lateinit var ugm: IUriGrantsManager private val instanceIdSequence = InstanceIdSequenceFake(1_000) private lateinit var controller: TileServiceRequestController @@ -88,15 +89,17 @@ class TileServiceRequestControllerTest : SysuiTestCase() { // Tile not present by default `when`(qsHost.indexOf(anyString())).thenReturn(-1) - controller = TileServiceRequestController( + controller = + TileServiceRequestController( qsHost, commandQueue, commandRegistry, logger, ugm, - ) { - tileRequestDialog - } + kosmos.tileRequestDialogComposeDelegateFactory, + ) { + tileRequestDialog + } controller.init() } @@ -104,24 +107,19 @@ class TileServiceRequestControllerTest : SysuiTestCase() { @Test fun requestTileAdd_dataIsPassedToDialog() { controller.requestTileAdd( - TEST_UID, - TEST_COMPONENT, - TEST_APP_NAME, - TEST_LABEL, - icon, - Callback(), + TEST_UID, + TEST_COMPONENT, + TEST_APP_NAME, + TEST_LABEL, + icon, + Callback(), ) - verify(tileRequestDialog).setTileData( - TileRequestDialog.TileData( - TEST_UID, - TEST_APP_NAME, - TEST_LABEL, - icon, - TEST_COMPONENT.packageName, - ), + verify(tileRequestDialog) + .setTileData( + TileData(TEST_UID, TEST_APP_NAME, TEST_LABEL, icon, TEST_COMPONENT.packageName), ugm, - ) + ) } @Test @@ -130,12 +128,12 @@ class TileServiceRequestControllerTest : SysuiTestCase() { val callback = Callback() controller.requestTileAdd( - TEST_UID, - TEST_COMPONENT, - TEST_APP_NAME, - TEST_LABEL, - icon, - callback, + TEST_UID, + TEST_COMPONENT, + TEST_APP_NAME, + TEST_LABEL, + icon, + callback, ) assertThat(callback.lastAccepted).isEqualTo(TileServiceRequestController.TILE_ALREADY_ADDED) @@ -156,12 +154,12 @@ class TileServiceRequestControllerTest : SysuiTestCase() { @Test fun showAllUsers_set() { controller.requestTileAdd( - TEST_UID, - TEST_COMPONENT, - TEST_APP_NAME, - TEST_LABEL, - icon, - Callback(), + TEST_UID, + TEST_COMPONENT, + TEST_APP_NAME, + TEST_LABEL, + icon, + Callback(), ) verify(tileRequestDialog).setShowForAllUsers(true) } @@ -169,12 +167,12 @@ class TileServiceRequestControllerTest : SysuiTestCase() { @Test fun cancelOnTouchOutside_set() { controller.requestTileAdd( - TEST_UID, - TEST_COMPONENT, - TEST_APP_NAME, - TEST_LABEL, - icon, - Callback(), + TEST_UID, + TEST_COMPONENT, + TEST_APP_NAME, + TEST_LABEL, + icon, + Callback(), ) verify(tileRequestDialog).setCanceledOnTouchOutside(true) } @@ -189,16 +187,16 @@ class TileServiceRequestControllerTest : SysuiTestCase() { @Test fun cancelListener_dismissResult() { val cancelListenerCaptor = - ArgumentCaptor.forClass(DialogInterface.OnCancelListener::class.java) + ArgumentCaptor.forClass(DialogInterface.OnCancelListener::class.java) val callback = Callback() controller.requestTileAdd( - TEST_UID, - TEST_COMPONENT, - TEST_APP_NAME, - TEST_LABEL, - icon, - callback, + TEST_UID, + TEST_COMPONENT, + TEST_APP_NAME, + TEST_LABEL, + icon, + callback, ) verify(tileRequestDialog).setOnCancelListener(capture(cancelListenerCaptor)) @@ -210,7 +208,7 @@ class TileServiceRequestControllerTest : SysuiTestCase() { @Test fun dialogCancelled_logged() { val cancelListenerCaptor = - ArgumentCaptor.forClass(DialogInterface.OnCancelListener::class.java) + ArgumentCaptor.forClass(DialogInterface.OnCancelListener::class.java) controller.requestTileAdd(TEST_UID, TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon) {} val instanceId = InstanceId.fakeInstanceId(instanceIdSequence.lastInstanceId) @@ -219,26 +217,27 @@ class TileServiceRequestControllerTest : SysuiTestCase() { verify(logger).logDialogShown(TEST_COMPONENT.packageName, instanceId) cancelListenerCaptor.value.onCancel(tileRequestDialog) - verify(logger).logUserResponse( + verify(logger) + .logUserResponse( StatusBarManager.TILE_ADD_REQUEST_RESULT_DIALOG_DISMISSED, TEST_COMPONENT.packageName, - instanceId - ) + instanceId, + ) } @Test fun positiveActionListener_tileAddedResult() { val clickListenerCaptor = - ArgumentCaptor.forClass(DialogInterface.OnClickListener::class.java) + ArgumentCaptor.forClass(DialogInterface.OnClickListener::class.java) val callback = Callback() controller.requestTileAdd( - TEST_UID, - TEST_COMPONENT, - TEST_APP_NAME, - TEST_LABEL, - icon, - callback, + TEST_UID, + TEST_COMPONENT, + TEST_APP_NAME, + TEST_LABEL, + icon, + callback, ) verify(tileRequestDialog).setPositiveButton(anyInt(), capture(clickListenerCaptor)) @@ -251,7 +250,7 @@ class TileServiceRequestControllerTest : SysuiTestCase() { @Test fun tileAdded_logged() { val clickListenerCaptor = - ArgumentCaptor.forClass(DialogInterface.OnClickListener::class.java) + ArgumentCaptor.forClass(DialogInterface.OnClickListener::class.java) controller.requestTileAdd(TEST_UID, TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon) {} val instanceId = InstanceId.fakeInstanceId(instanceIdSequence.lastInstanceId) @@ -260,26 +259,27 @@ class TileServiceRequestControllerTest : SysuiTestCase() { verify(logger).logDialogShown(TEST_COMPONENT.packageName, instanceId) clickListenerCaptor.value.onClick(tileRequestDialog, DialogInterface.BUTTON_POSITIVE) - verify(logger).logUserResponse( + verify(logger) + .logUserResponse( StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_ADDED, TEST_COMPONENT.packageName, - instanceId - ) + instanceId, + ) } @Test fun negativeActionListener_tileNotAddedResult() { val clickListenerCaptor = - ArgumentCaptor.forClass(DialogInterface.OnClickListener::class.java) + ArgumentCaptor.forClass(DialogInterface.OnClickListener::class.java) val callback = Callback() controller.requestTileAdd( - TEST_UID, - TEST_COMPONENT, - TEST_APP_NAME, - TEST_LABEL, - icon, - callback, + TEST_UID, + TEST_COMPONENT, + TEST_APP_NAME, + TEST_LABEL, + icon, + callback, ) verify(tileRequestDialog).setNegativeButton(anyInt(), capture(clickListenerCaptor)) @@ -292,7 +292,7 @@ class TileServiceRequestControllerTest : SysuiTestCase() { @Test fun tileNotAdded_logged() { val clickListenerCaptor = - ArgumentCaptor.forClass(DialogInterface.OnClickListener::class.java) + ArgumentCaptor.forClass(DialogInterface.OnClickListener::class.java) controller.requestTileAdd(TEST_UID, TEST_COMPONENT, TEST_APP_NAME, TEST_LABEL, icon) {} val instanceId = InstanceId.fakeInstanceId(instanceIdSequence.lastInstanceId) @@ -301,11 +301,12 @@ class TileServiceRequestControllerTest : SysuiTestCase() { verify(logger).logDialogShown(TEST_COMPONENT.packageName, instanceId) clickListenerCaptor.value.onClick(tileRequestDialog, DialogInterface.BUTTON_NEGATIVE) - verify(logger).logUserResponse( + verify(logger) + .logUserResponse( StatusBarManager.TILE_ADD_REQUEST_RESULT_TILE_NOT_ADDED, TEST_COMPONENT.packageName, - instanceId - ) + instanceId, + ) } @Test @@ -319,24 +320,19 @@ class TileServiceRequestControllerTest : SysuiTestCase() { verify(commandQueue, atLeastOnce()).addCallback(capture(captor)) captor.value.requestAddTile( - TEST_UID, - TEST_COMPONENT, - TEST_APP_NAME, - TEST_LABEL, - icon, - Callback(), + TEST_UID, + TEST_COMPONENT, + TEST_APP_NAME, + TEST_LABEL, + icon, + Callback(), ) - verify(tileRequestDialog).setTileData( - TileRequestDialog.TileData( - TEST_UID, - TEST_APP_NAME, - TEST_LABEL, - icon, - TEST_COMPONENT.packageName, - ), + verify(tileRequestDialog) + .setTileData( + TileData(TEST_UID, TEST_APP_NAME, TEST_LABEL, icon, TEST_COMPONENT.packageName), ugm, - ) + ) } @Test @@ -354,22 +350,23 @@ class TileServiceRequestControllerTest : SysuiTestCase() { @Test fun interfaceThrowsRemoteException_doesntCrash() { val cancelListenerCaptor = - ArgumentCaptor.forClass(DialogInterface.OnCancelListener::class.java) + ArgumentCaptor.forClass(DialogInterface.OnCancelListener::class.java) val captor = ArgumentCaptor.forClass(CommandQueue.Callbacks::class.java) verify(commandQueue, atLeastOnce()).addCallback(capture(captor)) - val callback = object : IAddTileResultCallback.Stub() { - override fun onTileRequest(p0: Int) { - throw RemoteException() + val callback = + object : IAddTileResultCallback.Stub() { + override fun onTileRequest(p0: Int) { + throw RemoteException() + } } - } captor.value.requestAddTile( - TEST_UID, - TEST_COMPONENT, - TEST_APP_NAME, - TEST_LABEL, - icon, - callback, + TEST_UID, + TEST_COMPONENT, + TEST_APP_NAME, + TEST_LABEL, + icon, + callback, ) verify(tileRequestDialog).setOnCancelListener(capture(cancelListenerCaptor)) @@ -383,12 +380,12 @@ class TileServiceRequestControllerTest : SysuiTestCase() { val callback = Callback() controller.requestTileAdd( - TEST_UID, - TEST_COMPONENT, - TEST_APP_NAME, - TEST_LABEL, - icon, - callback, + TEST_UID, + TEST_COMPONENT, + TEST_APP_NAME, + TEST_LABEL, + icon, + callback, ) verify(tileRequestDialog).setOnDismissListener(capture(dismissListenerCaptor)) @@ -407,12 +404,12 @@ class TileServiceRequestControllerTest : SysuiTestCase() { val callback = Callback() controller.requestTileAdd( - TEST_UID, - TEST_COMPONENT, - TEST_APP_NAME, - TEST_LABEL, - icon, - callback, + TEST_UID, + TEST_COMPONENT, + TEST_APP_NAME, + TEST_LABEL, + icon, + callback, ) verify(tileRequestDialog).setPositiveButton(anyInt(), capture(clickListenerCaptor)) verify(tileRequestDialog).setOnDismissListener(capture(dismissListenerCaptor)) @@ -435,12 +432,12 @@ class TileServiceRequestControllerTest : SysuiTestCase() { val callback = Callback() controller.requestTileAdd( - TEST_UID, - TEST_COMPONENT, - TEST_APP_NAME, - TEST_LABEL, - icon, - callback, + TEST_UID, + TEST_COMPONENT, + TEST_APP_NAME, + TEST_LABEL, + icon, + callback, ) verify(tileRequestDialog).setOnCancelListener(capture(cancelListenerCaptor)) verify(tileRequestDialog).setOnDismissListener(capture(dismissListenerCaptor)) diff --git a/packages/SystemUI/tests/utils/src/com/android/app/IUriGrantsManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/app/IUriGrantsManagerKosmos.kt new file mode 100644 index 000000000000..003777aca687 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/app/IUriGrantsManagerKosmos.kt @@ -0,0 +1,23 @@ +/* + * 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.app + +import android.app.IUriGrantsManager +import com.android.systemui.kosmos.Kosmos +import org.mockito.kotlin.mock + +val Kosmos.iUriGrantsManager by Kosmos.Fixture { mock<IUriGrantsManager>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/QSHostAdapterKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/QSHostAdapterKosmos.kt new file mode 100644 index 000000000000..0bf801b35ad1 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/QSHostAdapterKosmos.kt @@ -0,0 +1,35 @@ +/* + * 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 + +import android.content.applicationContext +import com.android.systemui.dump.dumpManager +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.qs.external.tileServiceRequestControllerBuilder +import com.android.systemui.qs.pipeline.domain.interactor.currentTilesInteractor + +val Kosmos.qsHostAdapter by + Kosmos.Fixture { + QSHostAdapter( + currentTilesInteractor, + applicationContext, + tileServiceRequestControllerBuilder, + applicationCoroutineScope, + dumpManager, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileServiceRequestControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileServiceRequestControllerKosmos.kt new file mode 100644 index 000000000000..296623462a54 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileServiceRequestControllerKosmos.kt @@ -0,0 +1,50 @@ +/* + * 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 com.android.app.iUriGrantsManager +import com.android.internal.logging.uiEventLoggerFake +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.qs.external.ui.dialog.tileRequestDialogComposeDelegateFactory +import com.android.systemui.qs.instanceIdSequenceFake +import com.android.systemui.qs.qsHostAdapter +import com.android.systemui.statusbar.commandQueue +import com.android.systemui.statusbar.commandline.commandRegistry +import org.mockito.kotlin.mock + +val Kosmos.tileServiceRequestControllerBuilder by + Kosmos.Fixture { + TileServiceRequestController.Builder( + commandQueue, + commandRegistry, + iUriGrantsManager, + tileRequestDialogComposeDelegateFactory, + ) + } + +val Kosmos.tileServiceRequestController by + Kosmos.Fixture { + TileServiceRequestController( + qsHostAdapter, + commandQueue, + commandRegistry, + TileRequestDialogEventLogger(uiEventLoggerFake, instanceIdSequenceFake), + iUriGrantsManager, + tileRequestDialogComposeDelegateFactory, + { mock() }, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/ui/dialog/FakeTileRequestDialogComposeDelegateFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/ui/dialog/FakeTileRequestDialogComposeDelegateFactory.kt new file mode 100644 index 000000000000..1e0ebe44ba2a --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/ui/dialog/FakeTileRequestDialogComposeDelegateFactory.kt @@ -0,0 +1,36 @@ +/* + * 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.dialog + +import android.content.DialogInterface +import com.android.systemui.qs.external.TileData +import org.mockito.Answers +import org.mockito.kotlin.mock + +class FakeTileRequestDialogComposeDelegateFactory : TileRequestDialogComposeDelegate.Factory { + lateinit var tileData: TileData + lateinit var clickListener: DialogInterface.OnClickListener + + override fun create( + tileData: TileData, + dialogListener: DialogInterface.OnClickListener, + ): TileRequestDialogComposeDelegate { + this.tileData = tileData + this.clickListener = dialogListener + return mock(defaultAnswer = Answers.RETURNS_MOCKS) + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/ui/dialog/TileRequestDialogComposeDelegateKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/ui/dialog/TileRequestDialogComposeDelegateKosmos.kt new file mode 100644 index 000000000000..030af61e5569 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/ui/dialog/TileRequestDialogComposeDelegateKosmos.kt @@ -0,0 +1,43 @@ +/* + * 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.dialog + +import android.content.DialogInterface +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.qs.external.TileData +import com.android.systemui.qs.external.ui.viewmodel.tileRequestDialogViewModelFactory +import com.android.systemui.statusbar.phone.systemUIDialogFactory + +var Kosmos.tileRequestDialogComposeDelegateFactory by + Kosmos.Fixture<TileRequestDialogComposeDelegate.Factory> { + object : TileRequestDialogComposeDelegate.Factory { + override fun create( + tiledata: TileData, + dialogListener: DialogInterface.OnClickListener, + ): TileRequestDialogComposeDelegate { + return TileRequestDialogComposeDelegate( + systemUIDialogFactory, + tileRequestDialogViewModelFactory, + tiledata, + dialogListener, + ) + } + } + } + +val TileRequestDialogComposeDelegate.Factory.fake: FakeTileRequestDialogComposeDelegateFactory + get() = this as FakeTileRequestDialogComposeDelegateFactory diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/ui/viewmodel/TileRequestDialogViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/ui/viewmodel/TileRequestDialogViewModelKosmos.kt new file mode 100644 index 000000000000..7b1797db24f7 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/ui/viewmodel/TileRequestDialogViewModelKosmos.kt @@ -0,0 +1,40 @@ +/* + * 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.Context +import com.android.app.iUriGrantsManager +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.qs.external.TileData + +val Kosmos.tileRequestDialogViewModelFactory by + Kosmos.Fixture { + object : TileRequestDialogViewModel.Factory { + override fun create( + dialogContext: Context, + tileData: TileData, + ): TileRequestDialogViewModel { + return TileRequestDialogViewModel( + iUriGrantsManager, + testDispatcher, + dialogContext, + tileData, + ) + } + } + } |