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)
+ }
+}