diff options
12 files changed, 501 insertions, 4 deletions
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt index 7886e85cbad8..49b974fa3f00 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt @@ -20,6 +20,7 @@ import android.app.NotificationManager import android.provider.Settings import com.android.settingslib.notification.modes.TestModeBuilder import com.android.settingslib.notification.modes.ZenMode +import java.time.Duration import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -35,8 +36,7 @@ class FakeZenModeRepository : ZenModeRepository { override val globalZenMode: StateFlow<Int> get() = mutableZenMode.asStateFlow() - private val mutableModesFlow: MutableStateFlow<List<ZenMode>> = - MutableStateFlow(listOf(TestModeBuilder.EXAMPLE)) + private val mutableModesFlow: MutableStateFlow<List<ZenMode>> = MutableStateFlow(listOf()) override val modes: Flow<List<ZenMode>> get() = mutableModesFlow.asStateFlow() @@ -52,6 +52,10 @@ class FakeZenModeRepository : ZenModeRepository { mutableZenMode.value = zenMode } + fun addModes(zenModes: List<ZenMode>) { + mutableModesFlow.value += zenModes + } + fun addMode(id: String, active: Boolean = false) { mutableModesFlow.value += newMode(id, active) } @@ -60,6 +64,20 @@ class FakeZenModeRepository : ZenModeRepository { mutableModesFlow.value = mutableModesFlow.value.filter { it.id != id } } + override fun activateMode(zenMode: ZenMode, duration: Duration?) { + activateMode(zenMode.id) + } + + override fun deactivateMode(zenMode: ZenMode) { + deactivateMode(zenMode.id) + } + + fun activateMode(id: String) { + val oldMode = mutableModesFlow.value.find { it.id == id } ?: return + removeMode(id) + mutableModesFlow.value += TestModeBuilder(oldMode).setActive(true).build() + } + fun deactivateMode(id: String) { val oldMode = mutableModesFlow.value.find { it.id == id } ?: return removeMode(id) diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt index b2fcb5f6da41..0ff7f84a08b9 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/ZenModeRepository.kt @@ -30,6 +30,7 @@ import android.provider.Settings import com.android.settingslib.flags.Flags import com.android.settingslib.notification.modes.ZenMode import com.android.settingslib.notification.modes.ZenModesBackend +import java.time.Duration import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose @@ -57,6 +58,10 @@ interface ZenModeRepository { /** A list of all existing priority modes. */ val modes: Flow<List<ZenMode>> + + fun activateMode(zenMode: ZenMode, duration: Duration? = null) + + fun deactivateMode(zenMode: ZenMode) } @SuppressLint("SharedFlowCreation") @@ -178,4 +183,12 @@ class ZenModeRepositoryImpl( flowOf(emptyList()) } } + + override fun activateMode(zenMode: ZenMode, duration: Duration?) { + backend.activateMode(zenMode, duration) + } + + override fun deactivateMode(zenMode: ZenMode) { + backend.deactivateMode(zenMode) + } } diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java index 7b994d59d963..2f7cdd617081 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java +++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java @@ -37,6 +37,13 @@ public class TestModeBuilder { private ZenModeConfig.ZenRule mConfigZenRule; public static final ZenMode EXAMPLE = new TestModeBuilder().build(); + public static final ZenMode MANUAL_DND = ZenMode.manualDndMode( + new AutomaticZenRule.Builder("Manual DND", Uri.parse("rule://dnd")) + .setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().disallowAllSounds().build()) + .build(), + true /* isActive */ + ); public TestModeBuilder() { // Reasonable defaults diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index a17076b525f4..4e01a71df113 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -539,6 +539,7 @@ android_library { "androidx.preference_preference", "androidx.appcompat_appcompat", "androidx.concurrent_concurrent-futures", + "androidx.concurrent_concurrent-futures-ktx", "androidx.mediarouter_mediarouter", "androidx.palette_palette", "androidx.legacy_legacy-preference-v14", diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt new file mode 100644 index 000000000000..fdfc7f13abf7 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModelTest.kt @@ -0,0 +1,164 @@ +/* + * 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.viewmodel + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.settingslib.notification.modes.TestModeBuilder +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +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.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class ModesDialogViewModelTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + val repository = kosmos.fakeZenModeRepository + val interactor = kosmos.zenModeInteractor + + val underTest = ModesDialogViewModel(context, interactor, kosmos.testDispatcher) + + @Test + fun tiles_filtersOutDisabledModes() = + testScope.runTest { + val tiles by collectLastValue(underTest.tiles) + + repository.addModes( + listOf( + TestModeBuilder().setName("Disabled").setEnabled(false).build(), + TestModeBuilder.MANUAL_DND, + TestModeBuilder() + .setName("Enabled") + .setEnabled(true) + .setManualInvocationAllowed(true) + .build(), + TestModeBuilder() + .setName("Disabled with manual") + .setEnabled(false) + .setManualInvocationAllowed(true) + .build(), + )) + runCurrent() + + assertThat(tiles?.size).isEqualTo(2) + with(tiles?.elementAt(0)!!) { + assertThat(this.text).isEqualTo("Manual DND") + assertThat(this.subtext).isEqualTo("On") + assertThat(this.enabled).isEqualTo(true) + } + with(tiles?.elementAt(1)!!) { + assertThat(this.text).isEqualTo("Enabled") + assertThat(this.subtext).isEqualTo("Off") + assertThat(this.enabled).isEqualTo(false) + } + } + + @Test + fun tiles_filtersOutInactiveModesWithoutManualInvocation() = + testScope.runTest { + val tiles by collectLastValue(underTest.tiles) + + repository.addModes( + listOf( + TestModeBuilder() + .setName("Active without manual") + .setActive(true) + .setManualInvocationAllowed(false) + .build(), + TestModeBuilder() + .setName("Active with manual") + .setTriggerDescription("trigger description") + .setActive(true) + .setManualInvocationAllowed(true) + .build(), + TestModeBuilder() + .setName("Inactive with manual") + .setActive(false) + .setManualInvocationAllowed(true) + .build(), + TestModeBuilder() + .setName("Inactive without manual") + .setActive(false) + .setManualInvocationAllowed(false) + .build(), + )) + runCurrent() + + assertThat(tiles?.size).isEqualTo(3) + with(tiles?.elementAt(0)!!) { + assertThat(this.text).isEqualTo("Active without manual") + assertThat(this.subtext).isEqualTo("On") + assertThat(this.enabled).isEqualTo(true) + } + with(tiles?.elementAt(1)!!) { + assertThat(this.text).isEqualTo("Active with manual") + assertThat(this.subtext).isEqualTo("trigger description") + assertThat(this.enabled).isEqualTo(true) + } + with(tiles?.elementAt(2)!!) { + assertThat(this.text).isEqualTo("Inactive with manual") + assertThat(this.subtext).isEqualTo("Off") + assertThat(this.enabled).isEqualTo(false) + } + } + + @Test + fun onClick_togglesTileState() = + testScope.runTest { + val tiles by collectLastValue(underTest.tiles) + + val modeId = "id" + repository.addModes( + listOf( + TestModeBuilder() + .setId(modeId) + .setName("Test") + .setManualInvocationAllowed(true) + .build() + ) + ) + runCurrent() + + assertThat(tiles?.size).isEqualTo(1) + assertThat(tiles?.elementAt(0)?.enabled).isFalse() + + // Trigger onClick + tiles?.first()?.onClick?.let { it() } + runCurrent() + + assertThat(tiles?.first()?.enabled).isTrue() + + // Trigger onClick + tiles?.first()?.onClick?.let { it() } + runCurrent() + + assertThat(tiles?.first()?.enabled).isFalse() + } +} diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index be2850448ca5..8322b6c85aed 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1093,6 +1093,12 @@ <!-- Priority modes dialog settings shortcut button [CHAR LIMIT=15] --> <string name="zen_modes_dialog_settings">Settings</string> + <!-- Priority modes: label for an active mode [CHAR LIMIT=35] --> + <string name="zen_mode_on">On</string> + + <!-- Priority modes: label for an inactive mode [CHAR LIMIT=35] --> + <string name="zen_mode_off">Off</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/statusbar/policy/domain/interactor/ZenModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt index e4d06681d439..7a521a6ba28f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt @@ -16,8 +16,14 @@ package com.android.systemui.statusbar.policy.domain.interactor +import android.content.Context import android.provider.Settings +import androidx.concurrent.futures.await import com.android.settingslib.notification.data.repository.ZenModeRepository +import com.android.settingslib.notification.modes.ZenIconLoader +import com.android.settingslib.notification.modes.ZenMode +import com.android.systemui.common.shared.model.Icon +import java.time.Duration import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine @@ -28,7 +34,9 @@ import kotlinx.coroutines.flow.map * An interactor that performs business logic related to the status and configuration of Zen Mode * (or Do Not Disturb/DND Mode). */ -class ZenModeInteractor @Inject constructor(repository: ZenModeRepository) { +class ZenModeInteractor @Inject constructor(private val repository: ZenModeRepository) { + private val iconLoader: ZenIconLoader = ZenIconLoader.getInstance() + val isZenModeEnabled: Flow<Boolean> = repository.globalZenMode .map { @@ -52,4 +60,18 @@ class ZenModeInteractor @Inject constructor(repository: ZenModeRepository) { } } .distinctUntilChanged() + + val modes: Flow<List<ZenMode>> = repository.modes + + suspend fun getModeIcon(mode: ZenMode, context: Context): Icon { + return Icon.Loaded(mode.getIcon(context, iconLoader).await(), contentDescription = null) + } + + fun activateMode(zenMode: ZenMode, duration: Duration? = null) { + repository.activateMode(zenMode, duration) + } + + fun deactivateMode(zenMode: ZenMode) { + repository.deactivateMode(zenMode) + } } 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 index 6db1eacaa706..2b094d6b4922 100644 --- 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 @@ -29,6 +29,8 @@ 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 com.android.systemui.statusbar.policy.ui.dialog.composable.ModeTileGrid +import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModesDialogViewModel import javax.inject.Inject class ModesDialogDelegate @@ -37,12 +39,13 @@ constructor( private val sysuiDialogFactory: SystemUIDialogFactory, private val dialogTransitionAnimator: DialogTransitionAnimator, private val activityStarter: ActivityStarter, + private val viewModel: ModesDialogViewModel, ) : SystemUIDialog.Delegate { override fun createDialog(): SystemUIDialog { return sysuiDialogFactory.create { dialog -> AlertDialogContent( title = { Text(stringResource(R.string.zen_modes_dialog_title)) }, - content = { Text("Under construction") }, + content = { ModeTileGrid(viewModel) }, neutralButton = { PlatformOutlinedButton( onClick = { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt new file mode 100644 index 000000000000..91bfdff1095e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt @@ -0,0 +1,91 @@ +/* + * 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.composable + +import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.android.systemui.common.ui.compose.Icon +import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModeTileViewModel + +@Composable +fun ModeTile(viewModel: ModeTileViewModel) { + val tileColor = + if (viewModel.enabled) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.surfaceVariant + val contentColor = + if (viewModel.enabled) MaterialTheme.colorScheme.onPrimary + else MaterialTheme.colorScheme.onSurfaceVariant + + CompositionLocalProvider(LocalContentColor provides contentColor) { + Surface( + color = tileColor, + shape = RoundedCornerShape(16.dp), + modifier = + Modifier.combinedClickable( + onClick = viewModel.onClick, + onLongClick = viewModel.onLongClick + ), + ) { + Row( + modifier = Modifier.padding(20.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = + Arrangement.spacedBy( + space = 10.dp, + alignment = Alignment.Start, + ), + ) { + Icon(icon = viewModel.icon, modifier = Modifier.size(24.dp)) + Column { + Text( + viewModel.text, + fontWeight = FontWeight.W500, + modifier = Modifier.tileMarquee() + ) + Text( + viewModel.subtext, + fontWeight = FontWeight.W400, + modifier = Modifier.tileMarquee() + ) + } + } + } + } +} + +private fun Modifier.tileMarquee(): Modifier { + return this.basicMarquee( + iterations = 1, + initialDelayMillis = 200, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt new file mode 100644 index 000000000000..73d361f69eac --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.policy.ui.dialog.composable + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModesDialogViewModel + +@Composable +fun ModeTileGrid(viewModel: ModesDialogViewModel) { + val tiles by viewModel.tiles.collectAsStateWithLifecycle(initialValue = emptyList()) + + // TODO(b/346519570): Handle what happens when we have more than a few modes. + LazyVerticalGrid( + columns = GridCells.Fixed(2), + modifier = Modifier.padding(8.dp).fillMaxWidth().heightIn(max = 300.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + items( + tiles.size, + key = { index -> tiles[index].id }, + ) { index -> + ModeTile(viewModel = tiles[index]) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModeTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModeTileViewModel.kt new file mode 100644 index 000000000000..5bd26ccc965f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModeTileViewModel.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.policy.ui.dialog.viewmodel + +import com.android.systemui.common.shared.model.Icon + +/** + * Viewmodel for a tile representing a single priority ("zen") mode, for use within the modes + * dialog. Not to be confused with ModesTile, which is the Quick Settings tile that opens the + * dialog. + */ +data class ModeTileViewModel( + val id: String, + val icon: Icon, + val text: String, + val subtext: String, + val enabled: Boolean, + val contentDescription: String, + val onClick: () -> Unit, + val onLongClick: () -> Unit, +) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt new file mode 100644 index 000000000000..e84c8b61ff54 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt @@ -0,0 +1,87 @@ +/* + * 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.viewmodel + +import android.content.Context +import com.android.settingslib.notification.modes.ZenMode +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.res.R +import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map + +/** + * Viewmodel for the priority ("zen") modes dialog that can be opened from quick settings. It allows + * the user to quickly toggle modes. + */ +@SysUISingleton +class ModesDialogViewModel +@Inject +constructor( + val context: Context, + zenModeInteractor: ZenModeInteractor, + @Background val bgDispatcher: CoroutineDispatcher, +) { + // Modes that should be displayed in the dialog + // TODO(b/346519570): Include modes that have not been set up yet. + private val visibleModes: Flow<List<ZenMode>> = + zenModeInteractor.modes.map { + it.filter { mode -> + mode.rule.isEnabled && (mode.isActive || mode.rule.isManualInvocationAllowed) + } + } + + val tiles: Flow<List<ModeTileViewModel>> = + visibleModes + .map { modesList -> + modesList.map { mode -> + ModeTileViewModel( + id = mode.id, + icon = zenModeInteractor.getModeIcon(mode, context), + text = mode.rule.name, + subtext = getTileSubtext(mode), + enabled = mode.isActive, + // TODO(b/346519570): This should be some combination of the above, e.g. + // "ON: Do Not Disturb, Until Mon 08:09"; see DndTile. + contentDescription = "", + onClick = { + if (mode.isActive) { + zenModeInteractor.deactivateMode(mode) + } else { + // TODO(b/346519570): Handle duration for DND mode. + zenModeInteractor.activateMode(mode) + } + }, + onLongClick = { + // TODO(b/346519570): Open settings page for mode. + } + ) + } + } + .flowOn(bgDispatcher) + + private fun getTileSubtext(mode: ZenMode): String { + // TODO(b/346519570): Use ZenModeConfig.getDescription for manual DND + val on = context.resources.getString(R.string.zen_mode_on) + val off = context.resources.getString(R.string.zen_mode_off) + return mode.rule.triggerDescription ?: if (mode.isActive) on else off + } +} |