mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
fix: Show signal strength as bar indicator instead of percent
This commit is contained in:
@@ -47,7 +47,6 @@ 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.getSignalQuality
|
||||
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
|
||||
@@ -91,12 +90,23 @@ fun DualPodsCard(
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = device.label ?: "?",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = device.label ?: "?",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f, fill = false),
|
||||
)
|
||||
SignalIndicator(
|
||||
signalQuality = device.signalQuality,
|
||||
isLive = device.isLive,
|
||||
modifier = Modifier.padding(start = 6.dp),
|
||||
)
|
||||
}
|
||||
val deviceLabel = buildString {
|
||||
append(device.getLabel(context))
|
||||
val podStyle = device.ble as? HasPodStyle
|
||||
@@ -109,6 +119,7 @@ fun DualPodsCard(
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
@@ -119,12 +130,10 @@ fun DualPodsCard(
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f, fill = false),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
SignalBadge(
|
||||
signalText = device.getSignalQuality(context),
|
||||
DeviceConnectionBadge(
|
||||
bleKeyState = device.bleKeyState,
|
||||
isAapConnected = device.isAapConnected,
|
||||
isLive = device.isLive,
|
||||
modifier = Modifier.padding(start = 6.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package eu.darken.capod.main.ui.overview.cards
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -20,14 +22,13 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.twotone.BatteryChargingFull
|
||||
import androidx.compose.material.icons.twotone.Hearing
|
||||
import androidx.compose.material.icons.twotone.KeyboardVoice
|
||||
import androidx.compose.material.icons.outlined.Key
|
||||
import androidx.compose.material.icons.twotone.BatteryChargingFull
|
||||
import androidx.compose.material.icons.twotone.Bluetooth
|
||||
import androidx.compose.material.icons.twotone.Hearing
|
||||
import androidx.compose.material.icons.twotone.Key
|
||||
import androidx.compose.material.icons.twotone.KeyboardVoice
|
||||
import androidx.compose.material.icons.twotone.LinkOff
|
||||
import androidx.compose.material.icons.twotone.SettingsInputAntenna
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SegmentedButton
|
||||
@@ -41,8 +42,13 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.CornerRadius
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.darken.capod.R
|
||||
@@ -159,14 +165,90 @@ fun StatusChipRow(
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal enum class SignalLevel { DISCONNECTED, BARS_0, BARS_1, BARS_2, BARS_3, BARS_4 }
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun signalLevelOf(signalQuality: Float, isLive: Boolean): SignalLevel {
|
||||
if (!isLive) return SignalLevel.DISCONNECTED
|
||||
if (!signalQuality.isFinite()) return SignalLevel.BARS_0
|
||||
val q = signalQuality.coerceIn(0f, 1f)
|
||||
return when {
|
||||
q >= 0.80f -> SignalLevel.BARS_4
|
||||
q >= 0.60f -> SignalLevel.BARS_3
|
||||
q >= 0.40f -> SignalLevel.BARS_2
|
||||
q >= 0.20f -> SignalLevel.BARS_1
|
||||
else -> SignalLevel.BARS_0
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SignalBadge(
|
||||
signalText: String,
|
||||
bleKeyState: BleKeyState = BleKeyState.NONE,
|
||||
isAapConnected: Boolean = false,
|
||||
isLive: Boolean = true,
|
||||
fun SignalIndicator(
|
||||
signalQuality: Float,
|
||||
isLive: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val level = signalLevelOf(signalQuality, isLive)
|
||||
val description = when (level) {
|
||||
SignalLevel.DISCONNECTED -> stringResource(R.string.signal_indicator_disconnected_cd)
|
||||
SignalLevel.BARS_0 -> stringResource(R.string.signal_indicator_bars_cd, 0)
|
||||
SignalLevel.BARS_1 -> stringResource(R.string.signal_indicator_bars_cd, 1)
|
||||
SignalLevel.BARS_2 -> stringResource(R.string.signal_indicator_bars_cd, 2)
|
||||
SignalLevel.BARS_3 -> stringResource(R.string.signal_indicator_bars_cd, 3)
|
||||
SignalLevel.BARS_4 -> stringResource(R.string.signal_indicator_bars_cd, 4)
|
||||
}
|
||||
val tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
val rootModifier = modifier
|
||||
.size(12.dp)
|
||||
.semantics(mergeDescendants = true) { contentDescription = description }
|
||||
|
||||
if (level == SignalLevel.DISCONNECTED) {
|
||||
Icon(
|
||||
imageVector = Icons.TwoTone.LinkOff,
|
||||
contentDescription = null,
|
||||
modifier = rootModifier,
|
||||
tint = tint,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
val activeBars = when (level) {
|
||||
SignalLevel.BARS_0 -> 0
|
||||
SignalLevel.BARS_1 -> 1
|
||||
SignalLevel.BARS_2 -> 2
|
||||
SignalLevel.BARS_3 -> 3
|
||||
SignalLevel.BARS_4 -> 4
|
||||
SignalLevel.DISCONNECTED -> 0 // unreachable; handled above
|
||||
}
|
||||
val inactiveColor = tint.copy(alpha = 0.3f)
|
||||
Canvas(modifier = rootModifier) {
|
||||
val barCount = 4
|
||||
val gapWidth = size.width * 0.12f
|
||||
val totalGap = gapWidth * (barCount - 1)
|
||||
val barWidth = (size.width - totalGap) / barCount
|
||||
val corner = CornerRadius(barWidth * 0.4f, barWidth * 0.4f)
|
||||
for (i in 0 until barCount) {
|
||||
val heightFraction = (i + 1).toFloat() / barCount
|
||||
val barHeight = size.height * heightFraction
|
||||
val x = i * (barWidth + gapWidth)
|
||||
val y = size.height - barHeight
|
||||
drawRoundRect(
|
||||
color = if (i < activeBars) tint else inactiveColor,
|
||||
topLeft = Offset(x, y),
|
||||
size = Size(barWidth, barHeight),
|
||||
cornerRadius = corner,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DeviceConnectionBadge(
|
||||
bleKeyState: BleKeyState,
|
||||
isAapConnected: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (bleKeyState == BleKeyState.NONE && !isAapConnected) return
|
||||
Surface(
|
||||
modifier = modifier,
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
@@ -176,47 +258,25 @@ fun SignalBadge(
|
||||
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (!isLive) {
|
||||
if (bleKeyState != BleKeyState.NONE) {
|
||||
Icon(
|
||||
imageVector = Icons.TwoTone.LinkOff,
|
||||
contentDescription = null,
|
||||
imageVector = if (bleKeyState == BleKeyState.IRK_AND_ENCRYPTED) Icons.TwoTone.Key else Icons.Outlined.Key,
|
||||
contentDescription = stringResource(
|
||||
if (bleKeyState == BleKeyState.IRK_AND_ENCRYPTED) R.string.signal_badge_key_encrypted_cd
|
||||
else R.string.signal_badge_key_irk_cd
|
||||
),
|
||||
modifier = Modifier.size(12.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
} else {
|
||||
if (bleKeyState != BleKeyState.NONE) {
|
||||
Icon(
|
||||
imageVector = if (bleKeyState == BleKeyState.IRK_AND_ENCRYPTED) Icons.TwoTone.Key else Icons.Outlined.Key,
|
||||
contentDescription = stringResource(
|
||||
if (bleKeyState == BleKeyState.IRK_AND_ENCRYPTED) R.string.signal_badge_key_encrypted_cd
|
||||
else R.string.signal_badge_key_irk_cd
|
||||
),
|
||||
modifier = Modifier.size(12.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(3.dp))
|
||||
}
|
||||
if (isAapConnected) {
|
||||
Icon(
|
||||
imageVector = Icons.TwoTone.Bluetooth,
|
||||
contentDescription = stringResource(R.string.signal_badge_aap_cd),
|
||||
modifier = Modifier.size(12.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(3.dp))
|
||||
}
|
||||
if (isAapConnected) Spacer(modifier = Modifier.width(3.dp))
|
||||
}
|
||||
if (isAapConnected) {
|
||||
Icon(
|
||||
imageVector = Icons.TwoTone.SettingsInputAntenna,
|
||||
contentDescription = null,
|
||||
imageVector = Icons.TwoTone.Bluetooth,
|
||||
contentDescription = stringResource(R.string.signal_badge_aap_cd),
|
||||
modifier = Modifier.size(12.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(3.dp))
|
||||
Text(
|
||||
text = signalText,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -307,18 +367,36 @@ fun ConversationAwarenessToggle(
|
||||
|
||||
@Preview2
|
||||
@Composable
|
||||
private fun SignalBadgeDefaultPreview() = PreviewWrapper {
|
||||
SignalBadge(signalText = "85%")
|
||||
private fun DeviceConnectionBadgeIrkPreview() = PreviewWrapper {
|
||||
DeviceConnectionBadge(bleKeyState = BleKeyState.IRK_ONLY, isAapConnected = false)
|
||||
}
|
||||
|
||||
@Preview2
|
||||
@Composable
|
||||
private fun SignalBadgeIrkOnlyPreview() = PreviewWrapper {
|
||||
SignalBadge(signalText = "85%", bleKeyState = BleKeyState.IRK_ONLY)
|
||||
private fun DeviceConnectionBadgeAllIconsPreview() = PreviewWrapper {
|
||||
DeviceConnectionBadge(bleKeyState = BleKeyState.IRK_AND_ENCRYPTED, isAapConnected = true)
|
||||
}
|
||||
|
||||
@Preview2
|
||||
@Composable
|
||||
private fun SignalBadgeAllIconsPreview() = PreviewWrapper {
|
||||
SignalBadge(signalText = "85%", bleKeyState = BleKeyState.IRK_AND_ENCRYPTED, isAapConnected = true)
|
||||
private fun DeviceConnectionBadgeEmptyPreview() = PreviewWrapper {
|
||||
DeviceConnectionBadge(bleKeyState = BleKeyState.NONE, isAapConnected = false)
|
||||
}
|
||||
|
||||
@Preview2
|
||||
@Composable
|
||||
private fun SignalIndicatorPreviewsGroup() = PreviewWrapper {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
SignalIndicator(signalQuality = 0.10f, isLive = true)
|
||||
SignalIndicator(signalQuality = 0.35f, isLive = true)
|
||||
SignalIndicator(signalQuality = 0.55f, isLive = true)
|
||||
SignalIndicator(signalQuality = 0.75f, isLive = true)
|
||||
SignalIndicator(signalQuality = 0.95f, isLive = true)
|
||||
SignalIndicator(signalQuality = Float.NaN, isLive = true)
|
||||
SignalIndicator(signalQuality = -0.3f, isLive = true)
|
||||
SignalIndicator(signalQuality = 0.85f, isLive = false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,6 @@ 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.getSignalQuality
|
||||
import eu.darken.capod.pods.core.apple.aap.protocol.AapSetting
|
||||
import eu.darken.capod.pods.core.apple.ble.formatBatteryPercent
|
||||
import java.time.Instant
|
||||
@@ -101,13 +100,25 @@ fun SinglePodsCard(
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = device.label ?: "?",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = device.label ?: "?",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f, fill = false),
|
||||
)
|
||||
SignalIndicator(
|
||||
signalQuality = device.signalQuality,
|
||||
isLive = device.isLive,
|
||||
modifier = Modifier.padding(start = 6.dp),
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
@@ -118,12 +129,10 @@ fun SinglePodsCard(
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f, fill = false),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
SignalBadge(
|
||||
signalText = device.getSignalQuality(context),
|
||||
DeviceConnectionBadge(
|
||||
bleKeyState = device.bleKeyState,
|
||||
isAapConnected = device.isAapConnected,
|
||||
isLive = device.isLive,
|
||||
modifier = Modifier.padding(start = 6.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,7 @@ 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.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.twotone.SettingsInputAntenna
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -19,13 +14,13 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.monitor.core.PodDevice
|
||||
import eu.darken.capod.monitor.core.getSignalQuality
|
||||
import eu.darken.capod.pods.core.apple.ble.devices.ApplePods
|
||||
|
||||
@Composable
|
||||
@@ -43,7 +38,7 @@ fun UnknownPodDeviceCard(
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
) {
|
||||
// Header with signal in top-right
|
||||
// Header: label + signal indicator (sits right after the text)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -51,20 +46,14 @@ fun UnknownPodDeviceCard(
|
||||
Text(
|
||||
text = device.getLabel(context),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.weight(1f),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f, fill = false),
|
||||
)
|
||||
|
||||
Text(
|
||||
text = device.getSignalQuality(context),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Icon(
|
||||
imageVector = Icons.TwoTone.SettingsInputAntenna,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
SignalIndicator(
|
||||
signalQuality = device.signalQuality,
|
||||
isLive = device.isLive,
|
||||
modifier = Modifier.padding(start = 6.dp),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +1,16 @@
|
||||
package eu.darken.capod.monitor.core
|
||||
|
||||
import android.content.Context
|
||||
import android.icu.text.RelativeDateTimeFormatter
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun DeviceMonitor.devicesWithProfiles(): Flow<List<PodDevice>> = devices
|
||||
.map { devices -> devices.filter { it.profileId != null } }
|
||||
|
||||
fun DeviceMonitor.primaryDevice(): Flow<PodDevice?> = devicesWithProfiles().map { it.firstOrNull() }
|
||||
|
||||
fun PodDevice.getSignalQuality(context: Context): String {
|
||||
val multiplier = 100 * signalQuality
|
||||
return "${multiplier.roundToInt()}%"
|
||||
}
|
||||
|
||||
fun PodDevice.lastSeenFormatted(now: Instant): String {
|
||||
val lastAt = seenLastAt ?: return ""
|
||||
val formatter = RelativeDateTimeFormatter.getInstance()
|
||||
|
||||
@@ -18,11 +18,6 @@ fun formatBatteryPercent(context: Context, percent: Float?): String =
|
||||
percent?.let { "${(it * 100).roundToInt()}%" }
|
||||
?: context.getString(R.string.general_value_not_available_label)
|
||||
|
||||
fun BlePodSnapshot.getSignalQuality(context: Context): String {
|
||||
val multiplier = 100 * signalQuality
|
||||
return "${multiplier.roundToInt()}%"
|
||||
}
|
||||
|
||||
@DrawableRes
|
||||
fun getBatteryDrawable(percent: Float?): Int = when {
|
||||
percent == null -> R.drawable.ic_baseline_battery_unknown_24
|
||||
|
||||
@@ -12,8 +12,6 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.twotone.SignalCellularAlt
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
@@ -34,8 +32,8 @@ 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.main.ui.overview.cards.SignalIndicator
|
||||
import eu.darken.capod.monitor.core.PodDevice
|
||||
import eu.darken.capod.monitor.core.getSignalQuality
|
||||
import eu.darken.capod.pods.core.apple.PodModel
|
||||
import eu.darken.capod.pods.core.apple.ble.formatBatteryPercent
|
||||
import eu.darken.capod.pods.core.apple.ble.getBatteryDrawable
|
||||
@@ -63,7 +61,7 @@ fun PopUpContent(
|
||||
.padding(top = 20.dp, bottom = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
// Header: label + signal quality
|
||||
// Header: label + signal indicator (sits right after the text)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@@ -76,21 +74,11 @@ fun PopUpContent(
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f, fill = false),
|
||||
)
|
||||
|
||||
val signalText = device.getSignalQuality(context)
|
||||
if (signalText.isNotBlank()) {
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Icon(
|
||||
imageVector = Icons.TwoTone.SignalCellularAlt,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(12.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = signalText,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
)
|
||||
}
|
||||
SignalIndicator(
|
||||
signalQuality = device.signalQuality,
|
||||
isLive = device.isLive,
|
||||
modifier = Modifier.padding(start = 6.dp),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
@@ -273,6 +273,8 @@
|
||||
<string name="signal_badge_key_irk_cd">Identity verified</string>
|
||||
<string name="signal_badge_key_encrypted_cd">Encrypted connection</string>
|
||||
<string name="signal_badge_aap_cd">Direct connection active</string>
|
||||
<string name="signal_indicator_bars_cd">Signal strength: %1$d of 4 bars</string>
|
||||
<string name="signal_indicator_disconnected_cd">Signal: disconnected</string>
|
||||
<string name="pods_yours">Yours</string>
|
||||
<string name="headset_being_worn_label">Being worn</string>
|
||||
<string name="headset_not_being_worn_label">Not being worn</string>
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package eu.darken.capod.main.ui.overview.cards
|
||||
|
||||
import io.kotest.matchers.shouldBe
|
||||
import org.junit.jupiter.api.Test
|
||||
import testhelpers.BaseTest
|
||||
|
||||
class SignalLevelTest : BaseTest() {
|
||||
|
||||
@Test
|
||||
fun `not live always returns DISCONNECTED regardless of quality`() {
|
||||
signalLevelOf(signalQuality = 1.0f, isLive = false) shouldBe SignalLevel.DISCONNECTED
|
||||
signalLevelOf(signalQuality = 0.5f, isLive = false) shouldBe SignalLevel.DISCONNECTED
|
||||
signalLevelOf(signalQuality = 0.0f, isLive = false) shouldBe SignalLevel.DISCONNECTED
|
||||
signalLevelOf(signalQuality = Float.NaN, isLive = false) shouldBe SignalLevel.DISCONNECTED
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `exact boundaries map upward`() {
|
||||
signalLevelOf(signalQuality = 0.20f, isLive = true) shouldBe SignalLevel.BARS_1
|
||||
signalLevelOf(signalQuality = 0.40f, isLive = true) shouldBe SignalLevel.BARS_2
|
||||
signalLevelOf(signalQuality = 0.60f, isLive = true) shouldBe SignalLevel.BARS_3
|
||||
signalLevelOf(signalQuality = 0.80f, isLive = true) shouldBe SignalLevel.BARS_4
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `just below boundaries fall into lower bucket`() {
|
||||
signalLevelOf(signalQuality = 0.199f, isLive = true) shouldBe SignalLevel.BARS_0
|
||||
signalLevelOf(signalQuality = 0.399f, isLive = true) shouldBe SignalLevel.BARS_1
|
||||
signalLevelOf(signalQuality = 0.599f, isLive = true) shouldBe SignalLevel.BARS_2
|
||||
signalLevelOf(signalQuality = 0.799f, isLive = true) shouldBe SignalLevel.BARS_3
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `zero quality maps to BARS_0`() {
|
||||
signalLevelOf(signalQuality = 0.0f, isLive = true) shouldBe SignalLevel.BARS_0
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `full quality maps to BARS_4`() {
|
||||
signalLevelOf(signalQuality = 1.0f, isLive = true) shouldBe SignalLevel.BARS_4
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `non-finite live inputs fall back to BARS_0`() {
|
||||
signalLevelOf(signalQuality = Float.NaN, isLive = true) shouldBe SignalLevel.BARS_0
|
||||
signalLevelOf(signalQuality = Float.POSITIVE_INFINITY, isLive = true) shouldBe SignalLevel.BARS_0
|
||||
signalLevelOf(signalQuality = Float.NEGATIVE_INFINITY, isLive = true) shouldBe SignalLevel.BARS_0
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `out of range values are clamped`() {
|
||||
signalLevelOf(signalQuality = -0.5f, isLive = true) shouldBe SignalLevel.BARS_0
|
||||
signalLevelOf(signalQuality = -0.001f, isLive = true) shouldBe SignalLevel.BARS_0
|
||||
signalLevelOf(signalQuality = 1.7f, isLive = true) shouldBe SignalLevel.BARS_4
|
||||
signalLevelOf(signalQuality = 99f, isLive = true) shouldBe SignalLevel.BARS_4
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user