Loading packages/SystemUI/Android.bp +1 −0 Original line number Original line Diff line number Diff line Loading @@ -258,6 +258,7 @@ filegroup { "tests/src/**/systemui/statusbar/policy/LocationControllerImplTest.java", "tests/src/**/systemui/statusbar/policy/LocationControllerImplTest.java", "tests/src/**/systemui/statusbar/policy/RemoteInputViewTest.java", "tests/src/**/systemui/statusbar/policy/RemoteInputViewTest.java", "tests/src/**/systemui/statusbar/policy/SmartReplyViewTest.java", "tests/src/**/systemui/statusbar/policy/SmartReplyViewTest.java", "tests/src/**/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateTest.kt", "tests/src/**/systemui/statusbar/StatusBarStateControllerImplTest.kt", "tests/src/**/systemui/statusbar/StatusBarStateControllerImplTest.kt", "tests/src/**/systemui/theme/ThemeOverlayApplierTest.java", "tests/src/**/systemui/theme/ThemeOverlayApplierTest.java", "tests/src/**/systemui/touch/TouchInsetManagerTest.java", "tests/src/**/systemui/touch/TouchInsetManagerTest.java", Loading packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt +35 −31 Original line number Original line Diff line number Diff line Loading @@ -21,64 +21,68 @@ import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.animation.Expandable import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject 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.base.interactor.QSTileInputTestKtx import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel 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.mockModesDialogDelegate import com.android.systemui.statusbar.policy.ui.dialog.ModesDialogDelegate import com.android.systemui.testKosmos import com.google.common.truth.Truth import com.google.common.truth.Truth.assertThat import kotlin.coroutines.EmptyCoroutineContext import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.Test import org.junit.runner.RunWith import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.kotlin.eq import org.mockito.MockitoAnnotations import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @SmallTest @SmallTest @RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class) @EnableFlags(android.app.Flags.FLAG_MODES_UI) @EnableFlags(android.app.Flags.FLAG_MODES_UI) class ModesTileUserActionInteractorTest : SysuiTestCase() { 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 private val underTest = @Mock private lateinit var dialogDelegate: ModesDialogDelegate ModesTileUserActionInteractor( @Mock private lateinit var mockDialog: SystemUIDialog 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 verify(mockDialogDelegate).showDialog(eq(expandable)) fun setup() { } MockitoAnnotations.initMocks(this) whenever(dialogDelegate.createDialog()).thenReturn(mockDialog) @Test fun handleClick_inactive() = runTest { val expandable = mock<Expandable>() underTest.handleInput( QSTileInputTestKtx.click(data = ModesTileModel(false), expandable = expandable)) underTest = verify(mockDialogDelegate).showDialog(eq(expandable)) ModesTileUserActionInteractor( EmptyCoroutineContext, inputHandler, dialogTransitionAnimator, dialogDelegate, ) } } @Test @Test fun handleClick() = runTest { fun handleLongClick_active() = runTest { underTest.handleInput(QSTileInputTestKtx.click(ModesTileModel(false))) underTest.handleInput(QSTileInputTestKtx.longClick(ModesTileModel(true))) verify(mockDialog).show() QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput { assertThat(it.intent.action).isEqualTo(Settings.ACTION_ZEN_MODE_SETTINGS) } } } @Test @Test fun handleLongClick() = runTest { fun handleLongClick_inactive() = runTest { underTest.handleInput(QSTileInputTestKtx.longClick(ModesTileModel(false))) underTest.handleInput(QSTileInputTestKtx.longClick(ModesTileModel(false))) QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput { 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/viewmodel/ModesDialogViewModelTest.kt +67 −5 Original line number Original line Diff line number Diff line Loading @@ -18,6 +18,8 @@ package com.android.systemui.statusbar.policy.ui.dialog.viewmodel 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.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest import com.android.settingslib.notification.modes.TestModeBuilder 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.kosmos.testScope import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor 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.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi Loading @@ -34,16 +37,21 @@ import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.Test import org.junit.runner.RunWith import org.junit.runner.RunWith import org.mockito.Mockito.clearInvocations import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.verify @SmallTest @SmallTest @RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class) class ModesDialogViewModelTest : SysuiTestCase() { class ModesDialogViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val kosmos = testKosmos() private val testScope = kosmos.testScope private val testScope = kosmos.testScope val repository = kosmos.fakeZenModeRepository private val repository = kosmos.fakeZenModeRepository val interactor = kosmos.zenModeInteractor 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 @Test fun tiles_filtersOutDisabledModes() = fun tiles_filtersOutDisabledModes() = Loading @@ -64,7 +72,8 @@ class ModesDialogViewModelTest : SysuiTestCase() { .setEnabled(false) .setEnabled(false) .setManualInvocationAllowed(true) .setManualInvocationAllowed(true) .build(), .build(), )) ) ) runCurrent() runCurrent() assertThat(tiles?.size).isEqualTo(2) assertThat(tiles?.size).isEqualTo(2) Loading Loading @@ -108,7 +117,8 @@ class ModesDialogViewModelTest : SysuiTestCase() { .setActive(false) .setActive(false) .setManualInvocationAllowed(false) .setManualInvocationAllowed(false) .build(), .build(), )) ) ) runCurrent() runCurrent() assertThat(tiles?.size).isEqualTo(3) assertThat(tiles?.size).isEqualTo(3) Loading Loading @@ -161,4 +171,56 @@ class ModesDialogViewModelTest : SysuiTestCase() { assertThat(tiles?.first()?.enabled).isFalse() 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 Original line Diff line number Diff line Loading @@ -16,14 +16,10 @@ package com.android.systemui.qs.tiles.impl.modes.domain.interactor 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.content.Intent import android.provider.Settings 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.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.actions.QSTileIntentUserInputHandler import com.android.systemui.qs.tiles.base.interactor.QSTileInput import com.android.systemui.qs.tiles.base.interactor.QSTileInput import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor 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.qs.tiles.viewmodel.QSTileUserAction import com.android.systemui.statusbar.policy.ui.dialog.ModesDialogDelegate import com.android.systemui.statusbar.policy.ui.dialog.ModesDialogDelegate import javax.inject.Inject import javax.inject.Inject import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.withContext @SysUISingleton class ModesTileUserActionInteractor class ModesTileUserActionInteractor @Inject @Inject constructor( constructor( @Main private val coroutineContext: CoroutineContext, private val qsTileIntentUserInputHandler: QSTileIntentUserInputHandler, private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler, // TODO(b/353896370): The domain layer should not have to depend on the UI layer. private val dialogTransitionAnimator: DialogTransitionAnimator, private val dialogDelegate: ModesDialogDelegate, private val dialogDelegate: ModesDialogDelegate, ) : QSTileUserActionInteractor<ModesTileModel> { ) : QSTileUserActionInteractor<ModesTileModel> { val longClickIntent = Intent(Settings.ACTION_ZEN_MODE_SETTINGS) val longClickIntent = Intent(Settings.ACTION_ZEN_MODE_SETTINGS) Loading @@ -51,29 +45,14 @@ constructor( handleClick(action.expandable) handleClick(action.expandable) } } is QSTileUserAction.LongClick -> { is QSTileUserAction.LongClick -> { qsTileIntentUserActionHandler.handle(action.expandable, longClickIntent) qsTileIntentUserInputHandler.handle(action.expandable, longClickIntent) } } } } } } } } suspend fun handleClick(expandable: Expandable?) { suspend fun handleClick(expandable: Expandable?) { // Show a dialog with the list of modes to configure. Dialogs shown by the // Show a dialog with the list of modes to configure. // DialogTransitionAnimator must be created and shown on the main thread, so we post it to dialogDelegate.showDialog(expandable) // 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" } } } } packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt +120 −32 Original line number Original line Diff line number Diff line Loading @@ -18,45 +18,97 @@ package com.android.systemui.statusbar.policy.ui.dialog import android.content.Intent import android.content.Intent import android.provider.Settings import android.provider.Settings import android.util.Log import androidx.compose.material3.Text import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import com.android.compose.PlatformButton import com.android.compose.PlatformButton import com.android.compose.PlatformOutlinedButton 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.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.dialog.ui.composable.AlertDialogContent import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.ActivityStarter import com.android.systemui.res.R 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.SystemUIDialog import com.android.systemui.statusbar.phone.SystemUIDialogFactory import com.android.systemui.statusbar.phone.SystemUIDialogFactory import com.android.systemui.statusbar.phone.create 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.composable.ModeTileGrid import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModesDialogViewModel import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModesDialogViewModel import com.android.systemui.util.Assert import javax.inject.Inject import javax.inject.Inject import javax.inject.Provider import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.withContext @SysUISingleton class ModesDialogDelegate class ModesDialogDelegate @Inject @Inject constructor( constructor( private val sysuiDialogFactory: SystemUIDialogFactory, private val sysuiDialogFactory: SystemUIDialogFactory, private val dialogTransitionAnimator: DialogTransitionAnimator, private val dialogTransitionAnimator: DialogTransitionAnimator, private val activityStarter: ActivityStarter, 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 { ) : SystemUIDialog.Delegate { // NOTE: This should only be accessed/written from the main thread. @VisibleForTesting var currentDialog: ComponentSystemUIDialog? = null override fun createDialog(): SystemUIDialog { 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( AlertDialogContent( title = { Text(stringResource(R.string.zen_modes_dialog_title)) }, title = { Text(stringResource(R.string.zen_modes_dialog_title)) }, content = { ModeTileGrid(viewModel) }, content = { ModeTileGrid(viewModel.get()) }, neutralButton = { neutralButton = { PlatformOutlinedButton( PlatformOutlinedButton(onClick = { openSettings(dialog) }) { onClick = { Text(stringResource(R.string.zen_modes_dialog_settings)) val animationController = } dialogTransitionAnimator.createActivityTransitionController( }, dialog.getButton(SystemUIDialog.BUTTON_NEUTRAL) 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) { if (animationController == null) { // The controller will take care of dismissing for us after the // The controller will take care of dismissing for us after // animation, but let's make sure we dismiss the dialog if we don't // the animation, but let's make sure we dismiss the dialog // animate it. // if we don't animate it. dialog.dismiss() dialog.dismiss() } } activityStarter.startActivity( activityStarter.startActivity( Loading @@ -65,20 +117,56 @@ constructor( animationController 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 = { expandable PlatformButton(onClick = { dialog.dismiss() }) { ?.dialogTransitionController( Text(stringResource(R.string.zen_modes_dialog_done)) 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 { companion object { private const val TAG = "ModesDialogDelegate" private val ZEN_MODE_SETTINGS_INTENT = Intent(Settings.ACTION_ZEN_MODE_SETTINGS) private val ZEN_MODE_SETTINGS_INTENT = Intent(Settings.ACTION_ZEN_MODE_SETTINGS) private const val INTERACTION_JANK_TAG = "configure_priority_modes" } } } } Loading
packages/SystemUI/Android.bp +1 −0 Original line number Original line Diff line number Diff line Loading @@ -258,6 +258,7 @@ filegroup { "tests/src/**/systemui/statusbar/policy/LocationControllerImplTest.java", "tests/src/**/systemui/statusbar/policy/LocationControllerImplTest.java", "tests/src/**/systemui/statusbar/policy/RemoteInputViewTest.java", "tests/src/**/systemui/statusbar/policy/RemoteInputViewTest.java", "tests/src/**/systemui/statusbar/policy/SmartReplyViewTest.java", "tests/src/**/systemui/statusbar/policy/SmartReplyViewTest.java", "tests/src/**/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateTest.kt", "tests/src/**/systemui/statusbar/StatusBarStateControllerImplTest.kt", "tests/src/**/systemui/statusbar/StatusBarStateControllerImplTest.kt", "tests/src/**/systemui/theme/ThemeOverlayApplierTest.java", "tests/src/**/systemui/theme/ThemeOverlayApplierTest.java", "tests/src/**/systemui/touch/TouchInsetManagerTest.java", "tests/src/**/systemui/touch/TouchInsetManagerTest.java", Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt +35 −31 Original line number Original line Diff line number Diff line Loading @@ -21,64 +21,68 @@ import android.provider.Settings import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.animation.Expandable import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject 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.base.interactor.QSTileInputTestKtx import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel 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.mockModesDialogDelegate import com.android.systemui.statusbar.policy.ui.dialog.ModesDialogDelegate import com.android.systemui.testKosmos import com.google.common.truth.Truth import com.google.common.truth.Truth.assertThat import kotlin.coroutines.EmptyCoroutineContext import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.Test import org.junit.runner.RunWith import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.kotlin.eq import org.mockito.MockitoAnnotations import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @SmallTest @SmallTest @RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class) @EnableFlags(android.app.Flags.FLAG_MODES_UI) @EnableFlags(android.app.Flags.FLAG_MODES_UI) class ModesTileUserActionInteractorTest : SysuiTestCase() { 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 private val underTest = @Mock private lateinit var dialogDelegate: ModesDialogDelegate ModesTileUserActionInteractor( @Mock private lateinit var mockDialog: SystemUIDialog 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 verify(mockDialogDelegate).showDialog(eq(expandable)) fun setup() { } MockitoAnnotations.initMocks(this) whenever(dialogDelegate.createDialog()).thenReturn(mockDialog) @Test fun handleClick_inactive() = runTest { val expandable = mock<Expandable>() underTest.handleInput( QSTileInputTestKtx.click(data = ModesTileModel(false), expandable = expandable)) underTest = verify(mockDialogDelegate).showDialog(eq(expandable)) ModesTileUserActionInteractor( EmptyCoroutineContext, inputHandler, dialogTransitionAnimator, dialogDelegate, ) } } @Test @Test fun handleClick() = runTest { fun handleLongClick_active() = runTest { underTest.handleInput(QSTileInputTestKtx.click(ModesTileModel(false))) underTest.handleInput(QSTileInputTestKtx.longClick(ModesTileModel(true))) verify(mockDialog).show() QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput { assertThat(it.intent.action).isEqualTo(Settings.ACTION_ZEN_MODE_SETTINGS) } } } @Test @Test fun handleLongClick() = runTest { fun handleLongClick_inactive() = runTest { underTest.handleInput(QSTileInputTestKtx.longClick(ModesTileModel(false))) underTest.handleInput(QSTileInputTestKtx.longClick(ModesTileModel(false))) QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput { 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/viewmodel/ModesDialogViewModelTest.kt +67 −5 Original line number Original line Diff line number Diff line Loading @@ -18,6 +18,8 @@ package com.android.systemui.statusbar.policy.ui.dialog.viewmodel 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.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest import com.android.settingslib.notification.modes.TestModeBuilder 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.kosmos.testScope import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository import com.android.systemui.statusbar.policy.data.repository.fakeZenModeRepository import com.android.systemui.statusbar.policy.domain.interactor.zenModeInteractor 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.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi Loading @@ -34,16 +37,21 @@ import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.Test import org.junit.runner.RunWith import org.junit.runner.RunWith import org.mockito.Mockito.clearInvocations import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.verify @SmallTest @SmallTest @RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class) class ModesDialogViewModelTest : SysuiTestCase() { class ModesDialogViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val kosmos = testKosmos() private val testScope = kosmos.testScope private val testScope = kosmos.testScope val repository = kosmos.fakeZenModeRepository private val repository = kosmos.fakeZenModeRepository val interactor = kosmos.zenModeInteractor 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 @Test fun tiles_filtersOutDisabledModes() = fun tiles_filtersOutDisabledModes() = Loading @@ -64,7 +72,8 @@ class ModesDialogViewModelTest : SysuiTestCase() { .setEnabled(false) .setEnabled(false) .setManualInvocationAllowed(true) .setManualInvocationAllowed(true) .build(), .build(), )) ) ) runCurrent() runCurrent() assertThat(tiles?.size).isEqualTo(2) assertThat(tiles?.size).isEqualTo(2) Loading Loading @@ -108,7 +117,8 @@ class ModesDialogViewModelTest : SysuiTestCase() { .setActive(false) .setActive(false) .setManualInvocationAllowed(false) .setManualInvocationAllowed(false) .build(), .build(), )) ) ) runCurrent() runCurrent() assertThat(tiles?.size).isEqualTo(3) assertThat(tiles?.size).isEqualTo(3) Loading Loading @@ -161,4 +171,56 @@ class ModesDialogViewModelTest : SysuiTestCase() { assertThat(tiles?.first()?.enabled).isFalse() 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 Original line Diff line number Diff line Loading @@ -16,14 +16,10 @@ package com.android.systemui.qs.tiles.impl.modes.domain.interactor 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.content.Intent import android.provider.Settings 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.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.actions.QSTileIntentUserInputHandler import com.android.systemui.qs.tiles.base.interactor.QSTileInput import com.android.systemui.qs.tiles.base.interactor.QSTileInput import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor 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.qs.tiles.viewmodel.QSTileUserAction import com.android.systemui.statusbar.policy.ui.dialog.ModesDialogDelegate import com.android.systemui.statusbar.policy.ui.dialog.ModesDialogDelegate import javax.inject.Inject import javax.inject.Inject import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.withContext @SysUISingleton class ModesTileUserActionInteractor class ModesTileUserActionInteractor @Inject @Inject constructor( constructor( @Main private val coroutineContext: CoroutineContext, private val qsTileIntentUserInputHandler: QSTileIntentUserInputHandler, private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler, // TODO(b/353896370): The domain layer should not have to depend on the UI layer. private val dialogTransitionAnimator: DialogTransitionAnimator, private val dialogDelegate: ModesDialogDelegate, private val dialogDelegate: ModesDialogDelegate, ) : QSTileUserActionInteractor<ModesTileModel> { ) : QSTileUserActionInteractor<ModesTileModel> { val longClickIntent = Intent(Settings.ACTION_ZEN_MODE_SETTINGS) val longClickIntent = Intent(Settings.ACTION_ZEN_MODE_SETTINGS) Loading @@ -51,29 +45,14 @@ constructor( handleClick(action.expandable) handleClick(action.expandable) } } is QSTileUserAction.LongClick -> { is QSTileUserAction.LongClick -> { qsTileIntentUserActionHandler.handle(action.expandable, longClickIntent) qsTileIntentUserInputHandler.handle(action.expandable, longClickIntent) } } } } } } } } suspend fun handleClick(expandable: Expandable?) { suspend fun handleClick(expandable: Expandable?) { // Show a dialog with the list of modes to configure. Dialogs shown by the // Show a dialog with the list of modes to configure. // DialogTransitionAnimator must be created and shown on the main thread, so we post it to dialogDelegate.showDialog(expandable) // 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" } } } }
packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt +120 −32 Original line number Original line Diff line number Diff line Loading @@ -18,45 +18,97 @@ package com.android.systemui.statusbar.policy.ui.dialog import android.content.Intent import android.content.Intent import android.provider.Settings import android.provider.Settings import android.util.Log import androidx.compose.material3.Text import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import com.android.compose.PlatformButton import com.android.compose.PlatformButton import com.android.compose.PlatformOutlinedButton 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.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.dialog.ui.composable.AlertDialogContent import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.ActivityStarter import com.android.systemui.res.R 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.SystemUIDialog import com.android.systemui.statusbar.phone.SystemUIDialogFactory import com.android.systemui.statusbar.phone.SystemUIDialogFactory import com.android.systemui.statusbar.phone.create 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.composable.ModeTileGrid import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModesDialogViewModel import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModesDialogViewModel import com.android.systemui.util.Assert import javax.inject.Inject import javax.inject.Inject import javax.inject.Provider import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.withContext @SysUISingleton class ModesDialogDelegate class ModesDialogDelegate @Inject @Inject constructor( constructor( private val sysuiDialogFactory: SystemUIDialogFactory, private val sysuiDialogFactory: SystemUIDialogFactory, private val dialogTransitionAnimator: DialogTransitionAnimator, private val dialogTransitionAnimator: DialogTransitionAnimator, private val activityStarter: ActivityStarter, 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 { ) : SystemUIDialog.Delegate { // NOTE: This should only be accessed/written from the main thread. @VisibleForTesting var currentDialog: ComponentSystemUIDialog? = null override fun createDialog(): SystemUIDialog { 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( AlertDialogContent( title = { Text(stringResource(R.string.zen_modes_dialog_title)) }, title = { Text(stringResource(R.string.zen_modes_dialog_title)) }, content = { ModeTileGrid(viewModel) }, content = { ModeTileGrid(viewModel.get()) }, neutralButton = { neutralButton = { PlatformOutlinedButton( PlatformOutlinedButton(onClick = { openSettings(dialog) }) { onClick = { Text(stringResource(R.string.zen_modes_dialog_settings)) val animationController = } dialogTransitionAnimator.createActivityTransitionController( }, dialog.getButton(SystemUIDialog.BUTTON_NEUTRAL) 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) { if (animationController == null) { // The controller will take care of dismissing for us after the // The controller will take care of dismissing for us after // animation, but let's make sure we dismiss the dialog if we don't // the animation, but let's make sure we dismiss the dialog // animate it. // if we don't animate it. dialog.dismiss() dialog.dismiss() } } activityStarter.startActivity( activityStarter.startActivity( Loading @@ -65,20 +117,56 @@ constructor( animationController 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 = { expandable PlatformButton(onClick = { dialog.dismiss() }) { ?.dialogTransitionController( Text(stringResource(R.string.zen_modes_dialog_done)) 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 { companion object { private const val TAG = "ModesDialogDelegate" private val ZEN_MODE_SETTINGS_INTENT = Intent(Settings.ACTION_ZEN_MODE_SETTINGS) private val ZEN_MODE_SETTINGS_INTENT = Intent(Settings.ACTION_ZEN_MODE_SETTINGS) private const val INTERACTION_JANK_TAG = "configure_priority_modes" } } } }