mirror of
https://github.com/usetrmnl/trmnl-android.git
synced 2026-04-29 13:35:26 -07:00
Merge pull request #228 from usetrmnl/feature/byod-override-display-model
feat: Add BYOD display model override with persistent preferences
This commit is contained in:
@@ -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<Map<String, DeviceModelSelection>>(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<Map<String, DeviceModelSelection>> =
|
||||
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
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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<DeviceModelSelectorScreen.State> {
|
||||
/**
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<DeviceModelSelection?>(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<DeviceModelSelectorScreen.Result>(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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user