diff --git a/app/src/main/java/ink/trmnl/android/data/TrmnlDeviceConfigDataStore.kt b/app/src/main/java/ink/trmnl/android/data/TrmnlDeviceConfigDataStore.kt index 79d5136..62d2779 100644 --- a/app/src/main/java/ink/trmnl/android/data/TrmnlDeviceConfigDataStore.kt +++ b/app/src/main/java/ink/trmnl/android/data/TrmnlDeviceConfigDataStore.kt @@ -14,9 +14,11 @@ import ink.trmnl.android.data.AppConfig.DEFAULT_REFRESH_INTERVAL_SEC import ink.trmnl.android.data.AppConfig.TRMNL_API_SERVER_BASE_URL import ink.trmnl.android.di.AppScope import ink.trmnl.android.di.ApplicationContext +import ink.trmnl.android.model.DeviceModelSelection import ink.trmnl.android.model.TrmnlDeviceConfig import ink.trmnl.android.model.TrmnlDeviceType import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.runBlocking @@ -54,11 +56,22 @@ class TrmnlDeviceConfigDataStore private val CONFIG_JSON_KEY = stringPreferencesKey("config_json") private val DEVICE_MAC_ID_KEY = stringPreferencesKey("device_mac_id") private val IS_MASTER_DEVICE_KEY = stringPreferencesKey("is_master_device") + private val DEVICE_MODEL_PREFERENCES_KEY = stringPreferencesKey("device_model_preferences") } private val deviceTypeAdapter = moshi.adapter(TrmnlDeviceType::class.java) private val deviceConfigAdapter = moshi.adapter(TrmnlDeviceConfig::class.java) + // Moshi adapter for device model preferences map + private val deviceModelPreferencesType = + com.squareup.moshi.Types.newParameterizedType( + Map::class.java, + String::class.java, + DeviceModelSelection::class.java, + ) + private val deviceModelPreferencesAdapter = + moshi.adapter>(deviceModelPreferencesType) + /** * Gets the device type as a Flow */ @@ -106,6 +119,26 @@ class TrmnlDeviceConfigDataStore preferences[DEVICE_MAC_ID_KEY] } + /** + * Gets the device model preferences (map of device type to model selection) as a Flow. + * Returns a map where keys are device type names (e.g., "BYOD") and values are DeviceModelSelection objects. + */ + val deviceModelPreferencesFlow: Flow> = + context.deviceConfigStore.data + .map { preferences -> + val json = preferences[DEVICE_MODEL_PREFERENCES_KEY] + if (json != null) { + try { + deviceModelPreferencesAdapter.fromJson(json) ?: emptyMap() + } catch (e: Exception) { + Timber.tag(TAG).e(e, "Failed to parse device model preferences") + emptyMap() + } + } else { + emptyMap() + } + }.distinctUntilChanged() + /** * Gets the complete device config as a Flow */ @@ -231,6 +264,56 @@ class TrmnlDeviceConfigDataStore } } + /** + * Saves the selected device model for a specific device type. + * + * @param deviceType The device type (e.g., BYOD, BYOS) + * @param modelName The model name (e.g., "amazon_kindle_2024") + * @param modelLabel The model label (e.g., "Amazon Kindle 2024") + */ + suspend fun saveDeviceModelForType( + deviceType: TrmnlDeviceType, + modelName: String, + modelLabel: String, + ) { + try { + context.deviceConfigStore.edit { preferences -> + // Get current map + val currentJson = preferences[DEVICE_MODEL_PREFERENCES_KEY] + val currentMap = + if (currentJson != null) { + try { + deviceModelPreferencesAdapter.fromJson(currentJson)?.toMutableMap() ?: mutableMapOf() + } catch (e: Exception) { + Timber.tag(TAG).e(e, "Failed to parse existing device model preferences") + mutableMapOf() + } + } else { + mutableMapOf() + } + + // Update the map with new value + currentMap[deviceType.name] = DeviceModelSelection(modelName, modelLabel) + + // Save back to preferences + preferences[DEVICE_MODEL_PREFERENCES_KEY] = deviceModelPreferencesAdapter.toJson(currentMap) + + Timber.tag(TAG).d("Saved device model preference: ${deviceType.name} -> $modelName ($modelLabel)") + } + } catch (e: Exception) { + Timber.tag(TAG).e(e, "Failed to save device model preference") + } + } + + /** + * Gets the selected device model selection for a specific device type. + * + * @param deviceType The device type to query + * @return The DeviceModelSelection if set, null otherwise + */ + suspend fun getDeviceModelForType(deviceType: TrmnlDeviceType): DeviceModelSelection? = + deviceModelPreferencesFlow.first()[deviceType.name] + /** * Checks if a token is already set */ diff --git a/app/src/main/java/ink/trmnl/android/model/DeviceModelSelection.kt b/app/src/main/java/ink/trmnl/android/model/DeviceModelSelection.kt new file mode 100644 index 0000000..1349c2f --- /dev/null +++ b/app/src/main/java/ink/trmnl/android/model/DeviceModelSelection.kt @@ -0,0 +1,15 @@ +package ink.trmnl.android.model + +import androidx.annotation.Keep + +/** + * Represents a user's device model selection preference. + * + * @property name Unique identifier for the model (e.g., "amazon_kindle_2024") + * @property label Human-readable label for display (e.g., "Amazon Kindle 2024") + */ +@Keep +data class DeviceModelSelection( + val name: String, + val label: String, +) diff --git a/app/src/main/java/ink/trmnl/android/ui/devicemodel/DeviceModelSelectorScreen.kt b/app/src/main/java/ink/trmnl/android/ui/devicemodel/DeviceModelSelectorScreen.kt index 3516e4a..4106a5f 100644 --- a/app/src/main/java/ink/trmnl/android/ui/devicemodel/DeviceModelSelectorScreen.kt +++ b/app/src/main/java/ink/trmnl/android/ui/devicemodel/DeviceModelSelectorScreen.kt @@ -52,6 +52,7 @@ import ink.trmnl.android.data.AppConfig.TRMNL_API_SERVER_BASE_URL import ink.trmnl.android.data.TrmnlDisplayRepository import ink.trmnl.android.di.AppScope import ink.trmnl.android.model.SupportedDeviceModel +import ink.trmnl.android.model.TrmnlDeviceType import ink.trmnl.android.ui.theme.TrmnlDisplayAppTheme import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize @@ -63,9 +64,13 @@ import kotlinx.parcelize.Parcelize * - View all available device models with their specifications * - Select a device model * - Return the selected model to the previous screen via PopResult + * + * @property deviceType The device type this model selection is for (e.g., BYOD, BYOS) */ @Parcelize -data object DeviceModelSelectorScreen : Screen { +data class DeviceModelSelectorScreen( + val deviceType: TrmnlDeviceType, +) : Screen { /** * Represents the UI state for the [DeviceModelSelectorScreen]. * @@ -112,10 +117,12 @@ data object DeviceModelSelectorScreen : Screen { * using Circuit's PopResult mechanism. * * @property selectedModel The device model that was selected by the user + * @property deviceType The device type this model selection was for */ @Parcelize data class Result( val selectedModel: SupportedDeviceModel, + val deviceType: TrmnlDeviceType, ) : PopResult } @@ -127,6 +134,7 @@ class DeviceModelSelectorPresenter @AssistedInject constructor( @Assisted private val navigator: Navigator, + @Assisted private val screen: DeviceModelSelectorScreen, private val repository: TrmnlDisplayRepository, ) : Presenter { /** @@ -169,7 +177,13 @@ class DeviceModelSelectorPresenter } is DeviceModelSelectorScreen.Event.ModelSelected -> { // Pop with result to return the selected model to the previous screen - navigator.pop(result = DeviceModelSelectorScreen.Result(event.model)) + navigator.pop( + result = + DeviceModelSelectorScreen.Result( + selectedModel = event.model, + deviceType = screen.deviceType, + ), + ) } is DeviceModelSelectorScreen.Event.RetryLoad -> { scope.launch { @@ -212,7 +226,10 @@ class DeviceModelSelectorPresenter @CircuitInject(DeviceModelSelectorScreen::class, AppScope::class) @AssistedFactory fun interface Factory { - fun create(navigator: Navigator): DeviceModelSelectorPresenter + fun create( + navigator: Navigator, + screen: DeviceModelSelectorScreen, + ): DeviceModelSelectorPresenter } } diff --git a/app/src/main/java/ink/trmnl/android/ui/settings/AppSettingsScreen.kt b/app/src/main/java/ink/trmnl/android/ui/settings/AppSettingsScreen.kt index a9d5b72..cc8ba91 100644 --- a/app/src/main/java/ink/trmnl/android/ui/settings/AppSettingsScreen.kt +++ b/app/src/main/java/ink/trmnl/android/ui/settings/AppSettingsScreen.kt @@ -41,6 +41,7 @@ import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.SegmentedButton @@ -76,6 +77,7 @@ import androidx.compose.ui.unit.dp import androidx.work.WorkInfo import coil3.compose.AsyncImage import com.slack.circuit.codegen.annotations.CircuitInject +import com.slack.circuit.foundation.rememberAnsweringNavigator import com.slack.circuit.runtime.CircuitUiEvent import com.slack.circuit.runtime.CircuitUiState import com.slack.circuit.runtime.Navigator @@ -92,9 +94,11 @@ import ink.trmnl.android.data.RepositoryConfigProvider import ink.trmnl.android.data.TrmnlDeviceConfigDataStore import ink.trmnl.android.data.TrmnlDisplayRepository import ink.trmnl.android.di.AppScope +import ink.trmnl.android.model.DeviceModelSelection import ink.trmnl.android.model.TrmnlDeviceConfig import ink.trmnl.android.model.TrmnlDeviceType import ink.trmnl.android.ui.aboutapp.AppInfoScreen +import ink.trmnl.android.ui.devicemodel.DeviceModelSelectorScreen import ink.trmnl.android.ui.display.TrmnlMirrorDisplayScreen import ink.trmnl.android.ui.settings.AppSettingsScreen.ValidationResult import ink.trmnl.android.ui.settings.AppSettingsScreen.ValidationResult.Failure @@ -117,6 +121,7 @@ import ink.trmnl.android.work.TrmnlWorkScheduler import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize +import timber.log.Timber import java.time.Instant import java.time.ZoneId import java.time.format.DateTimeFormatter @@ -146,6 +151,7 @@ data class AppSettingsScreen( val isDeviceSetupLoading: Boolean = false, val deviceSetupMessage: String? = null, val nextRefreshJobInfo: NextImageRefreshDisplayInfo? = null, + val savedDeviceModel: DeviceModelSelection? = null, val eventSink: (Event) -> Unit, ) : CircuitUiState @@ -233,6 +239,11 @@ data class AppSettingsScreen( data class SetupDevice( val deviceMacId: String, ) : Event() + + /** + * Event triggered when the override display model button is clicked. + */ + data object OverrideDisplayModelPressed : Event() } } @@ -272,6 +283,37 @@ class AppSettingsPresenter } } + // Load saved device model preference based on current device type + // Flow automatically updates when preferences change in DataStore + // Use a single collector that filters by current deviceType value instead of restarting on deviceType change + val savedDeviceModel by produceState(initialValue = null) { + deviceConfigStore.deviceModelPreferencesFlow.collect { preferences -> + // Update value based on current deviceType (captured from closure) + val newValue = preferences[deviceType.name] + if (value != newValue) { + value = newValue + } + } + } + + // Create answering navigator for DeviceModelSelectorScreen + val deviceModelNavigator = + rememberAnsweringNavigator(navigator) { result -> + // Save the selected device model using the device type from the result + // This ensures we save to the correct device type even if the user + // switched device types while on the selector screen + scope.launch { + deviceConfigStore.saveDeviceModelForType( + deviceType = result.deviceType, + modelName = result.selectedModel.name, + modelLabel = result.selectedModel.label, + ) + Timber.d( + "Saved device model preference: ${result.deviceType.name} -> ${result.selectedModel.name}", + ) + } + } + // Load saved token if available LaunchedEffect(Unit) { deviceConfigStore.deviceConfigFlow.filterNotNull().collect { @@ -305,6 +347,7 @@ class AppSettingsPresenter isDeviceSetupLoading = isDeviceSetupLoading, deviceSetupMessage = deviceSetupMessage, nextRefreshJobInfo = nextRefreshInfo, + savedDeviceModel = savedDeviceModel, eventSink = { event -> when (event) { is AppSettingsScreen.Event.AccessTokenChanged -> { @@ -469,6 +512,13 @@ class AppSettingsPresenter navigator.goTo(AppInfoScreen) } + AppSettingsScreen.Event.OverrideDisplayModelPressed -> { + // Navigate to DeviceModelSelectorScreen using answering navigator + // Pass the current device type so the screen knows which type this selection is for + Timber.d("Navigating to DeviceModelSelectorScreen for device type: ${deviceType.name}") + deviceModelNavigator.goTo(DeviceModelSelectorScreen(deviceType)) + } + is AppSettingsScreen.Event.SetupDevice -> { isDeviceSetupLoading = true deviceSetupMessage = null @@ -634,10 +684,12 @@ fun AppSettingsContent( serverUrl = state.serverBaseUrl, deviceId = state.deviceMacId, isByodMasterDevice = state.isByodMasterDevice, + savedDeviceModel = state.savedDeviceModel, onTypeSelected = { state.eventSink(AppSettingsScreen.Event.DeviceTypeChanged(it)) }, onServerUrlChanged = { state.eventSink(AppSettingsScreen.Event.ServerUrlChanged(it)) }, onDeviceIdChanged = { state.eventSink(AppSettingsScreen.Event.DeviceMacIdChanged(it)) }, onByodMasterDeviceChanged = { state.eventSink(AppSettingsScreen.Event.ByodMasterDeviceChanged(it)) }, + onOverrideDisplayModelPressed = { state.eventSink(AppSettingsScreen.Event.OverrideDisplayModelPressed) }, isServerUrlError = state.validationResult is InvalidServerUrl, serverUrlError = (state.validationResult as? InvalidServerUrl)?.message, isDeviceMacIdError = state.validationResult is ValidationResult.InvalidDeviceMacId, @@ -850,10 +902,12 @@ private fun DeviceTypeSelectorConfig( serverUrl: String = "", deviceId: String = "", isByodMasterDevice: Boolean = true, + savedDeviceModel: DeviceModelSelection? = null, onTypeSelected: (TrmnlDeviceType) -> Unit, onServerUrlChanged: (String) -> Unit, onDeviceIdChanged: (String) -> Unit, onByodMasterDeviceChanged: (Boolean) -> Unit = {}, + onOverrideDisplayModelPressed: () -> Unit = {}, isServerUrlError: Boolean = false, serverUrlError: String? = null, isDeviceMacIdError: Boolean = false, @@ -968,28 +1022,69 @@ private fun DeviceTypeSelectorConfig( enter = expandVertically() + fadeIn(), exit = shrinkVertically() + fadeOut(), ) { - Row( - modifier = - Modifier - .fillMaxWidth() - .padding(top = 16.dp, bottom = 8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Checkbox( - checked = isByodMasterDevice, - onCheckedChange = { onByodMasterDeviceChanged(it) }, - ) - Spacer(modifier = Modifier.width(8.dp)) - Column(modifier = Modifier.weight(1f)) { - Text( - text = "Act as master device (auto-advance playlist image)", - style = MaterialTheme.typography.bodyMedium, - ) - Text( - text = "Uncheck if this device should mirror another BYOD device that automatically auto-advances playlist image", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, + Column { + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(top = 16.dp, bottom = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Checkbox( + checked = isByodMasterDevice, + onCheckedChange = { onByodMasterDeviceChanged(it) }, ) + Spacer(modifier = Modifier.width(8.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = "Act as master device (auto-advance playlist image)", + style = MaterialTheme.typography.bodyMedium, + ) + Text( + text = + "Uncheck if this device should mirror another BYOD device " + + "that automatically auto-advances playlist image", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + + // Show saved device model if available + if (savedDeviceModel != null) { + Card( + modifier = + Modifier + .fillMaxWidth() + .padding(top = 8.dp, bottom = 8.dp), + colors = + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer, + ), + ) { + Column( + modifier = Modifier.padding(12.dp), + ) { + Text( + text = "Current Display Model", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSecondaryContainer, + ) + Text( + text = savedDeviceModel.label, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSecondaryContainer, + ) + } + } + } + + OutlinedButton( + onClick = onOverrideDisplayModelPressed, + modifier = Modifier.fillMaxWidth(), + ) { + Text("Override Display Model") } } } diff --git a/app/src/test/java/ink/trmnl/android/data/TrmnlDeviceConfigDataStoreTest.kt b/app/src/test/java/ink/trmnl/android/data/TrmnlDeviceConfigDataStoreTest.kt index e404979..672c99f 100644 --- a/app/src/test/java/ink/trmnl/android/data/TrmnlDeviceConfigDataStoreTest.kt +++ b/app/src/test/java/ink/trmnl/android/data/TrmnlDeviceConfigDataStoreTest.kt @@ -4,6 +4,7 @@ import android.content.Context import androidx.test.core.app.ApplicationProvider import com.google.common.truth.Truth.assertThat import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import ink.trmnl.android.model.TrmnlDeviceConfig import ink.trmnl.android.model.TrmnlDeviceType import kotlinx.coroutines.flow.first @@ -25,7 +26,7 @@ class TrmnlDeviceConfigDataStoreTest { @Before fun setUp() { context = ApplicationProvider.getApplicationContext() - moshi = Moshi.Builder().build() + moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() deviceConfigDataStore = TrmnlDeviceConfigDataStore(context, moshi) } @@ -461,4 +462,168 @@ class TrmnlDeviceConfigDataStoreTest { val savedConfig = deviceConfigDataStore.deviceConfigFlow.first() assertThat(savedConfig?.isMasterDevice).isFalse() } + + @Test + fun `deviceModelPreferencesFlow returns empty map when not saved`() = + runTest { + // Act + val preferences = deviceConfigDataStore.deviceModelPreferencesFlow.first() + + // Assert + assertThat(preferences).isEmpty() + } + + @Test + fun `saveDeviceModelForType stores model name for device type`() = + runTest { + // Arrange + val deviceType = TrmnlDeviceType.BYOD + val modelName = "amazon_kindle_2024" + val modelLabel = "Amazon Kindle 2024" + + // Act + deviceConfigDataStore.saveDeviceModelForType(deviceType, modelName, modelLabel) + + // Assert + val preferences = deviceConfigDataStore.deviceModelPreferencesFlow.first() + assertThat(preferences).containsKey("BYOD") + assertThat(preferences["BYOD"]?.name).isEqualTo("amazon_kindle_2024") + assertThat(preferences["BYOD"]?.label).isEqualTo("Amazon Kindle 2024") + } + + @Test + fun `saveDeviceModelForType updates existing model for device type`() = + runTest { + // Arrange - Save initial model + deviceConfigDataStore.saveDeviceModelForType( + TrmnlDeviceType.BYOD, + "amazon_kindle_2024", + "Amazon Kindle 2024", + ) + + // Act - Update to different model + deviceConfigDataStore.saveDeviceModelForType( + TrmnlDeviceType.BYOD, + "boox_tab_ultra_c_pro", + "Boox Tab Ultra C Pro", + ) + + // Assert + val preferences = deviceConfigDataStore.deviceModelPreferencesFlow.first() + assertThat(preferences["BYOD"]?.name).isEqualTo("boox_tab_ultra_c_pro") + assertThat(preferences["BYOD"]?.label).isEqualTo("Boox Tab Ultra C Pro") + assertThat(preferences).hasSize(1) + } + + @Test + fun `saveDeviceModelForType stores multiple device types independently`() = + runTest { + // Act - Save models for different device types + deviceConfigDataStore.saveDeviceModelForType( + TrmnlDeviceType.BYOD, + "amazon_kindle_2024", + "Amazon Kindle 2024", + ) + deviceConfigDataStore.saveDeviceModelForType( + TrmnlDeviceType.BYOS, + "boox_tab_ultra_c_pro", + "Boox Tab Ultra C Pro", + ) + + // Assert + val preferences = deviceConfigDataStore.deviceModelPreferencesFlow.first() + assertThat(preferences["BYOD"]?.name).isEqualTo("amazon_kindle_2024") + assertThat(preferences["BYOD"]?.label).isEqualTo("Amazon Kindle 2024") + assertThat(preferences["BYOS"]?.name).isEqualTo("boox_tab_ultra_c_pro") + assertThat(preferences["BYOS"]?.label).isEqualTo("Boox Tab Ultra C Pro") + assertThat(preferences).hasSize(2) + } + + @Test + fun `getDeviceModelForType returns null when no model saved`() = + runTest { + // Act + val modelSelection = deviceConfigDataStore.getDeviceModelForType(TrmnlDeviceType.BYOD) + + // Assert + assertThat(modelSelection).isNull() + } + + @Test + fun `getDeviceModelForType returns correct model selection when saved`() = + runTest { + // Arrange + val deviceType = TrmnlDeviceType.BYOD + val expectedModelName = "amazon_kindle_2024" + val expectedModelLabel = "Amazon Kindle 2024" + deviceConfigDataStore.saveDeviceModelForType(deviceType, expectedModelName, expectedModelLabel) + + // Act + val modelSelection = deviceConfigDataStore.getDeviceModelForType(deviceType) + + // Assert + assertThat(modelSelection).isNotNull() + assertThat(modelSelection?.name).isEqualTo(expectedModelName) + assertThat(modelSelection?.label).isEqualTo(expectedModelLabel) + } + + @Test + fun `getDeviceModelForType returns null for device type without saved model`() = + runTest { + // Arrange - Save model for BYOD only + deviceConfigDataStore.saveDeviceModelForType( + TrmnlDeviceType.BYOD, + "amazon_kindle_2024", + "Amazon Kindle 2024", + ) + + // Act - Query for BYOS which has no saved model + val modelSelection = deviceConfigDataStore.getDeviceModelForType(TrmnlDeviceType.BYOS) + + // Assert + assertThat(modelSelection).isNull() + } + + @Test + fun `deviceModelPreferencesFlow emits updated map when model saved`() = + runTest { + // Arrange - Start with empty preferences + val initialPreferences = deviceConfigDataStore.deviceModelPreferencesFlow.first() + assertThat(initialPreferences).isEmpty() + + // Act - Save a model + deviceConfigDataStore.saveDeviceModelForType( + TrmnlDeviceType.BYOD, + "amazon_kindle_2024", + "Amazon Kindle 2024", + ) + + // Assert - Flow emits updated map + val updatedPreferences = deviceConfigDataStore.deviceModelPreferencesFlow.first() + assertThat(updatedPreferences["BYOD"]?.name).isEqualTo("amazon_kindle_2024") + assertThat(updatedPreferences["BYOD"]?.label).isEqualTo("Amazon Kindle 2024") + } + + @Test + fun `clearAll removes device model preferences`() = + runTest { + // Arrange - Save some device model preferences + deviceConfigDataStore.saveDeviceModelForType( + TrmnlDeviceType.BYOD, + "amazon_kindle_2024", + "Amazon Kindle 2024", + ) + deviceConfigDataStore.saveDeviceModelForType( + TrmnlDeviceType.BYOS, + "boox_tab_ultra_c_pro", + "Boox Tab Ultra C Pro", + ) + + // Act + deviceConfigDataStore.clearAll() + + // Assert + val preferences = deviceConfigDataStore.deviceModelPreferencesFlow.first() + assertThat(preferences).isEmpty() + } }