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