Commit Graph
701 Commits
Author SHA1 Message Date
darken 985b73754e fix: Eliminate Float? boxing from battery display and cache merge
Two SIGSEGV native crashes recurred on Android 10 in 5.1.4-rc0 inside JIT-cached code at toBatteryFloat+4 (popup) and mergeBatterySlot+40 (cache merge). Both functions had a boxed Float? unbox at function entry that R8 horizontally merged into stdlib host classes, where Android 10 ART JIT miscompiled the unbox.

Convert PodDevice battery getters to non-null Float with BATTERY_UNKNOWN sentinel and propagate primitive Float through every display/persistence consumer. mergeBatterySlot now takes primitive Float; toBatteryFloat and toBatteryOrNull are deleted. Add isKnownBattery and batteryProgress helpers used everywhere instead of scattered nullable checks. Raw live extraction in toCachedState avoids touching the unified getter so cached values aren't refreshed as live.
2026-05-06 11:54:25 +02:00
Matthias Urhahn ebfb034fdd fix(navigation): Defer onNewIntent navCtrl call until Compose attaches
Move consumeUpgradeExtra() out of MainActivity.onNewIntent and into a Compose-level LaunchedEffect that collects a new MainActivity.warmIntents SingleEventFlow.

onNewIntent could fire before the setContent{} lambda registered the back stack with NavigationController, leaving navCtrl.goTo(Nav.Main.Upgrade) to throw IllegalStateException("NavigationController not initialized"). Same race shipped a fix for in octi (d4rken-org/octi#284).
2026-05-04 21:42:53 +02:00
Matthias Urhahn 8a81d354e6 chore(screenshots): Improve dashboard and add-profile previews
Use AAP-connected mocks for the dark dashboard to showcase full connectivity badges (BLE + IRK + encrypted + AAP) and the ANC mode selector. Populate the add-profile preview with a demo name, model, and paired device. Add user-facing labels to the dashboard mock devices.

Refactor BluetoothDevice2 so name/address are primary constructor fields, letting previews construct one without a real Android BluetoothDevice. Bump the screenshot test JVM heap to 4g — the smoke batch (42 renders) was hitting the test executor's default ceiling.
2026-05-04 21:30:25 +02:00
darken 8b9f953818 feat(widget): Show profile name and nearby state when ANC widget is disconnected 2026-05-04 13:03:27 +02:00
darken 2821da434d fix(settings): Hide Microphone & Press controls when AAP isn't ready 2026-05-03 19:27:52 +02:00
darken 07c72dc86c fix(aap): Cancel hung BluetoothSocket connect on timeout
BluetoothSocket.connect() is a blocking JNI call that ignores coroutine cancellation, so the previous withTimeout in AapAutoConnect only cancelled the suspending wrapper while the native thread stayed pinned. Hung threads accumulated and could trigger ANRs.

Move the timeout inside AapConnection and run the blocking connect on a daemon thread; on timeout, close the socket from the caller thread to unblock the native call (the documented Android pattern for cancelling in-flight L2CAP connects).

Also cancel appScope before delegating uncaught exceptions so coroutines have a best-effort window to release resources before the system handler terminates the process.
2026-05-03 15:39:03 +02:00
darken 7d93e772b4 fix(monitor): Prevent NPE in cache merge from freezing device flow
Battery slot percent comparisons in mergeBatterySlot/hasStateChanged compiled to Intrinsics.areEqual on boxed Float; R8 optimization on Android 10/11 dropped a null check during inlining and the resulting NPE escaped onEach { persistLiveDevices }, cancelling the upstream combine and freezing every observer of DeviceMonitor.devices.

Comparisons now operate on primitive float (cmpg-float in dex) so no Intrinsics.areEqual call remains in the merge path. The persist loop also catches and reports per-profile, and AAP-only profiles with active DeviceInfo are now persisted even when no BLE pod is in range.
2026-05-03 14:20:49 +02:00
darken 82dc88faac fix: Harden uncaught exception safety net
Wrap logging, reporting, and Looper resume calls so the foreground service timing exception suppression cannot itself trigger another crash. Extract handler into a dedicated class with seams for unit tests.
2026-05-03 12:41:32 +02:00
darken 06b41ec8eb fix(monitor): Recover from revoked Bluetooth scan permission 2026-05-03 11:54:11 +02:00
darken acc6e92b2d feat(tile): Add ANC Quick Settings tile 2026-05-03 10:58:05 +02:00
darken 5a8c4b94ef fix: Correct wear detection on AirPods Max 2
Bit 5 of pubStatus is always set on A3454 and no longer carries the wear flag (unlike Max gen 1). Read bits 1 and 3 instead — the per-earcup sensors. OR rather than AND so phones that only see one bit reliably still report worn correctly.

Closes #548
2026-05-01 21:50:51 +02:00
darken fc98db09a2 fix: Correct PodModel feature flags for ear detection and call controls
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.
2026-05-01 20:08:29 +02:00
darken d04abdf80c General: Fix translation issues across multiple locales
Targeted character / escape / vandalism fixes:

- tr: %80 -> 80%% (UnknownFormatConversionException crash risk)

- pt: replace Cyrillic 'не' with 'não' in autoconnect description

- sk: fix 'ladUciaho' -> 'ladiaceho' (corruption)

- ms: restore 'Discord' brand name

- ca: 'maxima' -> 'minima' for signal_quality_title; fix 'suprimrira' typo

- zh-rTW: fix homophones and Japanese-kanji variants (4 strings)

- zh-rHK: Simplified -> Traditional for press_action_fast_forward

- zu: remove spurious 'Zn' prefix in 2 strings (vandalism)

- lv: fix wrong cedilla character in 3 Bluetooth a11y descriptions

- kmr-rTR: fix 4 encoding artifacts (a-feminine-ordinal substituted for e-circumflex)

- ko: fix 2 Hangul typos (conversation_awareness, allow_off)

Garbled translations removed (fall back to English source):

- hy-rAM: 13 press_controls/press_action strings

- mn-rMN: 2 strings (aap_unavailable_description, listening_mode_cycle_minimum)

- lt: settings_popup_warning_manual_mode

- mk-rMK: device_settings_allow_off_description (Syriac char vandalism)

- lo-rLA: signal_badge_ble_cd (English fallback was effectively unchanged)

- km-rKH: device_settings_microphone_mode_label (Thai script in Khmer file)
2026-05-01 05:19:34 +02:00
darken 0609352cc9 General: Update app translations from Crowdin 2026-05-01 05:19:34 +02:00
darken abde0b7d6a feat(reactions): Add discovery hint and outline settings icon 2026-04-30 17:46:27 +02:00
darken b952aff38d fix(widget): Re-render widgets reactively after initial session start
Glance only calls provideGlance() once per widget session; subsequent update() calls recompose the existing composition without re-running provideGlance. The previous capture-once approach left widgets frozen at initial state because the composition had no reactive State to read.

Subscribe to a widgetDeviceFlow(profileId) inside provideContent that pre-filters by WidgetDeviceKey, so the composition recomposes on visible state changes without firing on every BLE advertisement. Replace updateAll() with explicit per-GlanceId update() calls in WidgetManager, with a platform-id fallback when Glance returns no IDs.
2026-04-30 16:44:57 +02:00
darken c8f5ceda34 fix(monitor): Auto-tune BLE scanner mode by foreground state and connections
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.
2026-04-30 15:26:45 +02:00
darken e6dbd2d660 fix(reaction): Restore BLE-only autoplay and stop false reactions on app start
- Apply seenLastAt freshness to all unauthenticated BLE samples (worn and not-worn). The earlier scoping to not-worn-only collapsed the second worn sample for BLE-only autoplay confirmation, so the staged play never fired.

- Replace distinctUntilChangedBy with a manual filter so worn samples that need to reset an active pause debounce (count went up) can pass through even when the monitor key is otherwise identical.

- Skip BLE-only autoplay confirmation for trusted sources. With BLE_IRK_MATCH and AAP, autoplay now fires on the first not-worn -> worn transition, mirroring the pause-debounce skip on the same sources.

- Skip the reaction entirely when the previous emission had no live evidence (NO_LIVE_BLE). Prevents app-process-start from synthesising a fake not-worn -> worn transition and firing autoplay while the user is already wearing the pods. Same guard handles mid-session BLE gap recoveries.

- Add MonitorFlowTests covering process-start-worn, genuine-insertion-after-startup, mid-session BLE-gap recovery, IRK-matched immediate autoplay, BLE-only autoplay confirmation, 3-sample pause debounce, and rebound-tolerated debounce reset.
2026-04-29 18:14:51 +02:00
darken 228d7d1f0c fix(reaction): Tighten auto-pause debounce after review findings
- Commit pending pause when a trusted source (AAP / BLE_IRK_MATCH) corroborates the not-worn condition mid-debounce, instead of dropping pending silently.

- Scope debounceFreshness to not-worn samples only; identical both-in samples no longer pass distinctUntilChangedBy and can't accidentally trigger BLE-only auto-play confirmation.

- Add resetTolerance to PendingPauseDebounce so a single corrupt count-up advert no longer kills a legitimate pending pause; reorder reset checks so rawDecision.shouldPlay resets immediately.

- Drop bleKeyState from the INFO autoPause log; source already encodes trust without leaking key-configuration state to logcat.

- Add flow-level MonitorFlowTests verifying the distinctUntilChangedBy interaction with seenLastAt freshness, plus the #557-direction test (AAP-worn vs corrupt-BLE-not-worn) and rebound-tolerance test.

- Clarify in BLE_ANONYMOUS KDoc that the path is unreachable in production via DeviceMonitor.primaryDevice.
2026-04-29 18:14:51 +02:00
darken 67fead3589 fix(reaction): Debounce auto-pause for unauthenticated BLE sources
Classifies the ear-detection source (AAP / BLE_IRK_MATCH / BLE_PROFILE_FALLBACK / BLE_ANONYMOUS / NO_LIVE_BLE) and applies a 3-sample debounce only to unauthenticated BLE paths. AAP and IRK-authenticated BLE pass through unchanged.

Also tightens toEarDetectionState() to prefer AAP aggregate over BLE per-side bits whenever AAP EarDetection is present, and suppresses pause on NO_LIVE_BLE (cache-only state) to avoid firing without live evidence.
2026-04-29 18:14:51 +02:00
darken d0aab583d2 fix(presscontrols): Auto-exit when device disconnects 2026-04-28 22:39:42 +02:00
darken f67e8aa7d1 fix: Harden foreground service exception handler 2026-04-25 23:40:10 +02:00
darken bcb4bdff08 fix(overview): Prevent crash from duplicate LazyList keys 2026-04-25 23:39:59 +02:00
darken 9d06839a1c General: Update app translations from Crowdin 2026-04-25 21:58:10 +02:00
darken 97f294e5f7 fix(widget): Persist theme settings across device reboot
Closes #549
2026-04-25 20:39:40 +02:00
darken a40599c1dc feat(settings): Tailor device settings to connection state
- 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
2026-04-25 19:00:19 +02:00
darken 175aef9e2f feat(logging): Log raw advertisement and decoded payload bytes 2026-04-25 16:19:21 +02:00
darken 36ae35918a refactor(widget): Move WidgetDeviceKey into widget package 2026-04-25 16:19:21 +02:00
darken 60ad1943b6 fix(widget): Gate refresh on display fields, drop log to VERBOSE 2026-04-25 16:19:21 +02:00
darken 5e727e907d refactor: Drop per-emission flow logger and redundant widget logs 2026-04-25 16:19:21 +02:00
darken 5360f54ea8 fix(privacy): Stop logging AirPods encryption keys 2026-04-25 16:19:21 +02:00
darken 486efe6e65 refactor: Reduce debug log noise from redundant state emissions 2026-04-25 16:19:21 +02:00
darken c2ff1f111e chore(reaction): Rename sleep notification channel label 2026-04-25 10:21:30 +02:00
darken 26b72726a7 feat(reaction): Pause media when AirPods detect sleep 2026-04-25 10:21:30 +02:00
darken f35fe968c6 docs(aap): Document SET_BAND_EDGES as RF band gating 2026-04-24 17:54:53 +02:00
darken 498f040a57 feat(aap): Drop experimental warning on Optimized Charge toggle
Verified on a real AirPods Pro 3: toggling flips pod charging state from CHARGING_OPTIMIZED to CHARGING and persists across reconnects. Apple-bool wire format is confirmed, so the 'experimental' warning box is no longer warranted.
2026-04-24 17:27:48 +02:00
darken 59b3106c3b feat(aap): Surface Optimized Charge indicator and Pro 3 toggle
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
2026-04-24 17:27:48 +02:00
darken 89e8b83be4 docs(aap): Revise PME hypothesis to Personal Medical Equipment 2026-04-24 15:27:18 +02:00
darken f0531796dd feat(device-settings): Surface hardware, pending firmware, and bonded dates 2026-04-24 13:08:57 +02:00
darken a701fdfba7 feat(aap): Build a comprehensive AAP protocol catalog
Adopts the Wireshark AAP dissector (pabloaul/apple-wireshark) as a third reference source alongside LibrePods and MagicPodsCore. Catalogues every known message type and control/setting ID, corrects DeviceInfo field labels, and adds sealed AapPacket hierarchy with Connect Response parsing. Case Info probe (Pro 3), Sleep event, and Dynamic End of Charge decoders are in place for future use.
2026-04-24 08:39:46 +02:00
darken 118eaa8d08 feat(widget): Add 1x1 size support to battery widget
Introduce BatteryLayout enum with size-driven dispatch. Drops minWidth from 80dp to 40dp so the battery widget can be placed at one cell. At 1-cell-wide placements render a compact icon+percent stack; wider sizes keep the current NARROW/WIDE layouts.
2026-04-23 16:11:07 +02:00
darken 016914ec05 fix(overview): Replace red triangle with inline missing-pairing banner 2026-04-23 14:17:16 +02:00
darken 0aadec0b75 feat(device-settings): Link AAP-unavailable card to compatibility tracker 2026-04-23 12:53:24 +02:00
darken 7632ba9e0a fix(widget): Complete placement after upgrade purchase 2026-04-17 21:58:05 +02:00
darken fd20ef0500 fix(bluetooth): Keep connectedDevices retrying while BLUETOOTH_CONNECT is missing 2026-04-17 21:48:55 +02:00
darken 250d0ab154 fix(monitor): Gate AAP lifecycle on BLUETOOTH_CONNECT permission 2026-04-17 21:48:55 +02:00
darken 79886e0bfb General: Update app translations from Crowdin 2026-04-17 21:19:24 +02:00
darken 86b685f28b feat(widget): Add ANC control widget
New home-screen widget that toggles AirPods ANC modes (Off / ANC / Transparency / Adaptive) directly, with six adaptive layouts that pick based on widget size (QUAD_CORNERS, ROW_ICONS, COLUMN_ICONS, GRID_2X2, ROW, COLUMN). Consolidated battery+ANC configuration into a single ViewModel that detects widget type from AppWidgetManager. ACTIVE state uses Material3 secondaryContainer/onSecondaryContainer for guaranteed contrast. Includes live config preview, preview subtitle, stale-selection guard, device-label toggle, and aligned icons with the app's AncModeSelector.
2026-04-17 20:10:48 +02:00
darken bba9155573 feat(press-controls): Add media and noise-control actions
Adds Stop, Fast Forward, Rewind, Mute, Cycle Noise Control, and Toggle Transparency as gesture targets. Migrates StemAction from enum to sealed interface with polymorphic serialization to unlock future parameterized actions. ANC actions are gated per-device capability and reuse the existing listening-mode cycle mask.
2026-04-17 17:56:31 +02:00
darken ec087e4b07 feat(device-settings): Split stem actions into dedicated Press Controls screen
Move press timing, call controls, and stem mappings out of the Controls card into a new Press Controls screen. The screen name is device-agnostic so it applies to both stemmed AirPods and the AirPods Max's Digital Crown/noise button.

Stem mappings are now per-device (stored on the AppleDeviceProfile) rather than a single app-global DataStore. The Pro gate for mappings moves from screen entry to per-change with an Upgrade badge on the mappings card header, so free users can still access the non-Pro press timing and call-control settings that live on the same screen.
2026-04-17 15:26:05 +02:00