Merge "Wallpaper Quick Switcher Reload (2/3)" into tm-qpr-dev
diff --git a/res/layout/color_option_2.xml b/res/layout/color_option_2.xml
new file mode 100644
index 0000000..2ac0fe6
--- /dev/null
+++ b/res/layout/color_option_2.xml
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+     Copyright (C) 2023 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.
+-->
+<!-- Content description is set programmatically on the parent FrameLayout -->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    android:clipChildren="false">
+    <FrameLayout
+        android:id="@+id/icon_container"
+        android:layout_width="@dimen/option_tile_width"
+        android:layout_height="@dimen/option_tile_width"
+        android:clipChildren="false">
+
+        <ImageView
+            android:id="@id/selection_border"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:background="@drawable/option_item_border"
+            android:alpha="0"
+            android:importantForAccessibility="no" />
+
+        <ImageView
+            android:id="@id/background"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:background="@drawable/option_item_background"
+            android:importantForAccessibility="no" />
+
+        <FrameLayout
+            android:id="@id/foreground"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent">
+            <ImageView
+                android:id="@+id/color_preview_0"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center"
+                android:layout_marginRight="@dimen/color_seed_chip_margin"
+                android:layout_marginBottom="@dimen/color_seed_chip_margin"
+                android:src="@drawable/color_chip_seed_filled0"
+                android:importantForAccessibility="no"/>
+
+            <ImageView
+                android:id="@+id/color_preview_1"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center"
+                android:layout_marginLeft="@dimen/color_seed_chip_margin"
+                android:layout_marginBottom="@dimen/color_seed_chip_margin"
+                android:src="@drawable/color_chip_seed_filled2"
+                android:importantForAccessibility="no"/>
+
+            <ImageView
+                android:id="@+id/color_preview_2"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center"
+                android:layout_marginRight="@dimen/color_seed_chip_margin"
+                android:layout_marginTop="@dimen/color_seed_chip_margin"
+                android:src="@drawable/color_chip_seed_filled1"
+                android:importantForAccessibility="no"/>
+
+            <ImageView
+                android:id="@+id/color_preview_3"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center"
+                android:layout_marginLeft="@dimen/color_seed_chip_margin"
+                android:layout_marginTop="@dimen/color_seed_chip_margin"
+                android:src="@drawable/color_chip_seed_filled3"
+                android:importantForAccessibility="no" />
+        </FrameLayout>
+    </FrameLayout>
+</LinearLayout>
+
diff --git a/res/layout/color_option_with_background.xml b/res/layout/color_option_with_background.xml
index 67079f7..dc6f3ed 100644
--- a/res/layout/color_option_with_background.xml
+++ b/res/layout/color_option_with_background.xml
@@ -14,6 +14,7 @@
      limitations under the License.
 -->
 <!-- Content description is set programmatically on the parent FrameLayout -->
+<!-- TODO (b/272109171): Remove after clock settings is refactored to use OptionItemAdapter -->
 <LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="wrap_content"
diff --git a/res/layout/fragment_color_picker.xml b/res/layout/fragment_color_picker.xml
index a9d2adc..efb5a87 100644
--- a/res/layout/fragment_color_picker.xml
+++ b/res/layout/fragment_color_picker.xml
@@ -60,7 +60,8 @@
         android:layout_marginHorizontal="24dp"
         android:layout_marginBottom="28dp"
         android:background="@drawable/picker_fragment_background"
-        android:paddingTop="22dp">
+        android:paddingTop="22dp"
+        android:clipChildren="false">
 
         <FrameLayout
             android:layout_width="match_parent"
@@ -97,14 +98,16 @@
 
         <FrameLayout
             android:layout_width="match_parent"
-            android:layout_height="wrap_content">
+            android:layout_height="wrap_content"
+            android:clipChildren="false">
 
             <androidx.recyclerview.widget.RecyclerView
                 android:id="@+id/color_options"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:clipToPadding="false"
-                android:paddingHorizontal="16dp" />
+                android:paddingHorizontal="16dp"
+                android:clipChildren="false" />
 
             <!--
             This is just an invisible placeholder put in place so that the parent keeps its height
@@ -116,7 +119,7 @@
             It's critical for any TextViews inside the included layout to have text.
             -->
             <include
-                layout="@layout/color_option_with_background"
+                layout="@layout/color_option_2"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:visibility="invisible" />
diff --git a/src/com/android/customization/model/color/ColorOption.java b/src/com/android/customization/model/color/ColorOption.java
index 66a3a3c..216bb9b 100644
--- a/src/com/android/customization/model/color/ColorOption.java
+++ b/src/com/android/customization/model/color/ColorOption.java
@@ -110,9 +110,12 @@
         if (mStyle != other.getStyle()) {
             return false;
         }
-        if (mIsDefault) {
-            return other.isDefault() || TextUtils.isEmpty(other.getSerializedPackages())
-                    || EMPTY_JSON.equals(other.getSerializedPackages());
+        String thisSerializedPackages = getSerializedPackages();
+        if (mIsDefault || TextUtils.isEmpty(thisSerializedPackages)
+                || EMPTY_JSON.equals(thisSerializedPackages)) {
+            String otherSerializedPackages = other.getSerializedPackages();
+            return other.isDefault() || TextUtils.isEmpty(otherSerializedPackages)
+                    || EMPTY_JSON.equals(otherSerializedPackages);
         }
         // Map#equals ensures keys and values are compared.
         return mPackagesByCategory.equals(other.mPackagesByCategory);
diff --git a/src/com/android/customization/model/grid/ui/binder/GridIconViewBinder.kt b/src/com/android/customization/model/grid/ui/binder/GridIconViewBinder.kt
new file mode 100644
index 0000000..fba89a7
--- /dev/null
+++ b/src/com/android/customization/model/grid/ui/binder/GridIconViewBinder.kt
@@ -0,0 +1,17 @@
+package com.android.customization.model.grid.ui.binder
+
+import android.widget.ImageView
+import com.android.customization.model.grid.ui.viewmodel.GridIconViewModel
+import com.android.customization.widget.GridTileDrawable
+
+object GridIconViewBinder {
+    fun bind(view: ImageView, viewModel: GridIconViewModel) {
+        view.setImageDrawable(
+            GridTileDrawable(
+                viewModel.columns,
+                viewModel.rows,
+                viewModel.path,
+            )
+        )
+    }
+}
diff --git a/src/com/android/customization/model/grid/ui/binder/GridScreenBinder.kt b/src/com/android/customization/model/grid/ui/binder/GridScreenBinder.kt
index d24467a..78536ca 100644
--- a/src/com/android/customization/model/grid/ui/binder/GridScreenBinder.kt
+++ b/src/com/android/customization/model/grid/ui/binder/GridScreenBinder.kt
@@ -18,12 +18,14 @@
 package com.android.customization.model.grid.ui.binder
 
 import android.view.View
+import android.widget.ImageView
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.repeatOnLifecycle
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
+import com.android.customization.model.grid.ui.viewmodel.GridIconViewModel
 import com.android.customization.model.grid.ui.viewmodel.GridScreenViewModel
 import com.android.customization.picker.common.ui.view.ItemSpacing
 import com.android.wallpaper.R
@@ -57,7 +59,11 @@
                     OptionItemBinder.TintSpec(
                         selectedColor = view.context.getColor(R.color.text_color_primary),
                         unselectedColor = view.context.getColor(R.color.text_color_secondary),
-                    )
+                    ),
+                bindIcon = { foregroundView: View, gridIcon: GridIconViewModel ->
+                    val imageView = foregroundView as? ImageView
+                    imageView?.let { GridIconViewBinder.bind(imageView, gridIcon) }
+                }
             )
         optionView.adapter = adapter
 
diff --git a/src/com/android/customization/model/grid/ui/viewmodel/GridIconViewModel.kt b/src/com/android/customization/model/grid/ui/viewmodel/GridIconViewModel.kt
new file mode 100644
index 0000000..3942d7c
--- /dev/null
+++ b/src/com/android/customization/model/grid/ui/viewmodel/GridIconViewModel.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2023 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.customization.model.grid.ui.viewmodel
+
+data class GridIconViewModel(
+    val columns: Int,
+    val rows: Int,
+    val path: String,
+)
diff --git a/src/com/android/customization/model/grid/ui/viewmodel/GridScreenViewModel.kt b/src/com/android/customization/model/grid/ui/viewmodel/GridScreenViewModel.kt
index af6ed0f..c11a594 100644
--- a/src/com/android/customization/model/grid/ui/viewmodel/GridScreenViewModel.kt
+++ b/src/com/android/customization/model/grid/ui/viewmodel/GridScreenViewModel.kt
@@ -26,12 +26,11 @@
 import com.android.customization.model.ResourceConstants
 import com.android.customization.model.grid.domain.interactor.GridInteractor
 import com.android.customization.model.grid.shared.model.GridOptionItemsModel
-import com.android.customization.widget.GridTileDrawable
-import com.android.wallpaper.picker.common.icon.ui.viewmodel.Icon
 import com.android.wallpaper.picker.common.text.ui.viewmodel.Text
 import com.android.wallpaper.picker.option.ui.viewmodel.OptionItemViewModel
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.launch
 
@@ -43,12 +42,12 @@
     @SuppressLint("StaticFieldLeak") // We're not leaking this context as it is the app context.
     private val applicationContext = context.applicationContext
 
-    val optionItems: Flow<List<OptionItemViewModel>> =
+    val optionItems: Flow<List<OptionItemViewModel<GridIconViewModel>>> =
         interactor.options.map { model -> toViewModel(model) }
 
     private fun toViewModel(
         model: GridOptionItemsModel,
-    ): List<OptionItemViewModel> {
+    ): List<OptionItemViewModel<GridIconViewModel>> {
         val iconShapePath =
             applicationContext.resources.getString(
                 Resources.getSystem()
@@ -63,17 +62,14 @@
             is GridOptionItemsModel.Loaded ->
                 model.options.map { option ->
                     val text = Text.Loaded(option.name)
-                    OptionItemViewModel(
-                        key = flowOf("${option.cols}x${option.rows}"),
-                        icon =
-                            Icon.Loaded(
-                                drawable =
-                                    GridTileDrawable(
-                                        option.cols,
-                                        option.rows,
-                                        iconShapePath,
-                                    ),
-                                contentDescription = text
+                    OptionItemViewModel<GridIconViewModel>(
+                        key =
+                            MutableStateFlow("${option.cols}x${option.rows}") as StateFlow<String>,
+                        payload =
+                            GridIconViewModel(
+                                columns = option.cols,
+                                rows = option.rows,
+                                path = iconShapePath,
                             ),
                         text = text,
                         isSelected = option.isSelected,
diff --git a/src/com/android/customization/picker/color/data/repository/ColorPickerRepositoryImpl.kt b/src/com/android/customization/picker/color/data/repository/ColorPickerRepositoryImpl.kt
index 7553dcb..d94acfc 100644
--- a/src/com/android/customization/picker/color/data/repository/ColorPickerRepositoryImpl.kt
+++ b/src/com/android/customization/picker/color/data/repository/ColorPickerRepositoryImpl.kt
@@ -120,8 +120,10 @@
         for (overlay in overlays) {
             colorOptionBuilder.addOverlayPackage(overlay.key, overlay.value)
         }
+        val colorOption = colorOptionBuilder.build()
         return ColorOptionModel(
-            colorOption = colorOptionBuilder.build(),
+            key = "${colorOption.style}::${colorOption.serializedPackages}",
+            colorOption = colorOption,
             isSelected = false,
         )
     }
@@ -132,6 +134,7 @@
 
     private fun ColorOption.toModel(): ColorOptionModel {
         return ColorOptionModel(
+            key = "${this.style}::${this.serializedPackages}",
             colorOption = this,
             isSelected = isActive(colorManager),
         )
diff --git a/src/com/android/customization/picker/color/data/repository/FakeColorPickerRepository.kt b/src/com/android/customization/picker/color/data/repository/FakeColorPickerRepository.kt
index 660a74f..b7d3a57 100644
--- a/src/com/android/customization/picker/color/data/repository/FakeColorPickerRepository.kt
+++ b/src/com/android/customization/picker/color/data/repository/FakeColorPickerRepository.kt
@@ -18,6 +18,7 @@
 
 import android.content.Context
 import android.graphics.Color
+import android.text.TextUtils
 import com.android.customization.model.color.ColorBundle
 import com.android.customization.model.color.ColorOptionsProvider
 import com.android.customization.model.color.ColorSeedOption
@@ -61,6 +62,7 @@
                                     selectedColorOptionIndex == index
                             val colorOption =
                                 ColorOptionModel(
+                                    key = "${ColorType.WALLPAPER_COLOR}::$index",
                                     colorOption = buildWallpaperOption(index),
                                     isSelected = isSelected,
                                 )
@@ -78,6 +80,7 @@
                                     selectedColorOptionIndex == index
                             val colorOption =
                                 ColorOptionModel(
+                                    key = "${ColorType.BASIC_COLOR}::$index",
                                     colorOption = buildPresetOption(index),
                                     isSelected =
                                         selectedColorOptionType == ColorType.BASIC_COLOR &&
@@ -131,6 +134,7 @@
             wallpaperColorOptions.forEach { option ->
                 add(
                     ColorOptionModel(
+                        key = option.key,
                         colorOption = option.colorOption,
                         isSelected = option.testEquals(colorOptionModel),
                     )
@@ -142,6 +146,7 @@
             basicColorOptions.forEach { option ->
                 add(
                     ColorOptionModel(
+                        key = option.key,
                         colorOption = option.colorOption,
                         isSelected = option.testEquals(colorOptionModel),
                     )
@@ -169,10 +174,7 @@
             return false
         }
         return if (other is ColorOptionModel) {
-            val thisColorOptionIsWallpaperColor = this.colorOption is ColorSeedOption
-            val otherColorOptionIsWallpaperColor = other.colorOption is ColorSeedOption
-            (thisColorOptionIsWallpaperColor == otherColorOptionIsWallpaperColor) &&
-                (this.colorOption.index == other.colorOption.index)
+            TextUtils.equals(this.key, other.key)
         } else {
             false
         }
diff --git a/src/com/android/customization/picker/color/domain/interactor/ColorPickerInteractor.kt b/src/com/android/customization/picker/color/domain/interactor/ColorPickerInteractor.kt
index a932067..8c7a4b7 100644
--- a/src/com/android/customization/picker/color/domain/interactor/ColorPickerInteractor.kt
+++ b/src/com/android/customization/picker/color/domain/interactor/ColorPickerInteractor.kt
@@ -16,12 +16,10 @@
  */
 package com.android.customization.picker.color.domain.interactor
 
-import androidx.annotation.VisibleForTesting
 import com.android.customization.picker.color.data.repository.ColorPickerRepository
 import com.android.customization.picker.color.shared.model.ColorOptionModel
 import javax.inject.Provider
 import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.combine
 
 /** Single entry-point for all application state and business logic related to system color. */
 class ColorPickerInteractor(
@@ -32,31 +30,10 @@
      * The newly selected color option for overwriting the current active option during an
      * optimistic update, the value is set to null when update fails
      */
-    @VisibleForTesting private val activeColorOption = MutableStateFlow<ColorOptionModel?>(null)
+    val activeColorOption = MutableStateFlow<ColorOptionModel?>(null)
 
     /** List of wallpaper and preset color options on the device, categorized by Color Type */
-    val colorOptions =
-        combine(repository.colorOptions, activeColorOption) { colorOptions, activeOption ->
-            colorOptions
-                .map { colorTypeEntry ->
-                    colorTypeEntry.key to
-                        colorTypeEntry.value.map { colorOptionModel ->
-                            val isSelected =
-                                if (activeOption != null) {
-                                    colorOptionModel.colorOption.isEquivalent(
-                                        activeOption.colorOption
-                                    )
-                                } else {
-                                    colorOptionModel.isSelected
-                                }
-                            ColorOptionModel(
-                                colorOption = colorOptionModel.colorOption,
-                                isSelected = isSelected
-                            )
-                        }
-                }
-                .toMap()
-        }
+    val colorOptions = repository.colorOptions
 
     suspend fun select(colorOptionModel: ColorOptionModel) {
         activeColorOption.value = colorOptionModel
diff --git a/src/com/android/customization/picker/color/shared/model/ColorOptionModel.kt b/src/com/android/customization/picker/color/shared/model/ColorOptionModel.kt
index 69ef62a..5fde08e 100644
--- a/src/com/android/customization/picker/color/shared/model/ColorOptionModel.kt
+++ b/src/com/android/customization/picker/color/shared/model/ColorOptionModel.kt
@@ -21,6 +21,8 @@
 
 /** Models application state for a color option in a picker experience. */
 data class ColorOptionModel(
+    val key: String,
+
     /** Colors for the color option. */
     val colorOption: ColorOption,
 
diff --git a/src/com/android/customization/picker/color/ui/adapter/ColorOptionAdapter.kt b/src/com/android/customization/picker/color/ui/adapter/ColorOptionAdapter.kt
index 0e53766..7aa390d 100644
--- a/src/com/android/customization/picker/color/ui/adapter/ColorOptionAdapter.kt
+++ b/src/com/android/customization/picker/color/ui/adapter/ColorOptionAdapter.kt
@@ -32,7 +32,7 @@
 /**
  * Adapts between color option items and views.
  *
- * TODO (b/262924623): Refactor color picker with animated option framework ag/21132368
+ * TODO (b/272109171): Remove after clock settings is refactored to use OptionItemAdapter
  */
 class ColorOptionAdapter : RecyclerView.Adapter<ColorOptionAdapter.ViewHolder>() {
 
diff --git a/src/com/android/customization/picker/color/ui/binder/ColorOptionIconBinder.kt b/src/com/android/customization/picker/color/ui/binder/ColorOptionIconBinder.kt
new file mode 100644
index 0000000..1478cc4
--- /dev/null
+++ b/src/com/android/customization/picker/color/ui/binder/ColorOptionIconBinder.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2023 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.customization.picker.color.ui.binder
+
+import android.graphics.BlendMode
+import android.graphics.BlendModeColorFilter
+import android.view.ViewGroup
+import android.widget.ImageView
+import com.android.customization.picker.color.ui.viewmodel.ColorOptionIconViewModel
+import com.android.wallpaper.R
+
+object ColorOptionIconBinder {
+    fun bind(
+        view: ViewGroup,
+        viewModel: ColorOptionIconViewModel,
+    ) {
+        val color0View: ImageView = view.requireViewById(R.id.color_preview_0)
+        val color1View: ImageView = view.requireViewById(R.id.color_preview_1)
+        val color2View: ImageView = view.requireViewById(R.id.color_preview_2)
+        val color3View: ImageView = view.requireViewById(R.id.color_preview_3)
+        color0View.drawable.colorFilter = BlendModeColorFilter(viewModel.color0, BlendMode.SRC)
+        color1View.drawable.colorFilter = BlendModeColorFilter(viewModel.color1, BlendMode.SRC)
+        color2View.drawable.colorFilter = BlendModeColorFilter(viewModel.color2, BlendMode.SRC)
+        color3View.drawable.colorFilter = BlendModeColorFilter(viewModel.color3, BlendMode.SRC)
+    }
+}
diff --git a/src/com/android/customization/picker/color/ui/binder/ColorPickerBinder.kt b/src/com/android/customization/picker/color/ui/binder/ColorPickerBinder.kt
index 887fef0..053d5dd 100644
--- a/src/com/android/customization/picker/color/ui/binder/ColorPickerBinder.kt
+++ b/src/com/android/customization/picker/color/ui/binder/ColorPickerBinder.kt
@@ -19,6 +19,7 @@
 
 import android.graphics.Rect
 import android.view.View
+import android.view.ViewGroup
 import androidx.core.view.ViewCompat
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleOwner
@@ -26,10 +27,11 @@
 import androidx.lifecycle.repeatOnLifecycle
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
-import com.android.customization.picker.color.ui.adapter.ColorOptionAdapter
 import com.android.customization.picker.color.ui.adapter.ColorTypeTabAdapter
+import com.android.customization.picker.color.ui.viewmodel.ColorOptionIconViewModel
 import com.android.customization.picker.color.ui.viewmodel.ColorPickerViewModel
 import com.android.wallpaper.R
+import com.android.wallpaper.picker.option.ui.adapter.OptionItemAdapter
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.launch
 
@@ -54,7 +56,15 @@
         colorTypeTabView.layoutManager =
             LinearLayoutManager(view.context, RecyclerView.HORIZONTAL, false)
         colorTypeTabView.addItemDecoration(ItemSpacing())
-        val colorOptionAdapter = ColorOptionAdapter()
+        val colorOptionAdapter =
+            OptionItemAdapter(
+                layoutResourceId = R.layout.color_option_2,
+                lifecycleOwner = lifecycleOwner,
+                bindIcon = { foregroundView: View, colorIcon: ColorOptionIconViewModel ->
+                    val viewGroup = foregroundView as? ViewGroup
+                    viewGroup?.let { ColorOptionIconBinder.bind(viewGroup, colorIcon) }
+                }
+            )
         colorOptionContainerView.adapter = colorOptionAdapter
         colorOptionContainerView.layoutManager =
             LinearLayoutManager(view.context, RecyclerView.HORIZONTAL, false)
diff --git a/src/com/android/customization/picker/color/ui/binder/ColorSectionViewBinder.kt b/src/com/android/customization/picker/color/ui/binder/ColorSectionViewBinder.kt
index 0842870..05b0916 100644
--- a/src/com/android/customization/picker/color/ui/binder/ColorSectionViewBinder.kt
+++ b/src/com/android/customization/picker/color/ui/binder/ColorSectionViewBinder.kt
@@ -17,10 +17,9 @@
 
 package com.android.customization.picker.color.ui.binder
 
-import android.graphics.BlendMode
-import android.graphics.BlendModeColorFilter
 import android.view.LayoutInflater
 import android.view.View
+import android.view.ViewGroup
 import android.widget.ImageView
 import android.widget.LinearLayout
 import androidx.core.view.isVisible
@@ -28,9 +27,10 @@
 import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.repeatOnLifecycle
-import com.android.customization.picker.color.ui.viewmodel.ColorOptionViewModel
+import com.android.customization.picker.color.ui.viewmodel.ColorOptionIconViewModel
 import com.android.customization.picker.color.ui.viewmodel.ColorPickerViewModel
 import com.android.wallpaper.R
+import com.android.wallpaper.picker.option.ui.viewmodel.OptionItemViewModel
 import kotlinx.coroutines.launch
 
 object ColorSectionViewBinder {
@@ -63,8 +63,9 @@
                         setOptions(
                             options = colorOptions,
                             view = optionContainer,
+                            lifecycleOwner = lifecycleOwner,
                             addOverflowOption = !isConnectedHorizontallyToOtherSections,
-                            overflowOnClick = navigationOnClick
+                            overflowOnClick = navigationOnClick,
                         )
                     }
                 }
@@ -73,10 +74,11 @@
     }
 
     fun setOptions(
-        options: List<ColorOptionViewModel>,
+        options: List<OptionItemViewModel<ColorOptionIconViewModel>>,
         view: LinearLayout,
+        lifecycleOwner: LifecycleOwner,
         addOverflowOption: Boolean = false,
-        overflowOnClick: (View) -> Unit = {}
+        overflowOnClick: (View) -> Unit = {},
     ) {
         view.removeAllViews()
         // Color option slot size is the minimum between the color option size and the view column
@@ -92,26 +94,29 @@
             val itemView =
                 LayoutInflater.from(view.context)
                     .inflate(R.layout.color_option_no_background, view, false)
-
-            val color0View: ImageView = itemView.requireViewById(R.id.color_preview_0)
-            val color1View: ImageView = itemView.requireViewById(R.id.color_preview_1)
-            val color2View: ImageView = itemView.requireViewById(R.id.color_preview_2)
-            val color3View: ImageView = itemView.requireViewById(R.id.color_preview_3)
-            color0View.drawable.colorFilter = BlendModeColorFilter(item.color0, BlendMode.SRC)
-            color1View.drawable.colorFilter = BlendModeColorFilter(item.color1, BlendMode.SRC)
-            color2View.drawable.colorFilter = BlendModeColorFilter(item.color2, BlendMode.SRC)
-            color3View.drawable.colorFilter = BlendModeColorFilter(item.color3, BlendMode.SRC)
-
+            item.payload?.let { ColorOptionIconBinder.bind(itemView as ViewGroup, item.payload) }
             val optionSelectedView = itemView.findViewById<ImageView>(R.id.option_selected)
-            optionSelectedView.isVisible = item.isSelected
 
-            itemView.setOnClickListener(
-                if (item.onClick != null) {
-                    View.OnClickListener { item.onClick.invoke() }
-                } else {
-                    null
+            lifecycleOwner.lifecycleScope.launch {
+                lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+                    launch {
+                        item.isSelected.collect { isSelected ->
+                            optionSelectedView.isVisible = isSelected
+                        }
+                    }
+                    launch {
+                        item.onClicked.collect { onClicked ->
+                            itemView.setOnClickListener(
+                                if (onClicked != null) {
+                                    View.OnClickListener { onClicked.invoke() }
+                                } else {
+                                    null
+                                }
+                            )
+                        }
+                    }
                 }
-            )
+            }
             view.addView(itemView)
         }
         // add overflow option
diff --git a/src/com/android/customization/picker/color/ui/viewmodel/ColorOptionIconViewModel.kt b/src/com/android/customization/picker/color/ui/viewmodel/ColorOptionIconViewModel.kt
new file mode 100644
index 0000000..d32538d
--- /dev/null
+++ b/src/com/android/customization/picker/color/ui/viewmodel/ColorOptionIconViewModel.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2023 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.customization.picker.color.ui.viewmodel
+
+import android.annotation.ColorInt
+
+data class ColorOptionIconViewModel(
+    @ColorInt val color0: Int,
+    @ColorInt val color1: Int,
+    @ColorInt val color2: Int,
+    @ColorInt val color3: Int,
+)
diff --git a/src/com/android/customization/picker/color/ui/viewmodel/ColorOptionViewModel.kt b/src/com/android/customization/picker/color/ui/viewmodel/ColorOptionViewModel.kt
index 784ec2e..7af2aa5 100644
--- a/src/com/android/customization/picker/color/ui/viewmodel/ColorOptionViewModel.kt
+++ b/src/com/android/customization/picker/color/ui/viewmodel/ColorOptionViewModel.kt
@@ -19,7 +19,11 @@
 
 import android.annotation.ColorInt
 
-/** Models UI state for a color options in a picker experience. */
+/**
+ * Models UI state for a color options in a picker experience.
+ *
+ * TODO (b/272109171): Remove after clock settings is refactored to use OptionItemAdapter
+ */
 data class ColorOptionViewModel(
     /** Colors for the color option. */
     @ColorInt val color0: Int,
diff --git a/src/com/android/customization/picker/color/ui/viewmodel/ColorPickerViewModel.kt b/src/com/android/customization/picker/color/ui/viewmodel/ColorPickerViewModel.kt
index 5784855..58bc858 100644
--- a/src/com/android/customization/picker/color/ui/viewmodel/ColorPickerViewModel.kt
+++ b/src/com/android/customization/picker/color/ui/viewmodel/ColorPickerViewModel.kt
@@ -25,12 +25,16 @@
 import com.android.customization.picker.color.domain.interactor.ColorPickerInteractor
 import com.android.customization.picker.color.shared.model.ColorType
 import com.android.wallpaper.R
+import com.android.wallpaper.picker.common.text.ui.viewmodel.Text
+import com.android.wallpaper.picker.option.ui.viewmodel.OptionItemViewModel
 import kotlin.math.max
 import kotlin.math.min
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
 
 /** Models UI state for a color picker experience. */
@@ -75,7 +79,8 @@
         }
 
     /** The list of all color options mapped by their color type */
-    private val allColorOptions: Flow<Map<ColorType, List<ColorOptionViewModel>>> =
+    private val allColorOptions:
+        Flow<Map<ColorType, List<OptionItemViewModel<ColorOptionIconViewModel>>>> =
         interactor.colorOptions.map { colorOptions ->
             colorOptions
                 .map { colorOptionEntry ->
@@ -87,26 +92,45 @@
                                         colorOptionModel.colorOption as ColorSeedOption
                                     val colors =
                                         colorSeedOption.previewInfo.resolveColors(context.resources)
-                                    ColorOptionViewModel(
-                                        color0 = colors[0],
-                                        color1 = colors[1],
-                                        color2 = colors[2],
-                                        color3 = colors[3],
-                                        contentDescription =
-                                            colorSeedOption
-                                                .getContentDescription(context)
-                                                .toString(),
-                                        isSelected = colorOptionModel.isSelected,
-                                        onClick =
-                                            if (colorOptionModel.isSelected) {
-                                                null
-                                            } else {
-                                                {
-                                                    viewModelScope.launch {
-                                                        interactor.select(colorOptionModel)
+                                    val isSelectedFlow: StateFlow<Boolean> =
+                                        interactor.activeColorOption
+                                            .map {
+                                                it?.colorOption?.isEquivalent(
+                                                    colorOptionModel.colorOption
+                                                )
+                                                    ?: colorOptionModel.isSelected
+                                            }
+                                            .stateIn(viewModelScope)
+                                    OptionItemViewModel<ColorOptionIconViewModel>(
+                                        key =
+                                            MutableStateFlow(colorOptionModel.key)
+                                                as StateFlow<String>,
+                                        payload =
+                                            ColorOptionIconViewModel(
+                                                colors[0],
+                                                colors[1],
+                                                colors[2],
+                                                colors[3]
+                                            ),
+                                        text =
+                                            Text.Loaded(
+                                                colorSeedOption
+                                                    .getContentDescription(context)
+                                                    .toString()
+                                            ),
+                                        isSelected = isSelectedFlow,
+                                        onClicked =
+                                            isSelectedFlow.map { isSelected ->
+                                                if (isSelected) {
+                                                    null
+                                                } else {
+                                                    {
+                                                        viewModelScope.launch {
+                                                            interactor.select(colorOptionModel)
+                                                        }
                                                     }
                                                 }
-                                            }
+                                            },
                                     )
                                 }
                             }
@@ -122,21 +146,42 @@
                                         colorBundle.previewInfo.resolveSecondaryColor(
                                             context.resources
                                         )
-                                    ColorOptionViewModel(
-                                        color0 = primaryColor,
-                                        color1 = secondaryColor,
-                                        color2 = primaryColor,
-                                        color3 = secondaryColor,
-                                        contentDescription =
-                                            colorBundle.getContentDescription(context).toString(),
-                                        isSelected = colorOptionModel.isSelected,
-                                        onClick =
-                                            if (colorOptionModel.isSelected) {
-                                                null
-                                            } else {
-                                                {
-                                                    viewModelScope.launch {
-                                                        interactor.select(colorOptionModel)
+                                    val isSelectedFlow: StateFlow<Boolean> =
+                                        interactor.activeColorOption
+                                            .map {
+                                                it?.colorOption?.isEquivalent(
+                                                    colorOptionModel.colorOption
+                                                )
+                                                    ?: colorOptionModel.isSelected
+                                            }
+                                            .stateIn(viewModelScope)
+                                    OptionItemViewModel<ColorOptionIconViewModel>(
+                                        key =
+                                            MutableStateFlow(colorOptionModel.key)
+                                                as StateFlow<String>,
+                                        payload =
+                                            ColorOptionIconViewModel(
+                                                primaryColor,
+                                                secondaryColor,
+                                                primaryColor,
+                                                secondaryColor
+                                            ),
+                                        text =
+                                            Text.Loaded(
+                                                colorBundle
+                                                    .getContentDescription(context)
+                                                    .toString()
+                                            ),
+                                        isSelected = isSelectedFlow,
+                                        onClicked =
+                                            isSelectedFlow.map { isSelected ->
+                                                if (isSelected) {
+                                                    null
+                                                } else {
+                                                    {
+                                                        viewModelScope.launch {
+                                                            interactor.select(colorOptionModel)
+                                                        }
                                                     }
                                                 }
                                             },
@@ -149,7 +194,7 @@
         }
 
     /** The list of all available color options for the selected Color Type. */
-    val colorOptions: Flow<List<ColorOptionViewModel>> =
+    val colorOptions: Flow<List<OptionItemViewModel<ColorOptionIconViewModel>>> =
         combine(allColorOptions, selectedColorTypeId) { allColorOptions, selectedColorTypeIdOrNull
             ->
             val selectedColorTypeId = selectedColorTypeIdOrNull ?: ColorType.WALLPAPER_COLOR
@@ -157,7 +202,7 @@
         }
 
     /** The list of color options for the color section */
-    val colorSectionOptions: Flow<List<ColorOptionViewModel>> =
+    val colorSectionOptions: Flow<List<OptionItemViewModel<ColorOptionIconViewModel>>> =
         allColorOptions.map { allColorOptions ->
             val wallpaperOptions = allColorOptions[ColorType.WALLPAPER_COLOR]
             val presetOptions = allColorOptions[ColorType.BASIC_COLOR]
diff --git a/src/com/android/customization/picker/quickaffordance/ui/binder/KeyguardQuickAffordancePickerBinder.kt b/src/com/android/customization/picker/quickaffordance/ui/binder/KeyguardQuickAffordancePickerBinder.kt
index af5cd13..4395f5e 100644
--- a/src/com/android/customization/picker/quickaffordance/ui/binder/KeyguardQuickAffordancePickerBinder.kt
+++ b/src/com/android/customization/picker/quickaffordance/ui/binder/KeyguardQuickAffordancePickerBinder.kt
@@ -20,6 +20,7 @@
 import android.app.Dialog
 import android.content.Context
 import android.view.View
+import android.widget.ImageView
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.lifecycleScope
@@ -32,6 +33,8 @@
 import com.android.wallpaper.R
 import com.android.wallpaper.picker.common.dialog.ui.viewbinder.DialogViewBinder
 import com.android.wallpaper.picker.common.dialog.ui.viewmodel.DialogViewModel
+import com.android.wallpaper.picker.common.icon.ui.viewbinder.IconViewBinder
+import com.android.wallpaper.picker.common.icon.ui.viewmodel.Icon
 import com.android.wallpaper.picker.option.ui.adapter.OptionItemAdapter
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.combine
@@ -62,6 +65,10 @@
             OptionItemAdapter(
                 layoutResourceId = R.layout.keyguard_quick_affordance,
                 lifecycleOwner = lifecycleOwner,
+                bindIcon = { foregroundView: View, gridIcon: Icon ->
+                    val imageView = foregroundView as? ImageView
+                    imageView?.let { IconViewBinder.bind(imageView, gridIcon) }
+                }
             )
         affordancesView.adapter = affordancesAdapter
         affordancesView.layoutManager =
diff --git a/src/com/android/customization/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordancePickerViewModel.kt b/src/com/android/customization/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordancePickerViewModel.kt
index d88edfa..14b6acc 100644
--- a/src/com/android/customization/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordancePickerViewModel.kt
+++ b/src/com/android/customization/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordancePickerViewModel.kt
@@ -147,16 +147,18 @@
                         isSelected = isSelected,
                         selectedQuickAffordances =
                             selectedAffordances.map { affordanceModel ->
-                                OptionItemViewModel(
-                                    key = flowOf("${slot.id}::${affordanceModel.id}"),
-                                    icon =
+                                OptionItemViewModel<Icon>(
+                                    key =
+                                        MutableStateFlow("${slot.id}::${affordanceModel.id}")
+                                            as StateFlow<String>,
+                                    payload =
                                         Icon.Loaded(
                                             drawable =
                                                 getAffordanceIcon(affordanceModel.iconResourceId),
                                             contentDescription = null,
                                         ),
                                     text = Text.Loaded(affordanceModel.name),
-                                    isSelected = flowOf(true),
+                                    isSelected = MutableStateFlow(true) as StateFlow<Boolean>,
                                     onClicked = flowOf(null),
                                     onLongClicked = null,
                                     isEnabled = true,
@@ -194,9 +196,9 @@
             )
 
     /** The list of all available quick affordances for the selected slot. */
-    val quickAffordances: Flow<List<OptionItemViewModel>> =
+    val quickAffordances: Flow<List<OptionItemViewModel<Icon>>> =
         quickAffordanceInteractor.affordances.map { affordances ->
-            val isNoneSelected = selectedAffordanceIds.map { it.isEmpty() }
+            val isNoneSelected = selectedAffordanceIds.map { it.isEmpty() }.stateIn(viewModelScope)
             listOf(
                 none(
                     slotId = selectedSlotId,
@@ -220,11 +222,16 @@
             ) +
                 affordances.map { affordance ->
                     val affordanceIcon = getAffordanceIcon(affordance.iconResourceId)
-                    val isSelectedFlow: Flow<Boolean> =
-                        selectedAffordanceIds.map { it.contains(affordance.id) }
-                    OptionItemViewModel(
-                        key = selectedSlotId.map { slotId -> "$slotId::${affordance.id}" },
-                        icon = Icon.Loaded(drawable = affordanceIcon, contentDescription = null),
+                    val isSelectedFlow: StateFlow<Boolean> =
+                        selectedAffordanceIds
+                            .map { it.contains(affordance.id) }
+                            .stateIn(viewModelScope)
+                    OptionItemViewModel<Icon>(
+                        key =
+                            selectedSlotId
+                                .map { slotId -> "$slotId::${affordance.id}" }
+                                .stateIn(viewModelScope),
+                        payload = Icon.Loaded(drawable = affordanceIcon, contentDescription = null),
                         text = Text.Loaded(affordance.name),
                         isSelected = isSelectedFlow,
                         onClicked =
@@ -273,15 +280,15 @@
     val summary: Flow<KeyguardQuickAffordanceSummaryViewModel> =
         slots.map { slots ->
             val icon2 =
-                slots[KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END]
-                    ?.selectedQuickAffordances
-                    ?.firstOrNull()
-                    ?.icon
+                (slots[KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END]
+                        ?.selectedQuickAffordances
+                        ?.firstOrNull())
+                    ?.payload
             val icon1 =
-                slots[KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START]
-                    ?.selectedQuickAffordances
-                    ?.firstOrNull()
-                    ?.icon
+                (slots[KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START]
+                        ?.selectedQuickAffordances
+                        ?.firstOrNull())
+                    ?.payload
 
             KeyguardQuickAffordanceSummaryViewModel(
                 description = toDescriptionText(context, slots),
@@ -359,14 +366,14 @@
 
     /** Returns a view-model for the special "None" option. */
     @SuppressLint("UseCompatLoadingForDrawables")
-    private fun none(
-        slotId: Flow<String>,
-        isSelected: Flow<Boolean>,
+    private suspend fun none(
+        slotId: StateFlow<String>,
+        isSelected: StateFlow<Boolean>,
         onSelected: Flow<(() -> Unit)?>,
-    ): OptionItemViewModel {
-        return OptionItemViewModel(
-            key = slotId.map { "$it::none" },
-            icon = Icon.Resource(res = R.drawable.link_off, contentDescription = null),
+    ): OptionItemViewModel<Icon> {
+        return OptionItemViewModel<Icon>(
+            key = slotId.map { "$it::none" }.stateIn(viewModelScope),
+            payload = Icon.Resource(res = R.drawable.link_off, contentDescription = null),
             text = Text.Resource(res = R.string.keyguard_affordance_none),
             isSelected = isSelected,
             onClicked = onSelected,
diff --git a/src/com/android/customization/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordanceSlotViewModel.kt b/src/com/android/customization/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordanceSlotViewModel.kt
index 6d8195a..4d11346 100644
--- a/src/com/android/customization/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordanceSlotViewModel.kt
+++ b/src/com/android/customization/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordanceSlotViewModel.kt
@@ -17,6 +17,7 @@
 
 package com.android.customization.picker.quickaffordance.ui.viewmodel
 
+import com.android.wallpaper.picker.common.icon.ui.viewmodel.Icon
 import com.android.wallpaper.picker.option.ui.viewmodel.OptionItemViewModel
 
 /** Models UI state for a single lock screen quick affordance slot in a picker experience. */
@@ -32,7 +33,7 @@
      *
      * Useful for preview.
      */
-    val selectedQuickAffordances: List<OptionItemViewModel>,
+    val selectedQuickAffordances: List<OptionItemViewModel<Icon>>,
 
     /**
      * The maximum number of quick affordances that can be selected for this slot.
diff --git a/tests/src/com/android/customization/model/grid/ui/viewmodel/GridScreenViewModelTest.kt b/tests/src/com/android/customization/model/grid/ui/viewmodel/GridScreenViewModelTest.kt
index 7d0a527..58c5d99 100644
--- a/tests/src/com/android/customization/model/grid/ui/viewmodel/GridScreenViewModelTest.kt
+++ b/tests/src/com/android/customization/model/grid/ui/viewmodel/GridScreenViewModelTest.kt
@@ -94,13 +94,17 @@
             assertThat(getOnClick(optionItemsValue[1])).isNull()
         }
 
-    private fun TestScope.getSelectedIndex(optionItems: List<OptionItemViewModel>): Int {
+    private fun TestScope.getSelectedIndex(
+        optionItems: List<OptionItemViewModel<GridIconViewModel>>
+    ): Int {
         return optionItems.indexOfFirst { optionItem ->
             collectLastValue(optionItem.isSelected).invoke() == true
         }
     }
 
-    private fun TestScope.getOnClick(optionItem: OptionItemViewModel): (() -> Unit)? {
+    private fun TestScope.getOnClick(
+        optionItem: OptionItemViewModel<GridIconViewModel>
+    ): (() -> Unit)? {
         return collectLastValue(optionItem.onClicked).invoke()
     }
 }
diff --git a/tests/src/com/android/customization/model/picker/color/ui/viewmodel/ColorPickerViewModelTest.kt b/tests/src/com/android/customization/model/picker/color/ui/viewmodel/ColorPickerViewModelTest.kt
index 7d87a55..533d1dc 100644
--- a/tests/src/com/android/customization/model/picker/color/ui/viewmodel/ColorPickerViewModelTest.kt
+++ b/tests/src/com/android/customization/model/picker/color/ui/viewmodel/ColorPickerViewModelTest.kt
@@ -23,9 +23,10 @@
 import com.android.customization.picker.color.domain.interactor.ColorPickerInteractor
 import com.android.customization.picker.color.domain.interactor.ColorPickerSnapshotRestorer
 import com.android.customization.picker.color.shared.model.ColorType
-import com.android.customization.picker.color.ui.viewmodel.ColorOptionViewModel
+import com.android.customization.picker.color.ui.viewmodel.ColorOptionIconViewModel
 import com.android.customization.picker.color.ui.viewmodel.ColorPickerViewModel
 import com.android.customization.picker.color.ui.viewmodel.ColorTypeViewModel
+import com.android.wallpaper.picker.option.ui.viewmodel.OptionItemViewModel
 import com.android.wallpaper.testing.FakeSnapshotStore
 import com.android.wallpaper.testing.collectLastValue
 import com.google.common.truth.Truth.assertThat
@@ -97,13 +98,13 @@
                 selectedColorOptionIndex = 0
             )
 
-            colorSectionOptions()?.get(2)?.onClick?.invoke()
+            selectColorOption(colorSectionOptions, 2)
             assertColorOptionUiState(
                 colorOptions = colorSectionOptions(),
                 selectedColorOptionIndex = 2
             )
 
-            colorSectionOptions()?.get(4)?.onClick?.invoke()
+            selectColorOption(colorSectionOptions, 4)
             assertColorOptionUiState(
                 colorOptions = colorSectionOptions(),
                 selectedColorOptionIndex = 4
@@ -134,7 +135,7 @@
             )
 
             // Select a color option
-            colorOptions()?.get(2)?.onClick?.invoke()
+            selectColorOption(colorOptions, 2)
 
             // Check original option is no longer selected
             colorTypes()?.get(ColorType.WALLPAPER_COLOR)?.onClick?.invoke()
@@ -155,6 +156,20 @@
             )
         }
 
+    /** Simulates a user selecting the affordance at the given index, if that is clickable. */
+    private fun TestScope.selectColorOption(
+        colorOptions: () -> List<OptionItemViewModel<ColorOptionIconViewModel>>?,
+        index: Int,
+    ) {
+        val onClickedFlow = colorOptions()?.get(index)?.onClicked
+        val onClickedLastValueOrNull: (() -> (() -> Unit)?)? =
+            onClickedFlow?.let { collectLastValue(it) }
+        onClickedLastValueOrNull?.let { onClickedLastValue ->
+            val onClickedOrNull: (() -> Unit)? = onClickedLastValue()
+            onClickedOrNull?.let { onClicked -> onClicked() }
+        }
+    }
+
     /**
      * Asserts the entire picker UI state is what is expected. This includes the color type tabs and
      * the color options list.
@@ -165,9 +180,9 @@
      * @param selectedColorOptionIndex The index of the color option that's expected to be selected,
      *   -1 stands for no color option should be selected
      */
-    private fun assertPickerUiState(
+    private fun TestScope.assertPickerUiState(
         colorTypes: Map<ColorType, ColorTypeViewModel>?,
-        colorOptions: List<ColorOptionViewModel>?,
+        colorOptions: List<OptionItemViewModel<ColorOptionIconViewModel>>?,
         selectedColorTypeText: String,
         selectedColorOptionIndex: Int,
     ) {
@@ -191,8 +206,8 @@
      * @param selectedColorOptionIndex The index of the color option that's expected to be selected,
      *   -1 stands for no color option should be selected
      */
-    private fun assertColorOptionUiState(
-        colorOptions: List<ColorOptionViewModel>?,
+    private fun TestScope.assertColorOptionUiState(
+        colorOptions: List<OptionItemViewModel<ColorOptionIconViewModel>>?,
         selectedColorOptionIndex: Int,
     ) {
         var foundSelectedColorOption = false
@@ -200,12 +215,13 @@
         if (colorOptions != null) {
             for (i in colorOptions.indices) {
                 val colorOptionHasSelectedIndex = i == selectedColorOptionIndex
+                val isSelected: Boolean? = collectLastValue(colorOptions[i].isSelected).invoke()
                 assertWithMessage(
                         "Expected color option with index \"${i}\" to have" +
                             " isSelected=$colorOptionHasSelectedIndex but it was" +
-                            " ${colorOptions[i].isSelected}, num options: ${colorOptions.size}"
+                            " ${isSelected}, num options: ${colorOptions.size}"
                     )
-                    .that(colorOptions[i].isSelected)
+                    .that(isSelected)
                     .isEqualTo(colorOptionHasSelectedIndex)
                 foundSelectedColorOption = foundSelectedColorOption || colorOptionHasSelectedIndex
             }
diff --git a/tests/src/com/android/customization/model/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordancePickerViewModelTest.kt b/tests/src/com/android/customization/model/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordancePickerViewModelTest.kt
index 5c99585..103ae84 100644
--- a/tests/src/com/android/customization/model/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordancePickerViewModelTest.kt
+++ b/tests/src/com/android/customization/model/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordancePickerViewModelTest.kt
@@ -382,7 +382,7 @@
 
     /** Simulates a user selecting the affordance at the given index, if that is clickable. */
     private fun TestScope.selectAffordance(
-        affordances: () -> List<OptionItemViewModel>?,
+        affordances: () -> List<OptionItemViewModel<Icon>>?,
         index: Int,
     ) {
         val onClickedFlow = affordances()?.get(index)?.onClicked
@@ -405,7 +405,7 @@
      */
     private fun TestScope.assertPickerUiState(
         slots: Map<String, KeyguardQuickAffordanceSlotViewModel>?,
-        affordances: List<OptionItemViewModel>?,
+        affordances: List<OptionItemViewModel<Icon>>?,
         selectedSlotText: String,
         selectedAffordanceText: String,
     ) {