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.panel.component.volume.slider.ui.viewmodel
18 
19 import android.content.Context
20 import android.media.AudioManager
21 import android.util.Log
22 import com.android.internal.logging.UiEventLogger
23 import com.android.settingslib.volume.domain.interactor.AudioVolumeInteractor
24 import com.android.settingslib.volume.shared.model.AudioStream
25 import com.android.settingslib.volume.shared.model.AudioStreamModel
26 import com.android.settingslib.volume.shared.model.RingerMode
27 import com.android.systemui.common.shared.model.Icon
28 import com.android.systemui.res.R
29 import com.android.systemui.volume.panel.ui.VolumePanelUiEvent
30 import dagger.assisted.Assisted
31 import dagger.assisted.AssistedFactory
32 import dagger.assisted.AssistedInject
33 import kotlin.math.roundToInt
34 import kotlinx.coroutines.CoroutineScope
35 import kotlinx.coroutines.flow.MutableStateFlow
36 import kotlinx.coroutines.flow.SharingStarted
37 import kotlinx.coroutines.flow.StateFlow
38 import kotlinx.coroutines.flow.combine
39 import kotlinx.coroutines.flow.filterNotNull
40 import kotlinx.coroutines.flow.launchIn
41 import kotlinx.coroutines.flow.onEach
42 import kotlinx.coroutines.flow.stateIn
43 import kotlinx.coroutines.launch
44 
45 /** Models a particular slider state. */
46 class AudioStreamSliderViewModel
47 @AssistedInject
48 constructor(
49     @Assisted private val audioStreamWrapper: FactoryAudioStreamWrapper,
50     @Assisted private val coroutineScope: CoroutineScope,
51     private val context: Context,
52     private val audioVolumeInteractor: AudioVolumeInteractor,
53     private val uiEventLogger: UiEventLogger,
54 ) : SliderViewModel {
55 
56     private val volumeChanges = MutableStateFlow<Int?>(null)
57     private val streamsAffectedByRing =
58         setOf(
59             AudioManager.STREAM_RING,
60             AudioManager.STREAM_NOTIFICATION,
61         )
62     private val audioStream = audioStreamWrapper.audioStream
63     private val iconsByStream =
64         mapOf(
65             AudioStream(AudioManager.STREAM_MUSIC) to R.drawable.ic_music_note,
66             AudioStream(AudioManager.STREAM_VOICE_CALL) to R.drawable.ic_call,
67             AudioStream(AudioManager.STREAM_BLUETOOTH_SCO) to R.drawable.ic_call,
68             AudioStream(AudioManager.STREAM_RING) to R.drawable.ic_ring_volume,
69             AudioStream(AudioManager.STREAM_NOTIFICATION) to R.drawable.ic_volume_ringer,
70             AudioStream(AudioManager.STREAM_ALARM) to R.drawable.ic_volume_alarm,
71         )
72     private val labelsByStream =
73         mapOf(
74             AudioStream(AudioManager.STREAM_MUSIC) to R.string.stream_music,
75             AudioStream(AudioManager.STREAM_VOICE_CALL) to R.string.stream_voice_call,
76             AudioStream(AudioManager.STREAM_BLUETOOTH_SCO) to R.string.stream_voice_call,
77             AudioStream(AudioManager.STREAM_RING) to R.string.stream_ring,
78             AudioStream(AudioManager.STREAM_NOTIFICATION) to R.string.stream_notification,
79             AudioStream(AudioManager.STREAM_ALARM) to R.string.stream_alarm,
80         )
81     private val disabledTextByStream =
82         mapOf(
83             AudioStream(AudioManager.STREAM_NOTIFICATION) to
84                 R.string.stream_notification_unavailable,
85         )
86     private val uiEventByStream =
87         mapOf(
88             AudioStream(AudioManager.STREAM_MUSIC) to
89                 VolumePanelUiEvent.VOLUME_PANEL_MUSIC_SLIDER_TOUCHED,
90             AudioStream(AudioManager.STREAM_VOICE_CALL) to
91                 VolumePanelUiEvent.VOLUME_PANEL_VOICE_CALL_SLIDER_TOUCHED,
92             AudioStream(AudioManager.STREAM_BLUETOOTH_SCO) to
93                 VolumePanelUiEvent.VOLUME_PANEL_VOICE_CALL_SLIDER_TOUCHED,
94             AudioStream(AudioManager.STREAM_RING) to
95                 VolumePanelUiEvent.VOLUME_PANEL_RING_SLIDER_TOUCHED,
96             AudioStream(AudioManager.STREAM_NOTIFICATION) to
97                 VolumePanelUiEvent.VOLUME_PANEL_NOTIFICATION_SLIDER_TOUCHED,
98             AudioStream(AudioManager.STREAM_ALARM) to
99                 VolumePanelUiEvent.VOLUME_PANEL_ALARM_SLIDER_TOUCHED,
100         )
101 
102     override val slider: StateFlow<SliderState> =
103         combine(
104                 audioVolumeInteractor.getAudioStream(audioStream),
105                 audioVolumeInteractor.canChangeVolume(audioStream),
106                 audioVolumeInteractor.ringerMode,
107             ) { model, isEnabled, ringerMode ->
108                 model.toState(isEnabled, ringerMode)
109             }
110             .stateIn(coroutineScope, SharingStarted.Eagerly, SliderState.Empty)
111 
112     init {
113         volumeChanges
114             .filterNotNull()
115             .onEach { audioVolumeInteractor.setVolume(audioStream, it) }
116             .launchIn(coroutineScope)
117     }
118 
119     override fun onValueChanged(state: SliderState, newValue: Float) {
120         val audioViewModel = state as? State
121         audioViewModel ?: return
122         volumeChanges.tryEmit(newValue.roundToInt())
123     }
124 
125     override fun onValueChangeFinished() {
126         uiEventByStream[audioStream]?.let { uiEventLogger.log(it) }
127     }
128 
129     override fun toggleMuted(state: SliderState) {
130         val audioViewModel = state as? State
131         audioViewModel ?: return
132         coroutineScope.launch {
133             audioVolumeInteractor.setMuted(audioStream, !audioViewModel.audioStreamModel.isMuted)
134         }
135     }
136 
137     private fun AudioStreamModel.toState(
138         isEnabled: Boolean,
139         ringerMode: RingerMode,
140     ): State {
141         val label =
142             labelsByStream[audioStream]?.let(context::getString)
143                 ?: error("No label for the stream: $audioStream")
144         return State(
145             value = volume.toFloat(),
146             valueRange = volumeRange.first.toFloat()..volumeRange.last.toFloat(),
147             icon = getIcon(ringerMode),
148             label = label,
149             disabledMessage =
150                 context.getString(
151                     disabledTextByStream.getOrDefault(
152                         audioStream,
153                         R.string.stream_alarm_unavailable,
154                     )
155                 ),
156             isEnabled = isEnabled,
157             a11yStep = volumeRange.step,
158             a11yClickDescription =
159                 if (isAffectedByMute) {
160                     context.getString(
161                         if (isMuted) {
162                             R.string.volume_panel_hint_unmute
163                         } else {
164                             R.string.volume_panel_hint_mute
165                         },
166                         label,
167                     )
168                 } else {
169                     null
170                 },
171             a11yStateDescription =
172                 if (volume == volumeRange.first) {
173                     context.getString(
174                         if (audioStream.value in streamsAffectedByRing) {
175                             if (ringerMode.value == AudioManager.RINGER_MODE_VIBRATE) {
176                                 R.string.volume_panel_hint_vibrate
177                             } else {
178                                 R.string.volume_panel_hint_muted
179                             }
180                         } else {
181                             R.string.volume_panel_hint_muted
182                         }
183                     )
184                 } else {
185                     null
186                 },
187             audioStreamModel = this,
188             isMutable = isAffectedByMute,
189         )
190     }
191 
192     private fun AudioStreamModel.getIcon(ringerMode: RingerMode): Icon {
193         val iconRes =
194             if (isAffectedByMute && isMuted) {
195                 if (audioStream.value in streamsAffectedByRing) {
196                     if (ringerMode.value == AudioManager.RINGER_MODE_VIBRATE) {
197                         R.drawable.ic_volume_ringer_vibrate
198                     } else {
199                         R.drawable.ic_volume_off
200                     }
201                 } else {
202                     R.drawable.ic_volume_off
203                 }
204             } else {
205                 iconsByStream[audioStream]
206                     ?: run {
207                         Log.wtf(TAG, "No icon for the stream: $audioStream")
208                         R.drawable.ic_music_note
209                     }
210             }
211         return Icon.Resource(iconRes, null)
212     }
213 
214     private val AudioStreamModel.volumeRange: IntRange
215         get() = minVolume..maxVolume
216 
217     private data class State(
218         override val value: Float,
219         override val valueRange: ClosedFloatingPointRange<Float>,
220         override val icon: Icon,
221         override val label: String,
222         override val disabledMessage: String?,
223         override val isEnabled: Boolean,
224         override val a11yStep: Int,
225         override val a11yClickDescription: String?,
226         override val a11yStateDescription: String?,
227         override val isMutable: Boolean,
228         val audioStreamModel: AudioStreamModel,
229     ) : SliderState
230 
231     @AssistedFactory
232     interface Factory {
233 
234         fun create(
235             audioStream: FactoryAudioStreamWrapper,
236             coroutineScope: CoroutineScope,
237         ): AudioStreamSliderViewModel
238     }
239 
240     /**
241      * AudioStream is a value class that compiles into a primitive. This fail AssistedFactory build
242      * when using [AudioStream] directly because it expects another type.
243      */
244     class FactoryAudioStreamWrapper(val audioStream: AudioStream)
245 
246     private companion object {
247         const val TAG = "AudioStreamSliderViewModel"
248     }
249 }
250