diff options
8 files changed, 252 insertions, 5 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt index 9b9e584a936e..d5c910248942 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt @@ -21,14 +21,23 @@ 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.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject 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 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.verify +import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) @@ -36,7 +45,33 @@ import org.junit.runner.RunWith class ModesTileUserActionInteractorTest : SysuiTestCase() { private val inputHandler = FakeQSTileIntentUserInputHandler() - val underTest = ModesTileUserActionInteractor(inputHandler) + @Mock private lateinit var dialogTransitionAnimator: DialogTransitionAnimator + @Mock private lateinit var dialogDelegate: ModesDialogDelegate + @Mock private lateinit var mockDialog: SystemUIDialog + + private lateinit var underTest: ModesTileUserActionInteractor + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + whenever(dialogDelegate.createDialog()).thenReturn(mockDialog) + + underTest = + ModesTileUserActionInteractor( + EmptyCoroutineContext, + inputHandler, + dialogTransitionAnimator, + dialogDelegate, + ) + } + + @Test + fun handleClick() = runTest { + underTest.handleInput(QSTileInputTestKtx.click(ModesTileModel(false))) + + verify(mockDialog).show() + } @Test fun handleLongClick() = runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt new file mode 100644 index 000000000000..3baf2f40f175 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt @@ -0,0 +1,74 @@ +/* + * 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.tiles.impl.modes.ui + +import android.graphics.drawable.TestStubDrawable +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel +import com.android.systemui.qs.tiles.viewmodel.QSTileConfigTestBuilder +import com.android.systemui.qs.tiles.viewmodel.QSTileState +import com.android.systemui.qs.tiles.viewmodel.QSTileUIConfig +import com.android.systemui.res.R +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class ModesTileMapperTest : SysuiTestCase() { + val config = + QSTileConfigTestBuilder.build { + uiConfig = + QSTileUIConfig.Resource( + iconRes = R.drawable.qs_dnd_icon_off, + labelRes = R.string.quick_settings_modes_label, + ) + } + + val underTest = + ModesTileMapper( + context.orCreateTestableResources + .apply { + addOverride(R.drawable.qs_dnd_icon_on, TestStubDrawable()) + addOverride(R.drawable.qs_dnd_icon_off, TestStubDrawable()) + } + .resources, + context.theme, + ) + + @Test + fun inactiveState() { + val model = ModesTileModel(isActivated = false) + + val state = underTest.map(config, model) + + assertThat(state.activationState).isEqualTo(QSTileState.ActivationState.INACTIVE) + assertThat(state.iconRes).isEqualTo(R.drawable.qs_dnd_icon_off) + } + + @Test + fun activeState() { + val model = ModesTileModel(isActivated = true) + + val state = underTest.map(config, model) + + assertThat(state.activationState).isEqualTo(QSTileState.ActivationState.ACTIVE) + assertThat(state.iconRes).isEqualTo(R.drawable.qs_dnd_icon_on) + } +} diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 2bd97d9a2f91..7caa2c65c64a 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1084,6 +1084,15 @@ <!-- QuickStep: Accessibility to toggle overview [CHAR LIMIT=40] --> <string name="quick_step_accessibility_toggle_overview">Toggle Overview</string> + <!-- Priority modes dialog title [CHAR LIMIT=35] --> + <string name="zen_modes_dialog_title">Priority modes</string> + + <!-- Priority modes dialog confirmation button [CHAR LIMIT=15] --> + <string name="zen_modes_dialog_done">Done</string> + + <!-- Priority modes dialog settings shortcut button [CHAR LIMIT=15] --> + <string name="zen_modes_dialog_settings">Settings</string> + <!-- Zen mode: Priority only introduction message on first use --> <string name="zen_priority_introduction">You won\'t be disturbed by sounds and vibrations, except from alarms, reminders, events, and callers you specify. You\'ll still hear anything you choose to play including music, videos, and games.</string> diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt index b91891cf7be0..a3000316057f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ModesTile.kt @@ -44,6 +44,7 @@ import com.android.systemui.qs.tiles.viewmodel.QSTileState import com.android.systemui.res.R import javax.inject.Inject import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking class ModesTile @Inject @@ -91,8 +92,8 @@ constructor( override fun newTileState() = BooleanState() - override fun handleClick(expandable: Expandable?) { - // TODO(b/346519570) open dialog + override fun handleClick(expandable: Expandable?) = runBlocking { + userActionInteractor.handleClick(expandable) } override fun getLongClickIntent(): Intent = userActionInteractor.longClickIntent @@ -107,6 +108,7 @@ constructor( label = tileLabel secondaryLabel = tileState.secondaryLabel contentDescription = tileState.contentDescription + forceExpandIcon = true } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt index fd1f3d8fb23a..4c6563d6c143 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractor.kt @@ -16,19 +16,31 @@ 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.qs.tiles.base.actions.QSTileIntentUserInputHandler import com.android.systemui.qs.tiles.base.interactor.QSTileInput import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor 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 class ModesTileUserActionInteractor @Inject constructor( + @Main private val coroutineContext: CoroutineContext, private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler, + private val dialogTransitionAnimator: DialogTransitionAnimator, + private val dialogDelegate: ModesDialogDelegate, ) : QSTileUserActionInteractor<ModesTileModel> { val longClickIntent = Intent(Settings.ACTION_ZEN_MODE_SETTINGS) @@ -36,7 +48,7 @@ constructor( with(input) { when (action) { is QSTileUserAction.Click -> { - // TODO(b/346519570) open dialog + handleClick(action.expandable) } is QSTileUserAction.LongClick -> { qsTileIntentUserActionHandler.handle(action.expandable, longClickIntent) @@ -44,4 +56,24 @@ constructor( } } } + + 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" + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt index 26b9a4c7f416..7048adab329d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt @@ -59,5 +59,6 @@ constructor( QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK, ) + sideViewIcon = QSTileState.SideViewIcon.Chevron } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt new file mode 100644 index 000000000000..6db1eacaa706 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt @@ -0,0 +1,81 @@ +/* + * 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.statusbar.policy.ui.dialog + +import android.content.Intent +import android.provider.Settings +import androidx.compose.material3.Text +import androidx.compose.ui.res.stringResource +import com.android.compose.PlatformButton +import com.android.compose.PlatformOutlinedButton +import com.android.systemui.animation.DialogTransitionAnimator +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.SystemUIDialog +import com.android.systemui.statusbar.phone.SystemUIDialogFactory +import com.android.systemui.statusbar.phone.create +import javax.inject.Inject + +class ModesDialogDelegate +@Inject +constructor( + private val sysuiDialogFactory: SystemUIDialogFactory, + private val dialogTransitionAnimator: DialogTransitionAnimator, + private val activityStarter: ActivityStarter, +) : SystemUIDialog.Delegate { + override fun createDialog(): SystemUIDialog { + return sysuiDialogFactory.create { dialog -> + AlertDialogContent( + title = { Text(stringResource(R.string.zen_modes_dialog_title)) }, + content = { Text("Under construction") }, + neutralButton = { + PlatformOutlinedButton( + onClick = { + val animationController = + dialogTransitionAnimator.createActivityTransitionController( + dialog.getButton(SystemUIDialog.BUTTON_NEUTRAL) + ) + 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. + dialog.dismiss() + } + activityStarter.startActivity( + ZEN_MODE_SETTINGS_INTENT, + true /* dismissShade */, + animationController + ) + } + ) { + Text(stringResource(R.string.zen_modes_dialog_settings)) + } + }, + positiveButton = { + PlatformButton(onClick = { dialog.dismiss() }) { + Text(stringResource(R.string.zen_modes_dialog_done)) + } + }, + ) + } + } + + companion object { + private val ZEN_MODE_SETTINGS_INTENT = Intent(Settings.ACTION_ZEN_MODE_SETTINGS) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt index 4c77fb84d8ce..27b6ea61a922 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/ModesTileTest.kt @@ -27,6 +27,7 @@ import androidx.test.filters.SmallTest import com.android.internal.logging.MetricsLogger import com.android.settingslib.notification.data.repository.FakeZenModeRepository import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.classifier.FalsingManagerFake import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.statusbar.StatusBarStateController @@ -41,10 +42,12 @@ import com.android.systemui.qs.tiles.viewmodel.QSTileConfigProvider import com.android.systemui.qs.tiles.viewmodel.QSTileConfigTestBuilder import com.android.systemui.qs.tiles.viewmodel.QSTileUIConfig import com.android.systemui.res.R +import com.android.systemui.statusbar.policy.ui.dialog.ModesDialogDelegate import com.android.systemui.util.mockito.any import com.android.systemui.util.settings.FakeSettings import com.android.systemui.util.settings.SecureSettings import com.google.common.truth.Truth.assertThat +import kotlin.coroutines.EmptyCoroutineContext import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope @@ -79,6 +82,10 @@ class ModesTileTest : SysuiTestCase() { @Mock private lateinit var qsTileConfigProvider: QSTileConfigProvider + @Mock private lateinit var dialogTransitionAnimator: DialogTransitionAnimator + + @Mock private lateinit var dialogDelegate: ModesDialogDelegate + private val inputHandler = FakeQSTileIntentUserInputHandler() private val zenModeRepository = FakeZenModeRepository() private val tileDataInteractor = ModesTileDataInteractor(zenModeRepository) @@ -122,7 +129,13 @@ class ModesTileTest : SysuiTestCase() { } ) - userActionInteractor = ModesTileUserActionInteractor(inputHandler) + userActionInteractor = + ModesTileUserActionInteractor( + EmptyCoroutineContext, + inputHandler, + dialogTransitionAnimator, + dialogDelegate, + ) underTest = ModesTile( |