mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
fix: Replace nullable Float composable params to avoid ART native crash
ComposerImpl.changed() boxes Float? and calls Float.equals() at the ART native level, triggering a known crash. Non-null Float params use the primitive overload with no boxing. Introduces BATTERY_UNKNOWN sentinel, toBatteryFloat(), and toBatteryOrNull() extensions to eliminate Float? from all composable signatures and WidgetRenderState data classes.
This commit is contained in:
@@ -56,6 +56,8 @@ import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods.LidState
|
||||
import eu.darken.capod.pods.core.firstSeenFormatted
|
||||
import eu.darken.capod.pods.core.formatBatteryPercent
|
||||
import eu.darken.capod.pods.core.toBatteryFloat
|
||||
import eu.darken.capod.pods.core.toBatteryOrNull
|
||||
import eu.darken.capod.pods.core.getSignalQuality
|
||||
import eu.darken.capod.pods.core.lastSeenFormatted
|
||||
import java.time.Duration
|
||||
@@ -164,7 +166,7 @@ fun DualPodsCard(
|
||||
) {
|
||||
PodGauge(
|
||||
iconRes = device.leftPodIcon,
|
||||
batteryPercent = device.batteryLeftPodPercent,
|
||||
batteryPercent = device.batteryLeftPodPercent.toBatteryFloat(),
|
||||
isCharging = (device as? HasChargeDetectionDual)?.isLeftPodCharging ?: false,
|
||||
isInEar = (device as? HasEarDetectionDual)?.isLeftPodInEar ?: false,
|
||||
showEarDetection = device is HasEarDetectionDual,
|
||||
@@ -175,7 +177,7 @@ fun DualPodsCard(
|
||||
|
||||
PodGauge(
|
||||
iconRes = device.rightPodIcon,
|
||||
batteryPercent = device.batteryRightPodPercent,
|
||||
batteryPercent = device.batteryRightPodPercent.toBatteryFloat(),
|
||||
isCharging = (device as? HasChargeDetectionDual)?.isRightPodCharging ?: false,
|
||||
isInEar = (device as? HasEarDetectionDual)?.isRightPodInEar ?: false,
|
||||
showEarDetection = device is HasEarDetectionDual,
|
||||
@@ -218,7 +220,7 @@ fun DualPodsCard(
|
||||
@Composable
|
||||
private fun PodGauge(
|
||||
iconRes: Int,
|
||||
batteryPercent: Float?,
|
||||
batteryPercent: Float,
|
||||
isCharging: Boolean,
|
||||
isInEar: Boolean,
|
||||
showEarDetection: Boolean,
|
||||
@@ -227,15 +229,15 @@ private fun PodGauge(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val clamped = batteryPercent?.coerceIn(0f, 1f)
|
||||
val clamped = if (batteryPercent >= 0f) batteryPercent.coerceIn(0f, 1f) else -1f
|
||||
val animatedProgress by animateFloatAsState(
|
||||
targetValue = clamped ?: 0f,
|
||||
targetValue = if (clamped >= 0f) clamped else 0f,
|
||||
animationSpec = tween(600, easing = FastOutSlowInEasing),
|
||||
label = "gaugeProgress",
|
||||
)
|
||||
|
||||
val ringColor = when {
|
||||
clamped == null -> MaterialTheme.colorScheme.surfaceVariant
|
||||
clamped < 0f -> MaterialTheme.colorScheme.surfaceVariant
|
||||
clamped > 0.30f -> MaterialTheme.colorScheme.primary
|
||||
clamped >= 0.15f -> MaterialTheme.colorScheme.tertiary
|
||||
else -> MaterialTheme.colorScheme.error
|
||||
@@ -261,7 +263,7 @@ private fun PodGauge(
|
||||
)
|
||||
|
||||
// Progress ring
|
||||
if (clamped != null) {
|
||||
if (clamped >= 0f) {
|
||||
CircularProgressIndicator(
|
||||
progress = { animatedProgress },
|
||||
modifier = Modifier.size(80.dp),
|
||||
@@ -284,9 +286,9 @@ private fun PodGauge(
|
||||
|
||||
// Battery percentage
|
||||
Text(
|
||||
text = formatBatteryPercent(context, batteryPercent),
|
||||
text = formatBatteryPercent(context, batteryPercent.toBatteryOrNull()),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = if (batteryPercent != null) {
|
||||
color = if (batteryPercent >= 0f) {
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
@@ -335,7 +337,7 @@ private fun CaseRow(
|
||||
)
|
||||
|
||||
BatteryCapsule(
|
||||
percent = device.batteryCasePercent,
|
||||
percent = device.batteryCasePercent.toBatteryFloat(),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(8.dp),
|
||||
|
||||
@@ -40,18 +40,18 @@ private val CapsuleShape = RoundedCornerShape(6.dp)
|
||||
|
||||
@Composable
|
||||
fun BatteryCapsule(
|
||||
percent: Float?,
|
||||
percent: Float,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val clamped = percent?.coerceIn(0f, 1f)
|
||||
val clamped = if (percent >= 0f) percent.coerceIn(0f, 1f) else -1f
|
||||
val animatedFraction by animateFloatAsState(
|
||||
targetValue = clamped ?: 0f,
|
||||
targetValue = if (clamped >= 0f) clamped else 0f,
|
||||
animationSpec = tween(600, easing = FastOutSlowInEasing),
|
||||
label = "batteryFill",
|
||||
)
|
||||
|
||||
val barColor = when {
|
||||
clamped == null -> MaterialTheme.colorScheme.surfaceVariant
|
||||
clamped < 0f -> MaterialTheme.colorScheme.surfaceVariant
|
||||
clamped > 0.30f -> MaterialTheme.colorScheme.primary
|
||||
clamped >= 0.15f -> MaterialTheme.colorScheme.tertiary
|
||||
else -> MaterialTheme.colorScheme.error
|
||||
@@ -62,7 +62,7 @@ fun BatteryCapsule(
|
||||
.clip(CapsuleShape)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant),
|
||||
) {
|
||||
if (clamped != null) {
|
||||
if (clamped >= 0f) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
|
||||
@@ -30,6 +30,7 @@ import androidx.compose.ui.unit.sp
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.compose.Preview2
|
||||
import eu.darken.capod.common.compose.PreviewWrapper
|
||||
import eu.darken.capod.pods.core.toBatteryOrNull
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
@@ -191,7 +192,7 @@ private fun SinglePodPreview(
|
||||
colorFilter = iconTint,
|
||||
)
|
||||
Text(
|
||||
text = formatPercent(state.percent),
|
||||
text = formatPercent(state.percent.toBatteryOrNull()),
|
||||
fontSize = 12.sp,
|
||||
color = textColor,
|
||||
modifier = Modifier.padding(horizontal = 8.dp),
|
||||
@@ -293,7 +294,7 @@ private fun WidgetContainer(
|
||||
@Composable
|
||||
private fun PodItemRow(
|
||||
icon: Int,
|
||||
percent: Float?,
|
||||
percent: Float,
|
||||
charging: Boolean,
|
||||
inEar: Boolean,
|
||||
textColor: Color,
|
||||
@@ -313,7 +314,7 @@ private fun PodItemRow(
|
||||
colorFilter = iconTint,
|
||||
)
|
||||
Text(
|
||||
text = formatPercent(percent),
|
||||
text = formatPercent(percent.toBatteryOrNull()),
|
||||
fontSize = 12.sp,
|
||||
color = textColor,
|
||||
modifier = Modifier.padding(horizontal = 4.dp),
|
||||
|
||||
@@ -26,6 +26,7 @@ import androidx.glance.text.TextAlign
|
||||
import androidx.glance.text.TextStyle
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.main.ui.MainActivity
|
||||
import eu.darken.capod.pods.core.toBatteryOrNull
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
@@ -105,7 +106,7 @@ private fun GlanceSinglePod(
|
||||
colorFilter = iconTint,
|
||||
)
|
||||
Text(
|
||||
text = formatGlancePercent(state.percent),
|
||||
text = formatGlancePercent(state.percent.toBatteryOrNull()),
|
||||
style = textStyle,
|
||||
modifier = GlanceModifier.padding(horizontal = 8.dp),
|
||||
)
|
||||
@@ -203,7 +204,7 @@ private fun GlanceWidgetRoot(
|
||||
@Composable
|
||||
private fun GlancePodItem(
|
||||
icon: Int,
|
||||
percent: Float?,
|
||||
percent: Float,
|
||||
charging: Boolean,
|
||||
inEar: Boolean,
|
||||
textStyle: TextStyle,
|
||||
@@ -222,7 +223,7 @@ private fun GlancePodItem(
|
||||
colorFilter = iconTint,
|
||||
)
|
||||
Text(
|
||||
text = formatGlancePercent(percent),
|
||||
text = formatGlancePercent(percent.toBatteryOrNull()),
|
||||
style = textStyle,
|
||||
modifier = GlanceModifier.padding(horizontal = 4.dp),
|
||||
)
|
||||
|
||||
@@ -18,15 +18,15 @@ sealed class WidgetRenderState {
|
||||
val isWide: Boolean,
|
||||
val deviceLabel: String?,
|
||||
@DrawableRes val leftIcon: Int,
|
||||
val leftPercent: Float?,
|
||||
val leftPercent: Float,
|
||||
val leftCharging: Boolean,
|
||||
val leftInEar: Boolean,
|
||||
@DrawableRes val rightIcon: Int,
|
||||
val rightPercent: Float?,
|
||||
val rightPercent: Float,
|
||||
val rightCharging: Boolean,
|
||||
val rightInEar: Boolean,
|
||||
@DrawableRes val caseIcon: Int,
|
||||
val casePercent: Float?,
|
||||
val casePercent: Float,
|
||||
val caseCharging: Boolean,
|
||||
) : WidgetRenderState()
|
||||
|
||||
@@ -37,7 +37,7 @@ sealed class WidgetRenderState {
|
||||
@ColorInt override val resolvedIconColor: Int,
|
||||
val deviceLabel: String?,
|
||||
@DrawableRes val headsetIcon: Int,
|
||||
val percent: Float?,
|
||||
val percent: Float,
|
||||
@DrawableRes val batteryIcon: Int,
|
||||
val charging: Boolean,
|
||||
val worn: Boolean,
|
||||
|
||||
@@ -12,6 +12,7 @@ import eu.darken.capod.pods.core.HasEarDetectionDual
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.SinglePodDevice
|
||||
import eu.darken.capod.pods.core.getBatteryDrawable
|
||||
import eu.darken.capod.pods.core.toBatteryFloat
|
||||
|
||||
object WidgetRenderStateMapper {
|
||||
|
||||
@@ -46,15 +47,15 @@ object WidgetRenderStateMapper {
|
||||
isWide = isWide,
|
||||
deviceLabel = profileLabel ?: device.getLabel(context),
|
||||
leftIcon = device.leftPodIcon,
|
||||
leftPercent = device.batteryLeftPodPercent,
|
||||
leftPercent = device.batteryLeftPodPercent.toBatteryFloat(),
|
||||
leftCharging = device is HasChargeDetectionDual && device.isLeftPodCharging,
|
||||
leftInEar = device is HasEarDetectionDual && device.isLeftPodInEar,
|
||||
rightIcon = device.rightPodIcon,
|
||||
rightPercent = device.batteryRightPodPercent,
|
||||
rightPercent = device.batteryRightPodPercent.toBatteryFloat(),
|
||||
rightCharging = device is HasChargeDetectionDual && device.isRightPodCharging,
|
||||
rightInEar = device is HasEarDetectionDual && device.isRightPodInEar,
|
||||
caseIcon = (device as? HasCase)?.caseIcon ?: R.drawable.device_airpods_gen1_case,
|
||||
casePercent = (device as? HasCase)?.batteryCasePercent,
|
||||
casePercent = (device as? HasCase)?.batteryCasePercent.toBatteryFloat(),
|
||||
caseCharging = device is HasCase && device.isCaseCharging,
|
||||
)
|
||||
|
||||
@@ -65,7 +66,7 @@ object WidgetRenderStateMapper {
|
||||
resolvedIconColor = iconColor,
|
||||
deviceLabel = profileLabel ?: device.getLabel(context),
|
||||
headsetIcon = device.iconRes,
|
||||
percent = device.batteryHeadsetPercent,
|
||||
percent = device.batteryHeadsetPercent.toBatteryFloat(),
|
||||
batteryIcon = getBatteryDrawable(device.batteryHeadsetPercent),
|
||||
charging = device is HasChargeDetectionDual && device.isHeadsetBeingCharged,
|
||||
worn = device is HasEarDetection && device.isBeingWorn,
|
||||
|
||||
@@ -8,6 +8,12 @@ import java.time.Duration
|
||||
import java.time.Instant
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
const val BATTERY_UNKNOWN = -1f
|
||||
|
||||
fun Float?.toBatteryFloat(): Float = this ?: BATTERY_UNKNOWN
|
||||
|
||||
fun Float.toBatteryOrNull(): Float? = takeIf { it >= 0f }
|
||||
|
||||
fun formatBatteryPercent(context: Context, percent: Float?): String =
|
||||
percent?.let { "${(it * 100).roundToInt()}%" }
|
||||
?: context.getString(R.string.general_value_not_available_label)
|
||||
|
||||
@@ -40,6 +40,8 @@ import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.SinglePodDevice
|
||||
import eu.darken.capod.pods.core.formatBatteryPercent
|
||||
import eu.darken.capod.pods.core.getBatteryDrawable
|
||||
import eu.darken.capod.pods.core.toBatteryFloat
|
||||
import eu.darken.capod.pods.core.toBatteryOrNull
|
||||
import eu.darken.capod.pods.core.getSignalQuality
|
||||
|
||||
@Composable
|
||||
@@ -127,7 +129,7 @@ private fun DualPodContent(device: DualPodDevice) {
|
||||
// Left pod
|
||||
BatteryColumn(
|
||||
iconRes = device.leftPodIcon,
|
||||
batteryPercent = device.batteryLeftPodPercent,
|
||||
batteryPercent = device.batteryLeftPodPercent.toBatteryFloat(),
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
|
||||
@@ -135,7 +137,7 @@ private fun DualPodContent(device: DualPodDevice) {
|
||||
if (hasCase != null) {
|
||||
BatteryColumn(
|
||||
iconRes = hasCase.caseIcon,
|
||||
batteryPercent = hasCase.batteryCasePercent,
|
||||
batteryPercent = hasCase.batteryCasePercent.toBatteryFloat(),
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
@@ -143,7 +145,7 @@ private fun DualPodContent(device: DualPodDevice) {
|
||||
// Right pod
|
||||
BatteryColumn(
|
||||
iconRes = device.rightPodIcon,
|
||||
batteryPercent = device.batteryRightPodPercent,
|
||||
batteryPercent = device.batteryRightPodPercent.toBatteryFloat(),
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
@@ -153,17 +155,18 @@ private fun DualPodContent(device: DualPodDevice) {
|
||||
private fun SinglePodContent(device: SinglePodDevice) {
|
||||
BatteryColumn(
|
||||
iconRes = device.iconRes,
|
||||
batteryPercent = device.batteryHeadsetPercent,
|
||||
batteryPercent = device.batteryHeadsetPercent.toBatteryFloat(),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BatteryColumn(
|
||||
iconRes: Int,
|
||||
batteryPercent: Float?,
|
||||
batteryPercent: Float,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val nullablePercent = batteryPercent.toBatteryOrNull()
|
||||
|
||||
Column(
|
||||
modifier = modifier,
|
||||
@@ -183,13 +186,13 @@ private fun BatteryColumn(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(getBatteryDrawable(batteryPercent)),
|
||||
painter = painterResource(getBatteryDrawable(nullablePercent)),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(2.dp))
|
||||
Text(
|
||||
text = formatBatteryPercent(context, batteryPercent),
|
||||
text = formatBatteryPercent(context, nullablePercent),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
|
||||
Reference in New Issue
Block a user