• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.statusbar.policy.domain.interactor
18 
19 import android.content.Context
20 import android.media.AudioManager
21 import android.provider.Settings
22 import android.provider.Settings.Secure.ZEN_DURATION_FOREVER
23 import android.provider.Settings.Secure.ZEN_DURATION_PROMPT
24 import android.service.notification.ZenPolicy.STATE_DISALLOW
25 import android.service.notification.ZenPolicy.VISUAL_EFFECT_NOTIFICATION_LIST
26 import android.util.Log
27 import androidx.concurrent.futures.await
28 import com.android.settingslib.notification.data.repository.ZenModeRepository
29 import com.android.settingslib.notification.modes.ZenIcon
30 import com.android.settingslib.notification.modes.ZenIconLoader
31 import com.android.settingslib.notification.modes.ZenMode
32 import com.android.settingslib.volume.shared.model.AudioStream
33 import com.android.systemui.dagger.SysUISingleton
34 import com.android.systemui.dagger.qualifiers.Background
35 import com.android.systemui.modes.shared.ModesUi
36 import com.android.systemui.shared.notifications.data.repository.NotificationSettingsRepository
37 import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix
38 import com.android.systemui.statusbar.policy.data.repository.DeviceProvisioningRepository
39 import com.android.systemui.statusbar.policy.data.repository.UserSetupRepository
40 import com.android.systemui.statusbar.policy.domain.model.ActiveZenModes
41 import com.android.systemui.statusbar.policy.domain.model.ZenModeInfo
42 import java.time.Duration
43 import javax.inject.Inject
44 import kotlinx.coroutines.CoroutineDispatcher
45 import kotlinx.coroutines.CoroutineScope
46 import kotlinx.coroutines.flow.Flow
47 import kotlinx.coroutines.flow.MutableStateFlow
48 import kotlinx.coroutines.flow.SharingStarted
49 import kotlinx.coroutines.flow.StateFlow
50 import kotlinx.coroutines.flow.combine
51 import kotlinx.coroutines.flow.distinctUntilChanged
52 import kotlinx.coroutines.flow.flowOf
53 import kotlinx.coroutines.flow.flowOn
54 import kotlinx.coroutines.flow.map
55 import kotlinx.coroutines.flow.stateIn
56 
57 /**
58  * An interactor that performs business logic related to the status and configuration of Zen Mode
59  * (or Do Not Disturb/DND Mode).
60  */
61 @SysUISingleton
62 class ZenModeInteractor
63 @Inject
64 constructor(
65     private val context: Context,
66     private val zenModeRepository: ZenModeRepository,
67     private val notificationSettingsRepository: NotificationSettingsRepository,
68     @Background private val bgDispatcher: CoroutineDispatcher,
69     @Background private val backgroundScope: CoroutineScope,
70     private val iconLoader: ZenIconLoader,
71     deviceProvisioningRepository: DeviceProvisioningRepository,
72     userSetupRepository: UserSetupRepository,
73 ) {
74     /**
75      * List of predicates to determine if the [ZenMode] blocks an audio stream. Typical use case
76      * would be: `zenModeByStreamPredicates[stream](zenMode)`
77      */
78     private val zenModeByStreamPredicates =
79         mapOf<Int, (ZenMode) -> Boolean>(
80             AudioManager.STREAM_MUSIC to { it.policy.priorityCategoryMedia == STATE_DISALLOW },
81             AudioManager.STREAM_ALARM to { it.policy.priorityCategoryAlarms == STATE_DISALLOW },
82             AudioManager.STREAM_SYSTEM to { it.policy.priorityCategorySystem == STATE_DISALLOW },
83         )
84 
85     val isZenAvailable: Flow<Boolean> =
86         combine(
87             deviceProvisioningRepository.isDeviceProvisioned,
88             userSetupRepository.isUserSetUp,
89         ) { isDeviceProvisioned, isUserSetUp ->
90             isDeviceProvisioned && isUserSetUp
91         }
92 
93     val isZenModeEnabled: Flow<Boolean> =
94         zenModeRepository.globalZenMode
95             .map {
96                 when (it ?: Settings.Global.ZEN_MODE_OFF) {
97                     Settings.Global.ZEN_MODE_ALARMS -> true
98                     Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS -> true
99                     Settings.Global.ZEN_MODE_NO_INTERRUPTIONS -> true
100                     Settings.Global.ZEN_MODE_OFF -> false
101                     else -> false
102                 }
103             }
104             .distinctUntilChanged()
105 
106     val areNotificationsHiddenInShade: Flow<Boolean> =
107         combine(isZenModeEnabled, zenModeRepository.consolidatedNotificationPolicy) {
108                 dndEnabled,
109                 policy ->
110                 if (!dndEnabled) {
111                     false
112                 } else {
113                     val showInNotificationList = policy?.showInNotificationList() ?: true
114                     !showInNotificationList
115                 }
116             }
117             .distinctUntilChanged()
118 
119     val modes: Flow<List<ZenMode>> = zenModeRepository.modes
120 
121     /**
122      * Returns the special "manual DND" mode.
123      *
124      * This should only be used when there is a strong reason to handle DND specifically (such as
125      * legacy UI pieces that haven't been updated to use modes more generally, or if the user
126      * explicitly wants a shortcut to DND). Please prefer using [modes] or [activeModes] in all
127      * other scenarios.
128      */
129     val dndMode: StateFlow<ZenMode?> =
130         if (ModesUi.isEnabled)
131             zenModeRepository.modes
132                 .map { modes -> modes.singleOrNull { it.isManualDnd } }
133                 .stateIn(
134                     scope = backgroundScope,
135                     started = SharingStarted.Eagerly,
136                     initialValue = null,
137                 )
138         else MutableStateFlow<ZenMode?>(null)
139         get() {
140             ModesUi.unsafeAssertInNewMode()
141             return field
142         }
143 
144     /**
145      * Returns the current state of the special "manual DND" mode.
146      *
147      * This should only be used when there is a strong reason to handle DND specifically (such as
148      * legacy UI pieces that haven't been updated to use modes more generally, or if the user
149      * explicitly wants a shortcut to DND). Please prefer using [modes] or [activeModes] in all
150      * other scenarios.
151      */
152     fun getDndMode(): ZenMode {
153         return zenModeRepository.getModes().single { it.isManualDnd }
154     }
155 
156     /** Flow returning the currently active mode(s), if any. */
157     val activeModes: Flow<ActiveZenModes> =
158         modes
159             .map { modes -> buildActiveZenModes(modes) }
160             .flowOn(bgDispatcher)
161             .distinctUntilChanged()
162 
163     fun canBeBlockedByZenMode(stream: AudioStream): Boolean =
164         zenModeByStreamPredicates.containsKey(stream.value)
165 
166     fun activeModesBlockingStream(stream: AudioStream): Flow<ActiveZenModes> {
167         val isBlockingStream = zenModeByStreamPredicates[stream.value]
168         require(isBlockingStream != null) {
169             "$stream is unsupported. Use canBeBlockedByZenMode to check if the stream can be affected by the Zen Mode."
170         }
171         return modes
172             .map { modes -> modes.filter { isBlockingStream(it) } }
173             .map { modes -> buildActiveZenModes(modes) }
174             .flowOn(bgDispatcher)
175             .distinctUntilChanged()
176     }
177 
178     suspend fun getActiveModes() = buildActiveZenModes(zenModeRepository.getModes())
179 
180     private suspend fun buildActiveZenModes(modes: List<ZenMode>): ActiveZenModes {
181         val activeModesList =
182             modes.filter { mode -> mode.isActive }.sortedWith(ZenMode.PRIORITIZING_COMPARATOR)
183         val mainActiveMode =
184             activeModesList.firstOrNull()?.let { ZenModeInfo(it.name, getModeIcon(it)) }
185 
186         return ActiveZenModes(activeModesList.map { m -> m.name }, mainActiveMode)
187     }
188 
189     val mainActiveMode: Flow<ZenModeInfo?> =
190         activeModes.map { a -> a.mainMode }.distinctUntilChanged()
191 
192     val modesHidingNotifications: Flow<List<ZenMode>> by lazy {
193         if (ModesEmptyShadeFix.isUnexpectedlyInLegacyMode() || !ModesUi.isEnabled) {
194             flowOf(listOf())
195         } else {
196             modes
197                 .map { modes ->
198                     modes.filter { mode ->
199                         mode.isActive &&
200                             !mode.policy.isVisualEffectAllowed(
201                                 /* effect = */ VISUAL_EFFECT_NOTIFICATION_LIST,
202                                 /* defaultVal = */ true,
203                             )
204                     }
205                 }
206                 .flowOn(bgDispatcher)
207                 .distinctUntilChanged()
208         }
209     }
210 
211     suspend fun getModeIcon(mode: ZenMode): ZenIcon {
212         return iconLoader.getIcon(context, mode).await()
213     }
214 
215     fun activateMode(zenMode: ZenMode) {
216         if (zenMode.isManualDnd) {
217             val duration =
218                 when (zenDuration) {
219                     ZEN_DURATION_PROMPT -> {
220                         Log.e(
221                             TAG,
222                             "Interactor cannot handle showing the zen duration prompt. " +
223                                 "Please use EnableZenModeDialog when this setting is active.",
224                         )
225                         null
226                     }
227                     ZEN_DURATION_FOREVER -> null
228                     else -> Duration.ofMinutes(zenDuration.toLong())
229                 }
230 
231             zenModeRepository.activateMode(zenMode, duration)
232         } else {
233             zenModeRepository.activateMode(zenMode)
234         }
235     }
236 
237     fun deactivateMode(zenMode: ZenMode) {
238         zenModeRepository.deactivateMode(zenMode)
239     }
240 
241     fun deactivateAllModes() {
242         for (mode in zenModeRepository.getModes()) {
243             if (mode.isActive) {
244                 deactivateMode(mode)
245             }
246         }
247     }
248 
249     private val zenDuration
250         get() = notificationSettingsRepository.zenDuration.value
251 
252     fun shouldAskForZenDuration(mode: ZenMode): Boolean =
253         mode.isManualDnd && (zenDuration == ZEN_DURATION_PROMPT)
254 
255     companion object {
256         private const val TAG = "ZenModeInteractor"
257     }
258 }
259