Loading packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt +35 −31 Original line number Diff line number Diff line Loading @@ -21,64 +21,68 @@ import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler import com.android.systemui.animation.Expandable import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject import com.android.systemui.qs.tiles.base.actions.qsTileIntentUserInputHandler import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.policy.ui.dialog.ModesDialogDelegate import com.google.common.truth.Truth import kotlin.coroutines.EmptyCoroutineContext import com.android.systemui.statusbar.policy.ui.dialog.mockModesDialogDelegate import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.MockitoAnnotations import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) @EnableFlags(android.app.Flags.FLAG_MODES_UI) class ModesTileUserActionInteractorTest : SysuiTestCase() { private val inputHandler = FakeQSTileIntentUserInputHandler() private val kosmos = testKosmos() private val inputHandler = kosmos.qsTileIntentUserInputHandler private val mockDialogDelegate = kosmos.mockModesDialogDelegate @Mock private lateinit var dialogTransitionAnimator: DialogTransitionAnimator @Mock private lateinit var dialogDelegate: ModesDialogDelegate @Mock private lateinit var mockDialog: SystemUIDialog private val underTest = ModesTileUserActionInteractor( inputHandler, mockDialogDelegate, ) private lateinit var underTest: ModesTileUserActionInteractor @Test fun handleClick_active() = runTest { val expandable = mock<Expandable>() underTest.handleInput( QSTileInputTestKtx.click(data = ModesTileModel(true), expandable = expandable)) @Before fun setup() { MockitoAnnotations.initMocks(this) verify(mockDialogDelegate).showDialog(eq(expandable)) } whenever(dialogDelegate.createDialog()).thenReturn(mockDialog) @Test fun handleClick_inactive() = runTest { val expandable = mock<Expandable>() underTest.handleInput( QSTileInputTestKtx.click(data = ModesTileModel(false), expandable = expandable)) underTest = ModesTileUserActionInteractor( EmptyCoroutineContext, inputHandler, dialogTransitionAnimator, dialogDelegate, ) verify(mockDialogDelegate).showDialog(eq(expandable)) } @Test fun handleClick() = runTest { underTest.handleInput(QSTileInputTestKtx.click(ModesTileModel(false))) fun handleLongClick_active() = runTest { underTest.handleInput(QSTileInputTestKtx.longClick(ModesTileModel(true))) verify(mockDialog).show() QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput { assertThat(it.intent.action).isEqualTo(Settings.ACTION_ZEN_MODE_SETTINGS) } } @Test fun handleLongClick() = runTest { fun handleLongClick_inactive() = runTest { underTest.handleInput(QSTileInputTestKtx.longClick(ModesTileModel(false))) QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput { Truth.assertThat(it.intent.action).isEqualTo(Settings.ACTION_ZEN_MODE_SETTINGS) assertThat(it.intent.action).isEqualTo(Settings.ACTION_ZEN_MODE_SETTINGS) } } } packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateTest.kt 0 → 100644 +124 −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. */ @file:OptIn(ExperimentalCoroutinesApi::class) package com.android.systemui.statusbar.policy.ui.dialog import android.app.Dialog import android.content.Intent import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.animation.ActivityTransitionAnimator import com.android.systemui.animation.mockActivityTransitionAnimatorController import com.android.systemui.animation.mockDialogTransitionAnimator import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.mainCoroutineContext import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.activityStarter import com.android.systemui.runOnMainThreadAndWaitForIdleSync import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.phone.systemUIDialogFactory import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.modesDialogViewModel import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) class ModesDialogDelegateTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope private val activityStarter = kosmos.activityStarter private val mockDialogTransitionAnimator = kosmos.mockDialogTransitionAnimator private val mockAnimationController = kosmos.mockActivityTransitionAnimatorController private lateinit var underTest: ModesDialogDelegate @Before fun setup() { whenever( mockDialogTransitionAnimator.createActivityTransitionController( any<SystemUIDialog>(), eq(null) ) ) .thenReturn(mockAnimationController) underTest = ModesDialogDelegate( kosmos.systemUIDialogFactory, mockDialogTransitionAnimator, activityStarter, { kosmos.modesDialogViewModel }, kosmos.mainCoroutineContext, ) } @Test fun launchFromDialog_whenDialogNotOpen() { val intent: Intent = mock() runOnMainThreadAndWaitForIdleSync { underTest.launchFromDialog(intent) } verify(activityStarter) .startActivity(eq(intent), eq(true), eq<ActivityTransitionAnimator.Controller?>(null)) } @Test fun launchFromDialog_whenDialogOpen() = testScope.runTest { val intent: Intent = mock() lateinit var dialog: Dialog runOnMainThreadAndWaitForIdleSync { kosmos.applicationCoroutineScope.launch { dialog = underTest.showDialog() } runCurrent() underTest.launchFromDialog(intent) } verify(mockDialogTransitionAnimator) .createActivityTransitionController(any<Dialog>(), eq(null)) verify(activityStarter).startActivity(eq(intent), eq(true), eq(mockAnimationController)) runOnMainThreadAndWaitForIdleSync { dialog.dismiss() } } @Test fun dismiss_clearsDialogReference() { val dialog = runOnMainThreadAndWaitForIdleSync { underTest.createDialog() } assertThat(underTest.currentDialog).isEqualTo(dialog) runOnMainThreadAndWaitForIdleSync { dialog.show() dialog.dismiss() } assertThat(underTest.currentDialog).isNull() } } packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt +67 −5 Original line number Diff line number Diff line Loading @@ -18,6 +18,8 @@ package com.android.systemui.statusbar.policy.ui.dialog.viewmodel import android.content.Intent import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.settingslib.notification.modes.TestModeBuilder Loading @@ -27,6 +29,7 @@ import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor import com.android.systemui.statusbar.policy.ui.dialog.mockModesDialogDelegate import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi Loading @@ -34,16 +37,21 @@ import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.clearInvocations import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.verify @SmallTest @RunWith(AndroidJUnit4::class) class ModesDialogViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope val repository = kosmos.fakeZenModeRepository val interactor = kosmos.zenModeInteractor private val repository = kosmos.fakeZenModeRepository private val interactor = kosmos.zenModeInteractor private val mockDialogDelegate = kosmos.mockModesDialogDelegate val underTest = ModesDialogViewModel(context, interactor, kosmos.testDispatcher) private val underTest = ModesDialogViewModel(context, interactor, kosmos.testDispatcher, mockDialogDelegate) @Test fun tiles_filtersOutDisabledModes() = Loading @@ -64,7 +72,8 @@ class ModesDialogViewModelTest : SysuiTestCase() { .setEnabled(false) .setManualInvocationAllowed(true) .build(), )) ) ) runCurrent() assertThat(tiles?.size).isEqualTo(2) Loading Loading @@ -108,7 +117,8 @@ class ModesDialogViewModelTest : SysuiTestCase() { .setActive(false) .setManualInvocationAllowed(false) .build(), )) ) ) runCurrent() assertThat(tiles?.size).isEqualTo(3) Loading Loading @@ -161,4 +171,56 @@ class ModesDialogViewModelTest : SysuiTestCase() { assertThat(tiles?.first()?.enabled).isFalse() } @Test fun onLongClick_launchesIntent() = testScope.runTest { val tiles by collectLastValue(underTest.tiles) val intentCaptor = argumentCaptor<Intent>() val modeId = "id" repository.addModes( listOf( TestModeBuilder() .setId(modeId) .setId("A") .setActive(true) .setManualInvocationAllowed(true) .build(), TestModeBuilder() .setId(modeId) .setId("B") .setActive(false) .setManualInvocationAllowed(true) .build(), ) ) runCurrent() assertThat(tiles?.size).isEqualTo(2) // Trigger onLongClick for A tiles?.first()?.onLongClick?.let { it() } runCurrent() // Check that it launched the correct intent verify(mockDialogDelegate).launchFromDialog(intentCaptor.capture()) var intent = intentCaptor.lastValue assertThat(intent.action).isEqualTo(Settings.ACTION_AUTOMATIC_ZEN_RULE_SETTINGS) assertThat(intent.extras?.getString(Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID)) .isEqualTo("A") clearInvocations(mockDialogDelegate) // Trigger onLongClick for B tiles?.last()?.onLongClick?.let { it() } runCurrent() // Check that it launched the correct intent verify(mockDialogDelegate).launchFromDialog(intentCaptor.capture()) intent = intentCaptor.lastValue assertThat(intent.action).isEqualTo(Settings.ACTION_AUTOMATIC_ZEN_RULE_SETTINGS) assertThat(intent.extras?.getString(Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID)) .isEqualTo("B") } } packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt +7 −28 Original line number Diff line number Diff line Loading @@ -16,14 +16,10 @@ package com.android.systemui.qs.tiles.impl.modes.domain.interactor //noinspection CleanArchitectureDependencyViolation: dialog needs to be opened on click import android.content.Intent import android.provider.Settings import com.android.internal.jank.InteractionJankMonitor import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.animation.Expandable import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dagger.SysUISingleton import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler import com.android.systemui.qs.tiles.base.interactor.QSTileInput import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor Loading @@ -31,15 +27,13 @@ import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction import com.android.systemui.statusbar.policy.ui.dialog.ModesDialogDelegate import javax.inject.Inject import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.withContext @SysUISingleton class ModesTileUserActionInteractor @Inject constructor( @Main private val coroutineContext: CoroutineContext, private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler, private val dialogTransitionAnimator: DialogTransitionAnimator, private val qsTileIntentUserInputHandler: QSTileIntentUserInputHandler, // TODO(b/353896370): The domain layer should not have to depend on the UI layer. private val dialogDelegate: ModesDialogDelegate, ) : QSTileUserActionInteractor<ModesTileModel> { val longClickIntent = Intent(Settings.ACTION_ZEN_MODE_SETTINGS) Loading @@ -51,29 +45,14 @@ constructor( handleClick(action.expandable) } is QSTileUserAction.LongClick -> { qsTileIntentUserActionHandler.handle(action.expandable, longClickIntent) qsTileIntentUserInputHandler.handle(action.expandable, longClickIntent) } } } } suspend fun handleClick(expandable: Expandable?) { // Show a dialog with the list of modes to configure. Dialogs shown by the // DialogTransitionAnimator must be created and shown on the main thread, so we post it to // the UI handler. withContext(coroutineContext) { val dialog = dialogDelegate.createDialog() expandable ?.dialogTransitionController( DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG) ) ?.let { controller -> dialogTransitionAnimator.show(dialog, controller) } ?: dialog.show() } } companion object { private const val INTERACTION_JANK_TAG = "configure_priority_modes" // Show a dialog with the list of modes to configure. dialogDelegate.showDialog(expandable) } } packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt +120 −32 Original line number Diff line number Diff line Loading @@ -18,45 +18,97 @@ package com.android.systemui.statusbar.policy.ui.dialog import android.content.Intent import android.provider.Settings import android.util.Log import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import com.android.compose.PlatformButton import com.android.compose.PlatformOutlinedButton import com.android.internal.annotations.VisibleForTesting import com.android.internal.jank.InteractionJankMonitor import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.animation.Expandable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dialog.ui.composable.AlertDialogContent import com.android.systemui.plugins.ActivityStarter import com.android.systemui.res.R import com.android.systemui.statusbar.phone.ComponentSystemUIDialog import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.phone.SystemUIDialogFactory import com.android.systemui.statusbar.phone.create import com.android.systemui.statusbar.policy.ui.dialog.composable.ModeTileGrid import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModesDialogViewModel import com.android.systemui.util.Assert import javax.inject.Inject import javax.inject.Provider import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.withContext @SysUISingleton class ModesDialogDelegate @Inject constructor( private val sysuiDialogFactory: SystemUIDialogFactory, private val dialogTransitionAnimator: DialogTransitionAnimator, private val activityStarter: ActivityStarter, private val viewModel: ModesDialogViewModel, // Using a provider to avoid a circular dependency. private val viewModel: Provider<ModesDialogViewModel>, @Main private val mainCoroutineContext: CoroutineContext, ) : SystemUIDialog.Delegate { // NOTE: This should only be accessed/written from the main thread. @VisibleForTesting var currentDialog: ComponentSystemUIDialog? = null override fun createDialog(): SystemUIDialog { return sysuiDialogFactory.create { dialog -> Assert.isMainThread() if (currentDialog != null) { Log.w(TAG, "Dialog is already open, dismissing it and creating a new one.") currentDialog?.dismiss() } currentDialog = sysuiDialogFactory.create() { ModesDialogContent(it) } currentDialog ?.lifecycle ?.addObserver( object : DefaultLifecycleObserver { override fun onStop(owner: LifecycleOwner) { Assert.isMainThread() currentDialog = null } } ) return currentDialog!! } @Composable private fun ModesDialogContent(dialog: SystemUIDialog) { AlertDialogContent( title = { Text(stringResource(R.string.zen_modes_dialog_title)) }, content = { ModeTileGrid(viewModel) }, content = { ModeTileGrid(viewModel.get()) }, neutralButton = { PlatformOutlinedButton( onClick = { val animationController = dialogTransitionAnimator.createActivityTransitionController( dialog.getButton(SystemUIDialog.BUTTON_NEUTRAL) PlatformOutlinedButton(onClick = { openSettings(dialog) }) { Text(stringResource(R.string.zen_modes_dialog_settings)) } }, positiveButton = { PlatformButton(onClick = { dialog.dismiss() }) { Text(stringResource(R.string.zen_modes_dialog_done)) } }, ) } private fun openSettings(dialog: SystemUIDialog) { val animationController = dialogTransitionAnimator.createActivityTransitionController(dialog) if (animationController == null) { // The controller will take care of dismissing for us after the // animation, but let's make sure we dismiss the dialog if we don't // animate it. // The controller will take care of dismissing for us after // the animation, but let's make sure we dismiss the dialog // if we don't animate it. dialog.dismiss() } activityStarter.startActivity( Loading @@ -65,20 +117,56 @@ constructor( animationController ) } ) { Text(stringResource(R.string.zen_modes_dialog_settings)) suspend fun showDialog(expandable: Expandable? = null): SystemUIDialog { // Dialogs shown by the DialogTransitionAnimator must be created and shown on the main // thread, so we post it to the UI handler. withContext(mainCoroutineContext) { // Create the dialog if necessary if (currentDialog == null) { createDialog() } }, positiveButton = { PlatformButton(onClick = { dialog.dismiss() }) { Text(stringResource(R.string.zen_modes_dialog_done)) expandable ?.dialogTransitionController( DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG) ) ?.let { controller -> dialogTransitionAnimator.show(currentDialog!!, controller) } ?: currentDialog!!.show() } }, return currentDialog!! } /** * Launches the [intent] by animating from the dialog. If the dialog is not showing, just * launches it normally without animating. */ fun launchFromDialog(intent: Intent) { Assert.isMainThread() if (currentDialog == null) { Log.w( TAG, "Cannot launch from dialog, the dialog is not present. " + "Will launch activity without animating." ) } val animationController = currentDialog?.let { dialogTransitionAnimator.createActivityTransitionController(it) } if (animationController == null) { currentDialog?.dismiss() } activityStarter.startActivity( intent, true, /* dismissShade */ animationController, ) } companion object { private const val TAG = "ModesDialogDelegate" private val ZEN_MODE_SETTINGS_INTENT = Intent(Settings.ACTION_ZEN_MODE_SETTINGS) private const val INTERACTION_JANK_TAG = "configure_priority_modes" } } Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt +35 −31 Original line number Diff line number Diff line Loading @@ -21,64 +21,68 @@ import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler import com.android.systemui.animation.Expandable import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject import com.android.systemui.qs.tiles.base.actions.qsTileIntentUserInputHandler import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.policy.ui.dialog.ModesDialogDelegate import com.google.common.truth.Truth import kotlin.coroutines.EmptyCoroutineContext import com.android.systemui.statusbar.policy.ui.dialog.mockModesDialogDelegate import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.MockitoAnnotations import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) @EnableFlags(android.app.Flags.FLAG_MODES_UI) class ModesTileUserActionInteractorTest : SysuiTestCase() { private val inputHandler = FakeQSTileIntentUserInputHandler() private val kosmos = testKosmos() private val inputHandler = kosmos.qsTileIntentUserInputHandler private val mockDialogDelegate = kosmos.mockModesDialogDelegate @Mock private lateinit var dialogTransitionAnimator: DialogTransitionAnimator @Mock private lateinit var dialogDelegate: ModesDialogDelegate @Mock private lateinit var mockDialog: SystemUIDialog private val underTest = ModesTileUserActionInteractor( inputHandler, mockDialogDelegate, ) private lateinit var underTest: ModesTileUserActionInteractor @Test fun handleClick_active() = runTest { val expandable = mock<Expandable>() underTest.handleInput( QSTileInputTestKtx.click(data = ModesTileModel(true), expandable = expandable)) @Before fun setup() { MockitoAnnotations.initMocks(this) verify(mockDialogDelegate).showDialog(eq(expandable)) } whenever(dialogDelegate.createDialog()).thenReturn(mockDialog) @Test fun handleClick_inactive() = runTest { val expandable = mock<Expandable>() underTest.handleInput( QSTileInputTestKtx.click(data = ModesTileModel(false), expandable = expandable)) underTest = ModesTileUserActionInteractor( EmptyCoroutineContext, inputHandler, dialogTransitionAnimator, dialogDelegate, ) verify(mockDialogDelegate).showDialog(eq(expandable)) } @Test fun handleClick() = runTest { underTest.handleInput(QSTileInputTestKtx.click(ModesTileModel(false))) fun handleLongClick_active() = runTest { underTest.handleInput(QSTileInputTestKtx.longClick(ModesTileModel(true))) verify(mockDialog).show() QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput { assertThat(it.intent.action).isEqualTo(Settings.ACTION_ZEN_MODE_SETTINGS) } } @Test fun handleLongClick() = runTest { fun handleLongClick_inactive() = runTest { underTest.handleInput(QSTileInputTestKtx.longClick(ModesTileModel(false))) QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput { Truth.assertThat(it.intent.action).isEqualTo(Settings.ACTION_ZEN_MODE_SETTINGS) assertThat(it.intent.action).isEqualTo(Settings.ACTION_ZEN_MODE_SETTINGS) } } }
packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateTest.kt 0 → 100644 +124 −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. */ @file:OptIn(ExperimentalCoroutinesApi::class) package com.android.systemui.statusbar.policy.ui.dialog import android.app.Dialog import android.content.Intent import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.animation.ActivityTransitionAnimator import com.android.systemui.animation.mockActivityTransitionAnimatorController import com.android.systemui.animation.mockDialogTransitionAnimator import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.mainCoroutineContext import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.activityStarter import com.android.systemui.runOnMainThreadAndWaitForIdleSync import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.phone.systemUIDialogFactory import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.modesDialogViewModel import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) class ModesDialogDelegateTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope private val activityStarter = kosmos.activityStarter private val mockDialogTransitionAnimator = kosmos.mockDialogTransitionAnimator private val mockAnimationController = kosmos.mockActivityTransitionAnimatorController private lateinit var underTest: ModesDialogDelegate @Before fun setup() { whenever( mockDialogTransitionAnimator.createActivityTransitionController( any<SystemUIDialog>(), eq(null) ) ) .thenReturn(mockAnimationController) underTest = ModesDialogDelegate( kosmos.systemUIDialogFactory, mockDialogTransitionAnimator, activityStarter, { kosmos.modesDialogViewModel }, kosmos.mainCoroutineContext, ) } @Test fun launchFromDialog_whenDialogNotOpen() { val intent: Intent = mock() runOnMainThreadAndWaitForIdleSync { underTest.launchFromDialog(intent) } verify(activityStarter) .startActivity(eq(intent), eq(true), eq<ActivityTransitionAnimator.Controller?>(null)) } @Test fun launchFromDialog_whenDialogOpen() = testScope.runTest { val intent: Intent = mock() lateinit var dialog: Dialog runOnMainThreadAndWaitForIdleSync { kosmos.applicationCoroutineScope.launch { dialog = underTest.showDialog() } runCurrent() underTest.launchFromDialog(intent) } verify(mockDialogTransitionAnimator) .createActivityTransitionController(any<Dialog>(), eq(null)) verify(activityStarter).startActivity(eq(intent), eq(true), eq(mockAnimationController)) runOnMainThreadAndWaitForIdleSync { dialog.dismiss() } } @Test fun dismiss_clearsDialogReference() { val dialog = runOnMainThreadAndWaitForIdleSync { underTest.createDialog() } assertThat(underTest.currentDialog).isEqualTo(dialog) runOnMainThreadAndWaitForIdleSync { dialog.show() dialog.dismiss() } assertThat(underTest.currentDialog).isNull() } }
packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt +67 −5 Original line number Diff line number Diff line Loading @@ -18,6 +18,8 @@ package com.android.systemui.statusbar.policy.ui.dialog.viewmodel import android.content.Intent import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.settingslib.notification.modes.TestModeBuilder Loading @@ -27,6 +29,7 @@ import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor import com.android.systemui.statusbar.policy.ui.dialog.mockModesDialogDelegate import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi Loading @@ -34,16 +37,21 @@ import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.clearInvocations import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.verify @SmallTest @RunWith(AndroidJUnit4::class) class ModesDialogViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope val repository = kosmos.fakeZenModeRepository val interactor = kosmos.zenModeInteractor private val repository = kosmos.fakeZenModeRepository private val interactor = kosmos.zenModeInteractor private val mockDialogDelegate = kosmos.mockModesDialogDelegate val underTest = ModesDialogViewModel(context, interactor, kosmos.testDispatcher) private val underTest = ModesDialogViewModel(context, interactor, kosmos.testDispatcher, mockDialogDelegate) @Test fun tiles_filtersOutDisabledModes() = Loading @@ -64,7 +72,8 @@ class ModesDialogViewModelTest : SysuiTestCase() { .setEnabled(false) .setManualInvocationAllowed(true) .build(), )) ) ) runCurrent() assertThat(tiles?.size).isEqualTo(2) Loading Loading @@ -108,7 +117,8 @@ class ModesDialogViewModelTest : SysuiTestCase() { .setActive(false) .setManualInvocationAllowed(false) .build(), )) ) ) runCurrent() assertThat(tiles?.size).isEqualTo(3) Loading Loading @@ -161,4 +171,56 @@ class ModesDialogViewModelTest : SysuiTestCase() { assertThat(tiles?.first()?.enabled).isFalse() } @Test fun onLongClick_launchesIntent() = testScope.runTest { val tiles by collectLastValue(underTest.tiles) val intentCaptor = argumentCaptor<Intent>() val modeId = "id" repository.addModes( listOf( TestModeBuilder() .setId(modeId) .setId("A") .setActive(true) .setManualInvocationAllowed(true) .build(), TestModeBuilder() .setId(modeId) .setId("B") .setActive(false) .setManualInvocationAllowed(true) .build(), ) ) runCurrent() assertThat(tiles?.size).isEqualTo(2) // Trigger onLongClick for A tiles?.first()?.onLongClick?.let { it() } runCurrent() // Check that it launched the correct intent verify(mockDialogDelegate).launchFromDialog(intentCaptor.capture()) var intent = intentCaptor.lastValue assertThat(intent.action).isEqualTo(Settings.ACTION_AUTOMATIC_ZEN_RULE_SETTINGS) assertThat(intent.extras?.getString(Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID)) .isEqualTo("A") clearInvocations(mockDialogDelegate) // Trigger onLongClick for B tiles?.last()?.onLongClick?.let { it() } runCurrent() // Check that it launched the correct intent verify(mockDialogDelegate).launchFromDialog(intentCaptor.capture()) intent = intentCaptor.lastValue assertThat(intent.action).isEqualTo(Settings.ACTION_AUTOMATIC_ZEN_RULE_SETTINGS) assertThat(intent.extras?.getString(Settings.EXTRA_AUTOMATIC_ZEN_RULE_ID)) .isEqualTo("B") } }
packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt +7 −28 Original line number Diff line number Diff line Loading @@ -16,14 +16,10 @@ package com.android.systemui.qs.tiles.impl.modes.domain.interactor //noinspection CleanArchitectureDependencyViolation: dialog needs to be opened on click import android.content.Intent import android.provider.Settings import com.android.internal.jank.InteractionJankMonitor import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.animation.Expandable import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dagger.SysUISingleton import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler import com.android.systemui.qs.tiles.base.interactor.QSTileInput import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor Loading @@ -31,15 +27,13 @@ import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction import com.android.systemui.statusbar.policy.ui.dialog.ModesDialogDelegate import javax.inject.Inject import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.withContext @SysUISingleton class ModesTileUserActionInteractor @Inject constructor( @Main private val coroutineContext: CoroutineContext, private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler, private val dialogTransitionAnimator: DialogTransitionAnimator, private val qsTileIntentUserInputHandler: QSTileIntentUserInputHandler, // TODO(b/353896370): The domain layer should not have to depend on the UI layer. private val dialogDelegate: ModesDialogDelegate, ) : QSTileUserActionInteractor<ModesTileModel> { val longClickIntent = Intent(Settings.ACTION_ZEN_MODE_SETTINGS) Loading @@ -51,29 +45,14 @@ constructor( handleClick(action.expandable) } is QSTileUserAction.LongClick -> { qsTileIntentUserActionHandler.handle(action.expandable, longClickIntent) qsTileIntentUserInputHandler.handle(action.expandable, longClickIntent) } } } } suspend fun handleClick(expandable: Expandable?) { // Show a dialog with the list of modes to configure. Dialogs shown by the // DialogTransitionAnimator must be created and shown on the main thread, so we post it to // the UI handler. withContext(coroutineContext) { val dialog = dialogDelegate.createDialog() expandable ?.dialogTransitionController( DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG) ) ?.let { controller -> dialogTransitionAnimator.show(dialog, controller) } ?: dialog.show() } } companion object { private const val INTERACTION_JANK_TAG = "configure_priority_modes" // Show a dialog with the list of modes to configure. dialogDelegate.showDialog(expandable) } }
packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt +120 −32 Original line number Diff line number Diff line Loading @@ -18,45 +18,97 @@ package com.android.systemui.statusbar.policy.ui.dialog import android.content.Intent import android.provider.Settings import android.util.Log import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import com.android.compose.PlatformButton import com.android.compose.PlatformOutlinedButton import com.android.internal.annotations.VisibleForTesting import com.android.internal.jank.InteractionJankMonitor import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.animation.Expandable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dialog.ui.composable.AlertDialogContent import com.android.systemui.plugins.ActivityStarter import com.android.systemui.res.R import com.android.systemui.statusbar.phone.ComponentSystemUIDialog import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.phone.SystemUIDialogFactory import com.android.systemui.statusbar.phone.create import com.android.systemui.statusbar.policy.ui.dialog.composable.ModeTileGrid import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModesDialogViewModel import com.android.systemui.util.Assert import javax.inject.Inject import javax.inject.Provider import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.withContext @SysUISingleton class ModesDialogDelegate @Inject constructor( private val sysuiDialogFactory: SystemUIDialogFactory, private val dialogTransitionAnimator: DialogTransitionAnimator, private val activityStarter: ActivityStarter, private val viewModel: ModesDialogViewModel, // Using a provider to avoid a circular dependency. private val viewModel: Provider<ModesDialogViewModel>, @Main private val mainCoroutineContext: CoroutineContext, ) : SystemUIDialog.Delegate { // NOTE: This should only be accessed/written from the main thread. @VisibleForTesting var currentDialog: ComponentSystemUIDialog? = null override fun createDialog(): SystemUIDialog { return sysuiDialogFactory.create { dialog -> Assert.isMainThread() if (currentDialog != null) { Log.w(TAG, "Dialog is already open, dismissing it and creating a new one.") currentDialog?.dismiss() } currentDialog = sysuiDialogFactory.create() { ModesDialogContent(it) } currentDialog ?.lifecycle ?.addObserver( object : DefaultLifecycleObserver { override fun onStop(owner: LifecycleOwner) { Assert.isMainThread() currentDialog = null } } ) return currentDialog!! } @Composable private fun ModesDialogContent(dialog: SystemUIDialog) { AlertDialogContent( title = { Text(stringResource(R.string.zen_modes_dialog_title)) }, content = { ModeTileGrid(viewModel) }, content = { ModeTileGrid(viewModel.get()) }, neutralButton = { PlatformOutlinedButton( onClick = { val animationController = dialogTransitionAnimator.createActivityTransitionController( dialog.getButton(SystemUIDialog.BUTTON_NEUTRAL) PlatformOutlinedButton(onClick = { openSettings(dialog) }) { Text(stringResource(R.string.zen_modes_dialog_settings)) } }, positiveButton = { PlatformButton(onClick = { dialog.dismiss() }) { Text(stringResource(R.string.zen_modes_dialog_done)) } }, ) } private fun openSettings(dialog: SystemUIDialog) { val animationController = dialogTransitionAnimator.createActivityTransitionController(dialog) if (animationController == null) { // The controller will take care of dismissing for us after the // animation, but let's make sure we dismiss the dialog if we don't // animate it. // The controller will take care of dismissing for us after // the animation, but let's make sure we dismiss the dialog // if we don't animate it. dialog.dismiss() } activityStarter.startActivity( Loading @@ -65,20 +117,56 @@ constructor( animationController ) } ) { Text(stringResource(R.string.zen_modes_dialog_settings)) suspend fun showDialog(expandable: Expandable? = null): SystemUIDialog { // Dialogs shown by the DialogTransitionAnimator must be created and shown on the main // thread, so we post it to the UI handler. withContext(mainCoroutineContext) { // Create the dialog if necessary if (currentDialog == null) { createDialog() } }, positiveButton = { PlatformButton(onClick = { dialog.dismiss() }) { Text(stringResource(R.string.zen_modes_dialog_done)) expandable ?.dialogTransitionController( DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG) ) ?.let { controller -> dialogTransitionAnimator.show(currentDialog!!, controller) } ?: currentDialog!!.show() } }, return currentDialog!! } /** * Launches the [intent] by animating from the dialog. If the dialog is not showing, just * launches it normally without animating. */ fun launchFromDialog(intent: Intent) { Assert.isMainThread() if (currentDialog == null) { Log.w( TAG, "Cannot launch from dialog, the dialog is not present. " + "Will launch activity without animating." ) } val animationController = currentDialog?.let { dialogTransitionAnimator.createActivityTransitionController(it) } if (animationController == null) { currentDialog?.dismiss() } activityStarter.startActivity( intent, true, /* dismissShade */ animationController, ) } companion object { private const val TAG = "ModesDialogDelegate" private val ZEN_MODE_SETTINGS_INTENT = Intent(Settings.ACTION_ZEN_MODE_SETTINGS) private const val INTERACTION_JANK_TAG = "configure_priority_modes" } }