mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-16 11:16:12 -04:00
fix(battery): Seed charge ETA from quick-charge specs, hide projection while charging
- While charging, the gauge showed the bare runtime projection when no charge
rate existed yet — "1% · 4m" next to a charging chip reads as a four-minute
charge. The line now shows the "until charged" ETA or nothing
- Seed the charge rate from Apple's published quick-charge claims ("5 minutes
in the case provides around 1 hour of listening"), normalized against the
rated listening hours, so the ETA is present from the very first charge; a
live fit still takes over within minutes
This commit is contained in:
@@ -240,7 +240,9 @@ private fun ColumnScope.DualPodsCardExpanded(
|
||||
isMicrophone = device.isLeftPodMicrophone ?: false,
|
||||
showMicrophone = device.hasDualMicrophone,
|
||||
modifier = Modifier.weight(1f),
|
||||
timeRemaining = batteryEstimate?.left?.let { formatEstimateText(context, it) },
|
||||
timeRemaining = batteryEstimate?.left?.let {
|
||||
formatEstimateText(context, it, isCharging = device.isLeftPodCharging == true)
|
||||
},
|
||||
)
|
||||
|
||||
PodGauge(
|
||||
@@ -253,7 +255,9 @@ private fun ColumnScope.DualPodsCardExpanded(
|
||||
isMicrophone = device.isRightPodMicrophone ?: false,
|
||||
showMicrophone = device.hasDualMicrophone,
|
||||
modifier = Modifier.weight(1f),
|
||||
timeRemaining = batteryEstimate?.right?.let { formatEstimateText(context, it) },
|
||||
timeRemaining = batteryEstimate?.right?.let {
|
||||
formatEstimateText(context, it, isCharging = device.isRightPodCharging == true)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -406,14 +410,17 @@ private fun PodGauge(
|
||||
}
|
||||
|
||||
/**
|
||||
* The gauge's small estimate line: while charging with a usable rate, the time until full
|
||||
* (language-neutral "⚡ 25m"); otherwise the usual time-remaining ("2h 15m"). Shared with
|
||||
* The gauge's small estimate line: while charging, the time until full (language-neutral "⚡ 25m")
|
||||
* or NOTHING — a bare runtime number next to a charging chip ("1% · 4m") inevitably reads as a
|
||||
* four-minute charge. The runtime estimate only shows while not charging. Shared with
|
||||
* [SinglePodsCard] (same package).
|
||||
*/
|
||||
internal fun formatEstimateText(context: Context, pod: BatteryEstimate.Pod): String =
|
||||
pod.minutesUntilCharged
|
||||
?.let { context.getString(R.string.battery_time_until_charged_short, formatBatteryDurationShort(context, it)) }
|
||||
?: formatBatteryDurationShort(context, pod.minutesRemaining)
|
||||
internal fun formatEstimateText(context: Context, pod: BatteryEstimate.Pod, isCharging: Boolean): String? = when {
|
||||
pod.minutesUntilCharged != null ->
|
||||
context.getString(R.string.battery_time_until_charged_short, formatBatteryDurationShort(context, pod.minutesUntilCharged))
|
||||
isCharging -> null
|
||||
else -> formatBatteryDurationShort(context, pod.minutesRemaining)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
|
||||
@@ -267,10 +267,12 @@ private fun ColumnScope.SinglePodsCardExpanded(
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
},
|
||||
)
|
||||
val headsetEstimate = batteryEstimate?.headset
|
||||
val headsetEstimate = batteryEstimate?.headset?.let {
|
||||
formatEstimateText(context, it, isCharging = device.isHeadsetBeingCharged == true)
|
||||
}
|
||||
if (headsetEstimate != null) {
|
||||
Text(
|
||||
text = formatEstimateText(context, headsetEstimate),
|
||||
text = headsetEstimate,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
|
||||
@@ -348,7 +348,13 @@ class BatteryEstimator @Inject constructor(
|
||||
val live = if (history.direction == SlotHistory.Direction.CHARGE) {
|
||||
DrainModel.chargeSlopeFractionPerHour(history.toList())
|
||||
} else null
|
||||
val rate = live ?: learnedChargeRate(profileId, device, slot) ?: return null
|
||||
// Rate preference mirrors the drain side: measured, then learned, then Apple's published
|
||||
// quick-charge claim ("5 minutes in the case = ~1 hour of listening") — so an ETA exists
|
||||
// even on the very first charge.
|
||||
val rate = live
|
||||
?: learnedChargeRate(profileId, device, slot)
|
||||
?: device.model.batterySpec?.chargeFractionPerHour
|
||||
?: return null
|
||||
|
||||
val lastRise = tracker.lastRiseMs[slot] ?: return null
|
||||
val step = if (device.liveReading(slot)?.second == DataSource.AAP) STEP_AAP else STEP_BLE
|
||||
|
||||
@@ -27,7 +27,7 @@ enum class PodModel(
|
||||
hasMicrophoneMode = true,
|
||||
hasEarDetectionToggle = true,
|
||||
),
|
||||
batterySpec = BatterySpec(listeningHoursAncOff = 5f),
|
||||
batterySpec = BatterySpec(listeningHoursAncOff = 5f, chargeFractionPerHour = 2.4f),
|
||||
modelNumbers = setOf("A1523", "A1722"), // L/R earphones
|
||||
leftPodIconRes = R.drawable.device_airpods_gen1_left,
|
||||
rightPodIconRes = R.drawable.device_airpods_gen1_right,
|
||||
@@ -45,7 +45,7 @@ enum class PodModel(
|
||||
hasMicrophoneMode = true,
|
||||
hasEarDetectionToggle = true,
|
||||
),
|
||||
batterySpec = BatterySpec(listeningHoursAncOff = 5f),
|
||||
batterySpec = BatterySpec(listeningHoursAncOff = 5f, chargeFractionPerHour = 2.4f),
|
||||
modelNumbers = setOf("A2031", "A2032"), // L/R earphones
|
||||
leftPodIconRes = R.drawable.device_airpods_gen1_left,
|
||||
rightPodIconRes = R.drawable.device_airpods_gen1_right,
|
||||
@@ -67,7 +67,7 @@ enum class PodModel(
|
||||
hasMicrophoneMode = true,
|
||||
hasEarDetectionToggle = true,
|
||||
),
|
||||
batterySpec = BatterySpec(listeningHoursAncOff = 6f),
|
||||
batterySpec = BatterySpec(listeningHoursAncOff = 6f, chargeFractionPerHour = 2.0f),
|
||||
modelNumbers = setOf("A2564", "A2565"), // L/R earphones
|
||||
leftPodIconRes = R.drawable.device_airpods_gen3_left,
|
||||
rightPodIconRes = R.drawable.device_airpods_gen3_right,
|
||||
@@ -90,7 +90,7 @@ enum class PodModel(
|
||||
hasEarDetectionToggle = true,
|
||||
hasSleepDetection = true,
|
||||
),
|
||||
batterySpec = BatterySpec(listeningHoursAncOff = 5f),
|
||||
batterySpec = BatterySpec(listeningHoursAncOff = 5f, chargeFractionPerHour = 2.4f),
|
||||
modelNumbers = setOf("A3050", "A3053", "A3054"), // earphones
|
||||
leftPodIconRes = R.drawable.device_airpods_gen3_left,
|
||||
rightPodIconRes = R.drawable.device_airpods_gen3_right,
|
||||
@@ -122,7 +122,7 @@ enum class PodModel(
|
||||
hasStemConfig = true,
|
||||
hasSleepDetection = true,
|
||||
),
|
||||
batterySpec = BatterySpec(listeningHoursAncOn = 4f, listeningHoursAncOff = 5f),
|
||||
batterySpec = BatterySpec(listeningHoursAncOn = 4f, listeningHoursAncOff = 5f, chargeFractionPerHour = 2.4f),
|
||||
modelNumbers = setOf("A3055", "A3056", "A3057"), // earphones
|
||||
leftPodIconRes = R.drawable.device_airpods_gen4anc_left,
|
||||
rightPodIconRes = R.drawable.device_airpods_gen4anc_right,
|
||||
@@ -148,7 +148,7 @@ enum class PodModel(
|
||||
hasListeningModeCycle = true,
|
||||
hasAllowOffOption = true,
|
||||
),
|
||||
batterySpec = BatterySpec(listeningHoursAncOn = 4.5f, listeningHoursAncOff = 5f),
|
||||
batterySpec = BatterySpec(listeningHoursAncOn = 4.5f, listeningHoursAncOff = 5f, chargeFractionPerHour = 2.4f),
|
||||
modelNumbers = setOf("A2083", "A2084"), // L/R earphones
|
||||
leftPodIconRes = R.drawable.device_airpods_pro2_left,
|
||||
rightPodIconRes = R.drawable.device_airpods_pro2_right,
|
||||
@@ -182,7 +182,7 @@ enum class PodModel(
|
||||
hasStemConfig = true,
|
||||
hasSleepDetection = true,
|
||||
),
|
||||
batterySpec = BatterySpec(listeningHoursAncOn = 6f),
|
||||
batterySpec = BatterySpec(listeningHoursAncOn = 6f, chargeFractionPerHour = 2.0f),
|
||||
modelNumbers = setOf("A2698", "A2699", "A2931"), // earphones
|
||||
leftPodIconRes = R.drawable.device_airpods_pro2_left,
|
||||
rightPodIconRes = R.drawable.device_airpods_pro2_right,
|
||||
@@ -216,7 +216,7 @@ enum class PodModel(
|
||||
hasStemConfig = true,
|
||||
hasSleepDetection = true,
|
||||
),
|
||||
batterySpec = BatterySpec(listeningHoursAncOn = 6f),
|
||||
batterySpec = BatterySpec(listeningHoursAncOn = 6f, chargeFractionPerHour = 2.0f),
|
||||
modelNumbers = setOf("A3047", "A3048", "A3049"), // earphones
|
||||
leftPodIconRes = R.drawable.device_airpods_pro2_left,
|
||||
rightPodIconRes = R.drawable.device_airpods_pro2_right,
|
||||
@@ -251,7 +251,7 @@ enum class PodModel(
|
||||
hasSleepDetection = true,
|
||||
hasDynamicEndOfCharge = true,
|
||||
),
|
||||
batterySpec = BatterySpec(listeningHoursAncOn = 8f),
|
||||
batterySpec = BatterySpec(listeningHoursAncOn = 8f, chargeFractionPerHour = 1.5f),
|
||||
modelNumbers = setOf("A3063", "A3064", "A3065"), // earphones
|
||||
leftPodIconRes = R.drawable.device_airpods_pro2_left,
|
||||
rightPodIconRes = R.drawable.device_airpods_pro2_right,
|
||||
@@ -272,7 +272,7 @@ enum class PodModel(
|
||||
hasListeningModeCycle = true,
|
||||
hasAllowOffOption = true,
|
||||
),
|
||||
batterySpec = BatterySpec(listeningHoursAncOn = 20f),
|
||||
batterySpec = BatterySpec(listeningHoursAncOn = 20f, chargeFractionPerHour = 0.9f),
|
||||
modelNumbers = setOf("A2096"), // headphones
|
||||
),
|
||||
|
||||
@@ -290,7 +290,7 @@ enum class PodModel(
|
||||
hasListeningModeCycle = true,
|
||||
hasAllowOffOption = true,
|
||||
),
|
||||
batterySpec = BatterySpec(listeningHoursAncOn = 20f),
|
||||
batterySpec = BatterySpec(listeningHoursAncOn = 20f, chargeFractionPerHour = 0.9f),
|
||||
modelNumbers = setOf("A3184"), // headphones
|
||||
),
|
||||
|
||||
@@ -312,7 +312,7 @@ enum class PodModel(
|
||||
hasListeningModeCycle = true,
|
||||
hasAllowOffOption = true,
|
||||
),
|
||||
batterySpec = BatterySpec(listeningHoursAncOn = 20f),
|
||||
batterySpec = BatterySpec(listeningHoursAncOn = 20f, chargeFractionPerHour = 0.9f),
|
||||
modelNumbers = setOf("A3454"), // headphones
|
||||
),
|
||||
|
||||
@@ -606,5 +606,14 @@ enum class PodModel(
|
||||
val listeningHoursAncOn: Float? = null,
|
||||
/** Listening hours with noise control off, when Apple publishes a distinct figure; else null. */
|
||||
val listeningHoursAncOff: Float? = null,
|
||||
/**
|
||||
* Nominal charge rate (battery fraction gained per hour of charging), derived from Apple's
|
||||
* published quick-charge claim: e.g. "5 minutes in the case provides around 1 hour of
|
||||
* listening time" against a 6 h rating is (1/6) / (5/60) = 2.0/hr. Where the model has two
|
||||
* listening ratings it's normalized against the LONGER one, so the seeded "until charged"
|
||||
* never under-promises. Seeds the charge ETA on the very first charge, before any charge
|
||||
* rate has been measured; a live fit takes over within minutes.
|
||||
*/
|
||||
val chargeFractionPerHour: Float? = null,
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user