• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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