Loading src/com/android/wallpaper/customization/ui/viewmodel/ColorPickerViewModel2.kt +20 −0 Original line number Diff line number Diff line Loading @@ -33,6 +33,8 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.scopes.ViewModelScoped import kotlin.coroutines.resume import kotlinx.coroutines.CancellableContinuation import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow Loading @@ -42,6 +44,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine /** Models UI state for a color picker experience. */ class ColorPickerViewModel2 Loading @@ -57,6 +60,7 @@ constructor( val previewingColorOption = overridingColorOption.asStateFlow() private val selectedColorTypeTabId = MutableStateFlow<ColorType?>(null) private var onApplyContinuation: CancellableContinuation<Unit>? = null /** View-models for each color tab. */ val colorTypeTabs: Flow<List<FloatingToolbarTabViewModel>> = Loading Loading @@ -165,6 +169,10 @@ constructor( .toMap() } /** * This function suspends until onApplyComplete is called to accommodate for configuration * change updates, which are applied with a latency. */ val onApply: Flow<(suspend () -> Unit)?> = previewingColorOption.map { previewingColorOption -> previewingColorOption?.let { Loading @@ -173,6 +181,12 @@ constructor( } else { { interactor.select(it) // Suspend until onApplyComplete is called, e.g. on configuration change suspendCancellableCoroutine { continuation: CancellableContinuation<Unit> -> onApplyContinuation?.cancel() onApplyContinuation = continuation continuation.invokeOnCancellation { onApplyContinuation = null } } logger.logThemeColorApplied( previewingColorOption.colorOption.sourceForLogging, previewingColorOption.colorOption.styleForLogging, Loading @@ -187,6 +201,12 @@ constructor( overridingColorOption.value = null } /** Resumes the onApply function if apply is in progress, otherwise no-op */ fun onApplyComplete() { onApplyContinuation?.resume(Unit) onApplyContinuation = null } /** The list of all available color options for the selected Color Type. */ val colorOptions: Flow<List<OptionItemViewModel2<ColorOptionIconViewModel>>> = combine(allColorOptions, selectedColorTypeTabId) { Loading src/com/android/wallpaper/customization/ui/viewmodel/ThemePickerCustomizationOptionsViewModel.kt +11 −3 Original line number Diff line number Diff line Loading @@ -27,6 +27,7 @@ import dagger.assisted.AssistedInject import dagger.hilt.android.scopes.ViewModelScoped import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine Loading Loading @@ -59,6 +60,8 @@ constructor( val shapeGridPickerViewModel = shapeGridPickerViewModelFactory.create(viewModelScope = viewModelScope) private var onApplyJob: Job? = null override val selectedOption = defaultCustomizationOptionsViewModel.selectedOption override fun handleBackPressed(): Boolean { Loading Loading @@ -167,9 +170,14 @@ constructor( .map { onApply -> if (onApply != null) { fun(onComplete: () -> Unit) { // Prevent double apply if (onApplyJob?.isActive != true) { onApplyJob = viewModelScope.launch { onApply() onComplete() onApplyJob = null } } } } else { Loading src/com/android/wallpaper/picker/common/preview/ui/binder/ThemePickerWorkspaceCallbackBinder.kt +8 −3 Original line number Diff line number Diff line Loading @@ -158,14 +158,19 @@ constructor( } } launch { colorUpdateViewModel.systemColorsUpdated.collect { viewModel.colorPickerViewModel2.onApplyComplete() } } launch { combine( viewModel.colorPickerViewModel2.previewingColorOption, viewModel.darkModeViewModel.overridingIsDarkMode, colorUpdateViewModel.systemColorsUpdated, ::Triple, ::Pair, ) .collect { (colorModel, darkMode, _) -> .collect { (colorModel, darkMode) -> val bundle = Bundle().apply { if (colorModel != null) { Loading tests/robotests/src/com/android/wallpaper/customization/ui/viewmodel/ColorPickerViewModel2Test.kt +26 −3 Original line number Diff line number Diff line Loading @@ -36,6 +36,7 @@ import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher Loading Loading @@ -95,6 +96,27 @@ class ColorPickerViewModel2Test { Dispatchers.resetMain() } @Test fun onApply_suspendsUntilOnApplyCompleteIsCalled() = testScope.runTest { val colorTypes = collectLastValue(underTest.colorTypeTabs) val colorOptions = collectLastValue(underTest.colorOptions) val onApply = collectLastValue(underTest.onApply) // Select "Wallpaper colors" tab colorTypes()?.get(0)?.onClick?.invoke() // Select a color option to preview selectColorOption(colorOptions, 1) // Apply the selected color option val job = testScope.launch { onApply()?.invoke() } assertThat(job.isActive).isTrue() underTest.onApplyComplete() assertThat(job.isActive).isFalse() } @Test fun onApply_wallpaperColor_shouldLogColor() = testScope.runTest { Loading Loading @@ -203,7 +225,7 @@ class ColorPickerViewModel2Test { ) } /** Simulates a user selecting the affordance at the given index, if that is clickable. */ /** Simulates a user selecting the color option at the given index. */ private fun TestScope.selectColorOption( colorOptions: () -> List<OptionItemViewModel2<ColorOptionIconViewModel>>?, index: Int, Loading @@ -217,10 +239,11 @@ class ColorPickerViewModel2Test { } } /** Simulates a user selecting the affordance at the given index, if that is clickable. */ /** Simulates a user applying the color option at the given index, and the apply completes. */ private suspend fun TestScope.applySelectedColorOption() { val onApply = collectLastValue(underTest.onApply)() onApply?.invoke() testScope.launch { onApply?.invoke() } underTest.onApplyComplete() } /** Loading Loading
src/com/android/wallpaper/customization/ui/viewmodel/ColorPickerViewModel2.kt +20 −0 Original line number Diff line number Diff line Loading @@ -33,6 +33,8 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.scopes.ViewModelScoped import kotlin.coroutines.resume import kotlinx.coroutines.CancellableContinuation import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow Loading @@ -42,6 +44,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine /** Models UI state for a color picker experience. */ class ColorPickerViewModel2 Loading @@ -57,6 +60,7 @@ constructor( val previewingColorOption = overridingColorOption.asStateFlow() private val selectedColorTypeTabId = MutableStateFlow<ColorType?>(null) private var onApplyContinuation: CancellableContinuation<Unit>? = null /** View-models for each color tab. */ val colorTypeTabs: Flow<List<FloatingToolbarTabViewModel>> = Loading Loading @@ -165,6 +169,10 @@ constructor( .toMap() } /** * This function suspends until onApplyComplete is called to accommodate for configuration * change updates, which are applied with a latency. */ val onApply: Flow<(suspend () -> Unit)?> = previewingColorOption.map { previewingColorOption -> previewingColorOption?.let { Loading @@ -173,6 +181,12 @@ constructor( } else { { interactor.select(it) // Suspend until onApplyComplete is called, e.g. on configuration change suspendCancellableCoroutine { continuation: CancellableContinuation<Unit> -> onApplyContinuation?.cancel() onApplyContinuation = continuation continuation.invokeOnCancellation { onApplyContinuation = null } } logger.logThemeColorApplied( previewingColorOption.colorOption.sourceForLogging, previewingColorOption.colorOption.styleForLogging, Loading @@ -187,6 +201,12 @@ constructor( overridingColorOption.value = null } /** Resumes the onApply function if apply is in progress, otherwise no-op */ fun onApplyComplete() { onApplyContinuation?.resume(Unit) onApplyContinuation = null } /** The list of all available color options for the selected Color Type. */ val colorOptions: Flow<List<OptionItemViewModel2<ColorOptionIconViewModel>>> = combine(allColorOptions, selectedColorTypeTabId) { Loading
src/com/android/wallpaper/customization/ui/viewmodel/ThemePickerCustomizationOptionsViewModel.kt +11 −3 Original line number Diff line number Diff line Loading @@ -27,6 +27,7 @@ import dagger.assisted.AssistedInject import dagger.hilt.android.scopes.ViewModelScoped import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine Loading Loading @@ -59,6 +60,8 @@ constructor( val shapeGridPickerViewModel = shapeGridPickerViewModelFactory.create(viewModelScope = viewModelScope) private var onApplyJob: Job? = null override val selectedOption = defaultCustomizationOptionsViewModel.selectedOption override fun handleBackPressed(): Boolean { Loading Loading @@ -167,9 +170,14 @@ constructor( .map { onApply -> if (onApply != null) { fun(onComplete: () -> Unit) { // Prevent double apply if (onApplyJob?.isActive != true) { onApplyJob = viewModelScope.launch { onApply() onComplete() onApplyJob = null } } } } else { Loading
src/com/android/wallpaper/picker/common/preview/ui/binder/ThemePickerWorkspaceCallbackBinder.kt +8 −3 Original line number Diff line number Diff line Loading @@ -158,14 +158,19 @@ constructor( } } launch { colorUpdateViewModel.systemColorsUpdated.collect { viewModel.colorPickerViewModel2.onApplyComplete() } } launch { combine( viewModel.colorPickerViewModel2.previewingColorOption, viewModel.darkModeViewModel.overridingIsDarkMode, colorUpdateViewModel.systemColorsUpdated, ::Triple, ::Pair, ) .collect { (colorModel, darkMode, _) -> .collect { (colorModel, darkMode) -> val bundle = Bundle().apply { if (colorModel != null) { Loading
tests/robotests/src/com/android/wallpaper/customization/ui/viewmodel/ColorPickerViewModel2Test.kt +26 −3 Original line number Diff line number Diff line Loading @@ -36,6 +36,7 @@ import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher Loading Loading @@ -95,6 +96,27 @@ class ColorPickerViewModel2Test { Dispatchers.resetMain() } @Test fun onApply_suspendsUntilOnApplyCompleteIsCalled() = testScope.runTest { val colorTypes = collectLastValue(underTest.colorTypeTabs) val colorOptions = collectLastValue(underTest.colorOptions) val onApply = collectLastValue(underTest.onApply) // Select "Wallpaper colors" tab colorTypes()?.get(0)?.onClick?.invoke() // Select a color option to preview selectColorOption(colorOptions, 1) // Apply the selected color option val job = testScope.launch { onApply()?.invoke() } assertThat(job.isActive).isTrue() underTest.onApplyComplete() assertThat(job.isActive).isFalse() } @Test fun onApply_wallpaperColor_shouldLogColor() = testScope.runTest { Loading Loading @@ -203,7 +225,7 @@ class ColorPickerViewModel2Test { ) } /** Simulates a user selecting the affordance at the given index, if that is clickable. */ /** Simulates a user selecting the color option at the given index. */ private fun TestScope.selectColorOption( colorOptions: () -> List<OptionItemViewModel2<ColorOptionIconViewModel>>?, index: Int, Loading @@ -217,10 +239,11 @@ class ColorPickerViewModel2Test { } } /** Simulates a user selecting the affordance at the given index, if that is clickable. */ /** Simulates a user applying the color option at the given index, and the apply completes. */ private suspend fun TestScope.applySelectedColorOption() { val onApply = collectLastValue(underTest.onApply)() onApply?.invoke() testScope.launch { onApply?.invoke() } underTest.onApplyComplete() } /** Loading