App icon view model

Create app icon view model to handle icon shape and themed icon
configuration.

Test: Unit tests
Bug: 402161932
Flag: com.android.systemui.shared.new_customization_picker_ui
Change-Id: Id00282f0ef352d4acaaa4641cfacbc8885ba4703
diff --git a/src/com/android/customization/model/grid/DefaultShapeGridManager.kt b/src/com/android/customization/model/grid/DefaultShapeGridManager.kt
index f8b0c3a..421333b 100644
--- a/src/com/android/customization/model/grid/DefaultShapeGridManager.kt
+++ b/src/com/android/customization/model/grid/DefaultShapeGridManager.kt
@@ -181,6 +181,7 @@
         const val SHAPE_OPTIONS: String = "shape_options"
         const val GRID_OPTIONS: String = "list_options"
         const val SHAPE_GRID: String = "default_grid"
+        const val SET_SHAPE: String = "set_shape"
         const val COL_SHAPE_KEY: String = "shape_key"
         const val COL_GRID_KEY: String = "name"
         const val COL_GRID_NAME: String = "grid_name"
diff --git a/src/com/android/customization/picker/grid/data/repository/ShapeRepository.kt b/src/com/android/customization/picker/grid/data/repository/ShapeRepository.kt
new file mode 100644
index 0000000..bcadcc4
--- /dev/null
+++ b/src/com/android/customization/picker/grid/data/repository/ShapeRepository.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2025 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.grid.data.repository
+
+import android.content.ContentValues
+import android.content.Context
+import com.android.customization.model.grid.DefaultShapeGridManager.Companion.COL_SHAPE_KEY
+import com.android.customization.model.grid.DefaultShapeGridManager.Companion.SET_SHAPE
+import com.android.customization.model.grid.ShapeGridManager
+import com.android.customization.model.grid.ShapeOptionModel
+import com.android.wallpaper.R
+import com.android.wallpaper.picker.di.modules.BackgroundDispatcher
+import com.android.wallpaper.util.PreviewUtils
+import dagger.hilt.android.qualifiers.ApplicationContext
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+@Singleton
+class ShapeRepository
+@Inject
+constructor(
+    @ApplicationContext private val context: Context,
+    private val shapeGridManager: ShapeGridManager,
+    @BackgroundDispatcher private val bgScope: CoroutineScope,
+    @BackgroundDispatcher private val bgDispatcher: CoroutineDispatcher,
+) {
+    private val authorityMetadataKey: String =
+        context.getString(R.string.grid_control_metadata_name)
+    private val previewUtils: PreviewUtils = PreviewUtils(context, authorityMetadataKey)
+
+    private val _shapeOptions = MutableStateFlow<List<ShapeOptionModel>?>(null)
+
+    init {
+        bgScope.launch { _shapeOptions.value = shapeGridManager.getShapeOptions() }
+    }
+
+    val shapeOptions: StateFlow<List<ShapeOptionModel>?> = _shapeOptions.asStateFlow()
+
+    val selectedShapeOption: Flow<ShapeOptionModel?> =
+        shapeOptions.map { shapeOptions -> shapeOptions?.firstOrNull { it.isCurrent } }
+
+    suspend fun applyShape(shapeKey: String) =
+        withContext(bgDispatcher) {
+            context.contentResolver.update(
+                previewUtils.getUri(SET_SHAPE),
+                ContentValues().apply { put(COL_SHAPE_KEY, shapeKey) },
+                null,
+                null,
+            )
+            // After applying, we should query and update shape and grid options again.
+            _shapeOptions.value = shapeGridManager.getShapeOptions()
+        }
+}
diff --git a/src/com/android/customization/picker/grid/domain/interactor/AppIconInteractor.kt b/src/com/android/customization/picker/grid/domain/interactor/AppIconInteractor.kt
new file mode 100644
index 0000000..07ef52a
--- /dev/null
+++ b/src/com/android/customization/picker/grid/domain/interactor/AppIconInteractor.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2025 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.grid.domain.interactor
+
+import com.android.customization.picker.grid.data.repository.ShapeRepository
+import com.android.customization.picker.themedicon.data.repository.ThemedIconRepository
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlinx.coroutines.flow.Flow
+
+@Singleton
+class AppIconInteractor
+@Inject
+constructor(
+    private val shapeRepository: ShapeRepository,
+    private val themedIconRepository: ThemedIconRepository,
+) {
+
+    val shapeOptions = shapeRepository.shapeOptions
+
+    val selectedShapeOption = shapeRepository.selectedShapeOption
+
+    val isThemedIconAvailable: Flow<Boolean> = themedIconRepository.isAvailable
+
+    val isThemedIconEnabled: Flow<Boolean> = themedIconRepository.isActivated
+
+    suspend fun applyThemedIconEnabled(enabled: Boolean) =
+        themedIconRepository.setThemedIconEnabled(enabled)
+
+    suspend fun applyShape(shapeKey: String) = shapeRepository.applyShape(shapeKey)
+}
diff --git a/src/com/android/wallpaper/customization/ui/viewmodel/AppIconPickerViewModel.kt b/src/com/android/wallpaper/customization/ui/viewmodel/AppIconPickerViewModel.kt
new file mode 100644
index 0000000..989879e
--- /dev/null
+++ b/src/com/android/wallpaper/customization/ui/viewmodel/AppIconPickerViewModel.kt
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2025 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.wallpaper.customization.ui.viewmodel
+
+import com.android.customization.model.grid.ShapeOptionModel
+import com.android.customization.picker.grid.domain.interactor.AppIconInteractor
+import com.android.customization.picker.grid.ui.viewmodel.ShapeIconViewModel
+import com.android.wallpaper.picker.common.text.ui.viewmodel.Text
+import com.android.wallpaper.picker.option.ui.viewmodel.OptionItemViewModel2
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import dagger.hilt.android.scopes.ViewModelScoped
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.flow.stateIn
+
+class AppIconPickerViewModel
+@AssistedInject
+constructor(interactor: AppIconInteractor, @Assisted private val viewModelScope: CoroutineScope) {
+    //// Shape
+
+    // The currently-set system shape option
+    val selectedShapeKey =
+        interactor.selectedShapeOption
+            .filterNotNull()
+            .map { it.key }
+            .shareIn(scope = viewModelScope, started = SharingStarted.Lazily, replay = 1)
+    private val overridingShapeKey = MutableStateFlow<String?>(null)
+    // If the overriding key is null, use the currently-set system shape option
+    val previewingShapeKey =
+        combine(overridingShapeKey, selectedShapeKey) { overridingShapeOptionKey, selectedShapeKey
+            ->
+            overridingShapeOptionKey ?: selectedShapeKey
+        }
+
+    val shapeOptions: Flow<List<OptionItemViewModel2<ShapeIconViewModel>>> =
+        interactor.shapeOptions
+            .filterNotNull()
+            .map { shapeOptions -> shapeOptions.map { toShapeOptionItemViewModel(it) } }
+            .shareIn(scope = viewModelScope, started = SharingStarted.Lazily, replay = 1)
+
+    //// Themed icons enabled
+    val isThemedIconAvailable =
+        interactor.isThemedIconAvailable.shareIn(
+            scope = viewModelScope,
+            started = SharingStarted.Lazily,
+            replay = 1,
+        )
+
+    private val overridingIsThemedIconEnabled = MutableStateFlow<Boolean?>(null)
+    val isThemedIconEnabled =
+        interactor.isThemedIconEnabled.shareIn(
+            scope = viewModelScope,
+            started = SharingStarted.Lazily,
+            replay = 1,
+        )
+    val previewingIsThemeIconEnabled =
+        combine(overridingIsThemedIconEnabled, isThemedIconEnabled) {
+            overridingIsThemeIconEnabled,
+            isThemeIconEnabled ->
+            overridingIsThemeIconEnabled ?: isThemeIconEnabled
+        }
+    val toggleThemedIcon: Flow<suspend () -> Unit> =
+        previewingIsThemeIconEnabled.map {
+            {
+                val newValue = !it
+                overridingIsThemedIconEnabled.value = newValue
+            }
+        }
+
+    val onApply: Flow<(suspend () -> Unit)?> =
+        combine(
+            overridingShapeKey,
+            selectedShapeKey,
+            overridingIsThemedIconEnabled,
+            isThemedIconEnabled,
+        ) { overridingShapeKey, selectedShapeKey, overridingIsThemeIconEnabled, isThemeIconEnabled
+            ->
+            if (
+                (overridingShapeKey != null && overridingShapeKey != selectedShapeKey) ||
+                    (overridingIsThemeIconEnabled != null &&
+                        overridingIsThemeIconEnabled != isThemeIconEnabled)
+            ) {
+                {
+                    overridingShapeKey?.let { interactor.applyShape(it) }
+                    overridingIsThemeIconEnabled?.let { interactor.applyThemedIconEnabled(it) }
+                }
+            } else {
+                null
+            }
+        }
+
+    fun resetPreview() {
+        overridingShapeKey.value = null
+        overridingIsThemedIconEnabled.value = null
+    }
+
+    private fun toShapeOptionItemViewModel(
+        option: ShapeOptionModel
+    ): OptionItemViewModel2<ShapeIconViewModel> {
+        val isSelected =
+            previewingShapeKey
+                .map { it == option.key }
+                .stateIn(
+                    scope = viewModelScope,
+                    started = SharingStarted.Lazily,
+                    initialValue = false,
+                )
+
+        return OptionItemViewModel2(
+            key = MutableStateFlow(option.key),
+            payload = ShapeIconViewModel(option.key, option.path),
+            text = Text.Loaded(option.title),
+            isSelected = isSelected,
+            onClicked =
+                isSelected.map {
+                    if (!it) {
+                        { overridingShapeKey.value = option.key }
+                    } else {
+                        null
+                    }
+                },
+        )
+    }
+
+    @ViewModelScoped
+    @AssistedFactory
+    interface Factory {
+        fun create(viewModelScope: CoroutineScope): AppIconPickerViewModel
+    }
+}
diff --git a/src/com/android/wallpaper/customization/ui/viewmodel/ThemePickerCustomizationOptionsViewModel.kt b/src/com/android/wallpaper/customization/ui/viewmodel/ThemePickerCustomizationOptionsViewModel.kt
index 99746e0..95f55c0 100644
--- a/src/com/android/wallpaper/customization/ui/viewmodel/ThemePickerCustomizationOptionsViewModel.kt
+++ b/src/com/android/wallpaper/customization/ui/viewmodel/ThemePickerCustomizationOptionsViewModel.kt
@@ -47,6 +47,7 @@
     colorPickerViewModel2Factory: ColorPickerViewModel2.Factory,
     clockPickerViewModelFactory: ClockPickerViewModel.Factory,
     shapeGridPickerViewModelFactory: ShapeGridPickerViewModel.Factory,
+    appIconPickerViewModelFactory: AppIconPickerViewModel.Factory,
     val colorContrastSectionViewModel: ColorContrastSectionViewModel2,
     val darkModeViewModel: DarkModeViewModel,
     val themedIconViewModel: ThemedIconViewModel,
@@ -65,6 +66,8 @@
     val colorPickerViewModel2 = colorPickerViewModel2Factory.create(viewModelScope = viewModelScope)
     val shapeGridPickerViewModel =
         shapeGridPickerViewModelFactory.create(viewModelScope = viewModelScope)
+    val appIconPickerViewModel =
+        appIconPickerViewModelFactory.create(viewModelScope = viewModelScope)
 
     private var onApplyJob: Job? = null
 
diff --git a/src/com/android/wallpaper/picker/common/preview/ui/binder/ThemePickerWorkspaceCallbackBinder.kt b/src/com/android/wallpaper/picker/common/preview/ui/binder/ThemePickerWorkspaceCallbackBinder.kt
index 1f2a0f3..cd63a75 100644
--- a/src/com/android/wallpaper/picker/common/preview/ui/binder/ThemePickerWorkspaceCallbackBinder.kt
+++ b/src/com/android/wallpaper/picker/common/preview/ui/binder/ThemePickerWorkspaceCallbackBinder.kt
@@ -228,6 +228,15 @@
                             workspaceCallback.sendMessage(MESSAGE_ID_UPDATE_COLOR, Bundle.EMPTY)
                         }
                     }
+
+                    launch {
+                        viewModel.appIconPickerViewModel.previewingIsThemeIconEnabled.collect {
+                            workspaceCallback.sendMessage(
+                                MESSAGE_ID_UPDATE_ICON_THEMED,
+                                Bundle().apply { putBoolean(KEY_BOOLEAN_VALUE, it) },
+                            )
+                        }
+                    }
                 }
         }
     }
@@ -235,10 +244,11 @@
     companion object {
         const val MESSAGE_ID_UPDATE_SHAPE = 2586
         const val MESSAGE_ID_UPDATE_GRID = 7414
-
         const val MESSAGE_ID_UPDATE_COLOR = 856
+        const val MESSAGE_ID_UPDATE_ICON_THEMED = 311
         const val KEY_COLOR_RESOURCE_IDS: String = "color_resource_ids"
         const val KEY_COLOR_VALUES: String = "color_values"
         const val KEY_DARK_MODE: String = "use_dark_mode"
+        const val KEY_BOOLEAN_VALUE: String = "boolean_value"
     }
 }
diff --git a/src/com/android/wallpaper/picker/di/modules/ThemePickerSharedAppModule.kt b/src/com/android/wallpaper/picker/di/modules/ThemePickerSharedAppModule.kt
index 98c881f..dc3d810 100644
--- a/src/com/android/wallpaper/picker/di/modules/ThemePickerSharedAppModule.kt
+++ b/src/com/android/wallpaper/picker/di/modules/ThemePickerSharedAppModule.kt
@@ -30,9 +30,9 @@
 @InstallIn(SingletonComponent::class)
 abstract class ThemePickerSharedAppModule {
 
+    @Binds @Singleton abstract fun bindDarkModeUtil(impl: DarkModeUtilImpl): DarkModeUtil
+
     @Binds
     @Singleton
-    abstract fun bindGridOptionsManager2(impl: DefaultShapeGridManager): ShapeGridManager
-
-    @Binds @Singleton abstract fun bindDarkModeUtil(impl: DarkModeUtilImpl): DarkModeUtil
+    abstract fun bindGridOptionsManager(impl: DefaultShapeGridManager): ShapeGridManager
 }
diff --git a/tests/common/src/com/android/wallpaper/di/modules/ThemePickerSharedAppTestModule.kt b/tests/common/src/com/android/wallpaper/di/modules/ThemePickerSharedAppTestModule.kt
index 4969db4..2422410 100644
--- a/tests/common/src/com/android/wallpaper/di/modules/ThemePickerSharedAppTestModule.kt
+++ b/tests/common/src/com/android/wallpaper/di/modules/ThemePickerSharedAppTestModule.kt
@@ -34,9 +34,9 @@
 )
 abstract class ThemePickerSharedAppTestModule {
 
+    @Binds @Singleton abstract fun bindDarkModeUtil(impl: FakeDarkModeUtil): DarkModeUtil
+
     @Binds
     @Singleton
-    abstract fun bindGridOptionsManager2(impl: FakeShapeGridManager): ShapeGridManager
-
-    @Binds @Singleton abstract fun bindDarkModeUtil(impl: FakeDarkModeUtil): DarkModeUtil
+    abstract fun bindGridOptionsManager(impl: FakeShapeGridManager): ShapeGridManager
 }
diff --git a/tests/robotests/src/com/android/wallpaper/customization/ui/viewmodel/AppIconPickerViewModelTest.kt b/tests/robotests/src/com/android/wallpaper/customization/ui/viewmodel/AppIconPickerViewModelTest.kt
new file mode 100644
index 0000000..dd5d48e
--- /dev/null
+++ b/tests/robotests/src/com/android/wallpaper/customization/ui/viewmodel/AppIconPickerViewModelTest.kt
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2025 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.wallpaper.customization.ui.viewmodel
+
+import android.content.Context
+import androidx.test.filters.SmallTest
+import com.android.customization.model.grid.FakeShapeGridManager
+import com.android.customization.picker.grid.domain.interactor.AppIconInteractor
+import com.android.customization.picker.grid.ui.viewmodel.ShapeIconViewModel
+import com.android.wallpaper.picker.common.text.ui.viewmodel.Text
+import com.android.wallpaper.picker.option.ui.viewmodel.OptionItemViewModel2
+import com.android.wallpaper.testing.collectLastValue
+import com.google.common.truth.Truth.assertThat
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import javax.inject.Inject
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@HiltAndroidTest
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(RobolectricTestRunner::class)
+class AppIconPickerViewModelTest {
+
+    @get:Rule var hiltRule = HiltAndroidRule(this)
+    @Inject lateinit var testScope: TestScope
+    @Inject lateinit var gridOptionsManager: FakeShapeGridManager
+    @Inject lateinit var interactor: AppIconInteractor
+    @Inject @ApplicationContext lateinit var appContext: Context
+
+    private lateinit var underTest: AppIconPickerViewModel
+
+    @Before
+    fun setUp() {
+        hiltRule.inject()
+        underTest = AppIconPickerViewModel(interactor, testScope.backgroundScope)
+    }
+
+    @After
+    fun tearDown() {
+        Dispatchers.resetMain()
+    }
+
+    @Test
+    fun selectedShapeKey() =
+        testScope.runTest {
+            val selectedShapeKey = collectLastValue(underTest.selectedShapeKey)
+
+            assertThat(selectedShapeKey()).isEqualTo("arch")
+        }
+
+    @Test
+    fun shapeOptions() =
+        testScope.runTest {
+            val shapeOptions = collectLastValue(underTest.shapeOptions)
+
+            for (i in 0 until FakeShapeGridManager.DEFAULT_SHAPE_OPTION_LIST.size) {
+                val (expectedKey, expectedPath, expectedTitle) =
+                    with(FakeShapeGridManager.DEFAULT_SHAPE_OPTION_LIST[i]) {
+                        arrayOf(key, path, title)
+                    }
+                assertShapeItem(
+                    optionItem = shapeOptions()?.get(i),
+                    key = FakeShapeGridManager.DEFAULT_SHAPE_OPTION_LIST[i].key,
+                    payload = ShapeIconViewModel(expectedKey, expectedPath),
+                    text = Text.Loaded(expectedTitle),
+                    isTextUserVisible = true,
+                    isSelected = expectedKey == "arch",
+                    isEnabled = true,
+                )
+            }
+        }
+
+    @Test
+    fun shapeOptions_whenClickOnCircleOption() =
+        testScope.runTest {
+            val shapeOptions = collectLastValue(underTest.shapeOptions)
+            val previewingShapeKey = collectLastValue(underTest.previewingShapeKey)
+            val circleOption = shapeOptions()?.firstOrNull { it.key.value == "circle" }
+            val onCircleOptionClicked = circleOption?.onClicked?.let { collectLastValue(it) }
+            checkNotNull(onCircleOptionClicked)
+
+            onCircleOptionClicked()?.invoke()
+
+            assertThat(previewingShapeKey()).isEqualTo("circle")
+            for (i in 0 until FakeShapeGridManager.DEFAULT_SHAPE_OPTION_LIST.size) {
+                val expectedKey = FakeShapeGridManager.DEFAULT_SHAPE_OPTION_LIST[i].key
+                val expectedPath = FakeShapeGridManager.DEFAULT_SHAPE_OPTION_LIST[i].path
+                val expectedTitle = FakeShapeGridManager.DEFAULT_SHAPE_OPTION_LIST[i].title
+                assertShapeItem(
+                    optionItem = shapeOptions()?.get(i),
+                    key = expectedKey,
+                    payload = ShapeIconViewModel(expectedKey, expectedPath),
+                    text = Text.Loaded(expectedTitle),
+                    isTextUserVisible = true,
+                    isSelected = expectedKey == "circle",
+                    isEnabled = true,
+                )
+            }
+        }
+
+    @Test
+    fun onApple_shouldBeNonnull_whenClickOnCircleOption() =
+        testScope.runTest {
+            val shapeOptions = collectLastValue(underTest.shapeOptions)
+            val circleOption = shapeOptions()?.firstOrNull { it.key.value == "circle" }
+            val onCircleOptionClicked = circleOption?.onClicked?.let { collectLastValue(it) }
+            val onApply = collectLastValue(underTest.onApply)
+            checkNotNull(onCircleOptionClicked)
+
+            assertThat(onApply()).isNull()
+
+            onCircleOptionClicked()?.invoke()
+
+            assertThat(onApply()).isNotNull()
+        }
+
+    @Test
+    fun isThemeIconEnabled_shouldBeFalseByDefault() =
+        testScope.runTest {
+            val isThemeIconEnabled = collectLastValue(underTest.isThemedIconEnabled)
+
+            assertThat(isThemeIconEnabled()).isFalse()
+        }
+
+    @Test
+    fun previewingIsThemeIconEnabled_shouldBeFalseByDefault() =
+        testScope.runTest {
+            val previewingIsThemeIconEnabled =
+                collectLastValue(underTest.previewingIsThemeIconEnabled)
+
+            assertThat(previewingIsThemeIconEnabled()).isFalse()
+        }
+
+    @Test
+    fun previewingIsThemeIconEnabled_shouldBeTrue_whenToggle() =
+        testScope.runTest {
+            val toggleThemedIcon = collectLastValue(underTest.toggleThemedIcon)
+            val previewingIsThemeIconEnabled =
+                collectLastValue(underTest.previewingIsThemeIconEnabled)
+
+            assertThat(previewingIsThemeIconEnabled()).isFalse()
+
+            toggleThemedIcon()?.invoke()
+
+            assertThat(previewingIsThemeIconEnabled()).isTrue()
+        }
+
+    @Test
+    fun onApple_shouldBeNonnull_whenToggle() =
+        testScope.runTest {
+            val toggleThemedIcon = collectLastValue(underTest.toggleThemedIcon)
+            val onApply = collectLastValue(underTest.onApply)
+
+            assertThat(onApply()).isNull()
+
+            toggleThemedIcon()?.invoke()
+
+            assertThat(onApply()).isNotNull()
+        }
+
+    private fun TestScope.assertShapeItem(
+        optionItem: OptionItemViewModel2<ShapeIconViewModel>?,
+        key: String,
+        payload: ShapeIconViewModel?,
+        text: Text,
+        isTextUserVisible: Boolean,
+        isSelected: Boolean,
+        isEnabled: Boolean,
+    ) {
+        checkNotNull(optionItem)
+        assertThat(collectLastValue(optionItem.key)()).isEqualTo(key)
+        assertThat(optionItem.text).isEqualTo(text)
+        assertThat(optionItem.payload).isEqualTo(payload)
+        assertThat(optionItem.isTextUserVisible).isEqualTo(isTextUserVisible)
+        assertThat(collectLastValue(optionItem.isSelected)()).isEqualTo(isSelected)
+        assertThat(optionItem.isEnabled).isEqualTo(isEnabled)
+    }
+}