From e014ad804567d90f0144092d730664d9152a0496 Mon Sep 17 00:00:00 2001 From: darken Date: Fri, 3 Apr 2026 15:33:19 +0200 Subject: [PATCH] feat: Slim down overview cards and fix cached device icons Remove timestamps, connection state, and conversation awareness from overview cards. Move signal badge inline with model subtitle. Relocate removed info to device settings screen info card. Fix cached devices showing Gen 1 icons by adding per-pod icon properties to PodModel. Add profile name subtitle to device settings toolbar. Simplify preview coverage to full/minimal/cached variants. --- .../compose/preview/MockPodDataProvider.kt | 46 ++++- .../ui/devicesettings/DeviceSettingsScreen.kt | 181 +++++++++++++++--- .../devicesettings/DeviceSettingsViewModel.kt | 21 +- .../capod/main/ui/overview/OverviewScreen.kt | 8 +- .../main/ui/overview/OverviewViewModel.kt | 12 -- .../main/ui/overview/cards/DualPodsCard.kt | 149 +++++--------- .../main/ui/overview/cards/SinglePodsCard.kt | 104 +++++----- .../ui/overview/cards/UnknownPodDeviceCard.kt | 15 +- .../eu/darken/capod/monitor/core/PodDevice.kt | 12 +- .../darken/capod/pods/core/apple/PodModel.kt | 54 ++++++ app/src/main/res/values/strings.xml | 3 + 11 files changed, 391 insertions(+), 214 deletions(-) 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