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