diff options
| author | 2024-10-07 17:36:35 +0100 | |
|---|---|---|
| committer | 2024-10-24 17:32:39 +0100 | |
| commit | ac7a0d7748be0b7716ca42e065b5d3cee7741c8f (patch) | |
| tree | c6f4808d1c36852094928c5053d93db5ae83ce4e | |
| parent | 046855a0cb247c69eea357616a1cd84a301e6d9c (diff) | |
Bind the new sliders to the state updates.
This CL makes updates slider values from the state and updates the
state when the slider value changes
Flag: com.android.systemui.volume_redesign
Test: manual on foldable. Adjust volume of different streams
Bug: 369992924
Change-Id: I39da31fab1b5598722b16d051ca18d96e97d0bbd
16 files changed, 399 insertions, 56 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractorTest.kt new file mode 100644 index 000000000000..bfafdab003aa --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractorTest.kt @@ -0,0 +1,90 @@ +/* + * 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.domain.interactor + +import android.testing.TestableLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testScope +import com.android.systemui.plugins.VolumeDialogController +import com.android.systemui.plugins.fakeVolumeDialogController +import com.android.systemui.testKosmos +import com.android.systemui.volume.dialog.sliders.domain.model.volumeDialogSliderType +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +@TestableLooper.RunWithLooper +class VolumeDialogSliderInteractorTest : SysuiTestCase() { + + private val kosmos = testKosmos() + + private lateinit var underTest: VolumeDialogSliderInteractor + + @Before + fun setUp() { + underTest = kosmos.volumeDialogSliderInteractor + } + + @Test + fun settingStreamVolume_setsActiveStream() = + with(kosmos) { + testScope.runTest { + runCurrent() + // initialize the stream model + fakeVolumeDialogController.setStreamVolume(volumeDialogSliderType.audioStream, 0) + + val sliderModel by collectLastValue(underTest.slider) + underTest.setStreamVolume(1) + runCurrent() + + assertThat(sliderModel!!.isActive).isTrue() + } + } + + @Test + fun streamVolumeIs_minMaxAreEnforced() = + with(kosmos) { + testScope.runTest { + runCurrent() + fakeVolumeDialogController.updateState { + states.put( + volumeDialogSliderType.audioStream, + VolumeDialogController.StreamState().apply { + levelMin = 0 + level = 2 + levelMax = 1 + }, + ) + } + + val sliderModel by collectLastValue(underTest.slider) + runCurrent() + + assertThat(sliderModel!!.level).isEqualTo(1) + } + } +} diff --git a/packages/SystemUI/res/layout-land-television/volume_dialog.xml b/packages/SystemUI/res/layout-land-television/volume_dialog.xml index f77db956a493..4321fb04df52 100644 --- a/packages/SystemUI/res/layout-land-television/volume_dialog.xml +++ b/packages/SystemUI/res/layout-land-television/volume_dialog.xml @@ -36,6 +36,7 @@ android:showDividers="middle" /> <LinearLayout + android:id="@+id/volume_dialog" android:layout_width="@dimen/volume_dialog_width" android:layout_height="wrap_content" android:background="@drawable/volume_dialog_background" @@ -50,9 +51,7 @@ android:layout_width="@dimen/volume_dialog_button_size" android:layout_height="@dimen/volume_dialog_button_size" /> - <include - android:id="@+id/volume_dialog_slider" - layout="@layout/volume_dialog_slider" /> + <include layout="@layout/volume_dialog_slider" /> <Button android:id="@+id/volume_dialog_settings" diff --git a/packages/SystemUI/res/layout-land/volume_dialog.xml b/packages/SystemUI/res/layout-land/volume_dialog.xml index f77db956a493..4321fb04df52 100644 --- a/packages/SystemUI/res/layout-land/volume_dialog.xml +++ b/packages/SystemUI/res/layout-land/volume_dialog.xml @@ -36,6 +36,7 @@ android:showDividers="middle" /> <LinearLayout + android:id="@+id/volume_dialog" android:layout_width="@dimen/volume_dialog_width" android:layout_height="wrap_content" android:background="@drawable/volume_dialog_background" @@ -50,9 +51,7 @@ android:layout_width="@dimen/volume_dialog_button_size" android:layout_height="@dimen/volume_dialog_button_size" /> - <include - android:id="@+id/volume_dialog_slider" - layout="@layout/volume_dialog_slider" /> + <include layout="@layout/volume_dialog_slider" /> <Button android:id="@+id/volume_dialog_settings" diff --git a/packages/SystemUI/res/layout/volume_dialog.xml b/packages/SystemUI/res/layout/volume_dialog.xml index f77db956a493..4321fb04df52 100644 --- a/packages/SystemUI/res/layout/volume_dialog.xml +++ b/packages/SystemUI/res/layout/volume_dialog.xml @@ -36,6 +36,7 @@ android:showDividers="middle" /> <LinearLayout + android:id="@+id/volume_dialog" android:layout_width="@dimen/volume_dialog_width" android:layout_height="wrap_content" android:background="@drawable/volume_dialog_background" @@ -50,9 +51,7 @@ android:layout_width="@dimen/volume_dialog_button_size" android:layout_height="@dimen/volume_dialog_button_size" /> - <include - android:id="@+id/volume_dialog_slider" - layout="@layout/volume_dialog_slider" /> + <include layout="@layout/volume_dialog_slider" /> <Button android:id="@+id/volume_dialog_settings" diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt index 3d125b81cbd2..fa1088426351 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogCallbacksInteractor.kt @@ -105,8 +105,8 @@ constructor( scope.trySend(VolumeDialogEventModel.ShowSafetyWarning(flags)) } - override fun onAccessibilityModeChanged(showA11yStream: Boolean) { - scope.trySend(VolumeDialogEventModel.AccessibilityModeChanged(showA11yStream)) + override fun onAccessibilityModeChanged(showA11yStream: Boolean?) { + scope.trySend(VolumeDialogEventModel.AccessibilityModeChanged(showA11yStream == true)) } // Captions button is remove from the Volume Dialog diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt index 2668589be1b3..fb108c5b7b15 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogVisibilityInteractor.kt @@ -60,7 +60,7 @@ constructor( ) { @SuppressLint("SharedFlowCreation") - private val mutableDismissDialogEvents = MutableSharedFlow<Unit>() + private val mutableDismissDialogEvents = MutableSharedFlow<Unit>(extraBufferCapacity = 1) val dialogVisibility: Flow<VolumeDialogVisibilityModel> = repository.dialogVisibility init { @@ -74,7 +74,7 @@ constructor( .mapNotNull { it.toVisibilityModel() } .onEach { model -> updateVisibility { model } - if (model is VolumeDialogVisibilityModel.Visible) { + if (model is Visible) { resetDismissTimeout() } } @@ -87,17 +87,17 @@ constructor( */ fun dismissDialog(reason: Int) { updateVisibility { visibilityModel -> - if (visibilityModel is VolumeDialogVisibilityModel.Dismissed) { + if (visibilityModel is Dismissed) { visibilityModel } else { - VolumeDialogVisibilityModel.Dismissed(reason) + Dismissed(reason) } } } /** Resets current dialog timeout. */ - suspend fun resetDismissTimeout() { - mutableDismissDialogEvents.emit(Unit) + fun resetDismissTimeout() { + mutableDismissDialogEvents.tryEmit(Unit) } private fun updateVisibility( diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractor.kt index f78a8dcabc1c..876bf2c4a154 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractor.kt @@ -25,6 +25,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.mapNotNull /** Operates a state of particular slider of the Volume Dialog. */ @@ -37,12 +38,23 @@ constructor( ) { val slider: Flow<VolumeDialogStreamModel> = - volumeDialogStateInteractor.volumeDialogState.mapNotNull { - it.streamModels[sliderType.audioStream] - } + volumeDialogStateInteractor.volumeDialogState + .mapNotNull { + it.streamModels[sliderType.audioStream]?.run { + if (level < levelMin || level > levelMax) { + copy(level = level.coerceIn(levelMin, levelMax)) + } else { + this + } + } + } + .distinctUntilChanged() fun setStreamVolume(userLevel: Int) { - volumeDialogController.setStreamVolume(sliderType.audioStream, userLevel) + with(volumeDialogController) { + setStreamVolume(sliderType.audioStream, userLevel) + setActiveStream(sliderType.audioStream) + } } @VolumeDialogScope diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt index 25a5f287c21f..5c4d53aaf2c5 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt @@ -16,32 +16,55 @@ package com.android.systemui.volume.dialog.sliders.ui +import android.animation.Animator +import android.animation.ObjectAnimator import android.view.View -import androidx.lifecycle.viewmodel.compose.viewModel +import android.view.animation.DecelerateInterpolator import com.android.systemui.lifecycle.WindowLifecycleState import com.android.systemui.lifecycle.repeatWhenAttached -import com.android.systemui.lifecycle.setSnapshotBinding import com.android.systemui.lifecycle.viewModel +import com.android.systemui.res.R import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope +import com.android.systemui.volume.dialog.shared.model.VolumeDialogStreamModel 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 +import com.google.android.material.slider.LabelFormatter +import com.google.android.material.slider.Slider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlin.math.roundToInt import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +private const val PROGRESS_CHANGE_ANIMATION_DURATION_MS = 80L class VolumeDialogSliderViewBinder @AssistedInject -constructor(@Assisted private val viewModelProvider: () -> VolumeDialogSliderViewModel) { +constructor( + @Assisted private val viewModelProvider: () -> VolumeDialogSliderViewModel, + private val jankListenerFactory: JankListenerFactory, +) { fun bind(view: View) { with(view) { + val sliderView: Slider = + requireViewById<Slider>(R.id.volume_dialog_slider).apply { + labelBehavior = LabelFormatter.LABEL_GONE + } repeatWhenAttached { viewModel( traceName = "VolumeDialogSliderViewBinder", minWindowLifecycleState = WindowLifecycleState.ATTACHED, factory = { viewModelProvider() }, ) { viewModel -> - setSnapshotBinding {} + sliderView.addOnChangeListener { _, value, fromUser -> + viewModel.setStreamVolume(value.roundToInt(), fromUser) + } + + viewModel.model.onEach { it.bindToSlider(sliderView) }.launchIn(this) awaitCancellation() } @@ -49,6 +72,19 @@ constructor(@Assisted private val viewModelProvider: () -> VolumeDialogSliderVie } } + private suspend fun VolumeDialogStreamModel.bindToSlider(slider: Slider) { + with(slider) { + valueFrom = levelMin.toFloat() + valueTo = levelMax.toFloat() + // coerce the current value to the new value range before animating it + value = value.coerceIn(valueFrom, valueTo) + setValueAnimated( + level.toFloat(), + jankListenerFactory.update(this, PROGRESS_CHANGE_ANIMATION_DURATION_MS), + ) + } + } + @AssistedFactory @VolumeDialogScope interface Factory { @@ -58,3 +94,16 @@ constructor(@Assisted private val viewModelProvider: () -> VolumeDialogSliderVie ): VolumeDialogSliderViewBinder } } + +private suspend fun Slider.setValueAnimated( + newValue: Float, + jankListener: Animator.AnimatorListener, +) { + ObjectAnimator.ofFloat(value, newValue) + .apply { + duration = PROGRESS_CHANGE_ANIMATION_DURATION_MS + interpolator = DecelerateInterpolator() + addListener(jankListener) + } + .awaitAnimation<Float> { value = it } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt index 0a00f70b54f1..f486fe11bd68 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt @@ -16,14 +16,20 @@ package com.android.systemui.volume.dialog.sliders.ui +import android.view.LayoutInflater import android.view.View +import android.view.ViewGroup +import androidx.annotation.LayoutRes +import androidx.compose.ui.util.fastForEachIndexed import com.android.systemui.lifecycle.WindowLifecycleState import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.lifecycle.setSnapshotBinding import com.android.systemui.lifecycle.viewModel +import com.android.systemui.res.R import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSlidersViewModel import javax.inject.Inject +import kotlin.math.abs import kotlinx.coroutines.awaitCancellation @VolumeDialogScope @@ -33,17 +39,44 @@ constructor(private val viewModelFactory: VolumeDialogSlidersViewModel.Factory) fun bind(view: View) { with(view) { + val volumeDialog: View = requireViewById(R.id.volume_dialog) + val floatingSlidersContainer: ViewGroup = + requireViewById(R.id.volume_dialog_floating_sliders_container) repeatWhenAttached { viewModel( traceName = "VolumeDialogSlidersViewBinder", minWindowLifecycleState = WindowLifecycleState.ATTACHED, factory = { viewModelFactory.create() }, ) { viewModel -> - setSnapshotBinding {} + setSnapshotBinding { + viewModel.uiModel?.sliderViewBinder?.bind(volumeDialog) + val floatingSliderViewBinders = + viewModel.uiModel?.floatingSliderViewBinders ?: emptyList() + floatingSlidersContainer.ensureChildCount( + viewLayoutId = R.layout.volume_dialog_slider_floating, + count = floatingSliderViewBinders.size, + ) + floatingSliderViewBinders.fastForEachIndexed { index, viewBinder -> + viewBinder.bind(floatingSlidersContainer.getChildAt(index)) + } + } awaitCancellation() } } } } } + +private fun ViewGroup.ensureChildCount(@LayoutRes viewLayoutId: Int, count: Int) { + val childCountDelta = childCount - count + when { + childCountDelta > 0 -> { + removeViews(0, childCountDelta) + } + childCountDelta < 0 -> { + val inflater = LayoutInflater.from(context) + repeat(abs(childCountDelta)) { inflater.inflate(viewLayoutId, this, true) } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt index 7ee722d97bbc..ea0b49d5294e 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt @@ -16,26 +16,85 @@ package com.android.systemui.volume.dialog.sliders.ui.viewmodel +import com.android.systemui.util.time.SystemClock +import com.android.systemui.volume.Events +import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog +import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogVisibilityInteractor import com.android.systemui.volume.dialog.shared.model.VolumeDialogStreamModel import com.android.systemui.volume.dialog.sliders.domain.interactor.VolumeDialogSliderInteractor import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn +/* + This prevents volume slider updates while user interacts with it. This is needed due to the + flawed VolumeDialogControllerImpl. It has a single threaded message queue that handles all state + updates and doesn't skip sequential updates of the same stream. This leads to a bottleneck when + user rigorously adjusts the slider. + + Remove this when getting rid of the VolumeDialogControllerImpl as this doesn't happen in the + Volume Panel that uses the new coroutine-backed AudioRepository. +*/ +// TODO(b/375355785) remove this +private const val VOLUME_UPDATE_GRACE_PERIOD = 1000 + +@OptIn(ExperimentalCoroutinesApi::class) class VolumeDialogSliderViewModel @AssistedInject -constructor(@Assisted private val interactor: VolumeDialogSliderInteractor) { +constructor( + @Assisted private val interactor: VolumeDialogSliderInteractor, + private val visibilityInteractor: VolumeDialogVisibilityInteractor, + @VolumeDialog private val coroutineScope: CoroutineScope, + private val systemClock: SystemClock, +) { + + private val userVolumeUpdates = MutableStateFlow<VolumeUpdate?>(null) - val model: Flow<VolumeDialogStreamModel> = interactor.slider + val model: Flow<VolumeDialogStreamModel> = + interactor.slider + .filter { + val lastVolumeUpdateTime = userVolumeUpdates.value?.timestampMillis ?: 0 + getTimestampMillis() - lastVolumeUpdateTime > VOLUME_UPDATE_GRACE_PERIOD + } + .stateIn(coroutineScope, SharingStarted.Eagerly, null) + .filterNotNull() - fun setStreamVolume(volume: Int) { - interactor.setStreamVolume(volume) + init { + userVolumeUpdates + .filterNotNull() + .mapLatest { volume -> + interactor.setStreamVolume(volume.newVolumeLevel) + Events.writeEvent(Events.EVENT_TOUCH_LEVEL_CHANGED, model.first().stream, volume) + } + .launchIn(coroutineScope) } + fun setStreamVolume(volume: Int, fromUser: Boolean) { + if (fromUser) { + visibilityInteractor.resetDismissTimeout() + userVolumeUpdates.value = + VolumeUpdate(newVolumeLevel = volume, timestampMillis = getTimestampMillis()) + } + } + + private fun getTimestampMillis(): Long = systemClock.uptimeMillis() + @AssistedFactory interface Factory { fun create(interactor: VolumeDialogSliderInteractor): VolumeDialogSliderViewModel } + + private data class VolumeUpdate(val newVolumeLevel: Int, val timestampMillis: Long) } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSlidersViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSlidersViewModel.kt index b5b292fa4a66..22cf89fa6bde 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSlidersViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSlidersViewModel.kt @@ -16,6 +16,9 @@ package com.android.systemui.volume.dialog.sliders.ui.viewmodel +import androidx.compose.runtime.getValue +import com.android.systemui.lifecycle.ExclusiveActivatable +import com.android.systemui.lifecycle.Hydrator import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog import com.android.systemui.volume.dialog.sliders.domain.interactor.VolumeDialogSliderInteractor import com.android.systemui.volume.dialog.sliders.domain.interactor.VolumeDialogSlidersInteractor @@ -24,10 +27,9 @@ import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSliderViewBinde import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn @@ -39,9 +41,10 @@ constructor( private val sliderInteractorFactory: VolumeDialogSliderInteractor.Factory, private val sliderViewModelFactory: VolumeDialogSliderViewModel.Factory, private val sliderViewBinderFactory: VolumeDialogSliderViewBinder.Factory, -) { +) : ExclusiveActivatable() { - val sliders: Flow<VolumeDialogSliderUiModel> = + private val hydrator = Hydrator("VolumeDialogSlidersViewModel") + private val slidersStateFlow: StateFlow<VolumeDialogSliderUiModel?> = slidersInteractor.sliders .distinctUntilChanged() .map { slidersModel -> @@ -52,7 +55,13 @@ constructor( ) } .stateIn(coroutineScope, SharingStarted.Eagerly, null) - .filterNotNull() + + val uiModel: VolumeDialogSliderUiModel? by + hydrator.hydratedStateOf("VolumeDialogSlidersViewModel#uiModel", slidersStateFlow) + + override suspend fun onActivated(): Nothing { + hydrator.activate() + } private fun createSliderViewBinder(type: VolumeDialogSliderType): VolumeDialogSliderViewBinder = sliderViewBinderFactory.create { diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogBinder.kt index 77733fe33275..eb9483f8ea68 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogBinder.kt @@ -20,6 +20,7 @@ import android.app.Dialog import android.graphics.Color import android.graphics.PixelFormat import android.graphics.drawable.ColorDrawable +import android.view.View import android.view.ViewGroup import android.view.Window import android.view.WindowManager @@ -27,6 +28,7 @@ import com.android.systemui.res.R import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope import com.android.systemui.volume.dialog.settings.ui.binder.VolumeDialogSettingsButtonViewBinder +import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSlidersViewBinder import com.android.systemui.volume.dialog.ui.viewmodel.VolumeDialogGravityViewModel import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -40,6 +42,7 @@ class VolumeDialogBinder constructor( @VolumeDialog private val coroutineScope: CoroutineScope, private val volumeDialogViewBinder: VolumeDialogViewBinder, + private val slidersViewBinder: VolumeDialogSlidersViewBinder, private val settingsButtonViewBinder: VolumeDialogSettingsButtonViewBinder, private val gravityViewModel: VolumeDialogGravityViewModel, ) { @@ -50,11 +53,11 @@ constructor( dialog.setContentView(R.layout.volume_dialog) dialog.setCanceledOnTouchOutside(true) - settingsButtonViewBinder.bind(dialog.requireViewById(R.id.volume_dialog_settings)) - volumeDialogViewBinder.bind( - dialog, - dialog.requireViewById(R.id.volume_dialog_container), - ) + with(dialog.requireViewById<View>(R.id.volume_dialog_container)) { + slidersViewBinder.bind(this) + settingsButtonViewBinder.bind(this) + volumeDialogViewBinder.bind(dialog, this) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/SuspendAnimators.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/SuspendAnimators.kt index 4eae3b9a8da5..c7f5801a87c2 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/SuspendAnimators.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/SuspendAnimators.kt @@ -17,8 +17,11 @@ package com.android.systemui.volume.dialog.ui.utils import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator import android.view.ViewPropertyAnimator import kotlin.coroutines.resume +import kotlinx.coroutines.CancellableContinuation import kotlinx.coroutines.suspendCancellableCoroutine /** @@ -39,11 +42,12 @@ suspend fun ViewPropertyAnimator.suspendAnimate( } override fun onAnimationEnd(animation: Animator) { - continuation.resume(Unit) + continuation.resumeIfCan(Unit) animationListener?.onAnimationEnd(animation) } override fun onAnimationCancel(animation: Animator) { + continuation.resumeIfCan(Unit) animationListener?.onAnimationCancel(animation) } @@ -54,3 +58,30 @@ suspend fun ViewPropertyAnimator.suspendAnimate( ) continuation.invokeOnCancellation { this.cancel() } } + +/** + * Starts animation and suspends until it's finished. Cancels the animation if the running coroutine + * is cancelled. + */ +@Suppress("UNCHECKED_CAST") +suspend fun <T> ValueAnimator.awaitAnimation(onValueChanged: (T) -> Unit) { + suspendCancellableCoroutine { continuation -> + addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) = continuation.resumeIfCan(Unit) + + override fun onAnimationCancel(animation: Animator) = continuation.resumeIfCan(Unit) + } + ) + addUpdateListener { onValueChanged(it.animatedValue as T) } + + start() + continuation.invokeOnCancellation { cancel() } + } +} + +private fun <T> CancellableContinuation<T>.resumeIfCan(value: T) { + if (!isCancelled && !isCompleted) { + resume(value) + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/FakeVolumeDialogController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/FakeVolumeDialogController.kt index b45120ee0aba..43eb93e4dd53 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/FakeVolumeDialogController.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/FakeVolumeDialogController.kt @@ -43,30 +43,36 @@ class FakeVolumeDialogController(private val audioManager: AudioManager) : Volum private var state = VolumeDialogController.State() override fun setActiveStream(stream: Int) { - // ensure streamState existence for the active stream - state.states.getOrElse(stream) { - VolumeDialogController.StreamState().also { streamState -> - state.states.put(stream, streamState) + updateState { + // ensure streamState existence for the active stream` + states.getOrElse(stream) { + VolumeDialogController.StreamState().also { streamState -> + state.states.put(stream, streamState) + } } + activeStream = stream } - state.activeStream = stream } override fun setStreamVolume(stream: Int, userLevel: Int) { - val streamState = - state.states.getOrElse(stream) { - VolumeDialogController.StreamState().also { streamState -> - state.states.put(stream, streamState) + updateState { + val streamState = + states.getOrElse(stream) { + VolumeDialogController.StreamState().also { streamState -> + states.put(stream, streamState) + } } - } - streamState.level = userLevel.coerceIn(streamState.levelMin, streamState.levelMax) + streamState.level = userLevel.coerceIn(streamState.levelMin, streamState.levelMax) + } } override fun setRingerMode(ringerModeNormal: Int, external: Boolean) { - if (external) { - state.ringerModeExternal = ringerModeNormal - } else { - state.ringerModeInternal = ringerModeNormal + updateState { + if (external) { + ringerModeExternal = ringerModeNormal + } else { + ringerModeInternal = ringerModeNormal + } } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractorKosmos.kt new file mode 100644 index 000000000000..423100a1addf --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractorKosmos.kt @@ -0,0 +1,31 @@ +/* + * 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.domain.interactor + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.plugins.volumeDialogController +import com.android.systemui.volume.dialog.domain.interactor.volumeDialogStateInteractor +import com.android.systemui.volume.dialog.sliders.domain.model.volumeDialogSliderType + +val Kosmos.volumeDialogSliderInteractor: VolumeDialogSliderInteractor by + Kosmos.Fixture { + VolumeDialogSliderInteractor( + volumeDialogSliderType, + volumeDialogStateInteractor, + volumeDialogController, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/domain/model/VolumeDialogSliderTypeKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/domain/model/VolumeDialogSliderTypeKosmos.kt new file mode 100644 index 000000000000..cc8c1ea3fc75 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/sliders/domain/model/VolumeDialogSliderTypeKosmos.kt @@ -0,0 +1,23 @@ +/* + * 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.domain.model + +import android.media.AudioManager +import com.android.systemui.kosmos.Kosmos + +var Kosmos.volumeDialogSliderType: VolumeDialogSliderType by + Kosmos.Fixture { VolumeDialogSliderType.Stream(AudioManager.STREAM_SYSTEM) } |