The visible count came from the pessimistic end of the interval alone, so
an interval straddling a full charge said "Less than one more full charge"
while the colour stayed neutral and TalkBack said "may not be enough". An
uncertain interval now rounds to a single charge instead, and an
open-ended spec that has not reached one charge names no number at all.
Fixes review finding F2
The case percentage alone doesn't answer the question people actually have,
which is whether the case can top the earbuds up again. The card now says so in
words, below the case row.
The reading is treated as the interval it really is: a decile source at 20% on
a 4.0-charge case means 0.8 to 1.2 charges, which is not an answer, so the line
stays neutral instead of flipping colour as the reading bounces between
adjacent frames. Only an interval that lies wholly above or below one full
charge is coloured, which removes the need to remember anything between frames.
The line is stacked rather than inline: the case row already has four children
with only the capsule weighted, so a plural in front of it would collapse the
capsule at large font scales and in locales with long plural forms.
Crossing the warn or critical threshold was expressed as a colour change only,
so it did not exist for a screen reader. Each battery slot on the overview card
now carries the level as a state description.
The auto connect description told users the toggle would set the monitor mode
setting to 'Always'. That setting was removed in 5.1.6 and the mode is derived
from profile state now, so the sentence pointed at a settings entry that no
longer exists. The behaviour it described is unchanged: enabling auto connect
still resolves to continuous monitoring.
Base string only, so Crowdin flags the existing translations as outdated.
The three compatibility toggles described their own mechanism ("Don't
delegate data filtering to the system", "broadcast instead of callback")
without saying which symptom they address, so a non-technical user had no
way to tell whether a row applied to them. Each summary now leads with the
symptom and keeps a plain description of what changes. Titles are
unchanged: they name the mechanism and are referenced verbatim in issue
replies.
The category description "Don't touch if everything works ;)" stopped
rendering in 63692595 when the res/xml preference screens were replaced by
Compose, since SettingsCategoryHeader only took a title. The string and its
~75 translations survived unused. Added an optional subtitle to the header
and wired it back up.
Also rewrote the extra-notification summary, which named the "Device
status" channel but never said where to find it.
Reported via a Play Store review (vi, 4 stars, 5.2.3-rc0).
AirPods Pro 3 can answer a listening mode write with 0x0D 0x01 (Off) while
audibly switching to the requested mode. Seen on firmware
81.2675000075000000.6503, intermittently, and not reproducible on demand.
CAPod took that report at face value: it surfaced an Off button that isn't even
in the device's listening mode cycle, selected it, and said nothing about the
request not having been confirmed.
- visibleAncModes no longer re-admits a mode purely because it is the current
one. That escape clause was what conjured the extra button.
- effectiveAncMode keeps showing the requested mode while our own request is
outstanding and the device reports a mode it should not be able to reach.
- A rejected listening mode request now surfaces a message for every mode, not
only Off. Other modes were dropped silently.
- The Allow Off inference ignores an Off report that arrived while a different
mode was pending, so a single glitch cannot permanently persist "Off is
allowed" into the device profile. An unsolicited Off still trains it, which
is what keeps the option discoverable after it is enabled elsewhere.
- Verification deadline moved from a hardcoded 1000ms to 2000ms, and a matching
device report now settles the verification when it arrives. Measured reply
latency is 833-1008ms, so the old deadline sat inside the device's normal
spread and could fire a bogus divergence plus a redundant re-send.
Settling verification on arrival is limited to SetAncMode deliberately. Every
other verified command is optimistically written into state when it is queued,
so its predicate is satisfied immediately and only the device's contradicting
echo makes it fail. Settling those early would swallow the rejection.
Note that AirPods never report AllowOffOption (0x34) or ListeningModeCycle
(0x1A), so which modes are permitted is always inferred, never device truth.
Phase 1 of the Crowdin migration: the flavour copies still override this
for their own builds, so behaviour is unchanged while translations are
gathered against the new source string.
Both are now derived: the title is composed from app_name and the
flavour's upgrade_badge_label through app_name_upgraded_template, so the
pre-composed strings have no remaining reader.
Adds a review prompt card to the overview. On Google Play it uses the
in-app review flow, gated on the user having been Pro for a while, not
having dismissed it recently and not having reviewed yet. FOSS gets a
no-op implementation.
The card is the lowest priority item on the overview and stays hidden
while a permission, troubleshooter, background-monitoring-off or
no-profiles card is on screen.
LocalizedError can now carry a fix action, and the shared error dialog shows
it next to a Dismiss button when one is present; errors without a fix keep
today's OK-only shape. The Google Play billing-unavailable error uses it to
open Play's app info as a generic troubleshooting affordance.
An auto-created profile without a paired Bluetooth device resolves to MANUAL
mode, so nothing runs in the background while the dashboard claimed to be
monitoring. The dashboard now states that background monitoring is off and
offers to pick a paired device, and the per-card banner names what a missing
paired device costs.
Closes#658
"FOSS" is the flavor's name, not prose. Locale copies of the FOSS badge
and of the composed "CAPod FOSS" title had drifted - some translated the
word, one Thai entry into an unrelated word entirely - all of it shown as
the app's own brand.
Both base entries are now translatable="false" and every locale copy is
gone; with the flag set, any remaining locale entry would trip a fatal
ExtraTranslation in the release lint. The GPlay "Pro" badge is prose and
stays translated.
Replaces capod's older billing core, upgrade UI and their tests with the
canonical sdmaid-se stack at the pinned revision.
Core (gplay): BillingManager/BillingConnection/BillingConnectionProvider on
billing 8.3 with the centralized connect loop, merging purchases-listener
overlay and the canonical ack pipeline; the dying ack collector, the
ackedTokens gate and the in-billing foreground loop are gone. Full canonical
exception set (internal/network/offer-unavailable added), OurSku with capod's
product ids, BillingCache with snapshot()/episode-guarded stampLastProState.
FOSS: UpgradeControlFoss becomes UpgradeRepoFoss and exposes the canonical API
surface over capod's RETAINED FossUpgrade/FossCache schema — existing supporter
records must keep decoding.
Diagnostics: UpgradeDiagnostics + gplay/foss implementations, read by
RecorderModule next to CurriculumVitae's Pro history as two independent,
isolated header reads.
UI: canonical upgrade screens for both flavors under common/upgrade/ui with
capod chrome (M3 AlertDialog keeping rotation-safety, capod Scaffold, capod
previews). Nav.Main.Upgrade gains `forced`. Entitlement refresh moves to a
per-resume, unthrottled MainActivity call.
Strings reuse capod's existing translated ids wherever equivalent; only
referenced-but-missing ones are authored.
mockk 1.12.4 -> 1.14.9: 1.12.4 cannot synthesize a sealed-class return value
while recording, which the ported restore tests need.
* feat(upgrade): Add Pro status view, grace UI and sub-to-IAP switch
* fix(upgrade): Pad restore purchase to a minimum visible duration
* ui(upgrade): Mention Play-website install fix in restore troubleshooting
* fix(upgrade): Stop re-acknowledging already-acked purchases
* ui(settings): Move upgrade status row into the Other category
- Show the time until the case is full inside its charging chip, learned from
the case's own rising level with the existing charge-band model; no rating
exists for case charging, so the first charge learns before it shows
- Derive a case battery health from observed transfer efficiency: pod percent
gained per case percent spent while docked and unplugged, corrected by each
pod's own health, compared against Apple's "with charging case" totals
- Case data is only genuine while a pod is docked — both transports silently
freeze the last value otherwise. Battery updates now flag whether the case
entry is live, and BLE gains strict same-frame case accessors, so estimates
never learn from frozen echoes
- The case deliberately gets no runtime estimate: idle-then-burst drain has no
meaningful hourly rate
- Mark the case metrics as experimental in the Battery settings card
- Show a "Still determining — check back after a few more listening sessions"
placeholder under Battery Health in the device info sheet while the estimate
is still accumulating data, so the feature is discoverable from day one; no
placeholder for profiles without a paired device, where it could never resolve
- Reorder the Battery card: time remaining & health settings first, then a
divider, then the charging-side settings (charge limit, charged notification)
- Mention the battery-health component in the estimate toggle and reset texts
- Ground the charge-band boundaries in Apple's documented fast-to-80%/trickle
charging behavior and standard lithium CC/CV charging references
- The gauge line under the percentage now always shows the runtime estimate
("if used now") even while charging; the time-until-charged moved into the
charging chip itself ("Charging · 25m"), so the two can't be confused
- Battery health is now computed and shown per pod (Left/Right paired row in
the info sheet, mirroring the serial rows) — single-pod listening habits or
a replaced earbud make the sides genuinely diverge, and a combined figure
would mask a failing pod
- While a pod charges, fit its rising level and show the time until full in the
gauge instead of the runtime estimate; learned charge rates are persisted per
slot so the ETA appears immediately on later charges
- Suppress the charge ETA during Optimized Battery Charging holds, the final
trickle phase, and whenever the level stalls longer than one visible step
should take (granularity-aware: 1% AAP steps vs 10% BLE steps)
- Clear a slot's fit window when its readings switch between AAP and BLE — the
granularity jump would otherwise read as a fake level step
- Derive a battery-health percentage (median of accumulated drain rates vs the
model's rated life) and show it in the device info sheet; the info button now
also appears for BLE-only devices once health data exists
- Tag learned rates with the model they came from so re-pointing a profile at
different hardware starts learning fresh instead of inheriting foreign rates
- Track how many sessions blended into each learned rate and require three
before a health figure is shown
- Replace the global estimate toggle with a per-device toggle stored on the profile
- Seed the estimate from each model's rated battery life and show it immediately, using
the rating as a hard upper bound on displayed life while the measured rate converges
- When the ANC mode is unknown, seed from the shorter of a model's ANC-on/off ratings
- Show a projection while charging ("if used now") without ever learning from a rising battery
- Consolidate charge limit, "notify when charged", the estimate toggle and reset into one
Battery card; the charge notification now works for any live device, not only classic
audio connections
- Smooth the displayed time asymmetrically (drop fast, rise slow) so a faster-than-rated
drain stops over-promising within a couple of updates
Adds a dashboard hint card pointing at the existing Troubleshooter when a profile is connected to the system (audio) but CAPod is receiving no live data — the symptom of a phone dropping AirPods BLE broadcasts (e.g. some HyperOS devices, #603). Debounced ~15s so it doesn't flash during the gap between an audio connection and the first broadcast, and suppressed whenever any pod is live so it never claims 'no data' while data is visibly arriving.
Reacts to the AirPods speaking-detection event (AAP 0x4B): lowers media volume by a configurable amount or pauses playback when you start talking, and reverts when you stop. Per-profile, Pro-gated, opt-in (default off).
Decodes the speaking status from the last payload byte ({1,2}=start, {6,8,9}=stop, else keep-alive); engage/disengage with a frame-idle stale timeout to recover a dropped stop, plus disconnect and service-stop cleanup.
Replaces the origin-tracking machinery with a simpler model that matches Apple's iOS/macOS behavior: auto-play strictly resumes a CAP-dispatched pause, gated by a sticky boolean cleared on inactive→active transitions. The original fire-on-cold-wear behavior is preserved as a per-device opt-in 'Start music on wear' setting.
Sync flags with what the BLE classes actually report and what iOS exposes:
- AirPods Gen 1/2/3: enable hasEarDetection (already parsed via DualApplePods) and hasEarDetectionToggle
- AirPods Gen 3, Pro 1: enable hasEndCallMuteMic (force-sensor stems)
- Powerbeats Pro, Beats Fit Pro: enable hasEarDetectionToggle (iOS exposes it)
- Beats Solo Pro, Studio 3: drop hasEarDetection (over-ear, BLE class is bare SingleApplePods)
- FAKE_AIRPODS_GEN1/2/3: enable hasEarDetection to match HasEarDetectionDual
- Generalize microphone mode description from 'AirPod' to 'earbud'
Tests rewritten as exhaustive set assertions plus implication invariants.
Replaces the user-facing scanner mode setting with an automatic policy that picks LOW_LATENCY when a profile-paired device is connected, BALANCED in the foreground, and LOW_POWER in the background. The TroubleShooter scopes a temporary LOW_LATENCY override via a refcounted withTemporaryOverride block so overlapping callers stay correct.
Fixes a regression where the controller could block BLE scanning entirely if BLUETOOTH_CONNECT was missing or the HEADSET profile proxy stalled, and adds a reactive bondedDeviceAddresses flow so bond changes propagate without waiting for an unrelated input. Cleans up the now-dead scanner mode strings across all locales and unused ScannerMode fields.
- Hide reactions and AAP sections unless device is classically connected
- Move advanced-settings-unavailable card to the bottom of the list
- Show 'device not nearby' infobox when out of range
- Show missing-paired-device banner with edit-profile action
- Replace pending banner with snackbar on user-initiated change
Adds a per-battery 'Optimized' chip on the overview card when pods report wire value 0x05 (CHARGING_OPTIMIZED), which was already decoded but collapsed into a plain 'Charging' in the UI. On AirPods Pro 3, also adds a user-facing toggle for the device-side Optimized Charge Limit (AAP setting 0x3B).
- Decode setting 0x3B via decodeAppleBool so unknown values fall through instead of coercing to false
- Bypass ear-detection queue for SetDynamicEndOfCharge so the toggle works while pods sit in the closed case
- Expose per-slot ChargingState? on PodDevice; StatusChipRow renders 'Optimized' for CHARGING_OPTIMIZED, 'Charging' for CHARGING
- New BatteryCard in device settings with experimental warning (pattern matches Sleep Detection)
- Generic settingRejectedEvents flow alongside the existing offRejectedEvents so the toggle can show a dedicated snackbar on verification failure