diff --git a/app/src/main/java/eu/darken/capod/common/compose/preview/MockPodDataProvider.kt b/app/src/main/java/eu/darken/capod/common/compose/preview/MockPodDataProvider.kt index 99b7c095..ee8eb402 100644 --- a/app/src/main/java/eu/darken/capod/common/compose/preview/MockPodDataProvider.kt +++ b/app/src/main/java/eu/darken/capod/common/compose/preview/MockPodDataProvider.kt @@ -8,6 +8,7 @@ import eu.darken.capod.monitor.core.PodDevice import eu.darken.capod.monitor.core.cache.CachedDeviceState import eu.darken.capod.pods.core.apple.PodModel import eu.darken.capod.pods.core.apple.aap.AapPodState +import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting import eu.darken.capod.pods.core.apple.ble.BlePodSnapshot import eu.darken.capod.pods.core.apple.ble.DualBlePodSnapshot import eu.darken.capod.pods.core.apple.ble.SingleBlePodSnapshot @@ -50,6 +51,7 @@ object MockPodDataProvider { leftPodIcon = R.drawable.device_airpods_pro2_left, rightPodIcon = R.drawable.device_airpods_pro2_right, _caseIcon = R.drawable.device_airpods_pro2_case, + _address = "AA:BB:CC:DD:EE:FF", ) fun airPodsProMixed(): DualBlePodSnapshot = MockDualBlePodSnapshot( @@ -127,6 +129,7 @@ object MockPodDataProvider { _isHeadsetBeingCharged = true, _isIRKMatch = true, _hasPrivatePayload = true, + _address = "11:22:33:44:55:66", ) fun beatsSolo3(): SingleBlePodSnapshot = MockSingleBlePodSnapshot( @@ -161,6 +164,7 @@ object MockPodDataProvider { leftPodIcon = R.drawable.device_airpods_pro2_left, rightPodIcon = R.drawable.device_airpods_pro2_right, _caseIcon = R.drawable.device_airpods_pro2_case, + _address = "AA:BB:CC:DD:EE:FF", ) // --- PodDevice wrappers --- @@ -186,7 +190,20 @@ object MockPodDataProvider { fun dualPodMonitoredWithAap(): PodDevice = PodDevice( profileId = "preview-dual-aap", ble = airPodsProWithKeys(), - aap = AapPodState(connectionState = AapPodState.ConnectionState.READY), + aap = AapPodState( + connectionState = AapPodState.ConnectionState.READY, + settings = mapOf( + AapSetting.AncMode::class to AapSetting.AncMode( + current = AapSetting.AncMode.Value.ADAPTIVE, + supported = listOf( + AapSetting.AncMode.Value.OFF, + AapSetting.AncMode.Value.ON, + AapSetting.AncMode.Value.TRANSPARENCY, + AapSetting.AncMode.Value.ADAPTIVE, + ), + ), + ), + ), ) fun singlePodMonitored(): PodDevice = PodDevice( @@ -195,6 +212,24 @@ object MockPodDataProvider { aap = null, ) + fun singlePodMonitoredWithAap(): PodDevice = PodDevice( + profileId = "preview-single-aap", + ble = airPodsMaxCharging(), + aap = AapPodState( + connectionState = AapPodState.ConnectionState.READY, + settings = mapOf( + AapSetting.AncMode::class to AapSetting.AncMode( + current = AapSetting.AncMode.Value.ON, + supported = listOf( + AapSetting.AncMode.Value.OFF, + AapSetting.AncMode.Value.ON, + AapSetting.AncMode.Value.TRANSPARENCY, + ), + ), + ), + ), + ) + fun unknownMonitored(): PodDevice = PodDevice( profileId = null, ble = unknownDevice(), @@ -269,6 +304,7 @@ private class MockDualBlePodSnapshot( override val leftPodIcon: Int = R.drawable.device_airpods_gen1_left, override val rightPodIcon: Int = R.drawable.device_airpods_gen1_right, private val _caseIcon: Int = R.drawable.device_airpods_gen1_case, + private val _address: String? = null, rssi: Int = -50, ) : DualBlePodSnapshot, HasCase, HasChargeDetectionDual, HasEarDetectionDual, HasDualMicrophone { override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id() @@ -281,7 +317,7 @@ private class MockDualBlePodSnapshot( override val signalQuality: Float = 0.75f override val iconRes: Int = _model.iconRes override val meta: BlePodSnapshot.Meta = object : BlePodSnapshot.Meta { - override val profile: DeviceProfile = AppleDeviceProfile(label = _label, model = _model) + override val profile: DeviceProfile = AppleDeviceProfile(label = _label, model = _model, address = _address) } override fun getLabel(context: Context): String = _model.label @@ -376,6 +412,7 @@ private class MockSingleAppleBlePodSnapshot( private val _isBeingWorn: Boolean = false, private val _isIRKMatch: Boolean = false, private val _hasPrivatePayload: Boolean = false, + private val _address: String? = null, rssi: Int = -50, ) : SingleBlePodSnapshot, ApplePods, HasChargeDetection, HasEarDetection { override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id() @@ -389,7 +426,7 @@ private class MockSingleAppleBlePodSnapshot( override val iconRes: Int = _model.iconRes override val meta: ApplePods.AppleMeta = ApplePods.AppleMeta( isIRKMatch = _isIRKMatch, - profile = AppleDeviceProfile(label = _label, model = _model), + profile = AppleDeviceProfile(label = _label, model = _model, address = _address), ) override val payload: ProximityPayload = ProximityPayload( public = ProximityPayload.Public(UByteArray(9)), @@ -424,6 +461,7 @@ private class MockDualAppleBlePodSnapshot( override val leftPodIcon: Int = R.drawable.device_airpods_gen1_left, override val rightPodIcon: Int = R.drawable.device_airpods_gen1_right, private val _caseIcon: Int = R.drawable.device_airpods_gen1_case, + private val _address: String? = null, rssi: Int = -50, ) : DualBlePodSnapshot, ApplePods, HasCase, HasChargeDetectionDual, HasEarDetectionDual, HasDualMicrophone { override val identifier: BlePodSnapshot.Id = BlePodSnapshot.Id() @@ -437,7 +475,7 @@ private class MockDualAppleBlePodSnapshot( override val iconRes: Int = _model.iconRes override val meta: ApplePods.AppleMeta = ApplePods.AppleMeta( isIRKMatch = _isIRKMatch, - profile = AppleDeviceProfile(label = _label, model = _model), + profile = AppleDeviceProfile(label = _label, model = _model, address = _address), ) override val payload: ProximityPayload = ProximityPayload( public = ProximityPayload.Public(UByteArray(9)), diff --git a/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsScreen.kt b/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsScreen.kt index 7290b585..7dd4e2b3 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsScreen.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsScreen.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.twotone.ArrowBack @@ -41,6 +40,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.Alignment +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow @@ -48,15 +48,24 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import eu.darken.capod.R +import eu.darken.capod.common.compose.Preview2 +import eu.darken.capod.common.compose.PreviewWrapper +import eu.darken.capod.common.compose.preview.MockPodDataProvider +import eu.darken.capod.common.compose.preview.MOCK_NOW import eu.darken.capod.common.error.ErrorEventHandler import eu.darken.capod.common.navigation.NavigationEventHandler import eu.darken.capod.common.settings.SettingsCategoryHeader import eu.darken.capod.common.settings.SettingsSliderItem import eu.darken.capod.common.settings.SettingsSwitchItem import eu.darken.capod.main.ui.overview.cards.AncModeSelector +import eu.darken.capod.pods.core.apple.aap.AapPodState import eu.darken.capod.monitor.core.PodDevice +import eu.darken.capod.monitor.core.firstSeenFormatted +import eu.darken.capod.monitor.core.lastSeenFormatted import eu.darken.capod.pods.core.apple.aap.protocol.AapDeviceInfo import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting +import eu.darken.capod.pods.core.apple.ble.devices.HasStateDetection +import java.time.Duration @Composable fun DeviceSettingsScreenHost( @@ -113,11 +122,23 @@ fun DeviceSettingsScreen( topBar = { TopAppBar( title = { - Text( - text = stringResource(R.string.device_settings_title), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) + Column { + Text( + text = stringResource(R.string.device_settings_title), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + val profileName = device?.label + if (profileName != null) { + Text( + text = profileName, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } }, navigationIcon = { IconButton(onClick = onNavigateUp) { @@ -134,10 +155,21 @@ fun DeviceSettingsScreen( modifier = Modifier.padding(paddingValues), ) { // Device Info - val deviceInfo = device?.deviceInfo - if (deviceInfo != null) { + if (device != null) { item("device_info") { - DeviceInfoCard(deviceInfo = deviceInfo) + val context = LocalContext.current + val stateDetection = device.ble as? HasStateDetection + val seenFirst = device.seenFirstAt + val seenLast = device.seenLastAt + val firstSeen = if (seenFirst != null && seenLast != null && Duration.between(seenFirst, seenLast).toMinutes() >= 1) { + device.firstSeenFormatted(state.now) + } else null + DeviceInfoCard( + deviceInfo = device.deviceInfo, + connectionStateLabel = stateDetection?.state?.getLabel(context), + lastSeen = device.lastSeenFormatted(state.now), + firstSeen = firstSeen, + ) } } @@ -322,7 +354,12 @@ fun DeviceSettingsScreen( } @Composable -private fun DeviceInfoCard(deviceInfo: AapDeviceInfo) { +private fun DeviceInfoCard( + deviceInfo: AapDeviceInfo?, + connectionStateLabel: String?, + lastSeen: String?, + firstSeen: String?, +) { ElevatedCard( modifier = Modifier .fillMaxWidth() @@ -330,22 +367,42 @@ private fun DeviceInfoCard(deviceInfo: AapDeviceInfo) { elevation = CardDefaults.elevatedCardElevation(defaultElevation = 1.dp), ) { Column(modifier = Modifier.padding(16.dp)) { - if (deviceInfo.name.isNotBlank()) { + if (deviceInfo != null) { + if (deviceInfo.name.isNotBlank()) { + InfoRow( + label = stringResource(R.string.device_settings_info_name_label), + value = deviceInfo.name, + ) + } + if (deviceInfo.serialNumber.isNotBlank()) { + InfoRow( + label = stringResource(R.string.device_settings_info_serial_label), + value = deviceInfo.serialNumber, + ) + } + if (deviceInfo.firmwareVersion.isNotBlank()) { + InfoRow( + label = stringResource(R.string.device_settings_info_firmware_label), + value = deviceInfo.firmwareVersion, + ) + } + } + if (connectionStateLabel != null) { InfoRow( - label = stringResource(R.string.device_settings_info_name_label), - value = deviceInfo.name, + label = stringResource(R.string.device_settings_info_status_label), + value = connectionStateLabel, ) } - if (deviceInfo.serialNumber.isNotBlank()) { + if (lastSeen != null) { InfoRow( - label = stringResource(R.string.device_settings_info_serial_label), - value = deviceInfo.serialNumber, + label = stringResource(R.string.device_settings_info_last_seen_label), + value = lastSeen, ) } - if (deviceInfo.firmwareVersion.isNotBlank()) { + if (firstSeen != null) { InfoRow( - label = stringResource(R.string.device_settings_info_firmware_label), - value = deviceInfo.firmwareVersion, + label = stringResource(R.string.device_settings_info_first_seen_label), + value = firstSeen, ) } } @@ -354,16 +411,15 @@ private fun DeviceInfoCard(deviceInfo: AapDeviceInfo) { @Composable private fun InfoRow(label: String, value: String) { - Row( + Column( modifier = Modifier .fillMaxWidth() - .padding(vertical = 4.dp), + .padding(vertical = 2.dp), ) { Text( text = label, - style = MaterialTheme.typography.bodyMedium, + style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.width(120.dp), ) Text( text = value, @@ -563,3 +619,82 @@ private fun CallControlOption( } } } + +@Preview2 +@Composable +private fun DeviceSettingsFullPreview() = PreviewWrapper { + DeviceSettingsScreen( + state = DeviceSettingsViewModel.State( + device = PodDevice( + profileId = "preview", + label = "My AirPods Pro", + ble = MockPodDataProvider.airPodsProWithKeys(), + aap = AapPodState( + connectionState = AapPodState.ConnectionState.READY, + deviceInfo = AapDeviceInfo( + name = "AirPods Pro", + modelNumber = "A2699", + manufacturer = "Apple Inc.", + serialNumber = "W5J7KV0N04", + firmwareVersion = "7A305", + ), + settings = mapOf( + AapSetting.AncMode::class to AapSetting.AncMode( + current = AapSetting.AncMode.Value.ADAPTIVE, + supported = listOf( + AapSetting.AncMode.Value.OFF, + AapSetting.AncMode.Value.ON, + AapSetting.AncMode.Value.TRANSPARENCY, + AapSetting.AncMode.Value.ADAPTIVE, + ), + ), + AapSetting.ConversationalAwareness::class to AapSetting.ConversationalAwareness(enabled = true), + AapSetting.NcWithOneAirPod::class to AapSetting.NcWithOneAirPod(enabled = true), + AapSetting.PersonalizedVolume::class to AapSetting.PersonalizedVolume(enabled = false), + AapSetting.ToneVolume::class to AapSetting.ToneVolume(level = 75), + AapSetting.AdaptiveAudioNoise::class to AapSetting.AdaptiveAudioNoise(level = 50), + AapSetting.PressSpeed::class to AapSetting.PressSpeed(value = AapSetting.PressSpeed.Value.DEFAULT), + AapSetting.PressHoldDuration::class to AapSetting.PressHoldDuration(value = AapSetting.PressHoldDuration.Value.DEFAULT), + AapSetting.VolumeSwipe::class to AapSetting.VolumeSwipe(enabled = true), + AapSetting.VolumeSwipeLength::class to AapSetting.VolumeSwipeLength(value = AapSetting.VolumeSwipeLength.Value.DEFAULT), + AapSetting.EndCallMuteMic::class to AapSetting.EndCallMuteMic( + muteMic = AapSetting.EndCallMuteMic.MuteMicMode.DOUBLE_PRESS, + endCall = AapSetting.EndCallMuteMic.EndCallMode.SINGLE_PRESS, + ), + ), + ), + ), + now = MOCK_NOW, + ), + onNavigateUp = {}, + ) +} + +@Preview2 +@Composable +private fun DeviceSettingsInfoOnlyPreview() = PreviewWrapper { + DeviceSettingsScreen( + state = DeviceSettingsViewModel.State( + device = PodDevice( + profileId = "preview-info", + label = "My AirPods Pro", + ble = MockPodDataProvider.airPodsProMixed(), + aap = null, + ), + now = MOCK_NOW, + ), + onNavigateUp = {}, + ) +} + +@Preview2 +@Composable +private fun DeviceSettingsCachedOnlyPreview() = PreviewWrapper { + DeviceSettingsScreen( + state = DeviceSettingsViewModel.State( + device = MockPodDataProvider.dualPodCachedOnly(), + now = MOCK_NOW, + ), + onNavigateUp = {}, + ) +} diff --git a/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsViewModel.kt b/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsViewModel.kt index 5ee48ff2..57039300 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsViewModel.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/devicesettings/DeviceSettingsViewModel.kt @@ -11,10 +11,14 @@ import eu.darken.capod.monitor.core.PodDevice import eu.darken.capod.pods.core.apple.aap.AapConnectionManager import eu.darken.capod.pods.core.apple.aap.protocol.AapCommand import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.isActive +import java.time.Instant import javax.inject.Inject @HiltViewModel @@ -33,15 +37,26 @@ class DeviceSettingsViewModel @Inject constructor( targetAddress.value = address } + private val updateTicker = channelFlow { + while (isActive) { + trySend(Unit) + delay(3000) + } + } + val state = targetAddress.flatMapLatest { address -> if (address == null) return@flatMapLatest flowOf(State(device = null)) - deviceMonitor.devices.map { devices -> - State(device = devices.firstOrNull { it.address == address }) + combine(updateTicker, deviceMonitor.devices) { _, devices -> + State( + device = devices.firstOrNull { it.address == address }, + now = Instant.now(), + ) } }.asLiveState() data class State( val device: PodDevice?, + val now: Instant = Instant.now(), ) private fun send(command: AapCommand) { diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewScreen.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewScreen.kt index e37b13ec..4992d24e 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewScreen.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewScreen.kt @@ -130,7 +130,6 @@ fun OverviewScreenHost(vm: OverviewViewModel = hiltViewModel()) { onUpgrade = { vm.onUpgrade() }, onToggleUnmatched = { vm.toggleUnmatchedDevices() }, onAncModeChange = { device, mode -> vm.setAncMode(device, mode) }, - onConversationAwarenessChange = { device, enabled -> vm.setConversationalAwareness(device, enabled) }, onDeviceSettings = { device -> vm.goToDeviceSettings(device) }, ) } @@ -144,7 +143,6 @@ fun OverviewScreen( onUpgrade: () -> Unit, onToggleUnmatched: () -> Unit, onAncModeChange: (PodDevice, AapSetting.AncMode.Value) -> Unit = { _, _ -> }, - onConversationAwarenessChange: (PodDevice, Boolean) -> Unit = { _, _ -> }, onDeviceSettings: (PodDevice) -> Unit = {}, ) { Scaffold( @@ -235,7 +233,6 @@ fun OverviewScreen( showDebug = state.isDebugMode, now = state.now, onAncModeChange = { mode -> onAncModeChange(device, mode) }, - onConversationAwarenessChange = { enabled -> onConversationAwarenessChange(device, enabled) }, onDeviceSettings = { onDeviceSettings(device) }, ) } @@ -278,7 +275,6 @@ fun OverviewScreen( showDebug = state.isDebugMode, now = state.now, onAncModeChange = { mode -> onAncModeChange(device, mode) }, - onConversationAwarenessChange = { enabled -> onConversationAwarenessChange(device, enabled) }, onDeviceSettings = { onDeviceSettings(device) }, ) } @@ -295,14 +291,12 @@ private fun PodDeviceCard( showDebug: Boolean, now: Instant, onAncModeChange: (AapSetting.AncMode.Value) -> Unit, - onConversationAwarenessChange: (Boolean) -> Unit = {}, onDeviceSettings: (() -> Unit)? = null, ) { when { device.hasDualPods -> DualPodsCard( device = device, showDebug = showDebug, now = now, onAncModeChange = onAncModeChange, - onConversationAwarenessChange = onConversationAwarenessChange, onDeviceSettings = onDeviceSettings, ) device.model != PodModel.UNKNOWN -> SinglePodsCard( @@ -310,7 +304,7 @@ private fun PodDeviceCard( onAncModeChange = onAncModeChange, onDeviceSettings = onDeviceSettings, ) - else -> UnknownPodDeviceCard(device = device, showDebug = showDebug, now = now) + else -> UnknownPodDeviceCard(device = device, showDebug = showDebug) } } diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewViewModel.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewViewModel.kt index 37e96a1c..afd33853 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewViewModel.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewViewModel.kt @@ -182,18 +182,6 @@ class OverviewViewModel @Inject constructor( } } - fun setConversationalAwareness(device: PodDevice, enabled: Boolean) { - val address = device.address ?: return - launch { - try { - aapManager.sendCommand(address, AapCommand.SetConversationalAwareness(enabled)) - log(TAG) { "Conversation awareness set to $enabled for $address" } - } catch (e: Exception) { - log(TAG) { "Failed to set conversation awareness: ${e.message}" } - } - } - } - companion object { private const val FREE_DEVICE_LIMIT = 1 private val TAG = logTag("Overview", "VM") diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/DualPodsCard.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/DualPodsCard.kt index f9f7af26..d8b7b27b 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/DualPodsCard.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/DualPodsCard.kt @@ -47,18 +47,14 @@ import eu.darken.capod.common.compose.PreviewWrapper import eu.darken.capod.common.compose.preview.MockPodDataProvider import eu.darken.capod.monitor.core.PodDevice import eu.darken.capod.monitor.core.cachedBatteryFormatted -import eu.darken.capod.monitor.core.firstSeenFormatted import eu.darken.capod.monitor.core.getSignalQuality -import eu.darken.capod.monitor.core.lastSeenFormatted -import eu.darken.capod.pods.core.apple.ble.devices.HasPodStyle -import eu.darken.capod.pods.core.apple.ble.devices.HasStateDetection +import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting import eu.darken.capod.pods.core.apple.ble.devices.DualApplePods import eu.darken.capod.pods.core.apple.ble.devices.DualApplePods.LidState -import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting +import eu.darken.capod.pods.core.apple.ble.devices.HasPodStyle import eu.darken.capod.pods.core.apple.ble.formatBatteryPercent import eu.darken.capod.pods.core.apple.ble.toBatteryFloat import eu.darken.capod.pods.core.apple.ble.toBatteryOrNull -import java.time.Duration import java.time.Instant @Composable @@ -67,7 +63,6 @@ fun DualPodsCard( showDebug: Boolean, now: Instant, onAncModeChange: ((AapSetting.AncMode.Value) -> Unit)? = null, - onConversationAwarenessChange: ((Boolean) -> Unit)? = null, onDeviceSettings: (() -> Unit)? = null, ) { val context = LocalContext.current @@ -80,9 +75,7 @@ fun DualPodsCard( elevation = CardDefaults.elevatedCardElevation(defaultElevation = 2.dp), ) { Column( - modifier = Modifier - .padding(16.dp) - .then(if (!device.isLive) Modifier.alpha(0.7f) else Modifier), + modifier = Modifier.padding(16.dp), ) { // Header Row( @@ -92,10 +85,10 @@ fun DualPodsCard( Image( painter = painterResource(device.iconRes), contentDescription = null, - modifier = Modifier.size(48.dp), + modifier = Modifier.size(44.dp), ) - Spacer(modifier = Modifier.width(12.dp)) + Spacer(modifier = Modifier.width(8.dp)) Column(modifier = Modifier.weight(1f)) { Text( @@ -115,22 +108,27 @@ fun DualPodsCard( append(" [${dualApple.primaryPod.name}]") } } - Text( - text = deviceLabel, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = deviceLabel, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f, fill = false), + ) + Spacer(modifier = Modifier.width(6.dp)) + SignalBadge( + signalText = device.getSignalQuality(context), + bleKeyState = device.bleKeyState, + isAapConnected = device.isAapConnected, + isLive = device.isLive, + ) + } } - SignalBadge( - signalText = device.getSignalQuality(context), - bleKeyState = device.bleKeyState, - isAapConnected = device.isAapConnected, - isLive = device.isLive, - ) - if (device.address != null && onDeviceSettings != null) { IconButton(onClick = onDeviceSettings) { Icon( @@ -142,31 +140,15 @@ fun DualPodsCard( } } - Spacer(modifier = Modifier.height(4.dp)) - - // Timestamps - Text( - text = stringResource(R.string.last_seen_x, device.lastSeenFormatted(now)), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - val seenFirst = device.seenFirstAt - val seenLast = device.seenLastAt - if (seenFirst != null && seenLast != null && Duration.between(seenFirst, seenLast).toMinutes() >= 1) { - Text( - text = stringResource(R.string.first_seen_x, device.firstSeenFormatted(now)), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - Spacer(modifier = Modifier.height(16.dp)) // Circular battery gauges side by side Surface( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .then(if (!device.isLive) Modifier.alpha(0.7f) else Modifier), shape = RoundedCornerShape(12.dp), - tonalElevation = 1.dp, + tonalElevation = 4.dp, ) { Column( modifier = Modifier.padding(16.dp), @@ -201,7 +183,7 @@ fun DualPodsCard( // Case row if (device.hasCase) { HorizontalDivider( - modifier = Modifier.padding(vertical = 12.dp), + modifier = Modifier.padding(vertical = 8.dp), color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f), ) @@ -217,24 +199,14 @@ fun DualPodsCard( text = stringResource(R.string.battery_cached_label, device.cachedBatteryFormatted(now)), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - - // Connection state - val stateDetection = device.ble as? HasStateDetection - if (stateDetection != null) { - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = stateDetection.state.getLabel(context), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.align(Alignment.End), ) } // ANC mode selector val ancMode = device.ancMode if (device.isAapConnected && device.hasAncControl && ancMode != null) { - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(8.dp)) AncModeSelector( currentMode = ancMode.current, supportedModes = ancMode.supported, @@ -243,16 +215,6 @@ fun DualPodsCard( ) } - // Conversation awareness toggle - val convAwareness = device.conversationalAwareness - if (device.isAapConnected && device.model.features.hasConversationAwareness && convAwareness != null) { - Spacer(modifier = Modifier.height(8.dp)) - ConversationAwarenessToggle( - enabled = convAwareness.enabled, - onToggle = { onConversationAwarenessChange?.invoke(it) }, - ) - } - // Debug info if (showDebug) { DebugSection(rawDataHex = device.rawDataHex) @@ -294,12 +256,12 @@ private fun PodGauge( // Ring with icon inside Box( contentAlignment = Alignment.Center, - modifier = Modifier.size(80.dp), + modifier = Modifier.size(68.dp), ) { // Track ring CircularProgressIndicator( progress = { 1f }, - modifier = Modifier.size(80.dp), + modifier = Modifier.size(68.dp), color = MaterialTheme.colorScheme.surfaceVariant, strokeWidth = 6.dp, trackColor = MaterialTheme.colorScheme.surfaceVariant, @@ -310,7 +272,7 @@ private fun PodGauge( if (clamped >= 0f) { CircularProgressIndicator( progress = { animatedProgress }, - modifier = Modifier.size(80.dp), + modifier = Modifier.size(68.dp), color = ringColor, strokeWidth = 6.dp, trackColor = MaterialTheme.colorScheme.surfaceVariant, @@ -417,36 +379,31 @@ private fun CaseRow( @Preview2 @Composable -private fun DualPodsCardFullChargePreview() = PreviewWrapper { - DualPodsCard(device = MockPodDataProvider.dualPodMonitored(), showDebug = false, now = Instant.now()) +private fun DualPodsCardFullPreview() = PreviewWrapper { + DualPodsCard( + device = MockPodDataProvider.dualPodMonitoredWithAap(), + showDebug = false, + now = Instant.now(), + onDeviceSettings = {}, + ) } @Preview2 @Composable -private fun DualPodsCardMixedBatteryPreview() = PreviewWrapper { - DualPodsCard(device = MockPodDataProvider.dualPodMonitoredMixed(), showDebug = false, now = Instant.now()) +private fun DualPodsCardMinimalPreview() = PreviewWrapper { + DualPodsCard( + device = MockPodDataProvider.dualPodMonitored(), + showDebug = false, + now = Instant.now(), + ) } @Preview2 @Composable -private fun DualPodsCardDebugPreview() = PreviewWrapper { - DualPodsCard(device = MockPodDataProvider.dualPodMonitoredMixed(), showDebug = true, now = Instant.now()) -} - -@Preview2 -@Composable -private fun DualPodsCardWithKeysPreview() = PreviewWrapper { - DualPodsCard(device = MockPodDataProvider.dualPodMonitoredWithKeys(), showDebug = false, now = Instant.now()) -} - -@Preview2 -@Composable -private fun DualPodsCardWithAapPreview() = PreviewWrapper { - DualPodsCard(device = MockPodDataProvider.dualPodMonitoredWithAap(), showDebug = false, now = Instant.now()) -} - -@Preview2 -@Composable -private fun DualPodsCardCachedOnlyPreview() = PreviewWrapper { - DualPodsCard(device = MockPodDataProvider.dualPodCachedOnly(), showDebug = false, now = Instant.now()) +private fun DualPodsCardCachedPreview() = PreviewWrapper { + DualPodsCard( + device = MockPodDataProvider.dualPodCachedOnly(), + showDebug = false, + now = Instant.now(), + ) } diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/SinglePodsCard.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/SinglePodsCard.kt index 91851f86..205df211 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/SinglePodsCard.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/SinglePodsCard.kt @@ -47,12 +47,9 @@ import eu.darken.capod.common.compose.PreviewWrapper import eu.darken.capod.common.compose.preview.MockPodDataProvider import eu.darken.capod.monitor.core.PodDevice import eu.darken.capod.monitor.core.cachedBatteryFormatted -import eu.darken.capod.monitor.core.firstSeenFormatted import eu.darken.capod.monitor.core.getSignalQuality -import eu.darken.capod.monitor.core.lastSeenFormatted import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting import eu.darken.capod.pods.core.apple.ble.formatBatteryPercent -import java.time.Duration import java.time.Instant @OptIn(ExperimentalLayoutApi::class) @@ -88,9 +85,7 @@ fun SinglePodsCard( elevation = CardDefaults.elevatedCardElevation(defaultElevation = 2.dp), ) { Column( - modifier = Modifier - .padding(16.dp) - .then(if (!device.isLive) Modifier.alpha(0.7f) else Modifier), + modifier = Modifier.padding(16.dp), ) { // Header Row( @@ -100,10 +95,10 @@ fun SinglePodsCard( Image( painter = painterResource(device.iconRes), contentDescription = null, - modifier = Modifier.size(48.dp), + modifier = Modifier.size(44.dp), ) - Spacer(modifier = Modifier.width(12.dp)) + Spacer(modifier = Modifier.width(8.dp)) Column(modifier = Modifier.weight(1f)) { Text( @@ -112,22 +107,27 @@ fun SinglePodsCard( maxLines = 1, overflow = TextOverflow.Ellipsis, ) - Text( - text = device.getLabel(context), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = device.getLabel(context), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f, fill = false), + ) + Spacer(modifier = Modifier.width(6.dp)) + SignalBadge( + signalText = device.getSignalQuality(context), + bleKeyState = device.bleKeyState, + isAapConnected = device.isAapConnected, + isLive = device.isLive, + ) + } } - SignalBadge( - signalText = device.getSignalQuality(context), - bleKeyState = device.bleKeyState, - isAapConnected = device.isAapConnected, - isLive = device.isLive, - ) - if (device.address != null && onDeviceSettings != null) { IconButton(onClick = onDeviceSettings) { Icon( @@ -139,31 +139,15 @@ fun SinglePodsCard( } } - Spacer(modifier = Modifier.height(4.dp)) - - // Timestamps - Text( - text = stringResource(R.string.last_seen_x, device.lastSeenFormatted(now)), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - val seenFirst = device.seenFirstAt - val seenLast = device.seenLastAt - if (seenFirst != null && seenLast != null && Duration.between(seenFirst, seenLast).toMinutes() >= 1) { - Text( - text = stringResource(R.string.first_seen_x, device.firstSeenFormatted(now)), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(12.dp)) // Central gauge Surface( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .then(if (!device.isLive) Modifier.alpha(0.7f) else Modifier), shape = RoundedCornerShape(12.dp), - tonalElevation = 1.dp, + tonalElevation = 4.dp, ) { Column( modifier = Modifier @@ -173,12 +157,12 @@ fun SinglePodsCard( ) { Box( contentAlignment = Alignment.Center, - modifier = Modifier.size(100.dp), + modifier = Modifier.size(88.dp), ) { // Track ring CircularProgressIndicator( progress = { 1f }, - modifier = Modifier.size(100.dp), + modifier = Modifier.size(88.dp), color = MaterialTheme.colorScheme.surfaceVariant, strokeWidth = 8.dp, trackColor = MaterialTheme.colorScheme.surfaceVariant, @@ -189,7 +173,7 @@ fun SinglePodsCard( if (clamped != null) { CircularProgressIndicator( progress = { animatedProgress }, - modifier = Modifier.size(100.dp), + modifier = Modifier.size(88.dp), color = ringColor, strokeWidth = 8.dp, trackColor = MaterialTheme.colorScheme.surfaceVariant, @@ -200,7 +184,7 @@ fun SinglePodsCard( // Battery text inside ring Text( text = formatBatteryPercent(context, device.batteryHeadset), - style = MaterialTheme.typography.headlineMedium, + style = MaterialTheme.typography.headlineSmall, color = if (device.batteryHeadset != null) { MaterialTheme.colorScheme.onSurface } else { @@ -240,6 +224,7 @@ fun SinglePodsCard( text = stringResource(R.string.battery_cached_label, device.cachedBatteryFormatted(now)), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.align(Alignment.End), ) } @@ -265,18 +250,31 @@ fun SinglePodsCard( @Preview2 @Composable -private fun SinglePodsCardPreview() = PreviewWrapper { - SinglePodsCard(device = MockPodDataProvider.singlePodMonitored(), showDebug = false, now = Instant.now()) +private fun SinglePodsCardFullPreview() = PreviewWrapper { + SinglePodsCard( + device = MockPodDataProvider.singlePodMonitoredWithAap(), + showDebug = false, + now = Instant.now(), + onDeviceSettings = {}, + ) } @Preview2 @Composable -private fun SinglePodsCardDebugPreview() = PreviewWrapper { - SinglePodsCard(device = MockPodDataProvider.singlePodMonitored(), showDebug = true, now = Instant.now()) +private fun SinglePodsCardMinimalPreview() = PreviewWrapper { + SinglePodsCard( + device = MockPodDataProvider.singlePodMonitored(), + showDebug = false, + now = Instant.now(), + ) } @Preview2 @Composable -private fun SinglePodsCardCachedOnlyPreview() = PreviewWrapper { - SinglePodsCard(device = MockPodDataProvider.singlePodCachedOnly(), showDebug = false, now = Instant.now()) +private fun SinglePodsCardCachedPreview() = PreviewWrapper { + SinglePodsCard( + device = MockPodDataProvider.singlePodCachedOnly(), + showDebug = false, + now = Instant.now(), + ) } diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/UnknownPodDeviceCard.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/UnknownPodDeviceCard.kt index 67e90146..9e40b5d0 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/UnknownPodDeviceCard.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/UnknownPodDeviceCard.kt @@ -26,15 +26,12 @@ import eu.darken.capod.common.compose.PreviewWrapper import eu.darken.capod.common.compose.preview.MockPodDataProvider import eu.darken.capod.monitor.core.PodDevice import eu.darken.capod.monitor.core.getSignalQuality -import eu.darken.capod.monitor.core.lastSeenFormatted import eu.darken.capod.pods.core.apple.ble.devices.ApplePods -import java.time.Instant @Composable fun UnknownPodDeviceCard( device: PodDevice, showDebug: Boolean, - now: Instant, ) { val context = LocalContext.current @@ -71,14 +68,6 @@ fun UnknownPodDeviceCard( ) } - Spacer(modifier = Modifier.height(4.dp)) - - Text( - text = stringResource(R.string.last_seen_x, device.lastSeenFormatted(now)), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - Spacer(modifier = Modifier.height(8.dp)) Text( @@ -111,11 +100,11 @@ fun UnknownPodDeviceCard( @Preview2 @Composable private fun UnknownPodDeviceCardPreview() = PreviewWrapper { - UnknownPodDeviceCard(device = MockPodDataProvider.unknownMonitored(), showDebug = false, now = Instant.now()) + UnknownPodDeviceCard(device = MockPodDataProvider.unknownMonitored(), showDebug = false) } @Preview2 @Composable private fun UnknownPodDeviceCardDebugPreview() = PreviewWrapper { - UnknownPodDeviceCard(device = MockPodDataProvider.unknownMonitored(), showDebug = true, now = Instant.now()) + UnknownPodDeviceCard(device = MockPodDataProvider.unknownMonitored(), showDebug = true) } diff --git a/app/src/main/java/eu/darken/capod/monitor/core/PodDevice.kt b/app/src/main/java/eu/darken/capod/monitor/core/PodDevice.kt index 8e873a23..c9922003 100644 --- a/app/src/main/java/eu/darken/capod/monitor/core/PodDevice.kt +++ b/app/src/main/java/eu/darken/capod/monitor/core/PodDevice.kt @@ -197,13 +197,19 @@ data class PodDevice( val iconRes: Int get() = ble?.iconRes ?: model.iconRes val leftPodIcon: Int - get() = (ble as? DualBlePodSnapshot)?.leftPodIcon ?: R.drawable.device_airpods_gen1_left + get() = (ble as? DualBlePodSnapshot)?.leftPodIcon + ?: model.leftPodIconRes + ?: R.drawable.device_airpods_gen1_left val rightPodIcon: Int - get() = (ble as? DualBlePodSnapshot)?.rightPodIcon ?: R.drawable.device_airpods_gen1_right + get() = (ble as? DualBlePodSnapshot)?.rightPodIcon + ?: model.rightPodIconRes + ?: R.drawable.device_airpods_gen1_right val caseIcon: Int - get() = (ble as? HasCase)?.caseIcon ?: R.drawable.device_airpods_gen1_case + get() = (ble as? HasCase)?.caseIcon + ?: model.caseIconRes + ?: R.drawable.device_airpods_gen1_case fun getLabel(context: Context): String = ble?.getLabel(context) ?: model.label diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/PodModel.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/PodModel.kt index a3b76819..dfc8e175 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/PodModel.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/PodModel.kt @@ -11,6 +11,9 @@ enum class PodModel( @DrawableRes val iconRes: Int = R.drawable.device_earbuds_generic_both, val features: Features = Features(), val modelNumbers: Set = emptySet(), + @DrawableRes val leftPodIconRes: Int? = null, + @DrawableRes val rightPodIconRes: Int? = null, + @DrawableRes val caseIconRes: Int? = null, ) { @SerialName("airpods.gen1") AIRPODS_GEN1( @@ -21,6 +24,9 @@ enum class PodModel( hasCase = true, ), modelNumbers = setOf("A1523", "A1722"), // L/R earphones + leftPodIconRes = R.drawable.device_airpods_gen1_left, + rightPodIconRes = R.drawable.device_airpods_gen1_right, + caseIconRes = R.drawable.device_airpods_gen1_case, ), @SerialName("airpods.gen2") @@ -32,6 +38,9 @@ enum class PodModel( hasCase = true, ), modelNumbers = setOf("A2031", "A2032"), // L/R earphones + leftPodIconRes = R.drawable.device_airpods_gen1_left, + rightPodIconRes = R.drawable.device_airpods_gen1_right, + caseIconRes = R.drawable.device_airpods_gen1_case, ), @SerialName("airpods.gen3") @@ -43,6 +52,9 @@ enum class PodModel( hasCase = true, ), modelNumbers = setOf("A2564", "A2565"), // L/R earphones + leftPodIconRes = R.drawable.device_airpods_gen3_left, + rightPodIconRes = R.drawable.device_airpods_gen3_right, + caseIconRes = R.drawable.device_airpods_gen3_case, ), @SerialName("airpods.gen4") @@ -55,6 +67,9 @@ enum class PodModel( hasEarDetection = true, ), modelNumbers = setOf("A3050", "A3053", "A3054"), // earphones + leftPodIconRes = R.drawable.device_airpods_gen3_left, + rightPodIconRes = R.drawable.device_airpods_gen3_right, + caseIconRes = R.drawable.device_airpods_gen3_case, ), @SerialName("airpods.gen4.anc") @@ -78,6 +93,9 @@ enum class PodModel( needsInitExt = true, ), modelNumbers = setOf("A3055", "A3056", "A3057"), // earphones + leftPodIconRes = R.drawable.device_airpods_gen4anc_left, + rightPodIconRes = R.drawable.device_airpods_gen4anc_right, + caseIconRes = R.drawable.device_airpods_gen4anc_case, ), @SerialName("airpods.pro") @@ -96,6 +114,9 @@ enum class PodModel( hasEndCallMuteMic = true, ), modelNumbers = setOf("A2083", "A2084"), // L/R earphones + leftPodIconRes = R.drawable.device_airpods_pro2_left, + rightPodIconRes = R.drawable.device_airpods_pro2_right, + caseIconRes = R.drawable.device_airpods_pro2_case, ), @SerialName("airpods.pro2") @@ -121,6 +142,9 @@ enum class PodModel( needsInitExt = true, ), modelNumbers = setOf("A2698", "A2699", "A2931"), // earphones + leftPodIconRes = R.drawable.device_airpods_pro2_left, + rightPodIconRes = R.drawable.device_airpods_pro2_right, + caseIconRes = R.drawable.device_airpods_pro2_case, ), @SerialName("airpods.pro2.usbc") @@ -146,6 +170,9 @@ enum class PodModel( needsInitExt = true, ), modelNumbers = setOf("A3047", "A3048", "A3049"), // earphones + leftPodIconRes = R.drawable.device_airpods_pro2_left, + rightPodIconRes = R.drawable.device_airpods_pro2_right, + caseIconRes = R.drawable.device_airpods_pro2_case, ), @SerialName("airpods.pro3") @@ -171,6 +198,9 @@ enum class PodModel( needsInitExt = true, ), modelNumbers = setOf("A3063", "A3064", "A3065"), // earphones + leftPodIconRes = R.drawable.device_airpods_pro2_left, + rightPodIconRes = R.drawable.device_airpods_pro2_right, + caseIconRes = R.drawable.device_airpods_pro2_case, ), @SerialName("airpods.max") @@ -318,6 +348,9 @@ enum class PodModel( hasEarDetection = true, ), modelNumbers = setOf("A2047", "A2048", "A2453", "A2454"), // L/R earbuds, 2019 + 2020 revisions + leftPodIconRes = R.drawable.device_powerbeats_pro_left, + rightPodIconRes = R.drawable.device_powerbeats_pro_right, + caseIconRes = R.drawable.device_powerbeats_pro_case, ), @SerialName("beats.powerbeats.pro2") @@ -331,6 +364,9 @@ enum class PodModel( hasAncControl = true, ), modelNumbers = setOf("A3157", "A3158", "A3159"), // L/R earbuds + case + leftPodIconRes = R.drawable.device_powerbeats_pro2_left, + rightPodIconRes = R.drawable.device_powerbeats_pro2_right, + caseIconRes = R.drawable.device_powerbeats_pro2_case, ), @SerialName("beats.fit.pro") @@ -344,6 +380,9 @@ enum class PodModel( hasAncControl = true, ), modelNumbers = setOf("A2576", "A2577", "A2578"), // L/R earbuds + case + leftPodIconRes = R.drawable.device_beats_fitpro_left, + rightPodIconRes = R.drawable.device_beats_fitpro_right, + caseIconRes = R.drawable.device_beats_fitpro_case, ), @SerialName("fakes.tws.i99999") @@ -354,6 +393,9 @@ enum class PodModel( hasDualPods = true, hasCase = true, ), + leftPodIconRes = R.drawable.device_airpods_gen1_left, + rightPodIconRes = R.drawable.device_airpods_gen1_right, + caseIconRes = R.drawable.device_airpods_gen1_case, ), @SerialName("fakes.generic.airpods.gen2") @@ -364,6 +406,9 @@ enum class PodModel( hasDualPods = true, hasCase = true, ), + leftPodIconRes = R.drawable.device_airpods_gen1_left, + rightPodIconRes = R.drawable.device_airpods_gen1_right, + caseIconRes = R.drawable.device_airpods_gen1_case, ), @SerialName("fakes.generic.airpods.gen3") @@ -374,6 +419,9 @@ enum class PodModel( hasDualPods = true, hasCase = true, ), + leftPodIconRes = R.drawable.device_airpods_gen3_left, + rightPodIconRes = R.drawable.device_airpods_gen3_right, + caseIconRes = R.drawable.device_airpods_gen3_case, ), @SerialName("fakes.varunr.airpodspro") @@ -386,6 +434,9 @@ enum class PodModel( hasEarDetection = true, hasAncControl = true, ), + leftPodIconRes = R.drawable.device_airpods_pro2_left, + rightPodIconRes = R.drawable.device_airpods_pro2_right, + caseIconRes = R.drawable.device_airpods_pro2_case, ), @SerialName("fakes.generic.airpods.pro2") @@ -398,6 +449,9 @@ enum class PodModel( hasEarDetection = true, hasAncControl = true, ), + leftPodIconRes = R.drawable.device_airpods_pro2_left, + rightPodIconRes = R.drawable.device_airpods_pro2_right, + caseIconRes = R.drawable.device_airpods_pro2_case, ), @SerialName("unknown") diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4be263a4..3a0b9ea6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -425,6 +425,9 @@ Name Serial Number Firmware + Status + Last Seen + First Seen Sound Controls Noise Cancellation with One AirPod