Loading packages/SystemUI/res/layout/volume_dialog_slider.xml +14 −8 Original line number Diff line number Diff line Loading @@ -14,15 +14,21 @@ limitations under the License. --> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="@dimen/volume_dialog_slider_width" android:layout_height="@dimen/volume_dialog_slider_height"> xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="wrap_content" android:layout_height="wrap_content"> <com.google.android.material.slider.Slider style="@style/SystemUI.Material3.Slider.Volume" android:id="@+id/volume_dialog_slider" android:layout_width="@dimen/volume_dialog_slider_height" android:layout_height="match_parent" style="@style/SystemUI.Material3.Slider.Volume" android:layout_width="@dimen/volume_dialog_slider_width" android:layout_height="@dimen/volume_dialog_slider_height" android:layout_gravity="center" android:rotation="270" android:theme="@style/Theme.Material3.Light" /> android:theme="@style/Theme.Material3.Light" android:orientation="vertical" app:thumbHeight="52dp" app:trackCornerSize="12dp" app:trackHeight="40dp" app:trackStopIndicatorSize="6dp" app:trackInsideCornerSize="2dp" /> </FrameLayout> No newline at end of file packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt +10 −6 Original line number Diff line number Diff line Loading @@ -18,11 +18,12 @@ package com.android.systemui.volume.dialog.sliders.ui import android.animation.Animator import android.animation.ObjectAnimator import android.annotation.SuppressLint import android.view.View import android.view.animation.DecelerateInterpolator import com.android.systemui.res.R import com.android.systemui.volume.dialog.shared.model.VolumeDialogStreamModel import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderScope import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderStateModel import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderViewModel import com.android.systemui.volume.dialog.ui.utils.JankListenerFactory import com.android.systemui.volume.dialog.ui.utils.awaitAnimation Loading @@ -48,24 +49,27 @@ constructor( val sliderView: Slider = view.requireViewById<Slider>(R.id.volume_dialog_slider).apply { labelBehavior = LabelFormatter.LABEL_GONE trackIconActiveColor = trackInactiveTintList } sliderView.addOnChangeListener { _, value, fromUser -> viewModel.setStreamVolume(value.roundToInt(), fromUser) } viewModel.model.onEach { it.bindToSlider(sliderView) }.launchIn(this) viewModel.state.onEach { it.bindToSlider(sliderView) }.launchIn(this) } private suspend fun VolumeDialogStreamModel.bindToSlider(slider: Slider) { @SuppressLint("UseCompatLoadingForDrawables") private suspend fun VolumeDialogSliderStateModel.bindToSlider(slider: Slider) { with(slider) { valueFrom = levelMin.toFloat() valueTo = levelMax.toFloat() valueFrom = minValue valueTo = maxValue // coerce the current value to the new value range before animating it value = value.coerceIn(valueFrom, valueTo) setValueAnimated( level.toFloat(), value, jankListenerFactory.update(this, PROGRESS_CHANGE_ANIMATION_DURATION_MS), ) trackIconActiveEnd = context.getDrawable(iconRes) } } } Loading packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProvider.kt 0 → 100644 +122 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.volume.dialog.sliders.ui.viewmodel import android.media.AudioManager import androidx.annotation.DrawableRes import com.android.settingslib.notification.domain.interactor.NotificationsSoundPolicyInteractor import com.android.settingslib.volume.domain.interactor.AudioVolumeInteractor import com.android.settingslib.volume.shared.model.AudioStream import com.android.settingslib.volume.shared.model.RingerMode import com.android.systemui.res.R import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOf class VolumeDialogSliderIconProvider @Inject constructor( private val notificationsSoundPolicyInteractor: NotificationsSoundPolicyInteractor, private val audioVolumeInteractor: AudioVolumeInteractor, ) { @DrawableRes fun getStreamIcon( stream: Int, level: Int, levelMin: Int, levelMax: Int, isMuted: Boolean, isRoutedToBluetooth: Boolean, ): Flow<Int> { return combine( notificationsSoundPolicyInteractor.isZenMuted(AudioStream(stream)), ringerModeForStream(stream), ) { isZenMuted, ringerMode -> val isStreamOffline = level == 0 || isMuted if (isZenMuted) { // TODO(b/372466264) use icon for the corresponding zenmode return@combine com.android.internal.R.drawable.ic_qs_dnd } when (ringerMode?.value) { AudioManager.RINGER_MODE_VIBRATE -> return@combine R.drawable.ic_volume_ringer_vibrate AudioManager.RINGER_MODE_SILENT -> return@combine R.drawable.ic_ring_volume_off } if (isRoutedToBluetooth) { return@combine if (stream == AudioManager.STREAM_VOICE_CALL) { R.drawable.ic_volume_bt_sco } else { if (isStreamOffline) { R.drawable.ic_volume_media_bt_mute } else { R.drawable.ic_volume_media_bt } } } return@combine if (isStreamOffline) { getMutedIconForStream(stream) ?: getIconForStream(stream) } else { if (level < (levelMax + levelMin) / 2) { // This icon is different on TV R.drawable.ic_volume_media_low } else { getIconForStream(stream) } } } } @DrawableRes private fun getMutedIconForStream(stream: Int): Int? { return when (stream) { AudioManager.STREAM_MUSIC -> R.drawable.ic_volume_media_mute AudioManager.STREAM_NOTIFICATION -> R.drawable.ic_volume_ringer_mute AudioManager.STREAM_ALARM -> R.drawable.ic_volume_alarm_mute AudioManager.STREAM_SYSTEM -> R.drawable.ic_volume_system_mute else -> null } } @DrawableRes private fun getIconForStream(stream: Int): Int { return when (stream) { AudioManager.STREAM_ACCESSIBILITY -> R.drawable.ic_volume_accessibility AudioManager.STREAM_MUSIC -> R.drawable.ic_volume_media AudioManager.STREAM_RING -> R.drawable.ic_ring_volume AudioManager.STREAM_NOTIFICATION -> R.drawable.ic_volume_ringer AudioManager.STREAM_ALARM -> R.drawable.ic_alarm AudioManager.STREAM_VOICE_CALL -> com.android.internal.R.drawable.ic_phone AudioManager.STREAM_SYSTEM -> R.drawable.ic_volume_system else -> error("Unsupported stream: $stream") } } /** * Emits [RingerMode] for the [stream] if it's affecting it and null when [RingerMode] doesn't * affect the [stream] */ private fun ringerModeForStream(stream: Int): Flow<RingerMode?> { return if (stream == AudioManager.STREAM_RING) { audioVolumeInteractor.ringerMode } else { flowOf(null) } } } packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt 0 → 100644 +36 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.volume.dialog.sliders.ui.viewmodel import androidx.annotation.DrawableRes import com.android.systemui.volume.dialog.shared.model.VolumeDialogStreamModel data class VolumeDialogSliderStateModel( val minValue: Float, val maxValue: Float, val value: Float, @DrawableRes val iconRes: Int, ) fun VolumeDialogStreamModel.toStateModel(@DrawableRes iconRes: Int): VolumeDialogSliderStateModel { return VolumeDialogSliderStateModel( minValue = levelMin.toFloat(), value = level.toFloat(), maxValue = levelMax.toFloat(), iconRes = iconRes, ) } packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt +19 −2 Original line number Diff line number Diff line Loading @@ -32,7 +32,9 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.stateIn Loading @@ -56,12 +58,12 @@ constructor( private val interactor: VolumeDialogSliderInteractor, private val visibilityInteractor: VolumeDialogVisibilityInteractor, @VolumeDialog private val coroutineScope: CoroutineScope, private val volumeDialogSliderIconProvider: VolumeDialogSliderIconProvider, private val systemClock: SystemClock, ) { private val userVolumeUpdates = MutableStateFlow<VolumeUpdate?>(null) val model: Flow<VolumeDialogStreamModel> = private val model: Flow<VolumeDialogStreamModel> = interactor.slider .filter { val lastVolumeUpdateTime = userVolumeUpdates.value?.timestampMillis ?: 0 Loading @@ -70,6 +72,21 @@ constructor( .stateIn(coroutineScope, SharingStarted.Eagerly, null) .filterNotNull() val state: Flow<VolumeDialogSliderStateModel> = model.flatMapLatest { streamModel -> with(streamModel) { volumeDialogSliderIconProvider.getStreamIcon( stream = stream, level = level, levelMin = levelMin, levelMax = levelMax, isMuted = muted, isRoutedToBluetooth = routedToBluetooth, ) } .map { icon -> streamModel.toStateModel(icon) } } init { userVolumeUpdates .filterNotNull() Loading Loading
packages/SystemUI/res/layout/volume_dialog_slider.xml +14 −8 Original line number Diff line number Diff line Loading @@ -14,15 +14,21 @@ limitations under the License. --> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="@dimen/volume_dialog_slider_width" android:layout_height="@dimen/volume_dialog_slider_height"> xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="wrap_content" android:layout_height="wrap_content"> <com.google.android.material.slider.Slider style="@style/SystemUI.Material3.Slider.Volume" android:id="@+id/volume_dialog_slider" android:layout_width="@dimen/volume_dialog_slider_height" android:layout_height="match_parent" style="@style/SystemUI.Material3.Slider.Volume" android:layout_width="@dimen/volume_dialog_slider_width" android:layout_height="@dimen/volume_dialog_slider_height" android:layout_gravity="center" android:rotation="270" android:theme="@style/Theme.Material3.Light" /> android:theme="@style/Theme.Material3.Light" android:orientation="vertical" app:thumbHeight="52dp" app:trackCornerSize="12dp" app:trackHeight="40dp" app:trackStopIndicatorSize="6dp" app:trackInsideCornerSize="2dp" /> </FrameLayout> No newline at end of file
packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt +10 −6 Original line number Diff line number Diff line Loading @@ -18,11 +18,12 @@ package com.android.systemui.volume.dialog.sliders.ui import android.animation.Animator import android.animation.ObjectAnimator import android.annotation.SuppressLint import android.view.View import android.view.animation.DecelerateInterpolator import com.android.systemui.res.R import com.android.systemui.volume.dialog.shared.model.VolumeDialogStreamModel import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderScope import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderStateModel import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderViewModel import com.android.systemui.volume.dialog.ui.utils.JankListenerFactory import com.android.systemui.volume.dialog.ui.utils.awaitAnimation Loading @@ -48,24 +49,27 @@ constructor( val sliderView: Slider = view.requireViewById<Slider>(R.id.volume_dialog_slider).apply { labelBehavior = LabelFormatter.LABEL_GONE trackIconActiveColor = trackInactiveTintList } sliderView.addOnChangeListener { _, value, fromUser -> viewModel.setStreamVolume(value.roundToInt(), fromUser) } viewModel.model.onEach { it.bindToSlider(sliderView) }.launchIn(this) viewModel.state.onEach { it.bindToSlider(sliderView) }.launchIn(this) } private suspend fun VolumeDialogStreamModel.bindToSlider(slider: Slider) { @SuppressLint("UseCompatLoadingForDrawables") private suspend fun VolumeDialogSliderStateModel.bindToSlider(slider: Slider) { with(slider) { valueFrom = levelMin.toFloat() valueTo = levelMax.toFloat() valueFrom = minValue valueTo = maxValue // coerce the current value to the new value range before animating it value = value.coerceIn(valueFrom, valueTo) setValueAnimated( level.toFloat(), value, jankListenerFactory.update(this, PROGRESS_CHANGE_ANIMATION_DURATION_MS), ) trackIconActiveEnd = context.getDrawable(iconRes) } } } Loading
packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderIconProvider.kt 0 → 100644 +122 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.volume.dialog.sliders.ui.viewmodel import android.media.AudioManager import androidx.annotation.DrawableRes import com.android.settingslib.notification.domain.interactor.NotificationsSoundPolicyInteractor import com.android.settingslib.volume.domain.interactor.AudioVolumeInteractor import com.android.settingslib.volume.shared.model.AudioStream import com.android.settingslib.volume.shared.model.RingerMode import com.android.systemui.res.R import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOf class VolumeDialogSliderIconProvider @Inject constructor( private val notificationsSoundPolicyInteractor: NotificationsSoundPolicyInteractor, private val audioVolumeInteractor: AudioVolumeInteractor, ) { @DrawableRes fun getStreamIcon( stream: Int, level: Int, levelMin: Int, levelMax: Int, isMuted: Boolean, isRoutedToBluetooth: Boolean, ): Flow<Int> { return combine( notificationsSoundPolicyInteractor.isZenMuted(AudioStream(stream)), ringerModeForStream(stream), ) { isZenMuted, ringerMode -> val isStreamOffline = level == 0 || isMuted if (isZenMuted) { // TODO(b/372466264) use icon for the corresponding zenmode return@combine com.android.internal.R.drawable.ic_qs_dnd } when (ringerMode?.value) { AudioManager.RINGER_MODE_VIBRATE -> return@combine R.drawable.ic_volume_ringer_vibrate AudioManager.RINGER_MODE_SILENT -> return@combine R.drawable.ic_ring_volume_off } if (isRoutedToBluetooth) { return@combine if (stream == AudioManager.STREAM_VOICE_CALL) { R.drawable.ic_volume_bt_sco } else { if (isStreamOffline) { R.drawable.ic_volume_media_bt_mute } else { R.drawable.ic_volume_media_bt } } } return@combine if (isStreamOffline) { getMutedIconForStream(stream) ?: getIconForStream(stream) } else { if (level < (levelMax + levelMin) / 2) { // This icon is different on TV R.drawable.ic_volume_media_low } else { getIconForStream(stream) } } } } @DrawableRes private fun getMutedIconForStream(stream: Int): Int? { return when (stream) { AudioManager.STREAM_MUSIC -> R.drawable.ic_volume_media_mute AudioManager.STREAM_NOTIFICATION -> R.drawable.ic_volume_ringer_mute AudioManager.STREAM_ALARM -> R.drawable.ic_volume_alarm_mute AudioManager.STREAM_SYSTEM -> R.drawable.ic_volume_system_mute else -> null } } @DrawableRes private fun getIconForStream(stream: Int): Int { return when (stream) { AudioManager.STREAM_ACCESSIBILITY -> R.drawable.ic_volume_accessibility AudioManager.STREAM_MUSIC -> R.drawable.ic_volume_media AudioManager.STREAM_RING -> R.drawable.ic_ring_volume AudioManager.STREAM_NOTIFICATION -> R.drawable.ic_volume_ringer AudioManager.STREAM_ALARM -> R.drawable.ic_alarm AudioManager.STREAM_VOICE_CALL -> com.android.internal.R.drawable.ic_phone AudioManager.STREAM_SYSTEM -> R.drawable.ic_volume_system else -> error("Unsupported stream: $stream") } } /** * Emits [RingerMode] for the [stream] if it's affecting it and null when [RingerMode] doesn't * affect the [stream] */ private fun ringerModeForStream(stream: Int): Flow<RingerMode?> { return if (stream == AudioManager.STREAM_RING) { audioVolumeInteractor.ringerMode } else { flowOf(null) } } }
packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderStateModel.kt 0 → 100644 +36 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.volume.dialog.sliders.ui.viewmodel import androidx.annotation.DrawableRes import com.android.systemui.volume.dialog.shared.model.VolumeDialogStreamModel data class VolumeDialogSliderStateModel( val minValue: Float, val maxValue: Float, val value: Float, @DrawableRes val iconRes: Int, ) fun VolumeDialogStreamModel.toStateModel(@DrawableRes iconRes: Int): VolumeDialogSliderStateModel { return VolumeDialogSliderStateModel( minValue = levelMin.toFloat(), value = level.toFloat(), maxValue = levelMax.toFloat(), iconRes = iconRes, ) }
packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt +19 −2 Original line number Diff line number Diff line Loading @@ -32,7 +32,9 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.stateIn Loading @@ -56,12 +58,12 @@ constructor( private val interactor: VolumeDialogSliderInteractor, private val visibilityInteractor: VolumeDialogVisibilityInteractor, @VolumeDialog private val coroutineScope: CoroutineScope, private val volumeDialogSliderIconProvider: VolumeDialogSliderIconProvider, private val systemClock: SystemClock, ) { private val userVolumeUpdates = MutableStateFlow<VolumeUpdate?>(null) val model: Flow<VolumeDialogStreamModel> = private val model: Flow<VolumeDialogStreamModel> = interactor.slider .filter { val lastVolumeUpdateTime = userVolumeUpdates.value?.timestampMillis ?: 0 Loading @@ -70,6 +72,21 @@ constructor( .stateIn(coroutineScope, SharingStarted.Eagerly, null) .filterNotNull() val state: Flow<VolumeDialogSliderStateModel> = model.flatMapLatest { streamModel -> with(streamModel) { volumeDialogSliderIconProvider.getStreamIcon( stream = stream, level = level, levelMin = levelMin, levelMax = levelMax, isMuted = muted, isRoutedToBluetooth = routedToBluetooth, ) } .map { icon -> streamModel.toStateModel(icon) } } init { userVolumeUpdates .filterNotNull() Loading