Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b1fee73c authored by Catherine Liang's avatar Catherine Liang
Browse files

Color picker preview & apply pattern

Establish color picker preview & apply pattern. Also ensure
selected color option is updated when color is updated since the new
picker no longer restarts after color updates.

Flag: com.android.systemui.shared.new_customization_picker_ui
Test: ColorPickerViewModelTest
Bug: 350718581
Bug: 288312530
Change-Id: Ib992d0ed34eee8e6ec9bda820dd110c37d9a238a
parent eaf7b356
Loading
Loading
Loading
Loading
+93 −17
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ import com.android.customization.model.color.ColorOptionImpl
import com.android.customization.picker.color.shared.model.ColorOptionModel
import com.android.customization.picker.color.shared.model.ColorType
import com.android.systemui.monet.Style
import com.android.wallpaper.config.BaseFlags
import com.android.wallpaper.picker.customization.data.repository.WallpaperColorsRepository
import com.android.wallpaper.picker.customization.shared.model.WallpaperColorsModel
import javax.inject.Inject
@@ -46,6 +47,8 @@ constructor(
    private val colorManager: ColorCustomizationManager,
) : ColorPickerRepository {

    private val isNewPickerUi = BaseFlags.get().isNewPickerUi()

    private val homeWallpaperColors: StateFlow<WallpaperColorsModel?> =
        wallpaperColorsRepository.homeWallpaperColors
    private val lockWallpaperColors: StateFlow<WallpaperColorsModel?> =
@@ -56,8 +59,82 @@ constructor(
    private val _isApplyingSystemColor = MutableStateFlow(false)
    override val isApplyingSystemColor = _isApplyingSystemColor.asStateFlow()

    // TODO (b/299510645): update color options on selected option change after restart is disabled
    private val generatedColorOptions: Flow<Map<ColorType, List<ColorOptionImpl>>> =
        combine(homeWallpaperColors, lockWallpaperColors) { homeColors, lockColors ->
                homeColors to lockColors
            }
            .map { (homeColors, lockColors) ->
                suspendCancellableCoroutine { continuation ->
                    if (
                        homeColors is WallpaperColorsModel.Loading ||
                            lockColors is WallpaperColorsModel.Loading
                    ) {
                        continuation.resumeWith(
                            Result.success(
                                mapOf(
                                    ColorType.WALLPAPER_COLOR to listOf(),
                                    ColorType.PRESET_COLOR to listOf(),
                                )
                            )
                        )
                        return@suspendCancellableCoroutine
                    }
                    val homeColorsLoaded = homeColors as WallpaperColorsModel.Loaded
                    val lockColorsLoaded = lockColors as WallpaperColorsModel.Loaded
                    colorManager.setWallpaperColors(
                        homeColorsLoaded.colors,
                        lockColorsLoaded.colors,
                    )
                    colorManager.fetchOptions(
                        object : CustomizationManager.OptionsFetchedListener<ColorOption?> {
                            override fun onOptionsLoaded(options: MutableList<ColorOption?>?) {
                                val wallpaperColorOptions: MutableList<ColorOptionImpl> =
                                    mutableListOf()
                                val presetColorOptions: MutableList<ColorOptionImpl> =
                                    mutableListOf()
                                options?.forEach { option ->
                                    when ((option as ColorOptionImpl).type) {
                                        ColorType.WALLPAPER_COLOR ->
                                            wallpaperColorOptions.add(option)
                                        ColorType.PRESET_COLOR -> presetColorOptions.add(option)
                                    }
                                }
                                continuation.resumeWith(
                                    Result.success(
                                        mapOf(
                                            ColorType.WALLPAPER_COLOR to wallpaperColorOptions,
                                            ColorType.PRESET_COLOR to presetColorOptions,
                                        )
                                    )
                                )
                            }

                            override fun onError(throwable: Throwable?) {
                                Log.e(TAG, "Error loading theme bundles", throwable)
                                continuation.resumeWith(
                                    Result.failure(
                                        throwable ?: Throwable("Error loading theme bundles")
                                    )
                                )
                            }
                        },
                        /* reload= */ false,
                    )
                }
            }

    override val colorOptions: Flow<Map<ColorType, List<ColorOptionModel>>> =
        if (isNewPickerUi) {
            // Convert to ColorOptionModel. When the selected color option changes, update each
            // ColorOptionModel's isSelected by calling toModel again.
            combine(generatedColorOptions, selectedColorOption) { generatedColorOptions, _ ->
                generatedColorOptions
                    .map { entry ->
                        entry.key to entry.value.map { colorOption -> colorOption.toModel() }
                    }
                    .toMap()
            }
        } else {
            combine(homeWallpaperColors, lockWallpaperColors) { homeColors, lockColors ->
                    homeColors to lockColors
                }
@@ -71,7 +148,7 @@ constructor(
                                Result.success(
                                    mapOf(
                                        ColorType.WALLPAPER_COLOR to listOf(),
                                    ColorType.PRESET_COLOR to listOf()
                                        ColorType.PRESET_COLOR to listOf(),
                                    )
                                )
                            )
@@ -81,7 +158,7 @@ constructor(
                        val lockColorsLoaded = lockColors as WallpaperColorsModel.Loaded
                        colorManager.setWallpaperColors(
                            homeColorsLoaded.colors,
                        lockColorsLoaded.colors
                            lockColorsLoaded.colors,
                        )
                        colorManager.fetchOptions(
                            object : CustomizationManager.OptionsFetchedListener<ColorOption?> {
@@ -102,7 +179,7 @@ constructor(
                                        Result.success(
                                            mapOf(
                                                ColorType.WALLPAPER_COLOR to wallpaperColorOptions,
                                            ColorType.PRESET_COLOR to presetColorOptions
                                                ColorType.PRESET_COLOR to presetColorOptions,
                                            )
                                        )
                                    )
@@ -117,10 +194,11 @@ constructor(
                                    )
                                }
                            },
                        /* reload= */ false
                            /* reload= */ false,
                        )
                    }
                }
        }

    override suspend fun select(colorOptionModel: ColorOptionModel) {
        _isApplyingSystemColor.value = true
@@ -141,7 +219,7 @@ constructor(
                            Result.failure(throwable ?: Throwable("Error loading theme bundles"))
                        )
                    }
                }
                },
            )
        }
    }
@@ -158,11 +236,7 @@ constructor(
            colorOptionBuilder.addOverlayPackage(overlay.key, overlay.value)
        }
        val colorOption = colorOptionBuilder.build()
        return ColorOptionModel(
            key = "",
            colorOption = colorOption,
            isSelected = false,
        )
        return ColorOptionModel(key = "", colorOption = colorOption, isSelected = false)
    }

    override fun getCurrentColorSource(): String? {
@@ -173,6 +247,8 @@ constructor(
        return ColorOptionModel(
            key = "${this.type}::${this.style}::${this.serializedPackages}",
            colorOption = this,
            // Instead of using the selectedColorOption flow to determine isSelected, we check the
            // source of truth, which is the settings, using ColorOption::isActive
            isSelected = isActive(colorManager),
        )
    }
+1 −1
Original line number Diff line number Diff line
@@ -27,5 +27,5 @@ data class ColorOptionModel(
    val colorOption: ColorOption,

    /** Whether this color option is selected. */
    var isSelected: Boolean,
    val isSelected: Boolean,
)
+32 −14
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import android.content.Context
import com.android.customization.model.color.ColorOptionImpl
import com.android.customization.module.logging.ThemesUserEventLogger
import com.android.customization.picker.color.domain.interactor.ColorPickerInteractor
import com.android.customization.picker.color.shared.model.ColorOptionModel
import com.android.customization.picker.color.shared.model.ColorType
import com.android.customization.picker.color.ui.viewmodel.ColorOptionIconViewModel
import com.android.themepicker.R
@@ -36,6 +37,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
@@ -51,14 +53,16 @@ constructor(
    @Assisted private val viewModelScope: CoroutineScope,
) {

    private val overridingColorOption = MutableStateFlow<ColorOptionModel?>(null)
    val previewingColorOption = overridingColorOption.asStateFlow()

    private val selectedColorTypeTabId = MutableStateFlow<ColorType?>(null)

    /** View-models for each color tab. */
    val colorTypeTabs: Flow<List<FloatingToolbarTabViewModel>> =
        combine(
            interactor.colorOptions,
            selectedColorTypeTabId,
        ) { colorOptions, selectedColorTypeIdOrNull ->
        combine(interactor.colorOptions, selectedColorTypeTabId) {
            colorOptions,
            selectedColorTypeIdOrNull ->
            colorOptions.keys.mapIndexed { index, colorType ->
                val isSelected =
                    (selectedColorTypeIdOrNull == null && index == 0) ||
@@ -118,7 +122,7 @@ constructor(
                            val darkThemeColors =
                                colorOption.previewInfo.resolveColors(/* darkTheme= */ true)
                            val isSelectedFlow: StateFlow<Boolean> =
                                interactor.selectingColorOption
                                previewingColorOption
                                    .map {
                                        it?.colorOption?.isEquivalent(colorOptionModel.colorOption)
                                            ?: colorOptionModel.isSelected
@@ -150,15 +154,7 @@ constructor(
                                        } else {
                                            {
                                                viewModelScope.launch {
                                                    interactor.select(colorOptionModel)
                                                    logger.logThemeColorApplied(
                                                        colorOptionModel.colorOption
                                                            .sourceForLogging,
                                                        colorOptionModel.colorOption
                                                            .styleForLogging,
                                                        colorOptionModel.colorOption
                                                            .seedColorForLogging,
                                                    )
                                                    overridingColorOption.value = colorOptionModel
                                                }
                                            }
                                        }
@@ -169,6 +165,28 @@ constructor(
                .toMap()
        }

    val onApply: Flow<(suspend () -> Unit)?> =
        previewingColorOption.map { previewingColorOption ->
            previewingColorOption?.let {
                if (it.isSelected) {
                    null
                } else {
                    {
                        interactor.select(it)
                        logger.logThemeColorApplied(
                            previewingColorOption.colorOption.sourceForLogging,
                            previewingColorOption.colorOption.styleForLogging,
                            previewingColorOption.colorOption.seedColorForLogging,
                        )
                    }
                }
            }
        }

    fun resetPreview() {
        overridingColorOption.value = null
    }

    /** The list of all available color options for the selected Color Type. */
    val colorOptions: Flow<List<OptionItemViewModel<ColorOptionIconViewModel>>> =
        combine(allColorOptions, selectedColorTypeTabId) {
+3 −0
Original line number Diff line number Diff line
@@ -61,6 +61,7 @@ constructor(
        keyguardQuickAffordancePickerViewModel2.resetPreview()
        shapeAndGridPickerViewModel.resetPreview()
        clockPickerViewModel.resetPreview()
        colorPickerViewModel2.resetPreview()
        return defaultCustomizationOptionsViewModel.deselectOption()
    }

@@ -129,6 +130,8 @@ constructor(
                        .SHORTCUTS -> keyguardQuickAffordancePickerViewModel2.onApply
                    ThemePickerCustomizationOptionUtil.ThemePickerHomeCustomizationOption
                        .APP_SHAPE_AND_GRID -> shapeAndGridPickerViewModel.onApply
                    ThemePickerCustomizationOptionUtil.ThemePickerHomeCustomizationOption.COLORS ->
                        colorPickerViewModel2.onApply
                    else -> flow { emit(null) }
                }
            }
+24 −14
Original line number Diff line number Diff line
@@ -97,19 +97,19 @@ class ColorPickerViewModel2Test {
    }

    @Test
    fun `Log selected wallpaper color`() =
    fun onApply_wallpaperColor_shouldLogColor() =
        testScope.runTest {
            repository.setOptions(
                listOf(
                    repository.buildWallpaperOption(
                        ColorOptionsProvider.COLOR_SOURCE_LOCK,
                        Style.EXPRESSIVE,
                        "121212"
                        "121212",
                    )
                ),
                listOf(repository.buildPresetOption(Style.FRUIT_SALAD, "#ABCDEF")),
                ColorType.PRESET_COLOR,
                0
                0,
            )

            val colorTypes = collectLastValue(underTest.colorTypeTabs)
@@ -117,8 +117,10 @@ class ColorPickerViewModel2Test {

            // Select "Wallpaper colors" tab
            colorTypes()?.get(0)?.onClick?.invoke()
            // Select a color option
            // Select a color option to preview
            selectColorOption(colorOptions, 0)
            // Apply the selected color option
            applySelectedColorOption()

            assertThat(logger.themeColorSource)
                .isEqualTo(StyleEnums.COLOR_SOURCE_LOCK_SCREEN_WALLPAPER)
@@ -127,19 +129,19 @@ class ColorPickerViewModel2Test {
        }

    @Test
    fun `Log selected preset color`() =
    fun onApply_presetColor_shouldLogColor() =
        testScope.runTest {
            repository.setOptions(
                listOf(
                    repository.buildWallpaperOption(
                        ColorOptionsProvider.COLOR_SOURCE_LOCK,
                        Style.EXPRESSIVE,
                        "121212"
                        "121212",
                    )
                ),
                listOf(repository.buildPresetOption(Style.FRUIT_SALAD, "#ABCDEF")),
                ColorType.WALLPAPER_COLOR,
                0
                0,
            )

            val colorTypes = collectLastValue(underTest.colorTypeTabs)
@@ -147,8 +149,10 @@ class ColorPickerViewModel2Test {

            // Select "Wallpaper colors" tab
            colorTypes()?.get(1)?.onClick?.invoke()
            // Select a color option
            // Select a color option to preview
            selectColorOption(colorOptions, 0)
            // Apply the selected color option
            applySelectedColorOption()

            assertThat(logger.themeColorSource).isEqualTo(StyleEnums.COLOR_SOURCE_PRESET_COLOR)
            assertThat(logger.themeColorStyle).isEqualTo(Style.FRUIT_SALAD.toString().hashCode())
@@ -156,7 +160,7 @@ class ColorPickerViewModel2Test {
        }

    @Test
    fun `Select a preset color`() =
    fun selectColorOption() =
        testScope.runTest {
            val colorTypes = collectLastValue(underTest.colorTypeTabs)
            val colorOptions = collectLastValue(underTest.colorOptions)
@@ -166,7 +170,7 @@ class ColorPickerViewModel2Test {
                colorTypes = colorTypes(),
                colorOptions = colorOptions(),
                selectedColorTypeText = "Wallpaper colors",
                selectedColorOptionIndex = 0
                selectedColorOptionIndex = 0,
            )

            // Select "Basic colors" tab
@@ -175,7 +179,7 @@ class ColorPickerViewModel2Test {
                colorTypes = colorTypes(),
                colorOptions = colorOptions(),
                selectedColorTypeText = "Basic colors",
                selectedColorOptionIndex = -1
                selectedColorOptionIndex = -1,
            )

            // Select a color option
@@ -187,7 +191,7 @@ class ColorPickerViewModel2Test {
                colorTypes = colorTypes(),
                colorOptions = colorOptions(),
                selectedColorTypeText = "Wallpaper colors",
                selectedColorOptionIndex = -1
                selectedColorOptionIndex = -1,
            )

            // Check new option is selected
@@ -196,7 +200,7 @@ class ColorPickerViewModel2Test {
                colorTypes = colorTypes(),
                colorOptions = colorOptions(),
                selectedColorTypeText = "Basic colors",
                selectedColorOptionIndex = 2
                selectedColorOptionIndex = 2,
            )
        }

@@ -210,8 +214,14 @@ class ColorPickerViewModel2Test {
            onClickedFlow?.let { collectLastValue(it) }
        onClickedLastValueOrNull?.let { onClickedLastValue ->
            val onClickedOrNull: (() -> Unit)? = onClickedLastValue()
            onClickedOrNull?.let { onClicked -> onClicked() }
            onClickedOrNull?.invoke()
        }
    }

    /** Simulates a user selecting the affordance at the given index, if that is clickable. */
    private suspend fun TestScope.applySelectedColorOption() {
        val onApply = collectLastValue(underTest.onApply)()
        onApply?.invoke()
    }

    /**