1 /*
<lambda>null2 * Copyright (C) 2024 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.volume.dialog.ringer.ui.viewmodel
18
19 import android.content.Context
20 import android.media.AudioAttributes
21 import android.media.AudioManager.RINGER_MODE_NORMAL
22 import android.media.AudioManager.RINGER_MODE_SILENT
23 import android.media.AudioManager.RINGER_MODE_VIBRATE
24 import android.media.AudioManager.STREAM_RING
25 import android.os.VibrationEffect
26 import android.widget.Toast
27 import com.android.internal.R as internalR
28 import com.android.internal.logging.UiEventLogger
29 import com.android.settingslib.Utils
30 import com.android.settingslib.notification.domain.interactor.NotificationsSoundPolicyInteractor
31 import com.android.settingslib.volume.shared.model.AudioStream
32 import com.android.settingslib.volume.shared.model.RingerMode
33 import com.android.systemui.dagger.qualifiers.Application
34 import com.android.systemui.dagger.qualifiers.Background
35 import com.android.systemui.res.R
36 import com.android.systemui.statusbar.VibratorHelper
37 import com.android.systemui.statusbar.policy.ConfigurationController
38 import com.android.systemui.statusbar.policy.onConfigChanged
39 import com.android.systemui.util.time.SystemClock
40 import com.android.systemui.volume.Events
41 import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
42 import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope
43 import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogVisibilityInteractor
44 import com.android.systemui.volume.dialog.ringer.domain.VolumeDialogRingerInteractor
45 import com.android.systemui.volume.dialog.ringer.shared.model.VolumeDialogRingerModel
46 import com.android.systemui.volume.dialog.shared.VolumeDialogLogger
47 import com.android.systemui.volume.dialog.ui.VolumeDialogUiEvent
48 import javax.inject.Inject
49 import kotlinx.coroutines.CoroutineDispatcher
50 import kotlinx.coroutines.CoroutineScope
51 import kotlinx.coroutines.flow.MutableStateFlow
52 import kotlinx.coroutines.flow.SharingStarted
53 import kotlinx.coroutines.flow.StateFlow
54 import kotlinx.coroutines.flow.combine
55 import kotlinx.coroutines.flow.flowOn
56 import kotlinx.coroutines.flow.launchIn
57 import kotlinx.coroutines.flow.map
58 import kotlinx.coroutines.flow.onEach
59 import kotlinx.coroutines.flow.stateIn
60 import kotlinx.coroutines.launch
61
62 private const val DRAWER_STATE_ANIMATION_DURATION = 400L
63 private const val SHOW_RINGER_TOAST_COUNT = 12
64
65 @VolumeDialogScope
66 class VolumeDialogRingerDrawerViewModel
67 @Inject
68 constructor(
69 @Application private val applicationContext: Context,
70 @VolumeDialog private val coroutineScope: CoroutineScope,
71 @Background private val backgroundDispatcher: CoroutineDispatcher,
72 soundPolicyInteractor: NotificationsSoundPolicyInteractor,
73 private val ringerInteractor: VolumeDialogRingerInteractor,
74 private val vibrator: VibratorHelper,
75 private val volumeDialogLogger: VolumeDialogLogger,
76 private val visibilityInteractor: VolumeDialogVisibilityInteractor,
77 configurationController: ConfigurationController,
78 private val uiEventLogger: UiEventLogger,
79 private val systemClock: SystemClock,
80 ) {
81
82 private val drawerState = MutableStateFlow<RingerDrawerState>(RingerDrawerState.Initial)
83 private val orientation: StateFlow<Int> =
84 configurationController.onConfigChanged
85 .map { it.orientation }
86 .stateIn(
87 coroutineScope,
88 SharingStarted.Eagerly,
89 applicationContext.resources.configuration.orientation,
90 )
91
92 val ringerViewModel: StateFlow<RingerViewModelState> =
93 combine(
94 soundPolicyInteractor.isZenMuted(AudioStream(STREAM_RING)),
95 ringerInteractor.ringerModel,
96 drawerState,
97 orientation,
98 ) { isZenMuted, ringerModel, state, orientation ->
99 level = ringerModel.level
100 levelMax = ringerModel.levelMax
101 ringerModel.toViewModel(state, isZenMuted, orientation)
102 }
103 .flowOn(backgroundDispatcher)
104 .stateIn(coroutineScope, SharingStarted.Eagerly, RingerViewModelState.Unavailable)
105
106 // Level and Maximum level of Ring Stream.
107 private var level = -1
108 private var levelMax = -1
109
110 // Vibration attributes.
111 private val sonificiationVibrationAttributes =
112 AudioAttributes.Builder()
113 .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
114 .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
115 .build()
116
117 private var lastClickTime = 0L
118
119 init {
120 ringerViewModel
121 .onEach { viewModelState ->
122 when (viewModelState) {
123 is RingerViewModelState.Available ->
124 volumeDialogLogger.onRingerDrawerAvailable(
125 viewModelState.uiModel.availableButtons.map { it.ringerMode }
126 )
127 is RingerViewModelState.Unavailable ->
128 volumeDialogLogger.onRingerDrawerUnavailable()
129 }
130 }
131 .launchIn(coroutineScope)
132 }
133
134 fun onRingerButtonClicked(ringerMode: RingerMode, isSelectedButton: Boolean = false) {
135 val currentTime = systemClock.currentTimeMillis()
136 if (currentTime - lastClickTime < DRAWER_STATE_ANIMATION_DURATION) return
137 lastClickTime = currentTime
138 if (drawerState.value is RingerDrawerState.Open && !isSelectedButton) {
139 Events.writeEvent(Events.EVENT_RINGER_TOGGLE, ringerMode.value)
140 volumeDialogLogger.onRingerModeChanged(ringerMode)
141 provideTouchFeedback(ringerMode)
142 maybeShowToast(ringerMode)
143 ringerInteractor.setRingerMode(ringerMode)
144 ringerMode.toVolumeDialogUiEvent()?.let(uiEventLogger::log)
145 }
146 visibilityInteractor.resetDismissTimeout()
147 drawerState.value =
148 when (drawerState.value) {
149 is RingerDrawerState.Initial -> {
150 RingerDrawerState.Open(ringerMode)
151 }
152 is RingerDrawerState.Open -> {
153 RingerDrawerState.Closed(
154 ringerMode,
155 (drawerState.value as RingerDrawerState.Open).mode,
156 )
157 }
158 is RingerDrawerState.Closed -> {
159 RingerDrawerState.Open(ringerMode)
160 }
161 }
162 }
163
164 private fun provideTouchFeedback(ringerMode: RingerMode) {
165 when (ringerMode.value) {
166 RINGER_MODE_NORMAL -> {
167 ringerInteractor.scheduleTouchFeedback()
168 null
169 }
170 RINGER_MODE_SILENT -> VibrationEffect.get(VibrationEffect.EFFECT_CLICK)
171 RINGER_MODE_VIBRATE -> VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK)
172 else -> VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK)
173 }?.let { vibrator.vibrate(it, sonificiationVibrationAttributes) }
174 }
175
176 private fun VolumeDialogRingerModel.toViewModel(
177 drawerState: RingerDrawerState,
178 isZenMuted: Boolean,
179 orientation: Int,
180 ): RingerViewModelState {
181 val currentIndex = availableModes.indexOf(currentRingerMode)
182 if (currentIndex == -1) {
183 volumeDialogLogger.onCurrentRingerModeIsUnsupported(currentRingerMode)
184 }
185 return if (currentIndex == -1 || isSingleVolume) {
186 RingerViewModelState.Unavailable
187 } else {
188 toButtonViewModel(currentRingerMode, isZenMuted, isSelectedButton = true)?.let {
189 RingerViewModelState.Available(
190 RingerViewModel(
191 availableButtons =
192 availableModes.mapNotNull { mode ->
193 toButtonViewModel(mode, isZenMuted)
194 },
195 currentButtonIndex = currentIndex,
196 selectedButton = it,
197 drawerState = drawerState,
198 ),
199 orientation,
200 )
201 } ?: RingerViewModelState.Unavailable
202 }
203 }
204
205 private fun VolumeDialogRingerModel.toButtonViewModel(
206 ringerMode: RingerMode,
207 isZenMuted: Boolean,
208 isSelectedButton: Boolean = false,
209 ): RingerButtonViewModel? {
210 return when (ringerMode.value) {
211 RINGER_MODE_SILENT ->
212 RingerButtonViewModel(
213 imageResId = R.drawable.ic_speaker_mute,
214 contentDescriptionResId =
215 if (isSelectedButton) {
216 R.string.volume_ringer_status_silent
217 } else {
218 R.string.volume_ringer_hint_mute
219 },
220 hintLabelResId = R.string.volume_ringer_hint_unmute,
221 ringerMode = ringerMode,
222 )
223 RINGER_MODE_VIBRATE ->
224 RingerButtonViewModel(
225 imageResId = R.drawable.ic_volume_ringer_vibrate,
226 contentDescriptionResId =
227 if (isSelectedButton) {
228 R.string.volume_ringer_status_vibrate
229 } else {
230 R.string.volume_ringer_hint_vibrate
231 },
232 hintLabelResId = R.string.volume_ringer_hint_vibrate,
233 ringerMode = ringerMode,
234 )
235 RINGER_MODE_NORMAL ->
236 when {
237 isMuted && !isZenMuted ->
238 RingerButtonViewModel(
239 imageResId =
240 if (isSelectedButton) {
241 R.drawable.ic_speaker_mute
242 } else {
243 R.drawable.ic_speaker_on
244 },
245 contentDescriptionResId =
246 if (isSelectedButton) {
247 R.string.volume_ringer_status_normal
248 } else {
249 R.string.volume_ringer_hint_unmute
250 },
251 hintLabelResId = R.string.volume_ringer_hint_unmute,
252 ringerMode = ringerMode,
253 )
254 availableModes.contains(RingerMode(RINGER_MODE_VIBRATE)) ->
255 RingerButtonViewModel(
256 imageResId = R.drawable.ic_speaker_on,
257 contentDescriptionResId =
258 if (isSelectedButton) {
259 R.string.volume_ringer_status_normal
260 } else {
261 R.string.volume_ringer_hint_unmute
262 },
263 hintLabelResId = R.string.volume_ringer_hint_vibrate,
264 ringerMode = ringerMode,
265 )
266 else ->
267 RingerButtonViewModel(
268 imageResId = R.drawable.ic_speaker_on,
269 contentDescriptionResId =
270 if (isSelectedButton) {
271 R.string.volume_ringer_status_normal
272 } else {
273 R.string.volume_ringer_hint_unmute
274 },
275 hintLabelResId = R.string.volume_ringer_hint_mute,
276 ringerMode = ringerMode,
277 )
278 }
279 else -> null
280 }
281 }
282
283 private fun maybeShowToast(ringerMode: RingerMode) {
284 coroutineScope.launch {
285 val seenToastCount = ringerInteractor.getToastCount()
286 if (seenToastCount > SHOW_RINGER_TOAST_COUNT) {
287 return@launch
288 }
289
290 val toastText =
291 when (ringerMode.value) {
292 RINGER_MODE_NORMAL -> {
293 if (level != -1 && levelMax != -1) {
294 applicationContext.getString(
295 R.string.volume_dialog_ringer_guidance_ring,
296 Utils.formatPercentage(level.toLong(), levelMax.toLong()),
297 )
298 } else {
299 null
300 }
301 }
302 RINGER_MODE_SILENT ->
303 applicationContext.getString(
304 internalR.string.volume_dialog_ringer_guidance_silent
305 )
306 RINGER_MODE_VIBRATE ->
307 applicationContext.getString(
308 internalR.string.volume_dialog_ringer_guidance_vibrate
309 )
310 else ->
311 applicationContext.getString(
312 internalR.string.volume_dialog_ringer_guidance_vibrate
313 )
314 }
315 toastText?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_SHORT).show() }
316 ringerInteractor.updateToastCount(seenToastCount)
317 }
318 }
319 }
320
toVolumeDialogUiEventnull321 private fun RingerMode.toVolumeDialogUiEvent(): VolumeDialogUiEvent? {
322 return when (value) {
323 RINGER_MODE_NORMAL -> VolumeDialogUiEvent.RINGER_MODE_NORMAL
324 RINGER_MODE_VIBRATE -> VolumeDialogUiEvent.RINGER_MODE_VIBRATE
325 RINGER_MODE_SILENT -> VolumeDialogUiEvent.RINGER_MODE_SILENT
326 else -> null
327 }
328 }
329