Color Picker Fragment Selection Panel (1/2)

Implemented the bottom color selection panel within the new color fragment using clean architecture

Bug: 262924623
Test: Manual Verified, screen recording - https://drive.google.com/file/d/1vozlF_PoasDEectXdVvyk-6rGH5pvXDV/view?usp=sharing&resourcekey=0-EUDWLiHyC9W7m2jmUAF4RA
Change-Id: Ib899a10baa24a882814396f77886747214591a71
diff --git a/res/drawable/color_option_section_selected.xml b/res/drawable/color_option_selected_no_background.xml
similarity index 100%
rename from res/drawable/color_option_section_selected.xml
rename to res/drawable/color_option_selected_no_background.xml
diff --git a/res/layout/color_option_section.xml b/res/layout/color_option_no_background.xml
similarity index 98%
rename from res/layout/color_option_section.xml
rename to res/layout/color_option_no_background.xml
index d10f8e5..b90b480 100644
--- a/res/layout/color_option_section.xml
+++ b/res/layout/color_option_no_background.xml
@@ -97,6 +97,6 @@
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:adjustViewBounds="true"
-        android:src="@drawable/color_option_section_selected"
+        android:src="@drawable/color_option_selected_no_background"
         android:visibility="gone"/>
 </androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/res/layout/color_option_section_overflow.xml b/res/layout/color_option_overflow_no_background.xml
similarity index 100%
rename from res/layout/color_option_section_overflow.xml
rename to res/layout/color_option_overflow_no_background.xml
diff --git a/res/layout/color_option_with_background.xml b/res/layout/color_option_with_background.xml
new file mode 100644
index 0000000..9d3be58
--- /dev/null
+++ b/res/layout/color_option_with_background.xml
@@ -0,0 +1,78 @@
+<?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 -->
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/icon_container"
+    android:layout_width="@dimen/option_tile_width"
+    android:layout_height="@dimen/option_tile_width"
+    android:importantForAccessibility="yes">
+
+    <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" />
+
+    <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>
diff --git a/res/layout/color_section_view2.xml b/res/layout/color_section_view2.xml
index 1811641..0a3fc7f 100644
--- a/res/layout/color_section_view2.xml
+++ b/res/layout/color_section_view2.xml
@@ -14,7 +14,7 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<com.android.customization.picker.color.ColorSectionView2
+<com.android.customization.picker.color.ui.view.ColorSectionView2
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/color_section_option_container"
     android:layout_width="match_parent"
@@ -39,7 +39,7 @@
     <include
         android:layout_width="0dp"
         android:layout_height="wrap_content"
-        layout="@layout/color_option_section_overflow"
+        layout="@layout/color_option_overflow_no_background"
         android:visibility="invisible"
         android:layout_weight="1"/>
-</com.android.customization.picker.color.ColorSectionView2>
+</com.android.customization.picker.color.ui.view.ColorSectionView2>
diff --git a/res/layout/fragment_color_picker.xml b/res/layout/fragment_color_picker.xml
index d91ac5e..bf0c414 100644
--- a/res/layout/fragment_color_picker.xml
+++ b/res/layout/fragment_color_picker.xml
@@ -17,7 +17,6 @@
 
 <LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:orientation="vertical">
@@ -94,13 +93,12 @@
             android:layout_width="match_parent"
             android:layout_height="wrap_content">
 
-            <androidx.viewpager2.widget.ViewPager2
-                android:id="@+id/color_section_view_pager"
+            <androidx.recyclerview.widget.RecyclerView
+                android:id="@+id/color_options"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
-                android:layout_marginTop="@dimen/color_options_container_top_margin"
-                android:clipChildren="false"
-                android:clipToPadding="false"/>
+                android:clipToPadding="false"
+                android:paddingHorizontal="16dp" />
 
             <!--
             This is just an invisible placeholder put in place so that the parent keeps its height
@@ -112,7 +110,7 @@
             It's critical for any TextViews inside the included layout to have text.
             -->
             <include
-                layout="@layout/color_option"
+                layout="@layout/color_option_with_background"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:visibility="invisible" />
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 83367d4..f24ea28 100755
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -285,6 +285,12 @@
     <!-- Title of a section of color selection option that obtains colors automatically from the
         wallpaper instead of a set color [CHAR LIMIT=15] -->
     <string name="adaptive_color_title">Dynamic</string>
+    <!--
+    Title for a screen where the user can configure the system colors by selecting from a list of
+    color options.
+    [CHAR LIMIT=32].
+    -->
+    <string name="color_picker_title">System Colors</string>
 
     <!--
     Name of the slot on the "start" side of the bottom of the lock screen, where lock screen
diff --git a/src/com/android/customization/model/color/ColorOption.java b/src/com/android/customization/model/color/ColorOption.java
index c8b28c2..26e025d 100644
--- a/src/com/android/customization/model/color/ColorOption.java
+++ b/src/com/android/customization/model/color/ColorOption.java
@@ -182,7 +182,8 @@
                 .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
     }
 
-    protected CharSequence getContentDescription(Context context) {
+    /** */
+    public CharSequence getContentDescription(Context context) {
         if (mContentDescription == null) {
             CharSequence defaultName = context.getString(R.string.default_theme_title);
             if (isDefault()) {
diff --git a/src/com/android/customization/model/color/ColorSectionController.java b/src/com/android/customization/model/color/ColorSectionController.java
index 3b8a927..2a067cb 100644
--- a/src/com/android/customization/model/color/ColorSectionController.java
+++ b/src/com/android/customization/model/color/ColorSectionController.java
@@ -174,13 +174,13 @@
         // TODO(b/202145216): Use just 2 views when tapping either button on top.
         mTabLayout.setViewPager(mColorSectionViewPager);
 
-        mWallpaperColorsViewModel.getHomeWallpaperColors().observe(mLifecycleOwner,
+        mWallpaperColorsViewModel.getHomeWallpaperColorsLiveData().observe(mLifecycleOwner,
                 homeColors -> {
                     mHomeWallpaperColors = homeColors;
                     mHomeWallpaperColorsReady = true;
                     maybeLoadColors();
                 });
-        mWallpaperColorsViewModel.getLockWallpaperColors().observe(mLifecycleOwner,
+        mWallpaperColorsViewModel.getLockWallpaperColorsLiveData().observe(mLifecycleOwner,
                 lockColors -> {
                     mLockWallpaperColors = lockColors;
                     mLockWallpaperColorsReady = true;
diff --git a/src/com/android/customization/model/color/ColorSectionController2.java b/src/com/android/customization/model/color/ColorSectionController2.java
index 257bb94..791a9a0 100644
--- a/src/com/android/customization/model/color/ColorSectionController2.java
+++ b/src/com/android/customization/model/color/ColorSectionController2.java
@@ -36,8 +36,8 @@
 import com.android.customization.model.theme.OverlayManagerCompat;
 import com.android.customization.module.CustomizationInjector;
 import com.android.customization.module.ThemesUserEventLogger;
-import com.android.customization.picker.color.ColorPickerFragment;
-import com.android.customization.picker.color.ColorSectionView2;
+import com.android.customization.picker.color.ui.fragment.ColorPickerFragment;
+import com.android.customization.picker.color.ui.view.ColorSectionView2;
 import com.android.wallpaper.R;
 import com.android.wallpaper.model.CustomizationSectionController;
 import com.android.wallpaper.model.WallpaperColorsViewModel;
@@ -51,6 +51,8 @@
 
 /**
  * Color section view's controller for the logic of color customization.
+ *
+ * TODO (b/262924584): Convert ColorSectionController2 into Kotlin & use new architecture
  */
 public class ColorSectionController2 implements CustomizationSectionController<ColorSectionView2> {
 
@@ -97,13 +99,13 @@
         mColorSectionView = (ColorSectionView2) LayoutInflater.from(context).inflate(
                 R.layout.color_section_view2, /* root= */ null);
 
-        mWallpaperColorsViewModel.getHomeWallpaperColors().observe(mLifecycleOwner,
+        mWallpaperColorsViewModel.getHomeWallpaperColorsLiveData().observe(mLifecycleOwner,
                 homeColors -> {
                     mHomeWallpaperColors = homeColors;
                     mHomeWallpaperColorsReady = true;
                     maybeLoadColors();
                 });
-        mWallpaperColorsViewModel.getLockWallpaperColors().observe(mLifecycleOwner,
+        mWallpaperColorsViewModel.getLockWallpaperColorsLiveData().observe(mLifecycleOwner,
                 lockColors -> {
                     mLockWallpaperColors = lockColors;
                     mLockWallpaperColorsReady = true;
diff --git a/src/com/android/customization/model/color/ColorSeedOption.java b/src/com/android/customization/model/color/ColorSeedOption.java
index 53d3954..ba61ed1 100644
--- a/src/com/android/customization/model/color/ColorSeedOption.java
+++ b/src/com/android/customization/model/color/ColorSeedOption.java
@@ -80,7 +80,7 @@
     }
 
     @Override
-    protected CharSequence getContentDescription(Context context) {
+    public CharSequence getContentDescription(Context context) {
         // Override because we want all options with the same description.
         return context.getString(R.string.wallpaper_color_title);
     }
diff --git a/src/com/android/customization/module/CustomizationInjector.kt b/src/com/android/customization/module/CustomizationInjector.kt
index 6194e11..1f0507e 100644
--- a/src/com/android/customization/module/CustomizationInjector.kt
+++ b/src/com/android/customization/module/CustomizationInjector.kt
@@ -23,8 +23,11 @@
 import com.android.customization.picker.clock.data.repository.ClockRegistryProvider
 import com.android.customization.picker.clock.domain.interactor.ClockPickerInteractor
 import com.android.customization.picker.clock.ui.viewmodel.ClockSectionViewModel
+import com.android.customization.picker.color.domain.interactor.ColorPickerInteractor
+import com.android.customization.picker.color.ui.viewmodel.ColorPickerViewModel
 import com.android.customization.picker.quickaffordance.domain.interactor.KeyguardQuickAffordancePickerInteractor
 import com.android.systemui.shared.clocks.ClockRegistry
+import com.android.wallpaper.model.WallpaperColorsViewModel
 import com.android.wallpaper.module.Injector
 
 interface CustomizationInjector : Injector {
@@ -52,4 +55,14 @@
         context: Context,
         clockRegistry: ClockRegistry
     ): ClockSectionViewModel
+
+    fun getColorPickerInteractor(
+        context: Context,
+        wallpaperColorsViewModel: WallpaperColorsViewModel,
+    ): ColorPickerInteractor
+
+    fun getColorPickerViewModelFactory(
+        context: Context,
+        wallpaperColorsViewModel: WallpaperColorsViewModel,
+    ): ColorPickerViewModel.Factory
 }
diff --git a/src/com/android/customization/module/ThemePickerInjector.kt b/src/com/android/customization/module/ThemePickerInjector.kt
index 9723326..581c233 100644
--- a/src/com/android/customization/module/ThemePickerInjector.kt
+++ b/src/com/android/customization/module/ThemePickerInjector.kt
@@ -29,6 +29,9 @@
 import com.android.customization.picker.clock.data.repository.ClockRegistryProvider
 import com.android.customization.picker.clock.domain.interactor.ClockPickerInteractor
 import com.android.customization.picker.clock.ui.viewmodel.ClockSectionViewModel
+import com.android.customization.picker.color.data.repository.ColorPickerRepositoryImpl
+import com.android.customization.picker.color.domain.interactor.ColorPickerInteractor
+import com.android.customization.picker.color.ui.viewmodel.ColorPickerViewModel
 import com.android.customization.picker.notifications.data.repository.NotificationsRepository
 import com.android.customization.picker.notifications.domain.interactor.NotificationsInteractor
 import com.android.customization.picker.notifications.ui.viewmodel.NotificationSectionViewModel
@@ -40,6 +43,7 @@
 import com.android.systemui.shared.customization.data.content.CustomizationProviderClient
 import com.android.systemui.shared.customization.data.content.CustomizationProviderClientImpl
 import com.android.wallpaper.model.LiveWallpaperInfo
+import com.android.wallpaper.model.WallpaperColorsViewModel
 import com.android.wallpaper.model.WallpaperInfo
 import com.android.wallpaper.module.CustomizationSections
 import com.android.wallpaper.module.FragmentFactory
@@ -72,6 +76,8 @@
     private var clockSectionViewModel: ClockSectionViewModel? = null
     private var notificationsInteractor: NotificationsInteractor? = null
     private var notificationSectionViewModelFactory: NotificationSectionViewModel.Factory? = null
+    private var colorPickerInteractor: ColorPickerInteractor? = null
+    private var colorPickerViewModelFactory: ColorPickerViewModel.Factory? = null
 
     override fun getCustomizationSections(activity: ComponentActivity): CustomizationSections {
         return customizationSections
@@ -259,6 +265,27 @@
                 .also { notificationsInteractor = it }
     }
 
+    override fun getColorPickerInteractor(
+        context: Context,
+        wallpaperColorsViewModel: WallpaperColorsViewModel,
+    ): ColorPickerInteractor {
+        return colorPickerInteractor
+            ?: ColorPickerInteractor(ColorPickerRepositoryImpl(context, wallpaperColorsViewModel))
+                .also { colorPickerInteractor = it }
+    }
+
+    override fun getColorPickerViewModelFactory(
+        context: Context,
+        wallpaperColorsViewModel: WallpaperColorsViewModel,
+    ): ColorPickerViewModel.Factory {
+        return colorPickerViewModelFactory
+            ?: ColorPickerViewModel.Factory(
+                    context,
+                    getColorPickerInteractor(context, wallpaperColorsViewModel),
+                )
+                .also { colorPickerViewModelFactory = it }
+    }
+
     companion object {
         @JvmStatic
         private val KEY_QUICK_AFFORDANCE_SNAPSHOT_RESTORER =
diff --git a/src/com/android/customization/picker/color/data/repository/ColorPickerRepository.kt b/src/com/android/customization/picker/color/data/repository/ColorPickerRepository.kt
new file mode 100644
index 0000000..c375574
--- /dev/null
+++ b/src/com/android/customization/picker/color/data/repository/ColorPickerRepository.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.data.repository
+
+import com.android.customization.picker.color.shared.model.ColorOptionModel
+import com.android.customization.picker.color.shared.model.ColorType
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Abstracts access to application state related to functionality for selecting, picking, or setting
+ * system color.
+ */
+interface ColorPickerRepository {
+    val colorOptions: Flow<Map<ColorType, List<ColorOptionModel>>>
+
+    fun select(colorOptionModel: ColorOptionModel)
+}
diff --git a/src/com/android/customization/picker/color/data/repository/ColorPickerRepositoryImpl.kt b/src/com/android/customization/picker/color/data/repository/ColorPickerRepositoryImpl.kt
new file mode 100644
index 0000000..e163c15
--- /dev/null
+++ b/src/com/android/customization/picker/color/data/repository/ColorPickerRepositoryImpl.kt
@@ -0,0 +1,97 @@
+/*
+ * 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.data.repository
+
+import android.app.WallpaperColors
+import android.content.Context
+import android.util.Log
+import com.android.customization.model.CustomizationManager
+import com.android.customization.model.color.ColorBundle
+import com.android.customization.model.color.ColorCustomizationManager
+import com.android.customization.model.color.ColorOption
+import com.android.customization.model.color.ColorSeedOption
+import com.android.customization.model.theme.OverlayManagerCompat
+import com.android.customization.picker.color.shared.model.ColorOptionModel
+import com.android.customization.picker.color.shared.model.ColorType
+import com.android.wallpaper.model.WallpaperColorsViewModel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+
+// TODO (b/262924623): refactor to remove dependency on ColorCustomizationManager & ColorOption
+class ColorPickerRepositoryImpl(
+    context: Context,
+    wallpaperColorsViewModel: WallpaperColorsViewModel,
+) : ColorPickerRepository {
+
+    private val homeWallpaperColors: StateFlow<WallpaperColors?> =
+        wallpaperColorsViewModel.homeWallpaperColors
+    private val lockWallpaperColors: StateFlow<WallpaperColors?> =
+        wallpaperColorsViewModel.lockWallpaperColors
+    private val colorManager: ColorCustomizationManager =
+        ColorCustomizationManager.getInstance(context, OverlayManagerCompat(context))
+
+    /** List of wallpaper and preset color options on the device, categorized by Color Type */
+    override val colorOptions: Flow<Map<ColorType, List<ColorOptionModel>>> =
+        combine(homeWallpaperColors, lockWallpaperColors) { homeColors, lockColors ->
+            colorManager.setWallpaperColors(homeColors, lockColors)
+            val wallpaperColorOptions: MutableList<ColorOptionModel> = mutableListOf()
+            val presetColorOptions: MutableList<ColorOptionModel> = mutableListOf()
+            colorManager.fetchOptions(
+                object : CustomizationManager.OptionsFetchedListener<ColorOption?> {
+                    override fun onOptionsLoaded(options: MutableList<ColorOption?>?) {
+                        options?.forEach { option ->
+                            when (option) {
+                                is ColorSeedOption -> wallpaperColorOptions.add(option.toModel())
+                                is ColorBundle -> presetColorOptions.add(option.toModel())
+                            }
+                        }
+                    }
+
+                    override fun onError(throwable: Throwable?) {
+                        Log.e("ColorPickerRepository", "Error loading theme bundles", throwable)
+                    }
+                },
+                /* reload= */ false
+            )
+            mapOf(
+                ColorType.WALLPAPER_COLOR to wallpaperColorOptions,
+                ColorType.BASIC_COLOR to presetColorOptions
+            )
+        }
+
+    override fun select(colorOptionModel: ColorOptionModel) {
+        val colorOption: ColorOption = colorOptionModel.colorOption
+        colorManager.apply(
+            colorOption,
+            object : CustomizationManager.Callback {
+                override fun onSuccess() = Unit
+
+                override fun onError(throwable: Throwable?) {
+                    Log.w("ColorPickerRepository", "Apply theme with error", throwable)
+                }
+            }
+        )
+    }
+
+    private fun ColorOption.toModel(): ColorOptionModel {
+        return ColorOptionModel(
+            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
new file mode 100644
index 0000000..6d8b7dc
--- /dev/null
+++ b/src/com/android/customization/picker/color/data/repository/FakeColorPickerRepository.kt
@@ -0,0 +1,161 @@
+/*
+ * 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.data.repository
+
+import android.content.Context
+import android.graphics.Color
+import com.android.customization.model.color.ColorBundle
+import com.android.customization.model.color.ColorSeedOption
+import com.android.customization.picker.color.shared.model.ColorOptionModel
+import com.android.customization.picker.color.shared.model.ColorType
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+class FakeColorPickerRepository(context: Context) : ColorPickerRepository {
+    private val colorSeedOption0: ColorSeedOption =
+        ColorSeedOption.Builder()
+            .setLightColors(
+                intArrayOf(
+                    Color.TRANSPARENT,
+                    Color.TRANSPARENT,
+                    Color.TRANSPARENT,
+                    Color.TRANSPARENT
+                )
+            )
+            .setDarkColors(
+                intArrayOf(
+                    Color.TRANSPARENT,
+                    Color.TRANSPARENT,
+                    Color.TRANSPARENT,
+                    Color.TRANSPARENT
+                )
+            )
+            .setIndex(0)
+            .build()
+    private val colorSeedOption1: ColorSeedOption =
+        ColorSeedOption.Builder()
+            .setLightColors(
+                intArrayOf(
+                    Color.TRANSPARENT,
+                    Color.TRANSPARENT,
+                    Color.TRANSPARENT,
+                    Color.TRANSPARENT
+                )
+            )
+            .setDarkColors(
+                intArrayOf(
+                    Color.TRANSPARENT,
+                    Color.TRANSPARENT,
+                    Color.TRANSPARENT,
+                    Color.TRANSPARENT
+                )
+            )
+            .setIndex(1)
+            .build()
+    private val colorSeedOption2: ColorSeedOption =
+        ColorSeedOption.Builder()
+            .setLightColors(
+                intArrayOf(
+                    Color.TRANSPARENT,
+                    Color.TRANSPARENT,
+                    Color.TRANSPARENT,
+                    Color.TRANSPARENT
+                )
+            )
+            .setDarkColors(
+                intArrayOf(
+                    Color.TRANSPARENT,
+                    Color.TRANSPARENT,
+                    Color.TRANSPARENT,
+                    Color.TRANSPARENT
+                )
+            )
+            .setIndex(2)
+            .build()
+    private val colorSeedOption3: ColorSeedOption =
+        ColorSeedOption.Builder()
+            .setLightColors(
+                intArrayOf(
+                    Color.TRANSPARENT,
+                    Color.TRANSPARENT,
+                    Color.TRANSPARENT,
+                    Color.TRANSPARENT
+                )
+            )
+            .setDarkColors(
+                intArrayOf(
+                    Color.TRANSPARENT,
+                    Color.TRANSPARENT,
+                    Color.TRANSPARENT,
+                    Color.TRANSPARENT
+                )
+            )
+            .setIndex(3)
+            .build()
+    private val colorBundle0: ColorBundle = ColorBundle.Builder().setIndex(0).build(context)
+    private val colorBundle1: ColorBundle = ColorBundle.Builder().setIndex(1).build(context)
+    private val colorBundle2: ColorBundle = ColorBundle.Builder().setIndex(2).build(context)
+    private val colorBundle3: ColorBundle = ColorBundle.Builder().setIndex(3).build(context)
+
+    private val _colorOptions =
+        MutableStateFlow(
+            mapOf(
+                ColorType.WALLPAPER_COLOR to
+                    listOf(
+                        ColorOptionModel(colorOption = colorSeedOption0, isSelected = true),
+                        ColorOptionModel(colorOption = colorSeedOption1, isSelected = false),
+                        ColorOptionModel(colorOption = colorSeedOption2, isSelected = false),
+                        ColorOptionModel(colorOption = colorSeedOption3, isSelected = false)
+                    ),
+                ColorType.BASIC_COLOR to
+                    listOf(
+                        ColorOptionModel(colorOption = colorBundle0, isSelected = false),
+                        ColorOptionModel(colorOption = colorBundle1, isSelected = false),
+                        ColorOptionModel(colorOption = colorBundle2, isSelected = false),
+                        ColorOptionModel(colorOption = colorBundle3, isSelected = false)
+                    )
+            )
+        )
+    override val colorOptions: StateFlow<Map<ColorType, List<ColorOptionModel>>> =
+        _colorOptions.asStateFlow()
+
+    override fun select(colorOptionModel: ColorOptionModel) {
+        val colorOptions = _colorOptions.value
+        colorOptions[ColorType.WALLPAPER_COLOR]?.forEach {
+            it.isSelected = (it.testEquals(colorOptionModel))
+        }
+        colorOptions[ColorType.BASIC_COLOR]?.forEach {
+            it.isSelected = (it.testEquals(colorOptionModel))
+        }
+        _colorOptions.value = colorOptions
+    }
+
+    private fun ColorOptionModel.testEquals(other: Any?): Boolean {
+        if (other == null) {
+            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)
+        } 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
new file mode 100644
index 0000000..ce453c3
--- /dev/null
+++ b/src/com/android/customization/picker/color/domain/interactor/ColorPickerInteractor.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.domain.interactor
+
+import com.android.customization.picker.color.data.repository.ColorPickerRepository
+import com.android.customization.picker.color.shared.model.ColorOptionModel
+
+/** Single entry-point for all application state and business logic related to system color. */
+class ColorPickerInteractor(
+    private val repository: ColorPickerRepository,
+) {
+    /** List of wallpaper and preset color options on the device, categorized by Color Type */
+    val colorOptions = repository.colorOptions
+
+    fun select(colorOptionModel: ColorOptionModel) {
+        repository.select(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
new file mode 100644
index 0000000..69ef62a
--- /dev/null
+++ b/src/com/android/customization/picker/color/shared/model/ColorOptionModel.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.shared.model
+
+import com.android.customization.model.color.ColorOption
+
+/** Models application state for a color option in a picker experience. */
+data class ColorOptionModel(
+    /** Colors for the color option. */
+    val colorOption: ColorOption,
+
+    /** Whether this color option is selected. */
+    var isSelected: Boolean,
+)
diff --git a/src/com/android/customization/picker/color/shared/model/ColorType.kt b/src/com/android/customization/picker/color/shared/model/ColorType.kt
new file mode 100644
index 0000000..631bb3d
--- /dev/null
+++ b/src/com/android/customization/picker/color/shared/model/ColorType.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.shared.model
+
+enum class ColorType {
+    /** Colors generated based on the current wallpaper */
+    WALLPAPER_COLOR,
+
+    /** Preset colors */
+    BASIC_COLOR,
+}
diff --git a/src/com/android/customization/picker/color/ui/adapter/ColorOptionAdapter.kt b/src/com/android/customization/picker/color/ui/adapter/ColorOptionAdapter.kt
new file mode 100644
index 0000000..791811d
--- /dev/null
+++ b/src/com/android/customization/picker/color/ui/adapter/ColorOptionAdapter.kt
@@ -0,0 +1,96 @@
+/*
+ * 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.adapter
+
+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 androidx.recyclerview.widget.RecyclerView
+import com.android.customization.picker.color.ui.viewmodel.ColorOptionViewModel
+import com.android.wallpaper.R
+
+/**
+ * Adapts between color option items and views.
+ *
+ * TODO (b/262924623): Refactor color picker with animated option framework ag/21132368
+ */
+class ColorOptionAdapter : RecyclerView.Adapter<ColorOptionAdapter.ViewHolder>() {
+
+    private val items = mutableListOf<ColorOptionViewModel>()
+
+    fun setItems(items: List<ColorOptionViewModel>) {
+        this.items.clear()
+        this.items.addAll(items)
+        notifyDataSetChanged()
+    }
+
+    class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+        val borderView: View = itemView.requireViewById(R.id.selection_border)
+        val backgroundView: View = itemView.requireViewById(R.id.background)
+        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)
+    }
+
+    override fun getItemCount(): Int {
+        return items.size
+    }
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+        return ViewHolder(
+            LayoutInflater.from(parent.context)
+                .inflate(
+                    R.layout.color_option_with_background,
+                    parent,
+                    false,
+                )
+        )
+    }
+
+    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+        val item = items[position]
+
+        holder.itemView.setOnClickListener(
+            if (item.onClick != null) {
+                View.OnClickListener { item.onClick.invoke() }
+            } else {
+                null
+            }
+        )
+        if (item.isSelected) {
+            holder.borderView.alpha = 1f
+            holder.borderView.scaleX = 1f
+            holder.borderView.scaleY = 1f
+            holder.backgroundView.scaleX = 0.86f
+            holder.backgroundView.scaleY = 0.86f
+        } else {
+            holder.borderView.alpha = 0f
+            holder.backgroundView.scaleX = 1f
+            holder.backgroundView.scaleY = 1f
+        }
+        holder.color0View.drawable.colorFilter = BlendModeColorFilter(item.color0, BlendMode.SRC)
+        holder.color1View.drawable.colorFilter = BlendModeColorFilter(item.color1, BlendMode.SRC)
+        holder.color2View.drawable.colorFilter = BlendModeColorFilter(item.color2, BlendMode.SRC)
+        holder.color3View.drawable.colorFilter = BlendModeColorFilter(item.color3, BlendMode.SRC)
+        holder.itemView.contentDescription = item.contentDescription
+    }
+}
diff --git a/src/com/android/customization/picker/color/ui/adapter/ColorTypeTabAdapter.kt b/src/com/android/customization/picker/color/ui/adapter/ColorTypeTabAdapter.kt
new file mode 100644
index 0000000..8f1340a
--- /dev/null
+++ b/src/com/android/customization/picker/color/ui/adapter/ColorTypeTabAdapter.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.adapter
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import com.android.customization.picker.color.ui.viewmodel.ColorTypeViewModel
+import com.android.wallpaper.R
+
+/** Adapts between color type items and views. */
+class ColorTypeTabAdapter : RecyclerView.Adapter<ColorTypeTabAdapter.ViewHolder>() {
+
+    private val items = mutableListOf<ColorTypeViewModel>()
+
+    fun setItems(items: List<ColorTypeViewModel>) {
+        this.items.clear()
+        this.items.addAll(items)
+        notifyDataSetChanged()
+    }
+
+    override fun getItemCount(): Int {
+        return items.size
+    }
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+        return ViewHolder(
+            LayoutInflater.from(parent.context)
+                .inflate(
+                    R.layout.picker_fragment_tab,
+                    parent,
+                    false,
+                )
+        )
+    }
+
+    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+        val item = items[position]
+        holder.itemView.isSelected = item.isSelected
+        holder.textView.text = item.name
+        holder.textView.setOnClickListener(
+            if (item.onClick != null) {
+                View.OnClickListener { item.onClick.invoke() }
+            } else {
+                null
+            }
+        )
+    }
+
+    class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+        val textView: TextView = itemView.requireViewById(R.id.text)
+    }
+}
diff --git a/src/com/android/customization/picker/color/ui/binder/ColorPickerBinder.kt b/src/com/android/customization/picker/color/ui/binder/ColorPickerBinder.kt
new file mode 100644
index 0000000..887fef0
--- /dev/null
+++ b/src/com/android/customization/picker/color/ui/binder/ColorPickerBinder.kt
@@ -0,0 +1,104 @@
+/*
+ * 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.Rect
+import android.view.View
+import androidx.core.view.ViewCompat
+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.picker.color.ui.adapter.ColorOptionAdapter
+import com.android.customization.picker.color.ui.adapter.ColorTypeTabAdapter
+import com.android.customization.picker.color.ui.viewmodel.ColorPickerViewModel
+import com.android.wallpaper.R
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+
+object ColorPickerBinder {
+
+    /**
+     * Binds view with view-model for a color picker experience. The view should include a Recycler
+     * View for color type tabs with id [R.id.color_type_tabs] and a Recycler View for color options
+     * with id [R.id.color_options]
+     */
+    @JvmStatic
+    fun bind(
+        view: View,
+        viewModel: ColorPickerViewModel,
+        lifecycleOwner: LifecycleOwner,
+    ) {
+        val colorTypeTabView: RecyclerView = view.requireViewById(R.id.color_type_tabs)
+        val colorOptionContainerView: RecyclerView = view.requireViewById(R.id.color_options)
+
+        val colorTypeTabAdapter = ColorTypeTabAdapter()
+        colorTypeTabView.adapter = colorTypeTabAdapter
+        colorTypeTabView.layoutManager =
+            LinearLayoutManager(view.context, RecyclerView.HORIZONTAL, false)
+        colorTypeTabView.addItemDecoration(ItemSpacing())
+        val colorOptionAdapter = ColorOptionAdapter()
+        colorOptionContainerView.adapter = colorOptionAdapter
+        colorOptionContainerView.layoutManager =
+            LinearLayoutManager(view.context, RecyclerView.HORIZONTAL, false)
+        colorOptionContainerView.addItemDecoration(ItemSpacing())
+
+        lifecycleOwner.lifecycleScope.launch {
+            lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+                launch {
+                    viewModel.colorTypes
+                        .map { colorTypeById -> colorTypeById.values }
+                        .collect { colorTypes -> colorTypeTabAdapter.setItems(colorTypes.toList()) }
+                }
+
+                launch {
+                    viewModel.colorOptions.collect { colorOptions ->
+                        colorOptionAdapter.setItems(colorOptions)
+                    }
+                }
+            }
+        }
+    }
+
+    // TODO (b/262924623): Remove function and use common ItemSpacing after ag/20929223 is merged
+    private class ItemSpacing : RecyclerView.ItemDecoration() {
+        override fun getItemOffsets(outRect: Rect, itemPosition: Int, parent: RecyclerView) {
+            val addSpacingToStart = itemPosition > 0
+            val addSpacingToEnd = itemPosition < (parent.adapter?.itemCount ?: 0) - 1
+            val isRtl = parent.layoutManager?.layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL
+            val density = parent.context.resources.displayMetrics.density
+            if (!isRtl) {
+                outRect.left = if (addSpacingToStart) ITEM_SPACING_DP.toPx(density) else 0
+                outRect.right = if (addSpacingToEnd) ITEM_SPACING_DP.toPx(density) else 0
+            } else {
+                outRect.left = if (addSpacingToEnd) ITEM_SPACING_DP.toPx(density) else 0
+                outRect.right = if (addSpacingToStart) ITEM_SPACING_DP.toPx(density) else 0
+            }
+        }
+
+        private fun Int.toPx(density: Float): Int {
+            return (this * density).toInt()
+        }
+
+        companion object {
+            private const val ITEM_SPACING_DP = 8
+        }
+    }
+}
diff --git a/src/com/android/customization/picker/color/ui/fragment/ColorPickerFragment.kt b/src/com/android/customization/picker/color/ui/fragment/ColorPickerFragment.kt
new file mode 100644
index 0000000..9b707a3
--- /dev/null
+++ b/src/com/android/customization/picker/color/ui/fragment/ColorPickerFragment.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.fragment
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.get
+import com.android.customization.module.ThemePickerInjector
+import com.android.customization.picker.color.ui.binder.ColorPickerBinder
+import com.android.wallpaper.R
+import com.android.wallpaper.model.WallpaperColorsViewModel
+import com.android.wallpaper.module.InjectorProvider
+import com.android.wallpaper.picker.AppbarFragment
+
+class ColorPickerFragment : AppbarFragment() {
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View {
+        val view =
+            inflater.inflate(
+                R.layout.fragment_color_picker,
+                container,
+                false,
+            )
+        setUpToolbar(view)
+        val injector = InjectorProvider.getInjector() as ThemePickerInjector
+        val wcViewModel = ViewModelProvider(requireActivity())[WallpaperColorsViewModel::class.java]
+        ColorPickerBinder.bind(
+            view = view,
+            viewModel =
+                ViewModelProvider(
+                        requireActivity(),
+                        injector.getColorPickerViewModelFactory(
+                            context = requireContext(),
+                            wallpaperColorsViewModel = wcViewModel,
+                        ),
+                    )
+                    .get(),
+            lifecycleOwner = this,
+        )
+        return view
+    }
+
+    override fun getDefaultTitle(): CharSequence {
+        return requireContext().getString(R.string.color_picker_title)
+    }
+}
diff --git a/src/com/android/customization/picker/color/ColorSectionView2.kt b/src/com/android/customization/picker/color/ui/view/ColorSectionView2.kt
similarity index 94%
rename from src/com/android/customization/picker/color/ColorSectionView2.kt
rename to src/com/android/customization/picker/color/ui/view/ColorSectionView2.kt
index 3e4e4bc..358514e 100644
--- a/src/com/android/customization/picker/color/ColorSectionView2.kt
+++ b/src/com/android/customization/picker/color/ui/view/ColorSectionView2.kt
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.customization.picker.color
+package com.android.customization.picker.color.ui.view
 
 import android.content.Context
 import android.util.AttributeSet
@@ -49,7 +49,7 @@
                 val item = items[position]
                 val itemView =
                     LayoutInflater.from(context)
-                        .inflate(R.layout.color_option_section, optionContainer, false)
+                        .inflate(R.layout.color_option_no_background, optionContainer, false)
                 item.bindThumbnailTile(itemView.findViewById(R.id.option_tile))
                 if (item.isActive(manager)) {
                     val optionSelectedView = itemView.findViewById<ImageView>(R.id.option_selected)
@@ -62,7 +62,7 @@
         // add overflow option
         val itemView =
             LayoutInflater.from(context)
-                .inflate(R.layout.color_option_section_overflow, optionContainer, false)
+                .inflate(R.layout.color_option_overflow_no_background, optionContainer, false)
         itemView.setOnClickListener { overflowOnClick?.invoke() }
         optionContainer.addView(itemView)
     }
diff --git a/src/com/android/customization/picker/color/ui/viewmodel/ColorOptionViewModel.kt b/src/com/android/customization/picker/color/ui/viewmodel/ColorOptionViewModel.kt
new file mode 100644
index 0000000..0ebc74b
--- /dev/null
+++ b/src/com/android/customization/picker/color/ui/viewmodel/ColorOptionViewModel.kt
@@ -0,0 +1,38 @@
+/*
+ * 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
+
+/** Models UI state for a color options in a picker experience. */
+data class ColorOptionViewModel(
+    /** Colors for the color option. */
+    @ColorInt val color0: Int,
+    @ColorInt val color1: Int,
+    @ColorInt val color2: Int,
+    @ColorInt val color3: Int,
+
+    /** A content description for the color. */
+    val contentDescription: String,
+
+    /** Whether this color is selected. */
+    val isSelected: Boolean,
+
+    /** Notifies that the color has been clicked by the user. */
+    val onClick: (() -> Unit)?,
+)
diff --git a/src/com/android/customization/picker/color/ui/viewmodel/ColorPickerViewModel.kt b/src/com/android/customization/picker/color/ui/viewmodel/ColorPickerViewModel.kt
new file mode 100644
index 0000000..15445fa
--- /dev/null
+++ b/src/com/android/customization/picker/color/ui/viewmodel/ColorPickerViewModel.kt
@@ -0,0 +1,142 @@
+/*
+ * 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.content.Context
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import com.android.customization.model.color.ColorBundle
+import com.android.customization.model.color.ColorSeedOption
+import com.android.customization.picker.color.domain.interactor.ColorPickerInteractor
+import com.android.customization.picker.color.shared.model.ColorOptionModel
+import com.android.customization.picker.color.shared.model.ColorType
+import com.android.wallpaper.R
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.combine
+
+/** Models UI state for a color picker experience. */
+class ColorPickerViewModel
+private constructor(
+    context: Context,
+    private val interactor: ColorPickerInteractor,
+) : ViewModel() {
+
+    private val selectedColorTypeId = MutableStateFlow<ColorType?>(null)
+
+    /** View-models for each color type. */
+    val colorTypes: Flow<Map<ColorType, ColorTypeViewModel>> =
+        combine(
+            interactor.colorOptions,
+            selectedColorTypeId,
+        ) { colorOptions, selectedColorTypeIdOrNull ->
+            colorOptions.keys
+                .mapIndexed { index, colorType ->
+                    val isSelected =
+                        (selectedColorTypeIdOrNull == null && index == 0) ||
+                            selectedColorTypeIdOrNull == colorType
+                    colorType to
+                        ColorTypeViewModel(
+                            name =
+                                when (colorType) {
+                                    ColorType.WALLPAPER_COLOR ->
+                                        context.resources.getString(R.string.wallpaper_color_tab)
+                                    ColorType.BASIC_COLOR ->
+                                        context.resources.getString(R.string.preset_color_tab)
+                                },
+                            isSelected = isSelected,
+                            onClick =
+                                if (isSelected) {
+                                    null
+                                } else {
+                                    { this.selectedColorTypeId.value = colorType }
+                                },
+                        )
+                }
+                .toMap()
+        }
+
+    /** The list of all available color options for the selected Color Type. */
+    val colorOptions: Flow<List<ColorOptionViewModel>> =
+        combine(interactor.colorOptions, selectedColorTypeId) {
+            colorOptions,
+            selectedColorTypeIdOrNull ->
+            val selectedColorType: ColorType =
+                selectedColorTypeIdOrNull ?: ColorType.WALLPAPER_COLOR
+            val selectedColorOptions: List<ColorOptionModel> = colorOptions[selectedColorType]!!
+            selectedColorOptions.map { colorOptionModel ->
+                when (selectedColorType) {
+                    ColorType.BASIC_COLOR -> {
+                        val colorBundle: ColorBundle = colorOptionModel.colorOption as ColorBundle
+                        val primaryColor =
+                            colorBundle.previewInfo.resolvePrimaryColor(context.resources)
+                        val secondaryColor =
+                            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 {
+                                    { interactor.select(colorOptionModel) }
+                                },
+                        )
+                    }
+                    ColorType.WALLPAPER_COLOR -> {
+                        val colorSeedOption: ColorSeedOption =
+                            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 {
+                                    { interactor.select(colorOptionModel) }
+                                },
+                        )
+                    }
+                }
+            }
+        }
+
+    class Factory(
+        private val context: Context,
+        private val interactor: ColorPickerInteractor,
+    ) : ViewModelProvider.Factory {
+        override fun <T : ViewModel> create(modelClass: Class<T>): T {
+            @Suppress("UNCHECKED_CAST")
+            return ColorPickerViewModel(
+                context = context,
+                interactor = interactor,
+            )
+                as T
+        }
+    }
+}
diff --git a/src/com/android/customization/picker/color/ui/viewmodel/ColorTypeViewModel.kt b/src/com/android/customization/picker/color/ui/viewmodel/ColorTypeViewModel.kt
new file mode 100644
index 0000000..7343748
--- /dev/null
+++ b/src/com/android/customization/picker/color/ui/viewmodel/ColorTypeViewModel.kt
@@ -0,0 +1,30 @@
+/*
+ * 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
+
+/** Models UI state for a single color type in a picker experience. */
+data class ColorTypeViewModel(
+    /** User-visible name for the color type. */
+    val name: String,
+
+    /** Whether this is the currently-selected color type in the picker. */
+    val isSelected: Boolean,
+
+    /** Notifies that the color type has been clicked by the user. */
+    val onClick: (() -> Unit)?,
+)
diff --git a/tests/src/com/android/customization/model/picker/color/domain/interactor/ColorPickerInteractorTest.kt b/tests/src/com/android/customization/model/picker/color/domain/interactor/ColorPickerInteractorTest.kt
new file mode 100644
index 0000000..81ef55f
--- /dev/null
+++ b/tests/src/com/android/customization/model/picker/color/domain/interactor/ColorPickerInteractorTest.kt
@@ -0,0 +1,69 @@
+/*
+ * 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.picker.color.domain.interactor
+
+import android.content.Context
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.customization.picker.color.data.repository.FakeColorPickerRepository
+import com.android.customization.picker.color.domain.interactor.ColorPickerInteractor
+import com.android.customization.picker.color.shared.model.ColorType
+import com.android.wallpaper.testing.collectLastValue
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class ColorPickerInteractorTest {
+    private lateinit var underTest: ColorPickerInteractor
+
+    private lateinit var context: Context
+
+    @Before
+    fun setUp() {
+        context = InstrumentationRegistry.getInstrumentation().targetContext
+        underTest =
+            ColorPickerInteractor(
+                repository = FakeColorPickerRepository(context = context),
+            )
+    }
+
+    @Test
+    fun select() = runTest {
+        val colorOptions = collectLastValue(underTest.colorOptions)
+
+        val wallpaperColorOptionModelBefore = colorOptions()?.get(ColorType.WALLPAPER_COLOR)?.get(2)
+        assertThat(wallpaperColorOptionModelBefore?.isSelected).isFalse()
+
+        wallpaperColorOptionModelBefore?.let { underTest.select(colorOptionModel = it) }
+        val wallpaperColorOptionModelAfter = colorOptions()?.get(ColorType.WALLPAPER_COLOR)?.get(2)
+        assertThat(wallpaperColorOptionModelAfter?.isSelected).isTrue()
+
+        val presetColorOptionModelBefore = colorOptions()?.get(ColorType.BASIC_COLOR)?.get(1)
+        assertThat(presetColorOptionModelBefore?.isSelected).isFalse()
+
+        presetColorOptionModelBefore?.let { underTest.select(colorOptionModel = it) }
+        val presetColorOptionModelAfter = colorOptions()?.get(ColorType.BASIC_COLOR)?.get(1)
+        assertThat(presetColorOptionModelAfter?.isSelected).isTrue()
+    }
+}
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
new file mode 100644
index 0000000..19d5dd1
--- /dev/null
+++ b/tests/src/com/android/customization/model/picker/color/ui/viewmodel/ColorPickerViewModelTest.kt
@@ -0,0 +1,180 @@
+/*
+ * 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.picker.color.ui.viewmodel
+
+import android.content.Context
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.customization.picker.color.data.repository.FakeColorPickerRepository
+import com.android.customization.picker.color.domain.interactor.ColorPickerInteractor
+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.ColorPickerViewModel
+import com.android.customization.picker.color.ui.viewmodel.ColorTypeViewModel
+import com.android.wallpaper.testing.collectLastValue
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class ColorPickerViewModelTest {
+    private lateinit var underTest: ColorPickerViewModel
+
+    private lateinit var context: Context
+
+    @Before
+    fun setUp() {
+        context = InstrumentationRegistry.getInstrumentation().targetContext
+
+        underTest =
+            ColorPickerViewModel.Factory(
+                    context = context,
+                    interactor =
+                        ColorPickerInteractor(
+                            repository = FakeColorPickerRepository(context = context),
+                        ),
+                )
+                .create(ColorPickerViewModel::class.java)
+    }
+
+    @Test
+    fun `Select a preset color`() = runTest {
+        val colorTypes = collectLastValue(underTest.colorTypes)
+        val colorOptions = collectLastValue(underTest.colorOptions)
+
+        // Initially, the wallpaper color tab should be selected
+        assertPickerUiState(
+            colorTypes = colorTypes(),
+            colorOptions = colorOptions(),
+            selectedColorTypeText = "Wallpaper colors",
+            selectedColorOptionIndex = 0
+        )
+
+        // Select "Basic colors" tab
+        colorTypes()?.get(ColorType.BASIC_COLOR)?.onClick?.invoke()
+        assertPickerUiState(
+            colorTypes = colorTypes(),
+            colorOptions = colorOptions(),
+            selectedColorTypeText = "Basic colors",
+            selectedColorOptionIndex = -1
+        )
+
+        // Select a color option
+        colorOptions()?.get(2)?.onClick?.invoke()
+
+        // Check original option is no longer selected
+        colorTypes()?.get(ColorType.WALLPAPER_COLOR)?.onClick?.invoke()
+        assertPickerUiState(
+            colorTypes = colorTypes(),
+            colorOptions = colorOptions(),
+            selectedColorTypeText = "Wallpaper colors",
+            selectedColorOptionIndex = -1
+        )
+
+        // Check new option is selected
+        colorTypes()?.get(ColorType.BASIC_COLOR)?.onClick?.invoke()
+        assertPickerUiState(
+            colorTypes = colorTypes(),
+            colorOptions = colorOptions(),
+            selectedColorTypeText = "Basic colors",
+            selectedColorOptionIndex = 2
+        )
+    }
+
+    /**
+     * Asserts the entire picker UI state is what is expected. This includes the color type tabs and
+     * the color options list.
+     *
+     * @param colorTypes The observed color type view-models, keyed by ColorType
+     * @param colorOptions The observed color options
+     * @param selectedColorTypeText The text of the color type that's expected to be selected
+     * @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(
+        colorTypes: Map<ColorType, ColorTypeViewModel>?,
+        colorOptions: List<ColorOptionViewModel>?,
+        selectedColorTypeText: String,
+        selectedColorOptionIndex: Int,
+    ) {
+        assertColorTypeTabUiState(
+            colorTypes = colorTypes,
+            colorTypeId = ColorType.WALLPAPER_COLOR,
+            isSelected = "Wallpaper colors" == selectedColorTypeText,
+        )
+        assertColorTypeTabUiState(
+            colorTypes = colorTypes,
+            colorTypeId = ColorType.BASIC_COLOR,
+            isSelected = "Basic colors" == selectedColorTypeText,
+        )
+
+        var foundSelectedColorOption = false
+        assertThat(colorOptions).isNotNull()
+        if (colorOptions != null) {
+            for (i in colorOptions.indices) {
+                val colorOptionHasSelectedIndex = i == selectedColorOptionIndex
+                assertWithMessage(
+                        "Expected color option with index \"${i}\" to have" +
+                            " isSelected=$colorOptionHasSelectedIndex but it was" +
+                            " ${colorOptions[i].isSelected}, num options: ${colorOptions.size}"
+                    )
+                    .that(colorOptions[i].isSelected)
+                    .isEqualTo(colorOptionHasSelectedIndex)
+                foundSelectedColorOption = foundSelectedColorOption || colorOptionHasSelectedIndex
+            }
+            if (selectedColorOptionIndex == -1) {
+                assertWithMessage(
+                        "Expected no color options to be selected, but a color option is" +
+                            " selected"
+                    )
+                    .that(foundSelectedColorOption)
+                    .isFalse()
+            } else {
+                assertWithMessage(
+                        "Expected a color option to be selected, but no color option is" +
+                            " selected"
+                    )
+                    .that(foundSelectedColorOption)
+                    .isTrue()
+            }
+        }
+    }
+
+    /**
+     * Asserts that a color type tab has the correct UI state.
+     *
+     * @param colorTypes The observed color type view-models, keyed by ColorType enum
+     * @param colorTypeId the ID of the color type to assert
+     * @param isSelected Whether that color type should be selected
+     */
+    private fun assertColorTypeTabUiState(
+        colorTypes: Map<ColorType, ColorTypeViewModel>?,
+        colorTypeId: ColorType,
+        isSelected: Boolean,
+    ) {
+        val viewModel =
+            colorTypes?.get(colorTypeId) ?: error("No color type with ID \"$colorTypeId\"!")
+        assertThat(viewModel.isSelected).isEqualTo(isSelected)
+    }
+}
diff --git a/tests/src/com/android/customization/testing/TestCustomizationInjector.kt b/tests/src/com/android/customization/testing/TestCustomizationInjector.kt
index 735ba8e..fca4904 100644
--- a/tests/src/com/android/customization/testing/TestCustomizationInjector.kt
+++ b/tests/src/com/android/customization/testing/TestCustomizationInjector.kt
@@ -12,6 +12,9 @@
 import com.android.customization.picker.clock.data.repository.FakeClockPickerRepository
 import com.android.customization.picker.clock.domain.interactor.ClockPickerInteractor
 import com.android.customization.picker.clock.ui.viewmodel.ClockSectionViewModel
+import com.android.customization.picker.color.data.repository.ColorPickerRepositoryImpl
+import com.android.customization.picker.color.domain.interactor.ColorPickerInteractor
+import com.android.customization.picker.color.ui.viewmodel.ColorPickerViewModel
 import com.android.customization.picker.quickaffordance.data.repository.KeyguardQuickAffordancePickerRepository
 import com.android.customization.picker.quickaffordance.domain.interactor.KeyguardQuickAffordancePickerInteractor
 import com.android.customization.picker.quickaffordance.domain.interactor.KeyguardQuickAffordanceSnapshotRestorer
@@ -19,6 +22,7 @@
 import com.android.systemui.shared.customization.data.content.CustomizationProviderClient
 import com.android.systemui.shared.customization.data.content.CustomizationProviderClientImpl
 import com.android.wallpaper.config.BaseFlags
+import com.android.wallpaper.model.WallpaperColorsViewModel
 import com.android.wallpaper.module.DrawableLayerResolver
 import com.android.wallpaper.module.PackageStatusNotifier
 import com.android.wallpaper.module.UserEventLogger
@@ -42,6 +46,8 @@
     private var clockRegistryProvider: ClockRegistryProvider? = null
     private var clockPickerInteractor: ClockPickerInteractor? = null
     private var clockSectionViewModel: ClockSectionViewModel? = null
+    private var colorPickerInteractor: ColorPickerInteractor? = null
+    private var colorPickerViewModelFactory: ColorPickerViewModel.Factory? = null
 
     override fun getCustomizationPreferences(context: Context): CustomizationPreferences {
         return customizationPreferences
@@ -152,6 +158,27 @@
             }
     }
 
+    override fun getColorPickerInteractor(
+        context: Context,
+        wallpaperColorsViewModel: WallpaperColorsViewModel,
+    ): ColorPickerInteractor {
+        return colorPickerInteractor
+            ?: ColorPickerInteractor(ColorPickerRepositoryImpl(context, wallpaperColorsViewModel))
+                .also { colorPickerInteractor = it }
+    }
+
+    override fun getColorPickerViewModelFactory(
+        context: Context,
+        wallpaperColorsViewModel: WallpaperColorsViewModel,
+    ): ColorPickerViewModel.Factory {
+        return colorPickerViewModelFactory
+            ?: ColorPickerViewModel.Factory(
+                    context,
+                    getColorPickerInteractor(context, wallpaperColorsViewModel),
+                )
+                .also { colorPickerViewModelFactory = it }
+    }
+
     companion object {
         private const val KEY_QUICK_AFFORDANCE_SNAPSHOT_RESTORER = 1
     }