Merge "Import translations. DO NOT MERGE ANYWHERE" into udc-qpr-dev
diff --git a/OWNERS b/OWNERS
new file mode 100644
index 0000000..04cedef
--- /dev/null
+++ b/OWNERS
@@ -0,0 +1,2 @@
+#Refer the WallpaperPicker2/ThemePicker owners here
+include platform/packages/apps/WallpaperPicker2:/OWNERS
diff --git a/res/layout/fragment_grid.xml b/res/layout/fragment_grid.xml
index 4f0aaef..8c97d45 100644
--- a/res/layout/fragment_grid.xml
+++ b/res/layout/fragment_grid.xml
@@ -88,6 +88,24 @@
 
         </FrameLayout>
 
+        <Button
+            android:id="@+id/apply_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_horizontal"
+            android:visibility="gone"
+            android:enabled="false"
+            style="@style/ActionPrimaryButton"
+            android:text="@string/apply_btn" />
+
+        <TextView
+            android:id="@+id/apply_button_note"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_horizontal"
+            android:textSize="@dimen/grid_apply_button_note_text_size"
+            android:visibility="gone"
+            android:text="@string/apply_grid_btn_note" />
     </LinearLayout>
 
 </LinearLayout>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 47171e5..aa6c477 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -61,6 +61,7 @@
     <!-- Dimensions for the grid options -->
     <dimen name="grid_options_container_bottom_margin">@dimen/bottom_actions_height</dimen>
     <dimen name="grid_options_container_horizontal_margin">24dp</dimen>
+    <dimen name="grid_apply_button_note_text_size">11sp</dimen>
 
     <dimen name="card_title_text_size">16sp</dimen>
     <dimen name="card_header_icon_size">32dp</dimen>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index eb68dce..9b55965 100755
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -178,6 +178,15 @@
     <!--Title for a grid option, describing the number of columns and rows, eg: 4x4 [CHAR LIMIT=10] -->
     <string name="grid_title_pattern"><xliff:g name="num_cols" example="1">%1$d</xliff:g>x<xliff:g name="num_rows" example="1">%2$d</xliff:g></string>
 
+    <!-- Label of what will happen when user tap on apply button to change grid. [CHAR LIMIT=50] -->
+    <string name="apply_grid_btn_note">Changing grid size will reload workspace and may take a few seconds.</string>
+
+    <!-- Toast of reloading workspace with new grid. -->
+    <string name="toast_of_changing_grid">Reloading workspace with %1$s grid</string>
+
+    <!-- Toast of failure to reload workspace with new grid. -->
+    <string name="toast_of_failure_to_change_grid">Failed to reload workspace with %1$s grid</string>
+
     <!-- Message shown when a theme has been applied successfully in the system [CHAR LIMIT=NONE] -->
     <string name="applied_theme_msg">Style set successfully</string>
 
diff --git a/src/com/android/customization/model/grid/GridOption.java b/src/com/android/customization/model/grid/GridOption.java
index 17ba115..a5307c9 100644
--- a/src/com/android/customization/model/grid/GridOption.java
+++ b/src/com/android/customization/model/grid/GridOption.java
@@ -48,9 +48,9 @@
         }
     };
 
-    private final String mTitle;
     private final String mIconShapePath;
     private final GridTileDrawable mTileDrawable;
+    public final String title;
     public final String name;
     public final int rows;
     public final int cols;
@@ -60,7 +60,7 @@
 
     public GridOption(String title, String name, boolean isCurrent, int rows, int cols,
             Uri previewImageUri, int previewPagesCount, String iconShapePath) {
-        mTitle = title;
+        this.title = title;
         mIsCurrent = isCurrent;
         mIconShapePath = iconShapePath;
         mTileDrawable = new GridTileDrawable(rows, cols, mIconShapePath);
@@ -76,7 +76,7 @@
     }
 
     protected GridOption(Parcel in) {
-        mTitle = in.readString();
+        title = in.readString();
         mIsCurrent = in.readByte() != 0;
         mIconShapePath = in.readString();
         name = in.readString();
@@ -89,7 +89,7 @@
 
     @Override
     public String getTitle() {
-        return mTitle;
+        return title;
     }
 
     @Override
@@ -143,7 +143,7 @@
 
     @Override
     public void writeToParcel(Parcel parcel, int i) {
-        parcel.writeString(mTitle);
+        parcel.writeString(title);
         parcel.writeByte((byte) (mIsCurrent ? 1 : 0));
         parcel.writeString(mIconShapePath);
         parcel.writeString(name);
@@ -158,7 +158,7 @@
         return String.format(
                 "GridOption{mTitle='%s', mIsCurrent=%s, mTileDrawable=%s, name='%s', rows=%d, "
                         + "cols=%d, previewImageUri=%s, previewPagesCount=%d}\n",
-                mTitle, mIsCurrent, mTileDrawable, name, rows, cols, previewImageUri,
+                title, mIsCurrent, mTileDrawable, name, rows, cols, previewImageUri,
                 previewPagesCount);
     }
 }
diff --git a/src/com/android/customization/model/grid/LauncherGridOptionsProvider.java b/src/com/android/customization/model/grid/LauncherGridOptionsProvider.java
index 8f1860e..e71cca9 100644
--- a/src/com/android/customization/model/grid/LauncherGridOptionsProvider.java
+++ b/src/com/android/customization/model/grid/LauncherGridOptionsProvider.java
@@ -33,6 +33,7 @@
 
 import com.android.customization.model.ResourceConstants;
 import com.android.wallpaper.R;
+import com.android.wallpaper.config.BaseFlags;
 import com.android.wallpaper.util.PreviewUtils;
 
 import java.util.ArrayList;
@@ -57,12 +58,14 @@
 
     private final Context mContext;
     private final PreviewUtils mPreviewUtils;
+    private final boolean mIsGridApplyButtonEnabled;
     private List<GridOption> mOptions;
     private OptionChangeLiveData mLiveData;
 
     public LauncherGridOptionsProvider(Context context, String authorityMetadataKey) {
         mPreviewUtils = new PreviewUtils(context, authorityMetadataKey);
         mContext = context;
+        mIsGridApplyButtonEnabled = BaseFlags.get().isGridApplyButtonEnabled(context);
     }
 
     boolean areGridsAvailable() {
@@ -124,6 +127,7 @@
     int applyGrid(String name) {
         ContentValues values = new ContentValues();
         values.put("name", name);
+        values.put("enable_apply_button", mIsGridApplyButtonEnabled);
         return mContext.getContentResolver().update(mPreviewUtils.getUri(DEFAULT_GRID), values,
                 null, null);
     }
@@ -157,6 +161,14 @@
             mContentObserver = new ContentObserver(handler) {
                 @Override
                 public void onChange(boolean selfChange) {
+                    // If grid apply button is enabled, user has previewed the grid before applying
+                    // the grid change. Thus there is no need to preview again (which will cause a
+                    // blank preview as launcher's is loader thread is busy reloading workspace)
+                    // after applying grid change. Thus we should ignore ContentObserver#onChange
+                    // from launcher
+                    if (BaseFlags.get().isGridApplyButtonEnabled(context.getApplicationContext())) {
+                        return;
+                    }
                     postValue(new Object());
                 }
             };
diff --git a/src/com/android/customization/model/grid/data/repository/GridRepository.kt b/src/com/android/customization/model/grid/data/repository/GridRepository.kt
index e65c18e..4379dad 100644
--- a/src/com/android/customization/model/grid/data/repository/GridRepository.kt
+++ b/src/com/android/customization/model/grid/data/repository/GridRepository.kt
@@ -19,6 +19,7 @@
 
 import androidx.lifecycle.asFlow
 import com.android.customization.model.CustomizationManager
+import com.android.customization.model.CustomizationManager.Callback
 import com.android.customization.model.grid.GridOption
 import com.android.customization.model.grid.GridOptionsManager
 import com.android.customization.model.grid.shared.model.GridOptionItemModel
@@ -39,6 +40,9 @@
     fun getOptionChanges(): Flow<Unit>
     suspend fun getOptions(): GridOptionItemsModel
     fun getSelectedOption(): GridOption?
+    fun applySelectedOption(callback: Callback)
+    fun clearSelectedOption()
+    fun isSelectedOptionApplied(): Boolean
 }
 
 class GridRepositoryImpl(
@@ -57,6 +61,8 @@
 
     private val selectedOption = MutableStateFlow<GridOption?>(null)
 
+    private var appliedOption: GridOption? = null
+
     override fun getSelectedOption() = selectedOption.value
 
     override suspend fun getOptions(): GridOptionItemsModel {
@@ -71,6 +77,9 @@
                             if (!isGridApplyButtonEnabled || selectedOption.value == null) {
                                 selectedOption.value = optionsOrEmpty.find { it.isActive(manager) }
                             }
+                            if (isGridApplyButtonEnabled && appliedOption == null) {
+                                appliedOption = selectedOption.value
+                            }
                             continuation.resume(
                                 GridOptionItemsModel.Loaded(
                                     optionsOrEmpty.map { option -> toModel(option) }
@@ -137,6 +146,35 @@
         }
     }
 
+    override fun applySelectedOption(callback: Callback) {
+        val option = getSelectedOption()
+        manager.apply(
+            option,
+            if (isGridApplyButtonEnabled) {
+                object : Callback {
+                    override fun onSuccess() {
+                        callback.onSuccess()
+                        appliedOption = option
+                    }
+
+                    override fun onError(throwable: Throwable?) {
+                        callback.onError(throwable)
+                    }
+                }
+            } else callback
+        )
+    }
+
+    override fun clearSelectedOption() {
+        if (!isGridApplyButtonEnabled) {
+            return
+        }
+        selectedOption.value?.setIsCurrent(false)
+        selectedOption.value = null
+    }
+
+    override fun isSelectedOptionApplied() = selectedOption.value?.name == appliedOption?.name
+
     private fun GridOption?.key(): String? {
         return if (this != null) "${cols}x${rows}" else null
     }
diff --git a/src/com/android/customization/model/grid/domain/interactor/GridInteractor.kt b/src/com/android/customization/model/grid/domain/interactor/GridInteractor.kt
index 6cb00ff..7abd605 100644
--- a/src/com/android/customization/model/grid/domain/interactor/GridInteractor.kt
+++ b/src/com/android/customization/model/grid/domain/interactor/GridInteractor.kt
@@ -17,6 +17,8 @@
 
 package com.android.customization.model.grid.domain.interactor
 
+import com.android.customization.model.CustomizationManager
+import com.android.customization.model.grid.GridOption
 import com.android.customization.model.grid.data.repository.GridRepository
 import com.android.customization.model.grid.shared.model.GridOptionItemModel
 import com.android.customization.model.grid.shared.model.GridOptionItemsModel
@@ -73,6 +75,16 @@
         }
     }
 
+    fun getSelectOptionNonSuspend(): GridOption? = repository.getSelectedOption()
+
+    fun clearSelectedOption() = repository.clearSelectedOption()
+
+    fun isSelectedOptionApplied() = repository.isSelectedOptionApplied()
+
+    fun applySelectedOption(callback: CustomizationManager.Callback) {
+        repository.applySelectedOption(callback)
+    }
+
     private suspend fun reload(): GridOptionItemsModel {
         val model = repository.getOptions()
         return if (model is GridOptionItemsModel.Loaded) {
@@ -95,6 +107,4 @@
             model
         }
     }
-
-    fun getSelectedOptionName(): String? = repository.getSelectedOption()?.name
 }
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 1c3561d..56fe425 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.Button
 import android.widget.ImageView
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleOwner
@@ -41,6 +42,8 @@
         lifecycleOwner: LifecycleOwner,
         backgroundDispatcher: CoroutineDispatcher,
         onOptionsChanged: () -> Unit,
+        isGridApplyButtonEnabled: Boolean,
+        onOptionApplied: () -> Unit,
     ) {
         val optionView: RecyclerView = view.requireViewById(R.id.options)
         optionView.layoutManager =
@@ -67,6 +70,13 @@
             )
         optionView.adapter = adapter
 
+        if (isGridApplyButtonEnabled) {
+            val applyButton: Button = view.requireViewById(R.id.apply_button)
+            applyButton.visibility = View.VISIBLE
+            view.requireViewById<View>(R.id.apply_button_note).visibility = View.VISIBLE
+            applyButton.setOnClickListener { onOptionApplied() }
+        }
+
         lifecycleOwner.lifecycleScope.launch {
             lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                 launch {
diff --git a/src/com/android/customization/model/grid/ui/fragment/GridFragment2.kt b/src/com/android/customization/model/grid/ui/fragment/GridFragment2.kt
index 14d1344..9e99efe 100644
--- a/src/com/android/customization/model/grid/ui/fragment/GridFragment2.kt
+++ b/src/com/android/customization/model/grid/ui/fragment/GridFragment2.kt
@@ -18,19 +18,24 @@
 package com.android.customization.model.grid.ui.fragment
 
 import android.os.Bundle
+import android.util.Log
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
+import android.widget.Button
+import android.widget.Toast
 import androidx.core.content.ContextCompat
 import androidx.core.view.isVisible
 import androidx.lifecycle.ViewModelProvider
 import androidx.transition.Transition
 import androidx.transition.doOnStart
+import com.android.customization.model.CustomizationManager.Callback
 import com.android.customization.model.grid.domain.interactor.GridInteractor
 import com.android.customization.model.grid.ui.binder.GridScreenBinder
 import com.android.customization.model.grid.ui.viewmodel.GridScreenViewModel
 import com.android.customization.module.ThemePickerInjector
 import com.android.wallpaper.R
+import com.android.wallpaper.config.BaseFlags
 import com.android.wallpaper.module.CurrentWallpaperInfoFactory
 import com.android.wallpaper.module.CustomizationSections
 import com.android.wallpaper.module.InjectorProvider
@@ -43,9 +48,13 @@
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.suspendCancellableCoroutine
 
+private val TAG = GridFragment2::class.java.simpleName
+
 @OptIn(ExperimentalCoroutinesApi::class)
 class GridFragment2 : AppbarFragment() {
 
+    private lateinit var gridInteractor: GridInteractor
+
     override fun onCreateView(
         inflater: LayoutInflater,
         container: ViewGroup?,
@@ -59,6 +68,8 @@
             )
         setUpToolbar(view)
 
+        val isGridApplyButtonEnabled = BaseFlags.get().isGridApplyButtonEnabled(requireContext())
+
         val injector = InjectorProvider.getInjector() as ThemePickerInjector
 
         val wallpaperInfoFactory = injector.getCurrentWallpaperInfoFactory(requireContext())
@@ -71,6 +82,7 @@
             )
 
         val viewModelFactory = injector.getGridScreenViewModelFactory(requireContext())
+        gridInteractor = injector.getGridInteractor(requireContext())
         GridScreenBinder.bind(
             view = view,
             viewModel =
@@ -87,8 +99,42 @@
                         view,
                         wallpaperInfoFactory,
                         injector.getWallpaperInteractor(requireContext()),
-                        injector.getGridInteractor(requireContext())
+                        gridInteractor,
                     )
+                if (isGridApplyButtonEnabled) {
+                    val applyButton: Button = view.requireViewById(R.id.apply_button)
+                    applyButton.isEnabled = !gridInteractor.isSelectedOptionApplied()
+                }
+            },
+            isGridApplyButtonEnabled = isGridApplyButtonEnabled,
+            onOptionApplied = {
+                gridInteractor.applySelectedOption(
+                    object : Callback {
+                        override fun onSuccess() {
+                            Toast.makeText(
+                                    context,
+                                    getString(
+                                        R.string.toast_of_changing_grid,
+                                        gridInteractor.getSelectOptionNonSuspend()?.title
+                                    ),
+                                    Toast.LENGTH_SHORT
+                                )
+                                .show()
+                            val applyButton: Button = view.requireViewById(R.id.apply_button)
+                            applyButton.isEnabled = false
+                        }
+
+                        override fun onError(throwable: Throwable?) {
+                            val errorMsg =
+                                getString(
+                                    R.string.toast_of_failure_to_change_grid,
+                                    gridInteractor.getSelectOptionNonSuspend()?.title
+                                )
+                            Toast.makeText(context, errorMsg, Toast.LENGTH_SHORT).show()
+                            Log.e(TAG, errorMsg, throwable)
+                        }
+                    }
+                )
             }
         )
 
@@ -133,7 +179,7 @@
                         ),
                     initialExtrasProvider = {
                         val bundle = Bundle()
-                        bundle.putString("name", gridInteractor.getSelectedOptionName())
+                        bundle.putString("name", gridInteractor.getSelectOptionNonSuspend()?.name)
                         bundle
                     },
                     wallpaperInfoProvider = {
@@ -154,4 +200,11 @@
             onWallpaperPreviewDirty = { activity?.recreate() },
         )
     }
+
+    override fun onBackPressed(): Boolean {
+        if (BaseFlags.get().isGridApplyButtonEnabled(requireContext())) {
+            gridInteractor.clearSelectedOption()
+        }
+        return super.onBackPressed()
+    }
 }
diff --git a/src/com/android/customization/module/ThemePickerInjector.kt b/src/com/android/customization/module/ThemePickerInjector.kt
index 7d19400..1486884 100644
--- a/src/com/android/customization/module/ThemePickerInjector.kt
+++ b/src/com/android/customization/module/ThemePickerInjector.kt
@@ -81,9 +81,9 @@
 import com.android.wallpaper.module.UserEventLogger
 import com.android.wallpaper.module.WallpaperPicker2Injector
 import com.android.wallpaper.picker.CustomizationPickerActivity
-import com.android.wallpaper.picker.ImagePreviewFragment
-import com.android.wallpaper.picker.LivePreviewFragment
-import com.android.wallpaper.picker.PreviewFragment
+import com.android.wallpaper.picker.ImagePreviewFragment2
+import com.android.wallpaper.picker.LivePreviewFragment2
+import com.android.wallpaper.picker.PreviewFragment2
 import com.android.wallpaper.picker.customization.data.content.WallpaperClientImpl
 import com.android.wallpaper.picker.customization.data.repository.WallpaperRepository
 import com.android.wallpaper.picker.customization.domain.interactor.WallpaperInteractor
@@ -182,16 +182,16 @@
         testingModeEnabled: Boolean,
         isAssetIdPresent: Boolean
     ): Fragment {
-        return if (wallpaperInfo is LiveWallpaperInfo) LivePreviewFragment()
+        return if (wallpaperInfo is LiveWallpaperInfo) LivePreviewFragment2()
         else
-            ImagePreviewFragment().apply {
+            ImagePreviewFragment2().apply {
                 arguments =
                     Bundle().apply {
-                        putParcelable(PreviewFragment.ARG_WALLPAPER, wallpaperInfo)
-                        putInt(PreviewFragment.ARG_PREVIEW_MODE, mode)
-                        putBoolean(PreviewFragment.ARG_VIEW_AS_HOME, viewAsHome)
-                        putBoolean(PreviewFragment.ARG_FULL_SCREEN, viewFullScreen)
-                        putBoolean(PreviewFragment.ARG_TESTING_MODE_ENABLED, testingModeEnabled)
+                        putParcelable(PreviewFragment2.ARG_WALLPAPER, wallpaperInfo)
+                        putInt(PreviewFragment2.ARG_PREVIEW_MODE, mode)
+                        putBoolean(PreviewFragment2.ARG_VIEW_AS_HOME, viewAsHome)
+                        putBoolean(PreviewFragment2.ARG_FULL_SCREEN, viewFullScreen)
+                        putBoolean(PreviewFragment2.ARG_TESTING_MODE_ENABLED, testingModeEnabled)
                     }
             }
     }
@@ -566,9 +566,7 @@
                 .also { gridScreenViewModelFactory = it }
     }
 
-    fun getGridInteractor(
-        context: Context,
-    ): GridInteractor {
+    fun getGridInteractor(context: Context): GridInteractor {
         val appContext = context.applicationContext
         return gridInteractor
             ?: GridInteractor(
diff --git a/src/com/android/customization/picker/clock/ui/binder/ClockCarouselViewBinder.kt b/src/com/android/customization/picker/clock/ui/binder/ClockCarouselViewBinder.kt
index 7880d82..b55464d 100644
--- a/src/com/android/customization/picker/clock/ui/binder/ClockCarouselViewBinder.kt
+++ b/src/com/android/customization/picker/clock/ui/binder/ClockCarouselViewBinder.kt
@@ -16,6 +16,8 @@
 package com.android.customization.picker.clock.ui.binder
 
 import android.content.Context
+import android.content.res.Configuration
+import androidx.core.content.ContextCompat
 import androidx.core.view.isVisible
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleEventObserver
@@ -91,6 +93,15 @@
                 launch {
                     viewModel.seedColor.collect { clockViewFactory.updateColorForAllClocks(it) }
                 }
+
+                launch {
+                    val night =
+                        (context.resources.configuration.uiMode and
+                            Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES)
+                    viewModel.getClockCardColorResId(night).collect {
+                        carouselView.setCarouselCardColor(ContextCompat.getColor(context, it))
+                    }
+                }
             }
         }
 
diff --git a/src/com/android/customization/picker/clock/ui/view/ClockCarouselView.kt b/src/com/android/customization/picker/clock/ui/view/ClockCarouselView.kt
index 2d18ab3..0b62ffa 100644
--- a/src/com/android/customization/picker/clock/ui/view/ClockCarouselView.kt
+++ b/src/com/android/customization/picker/clock/ui/view/ClockCarouselView.kt
@@ -16,6 +16,7 @@
 package com.android.customization.picker.clock.ui.view
 
 import android.content.Context
+import android.content.res.ColorStateList
 import android.content.res.Resources
 import android.util.AttributeSet
 import android.view.LayoutInflater
@@ -297,6 +298,16 @@
         }
     }
 
+    fun setCarouselCardColor(color: Int) {
+        itemViewIds.forEach { id ->
+            val cardViewId = getClockCardViewId(id)
+            cardViewId?.let {
+                val cardView = motionLayout.requireViewById<View>(it)
+                cardView.backgroundTintList = ColorStateList.valueOf(color)
+            }
+        }
+    }
+
     private fun overrideScreenPreviewWidth() {
         val overrideWidth =
             context.resources.getDimensionPixelSize(
diff --git a/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModel.kt b/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModel.kt
index b2dfa61..27c25a2 100644
--- a/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModel.kt
+++ b/src/com/android/customization/picker/clock/ui/viewmodel/ClockCarouselViewModel.kt
@@ -15,11 +15,13 @@
  */
 package com.android.customization.picker.clock.ui.viewmodel
 
+import android.graphics.Color
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.ViewModelProvider
 import androidx.lifecycle.viewModelScope
 import com.android.customization.picker.clock.domain.interactor.ClockPickerInteractor
 import com.android.customization.picker.clock.shared.ClockSize
+import com.android.wallpaper.R
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.Job
@@ -56,6 +58,38 @@
 
     val seedColor: Flow<Int?> = interactor.seedColor
 
+    fun getClockCardColorResId(isDarkThemeEnabled: Boolean): Flow<Int> {
+        return interactor.seedColor.map {
+            it.let { seedColor ->
+                // if seedColor is null, default clock color is selected
+                if (seedColor == null) {
+                    if (isDarkThemeEnabled) {
+                        // In dark mode, use darkest surface container color
+                        R.color.system_surface_container_high
+                    } else {
+                        // In light mode, use lightest surface container color
+                        R.color.system_surface_bright
+                    }
+                } else {
+                    val luminance = Color.luminance(seedColor)
+                    if (isDarkThemeEnabled) {
+                        if (luminance <= CARD_COLOR_CHANGE_LUMINANCE_THRESHOLD_DARK_THEME) {
+                            R.color.system_surface_bright
+                        } else {
+                            R.color.system_surface_container_high
+                        }
+                    } else {
+                        if (luminance <= CARD_COLOR_CHANGE_LUMINANCE_THRESHOLD_LIGHT_THEME) {
+                            R.color.system_surface_bright
+                        } else {
+                            R.color.system_surface_container_highest
+                        }
+                    }
+                }
+            }
+        }
+    }
+
     @OptIn(ExperimentalCoroutinesApi::class)
     val selectedIndex: Flow<Int> =
         allClocks
@@ -96,5 +130,7 @@
 
     companion object {
         const val CLOCKS_EVENT_UPDATE_DELAY_MILLIS: Long = 100
+        const val CARD_COLOR_CHANGE_LUMINANCE_THRESHOLD_LIGHT_THEME: Float = 0.85f
+        const val CARD_COLOR_CHANGE_LUMINANCE_THRESHOLD_DARK_THEME: Float = 0.03f
     }
 }
diff --git a/tests/robotests/src/com/android/customization/model/grid/data/repository/FakeGridRepository.kt b/tests/robotests/src/com/android/customization/model/grid/data/repository/FakeGridRepository.kt
index 0c80e28..317ad3a 100644
--- a/tests/robotests/src/com/android/customization/model/grid/data/repository/FakeGridRepository.kt
+++ b/tests/robotests/src/com/android/customization/model/grid/data/repository/FakeGridRepository.kt
@@ -17,6 +17,7 @@
 
 package com.android.customization.model.grid.data.repository
 
+import com.android.customization.model.CustomizationManager
 import com.android.customization.model.grid.GridOption
 import com.android.customization.model.grid.shared.model.GridOptionItemModel
 import com.android.customization.model.grid.shared.model.GridOptionItemsModel
@@ -54,6 +55,12 @@
 
     override fun getSelectedOption(): GridOption? = null
 
+    override fun applySelectedOption(callback: CustomizationManager.Callback) {}
+
+    override fun clearSelectedOption() {}
+
+    override fun isSelectedOptionApplied() = false
+
     fun setOptions(
         count: Int,
         selectedIndex: Int = 0,