System Colors picker selection animation (1/2)

Added selection animation to system colors by adapting color picker to
use option item framework. Adjusted icon type used by option item
framework to accomdate for color icons made up of multiple images.
Animation on optimistic update can occasionally lead to animation being
cut off due to restart, working with designers to resolve in follow-up
CL

Bug: 270188654
Test: existing view-model and interactor tests updated for field type
changes, and still pass without logic changes. Manually verified picker
is unchanged and funactional with revamped UI flag turned off. Manually
verified animations, recording - https://drive.google.com/file/d/1BGFf9gyYMBKS9t8uLD1eQB4unb6Dv_z3/view?usp=sharing&resourcekey=0-IZFfkwrM9nmcgsD6RyW3lA

Change-Id: I1e42106bb289c1d53eb63cd1b2288c4757564a73
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/GridScreenBinder.kt b/src/com/android/customization/model/grid/ui/binder/GridScreenBinder.kt
index d24467a..78e0376 100644
--- a/src/com/android/customization/model/grid/ui/binder/GridScreenBinder.kt
+++ b/src/com/android/customization/model/grid/ui/binder/GridScreenBinder.kt
@@ -18,6 +18,7 @@
 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
@@ -27,6 +28,8 @@
 import com.android.customization.model.grid.ui.viewmodel.GridScreenViewModel
 import com.android.customization.picker.common.ui.view.ItemSpacing
 import com.android.wallpaper.R
+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 com.android.wallpaper.picker.option.ui.binder.OptionItemBinder
 import kotlinx.coroutines.CoroutineDispatcher
@@ -57,7 +60,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: Icon ->
+                    val imageView = foregroundView as? ImageView
+                    imageView?.let { IconViewBinder.bind(imageView, gridIcon) }
+                }
             )
         optionView.adapter = adapter
 
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..69d938c 100644
--- a/src/com/android/customization/model/grid/ui/viewmodel/GridScreenViewModel.kt
+++ b/src/com/android/customization/model/grid/ui/viewmodel/GridScreenViewModel.kt
@@ -43,12 +43,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<Icon>>> =
         interactor.options.map { model -> toViewModel(model) }
 
     private fun toViewModel(
         model: GridOptionItemsModel,
-    ): List<OptionItemViewModel> {
+    ): List<OptionItemViewModel<Icon>> {
         val iconShapePath =
             applicationContext.resources.getString(
                 Resources.getSystem()
@@ -63,9 +63,9 @@
             is GridOptionItemsModel.Loaded ->
                 model.options.map { option ->
                     val text = Text.Loaded(option.name)
-                    OptionItemViewModel(
+                    OptionItemViewModel<Icon>(
                         key = flowOf("${option.cols}x${option.rows}"),
-                        icon =
+                        payload =
                             Icon.Loaded(
                                 drawable =
                                     GridTileDrawable(
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 4c43d78..dfc7a1b 100644
--- a/src/com/android/customization/picker/color/data/repository/ColorPickerRepositoryImpl.kt
+++ b/src/com/android/customization/picker/color/data/repository/ColorPickerRepositoryImpl.kt
@@ -124,14 +124,17 @@
         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,
         )
     }
 
     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 7dab2d8..f581c89 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.ColorSeedOption
 import com.android.customization.picker.color.shared.model.ColorOptionModel
@@ -60,6 +61,7 @@
                                     selectedColorOptionIndex == index
                             val colorOption =
                                 ColorOptionModel(
+                                    key = "${ColorType.WALLPAPER_COLOR}::$index",
                                     colorOption = buildWallpaperOption(index),
                                     isSelected = isSelected,
                                 )
@@ -77,6 +79,7 @@
                                     selectedColorOptionIndex == index
                             val colorOption =
                                 ColorOptionModel(
+                                    key = "${ColorType.BASIC_COLOR}::$index",
                                     colorOption = buildPresetOption(index),
                                     isSelected =
                                         selectedColorOptionType == ColorType.BASIC_COLOR &&
@@ -130,6 +133,7 @@
             wallpaperColorOptions.forEach { option ->
                 add(
                     ColorOptionModel(
+                        key = option.key,
                         colorOption = option.colorOption,
                         isSelected = option.testEquals(colorOptionModel),
                     )
@@ -141,6 +145,7 @@
             basicColorOptions.forEach { option ->
                 add(
                     ColorOptionModel(
+                        key = option.key,
                         colorOption = option.colorOption,
                         isSelected = option.testEquals(colorOptionModel),
                     )
@@ -161,10 +166,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..803663d 100644
--- a/src/com/android/customization/picker/color/ui/viewmodel/ColorPickerViewModel.kt
+++ b/src/com/android/customization/picker/color/ui/viewmodel/ColorPickerViewModel.kt
@@ -25,11 +25,14 @@
 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.combine
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.launch
 
@@ -75,7 +78,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 +91,41 @@
                                         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: Flow<Boolean> =
+                                        interactor.activeColorOption.map {
+                                            it?.colorOption?.isEquivalent(
+                                                colorOptionModel.colorOption
+                                            )
+                                                ?: colorOptionModel.isSelected
+                                        }
+                                    OptionItemViewModel<ColorOptionIconViewModel>(
+                                        key = flowOf(colorOptionModel.key),
+                                        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 +141,38 @@
                                         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: Flow<Boolean> =
+                                        interactor.activeColorOption.map {
+                                            it?.colorOption?.isEquivalent(
+                                                colorOptionModel.colorOption
+                                            )
+                                                ?: colorOptionModel.isSelected
+                                        }
+                                    OptionItemViewModel<ColorOptionIconViewModel>(
+                                        key = flowOf(colorOptionModel.key),
+                                        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 +185,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 +193,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..cbc140e 100644
--- a/src/com/android/customization/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordancePickerViewModel.kt
+++ b/src/com/android/customization/picker/quickaffordance/ui/viewmodel/KeyguardQuickAffordancePickerViewModel.kt
@@ -147,9 +147,9 @@
                         isSelected = isSelected,
                         selectedQuickAffordances =
                             selectedAffordances.map { affordanceModel ->
-                                OptionItemViewModel(
+                                OptionItemViewModel<Icon>(
                                     key = flowOf("${slot.id}::${affordanceModel.id}"),
-                                    icon =
+                                    payload =
                                         Icon.Loaded(
                                             drawable =
                                                 getAffordanceIcon(affordanceModel.iconResourceId),
@@ -194,7 +194,7 @@
             )
 
     /** 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() }
             listOf(
@@ -222,9 +222,9 @@
                     val affordanceIcon = getAffordanceIcon(affordance.iconResourceId)
                     val isSelectedFlow: Flow<Boolean> =
                         selectedAffordanceIds.map { it.contains(affordance.id) }
-                    OptionItemViewModel(
+                    OptionItemViewModel<Icon>(
                         key = selectedSlotId.map { slotId -> "$slotId::${affordance.id}" },
-                        icon = Icon.Loaded(drawable = affordanceIcon, contentDescription = null),
+                        payload = Icon.Loaded(drawable = affordanceIcon, contentDescription = null),
                         text = Text.Loaded(affordance.name),
                         isSelected = isSelectedFlow,
                         onClicked =
@@ -273,15 +273,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),
@@ -363,10 +363,10 @@
         slotId: Flow<String>,
         isSelected: Flow<Boolean>,
         onSelected: Flow<(() -> Unit)?>,
-    ): OptionItemViewModel {
-        return OptionItemViewModel(
+    ): OptionItemViewModel<Icon> {
+        return OptionItemViewModel<Icon>(
             key = slotId.map { "$it::none" },
-            icon = Icon.Resource(res = R.drawable.link_off, contentDescription = null),
+            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..301dbe8 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
@@ -22,6 +22,7 @@
 import com.android.customization.model.grid.data.repository.FakeGridRepository
 import com.android.customization.model.grid.domain.interactor.GridInteractor
 import com.android.customization.model.grid.domain.interactor.GridSnapshotRestorer
+import com.android.wallpaper.picker.common.icon.ui.viewmodel.Icon
 import com.android.wallpaper.picker.option.ui.viewmodel.OptionItemViewModel
 import com.android.wallpaper.testing.FakeSnapshotStore
 import com.android.wallpaper.testing.collectLastValue
@@ -94,13 +95,13 @@
             assertThat(getOnClick(optionItemsValue[1])).isNull()
         }
 
-    private fun TestScope.getSelectedIndex(optionItems: List<OptionItemViewModel>): Int {
+    private fun TestScope.getSelectedIndex(optionItems: List<OptionItemViewModel<Icon>>): Int {
         return optionItems.indexOfFirst { optionItem ->
             collectLastValue(optionItem.isSelected).invoke() == true
         }
     }
 
-    private fun TestScope.getOnClick(optionItem: OptionItemViewModel): (() -> Unit)? {
+    private fun TestScope.getOnClick(optionItem: OptionItemViewModel<Icon>): (() -> 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,
     ) {