initialConnect() only ran on profile changes, so if the L2CAP connection failed at service start (device not yet connected), it was never retried. Now also triggers on connectedDevices changes.
Also fix reconnect BLE address comparison: was comparing BLE RPA with bonded BR/EDR address (never matches), now uses profile address.
Decode cmd 0x0006 as EarDetection with per-pod placement (IN_EAR, NOT_IN_EAR, IN_CASE, DISCONNECTED). Map AAP primary/secondary to left/right using BLE primary pod bit.
Queue ANC mode changes when no pod is in ear, auto-send when a pod goes in ear. Show pending mode in UI with secondary color treatment.
Debounce device-initiated ANC mode cycling during ear transitions. Skip debounce for user-initiated commands and initial handshake. Optimistic UI update on send for instant feedback.
Move BLE key and AAP connection icons into the SignalBadge pill.
Add BleKeyState enum on PodDevice to expose IRK/ENC state cleanly.
Key icon: outlined for IRK-only, solid for IRK+ENC.
Bluetooth icon shown when AAP transport is active.
Solo Pro (0x0C20), Solo 4 (0x2520), Solo Buds (0x2620), Studio Buds (0x1120), Studio Buds+ (0x1620), Studio Pro (0x1720). Tests use handcrafted data pending real captures.
Move BLE-specific code (snapshots, devices, factories, protocol) under apple/ble/. Move AAP code under apple/aap/ with protocol/ subdirectory. PodModel stays in apple/ as the only shared type.
Standardize the formatting of pod models and their feature sets. This improves the readability of device-specific capabilities and ensures cleaner diffs for future hardware additions.
Full integration of the Apple Accessory Protocol (AAP) over L2CAP, enabling direct communication with AirPods for 1% battery granularity, ANC mode control, Conversation Awareness toggle, and private key exchange for BLE encrypted battery.
Complete type rename chain: PodDevice (interface) becomes BlePodSnapshot, MonitoredDevice (facade) claims PodDevice name, Model enum extracted to top-level PodModel. Delete redundant type alias files. Fix stale comments and log tags.
Replace direct PodDevice/PodMonitor usage with MonitoredDevice/DeviceMonitor across ViewModels, UI screens, notifications, widgets, reactions, and service. All interface cast-based property access replaced with flat MonitoredDevice properties. Make L2capSocketFactory injectable.
Introduce type aliases for the planned rename (PodDevice -> BlePodSnapshot, PodMonitor -> BlePodMonitor). New code uses the aliases to clarify BLE-specific types vs the unified MonitoredDevice facade.
Full codebase rename deferred to IDE refactoring pass (103 files, 470 occurrences).
MonitoredDevice unifies BLE and AAP data sources with dynamic resolution.
DeviceMonitor combines PodMonitor + AapConnectionManager into a single Flow.
To be renamed to PodDevice/PodMonitor when the old types are renamed to BlePodSnapshot/BlePodMonitor.
BluetoothHeadset.connect() requires MODIFY_PHONE_STATE on modern Android, which is a system-only permission. Detect the SecurityException and stop further attempts instead of retrying every second.
Wrap super.onCreate() (Hilt injection) in try-catch to prevent
DI failures from crashing the process. The service is already
foreground at this point, so a graceful stopSelf() satisfies the
FGS timeout requirement without killing the app.
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.
Calling startMonitor() in App.onCreate() started the foreground service timeout
clock before the BroadcastReceiver even ran, consuming timeout budget
with post-init work and receiver processing. The service is already
started by BluetoothEventReceiver, BootCompletedReceiver, and
OverviewViewModel, making this call redundant.
Allow the app to scan for AirPods when only BLE scan permissions are granted, without waiting for all optional permissions (notifications, overlay, etc.).
Add isScanBlocking flag to Permission enum. Gate monitor service and pod scanning on scan permissions only. Show scan-blocking permission cards with error color and sorted first. Wrap BLUETOOTH_CONNECT-dependent calls in try-catch for graceful degradation. Fix POST_NOTIFICATIONS minApiLevel from S (31) to TIRAMISU (33).
The keep button's onClick handler was already named onKeep but the label said Close. Renamed to Keep for clarity alongside the Delete/Share buttons. Added dedicated string resource with translations for all 75 locales.
Remove Moshi dependency entirely, completing the migration to kotlinx.serialization. All JSON serialization now uses kotlinx with explicit @SerialName annotations for wire format stability.
- Migrate PodDeviceCache from Moshi to kotlinx Json injection
- Add MapIntByteArrayBase64Serializer for BleScanResult cache compat
- Strip @JsonClass/@Json annotations from all dual-annotated classes
- Delete Moshi adapters, ProGuard rules, and build config
- Convert compat tests to pure kotlinx round-trip tests
The debug recording trigger file now stores the session directory path and start timestamp. On app restart, the recorder resumes into the same session directory and log file instead of creating a new one, preserving the original start time so the short-recording guard doesn't reset.
Replace all 11 instances of MaterialAlertDialogBuilder across 5 files with Compose AlertDialog. Use sealed dialog state per screen to prevent dialog stacking. Add shared ConfirmationDialog composable. Delete dead ErrorDialog.kt.
Parse creation time from the embedded filename timestamp instead of relying on filesystem attributes, which are non-deterministic and caused flaky test failures in CI.
After sharing a debug log or sending a contact form email, show a dialog on return asking if the send was successful. Confirming deletes the log session and closes/navigates back.
Replace Array<String> with List<String> in Zipper.zip().
Add global -opt-in flag for ExperimentalMaterial3Api in build.gradle.kts
and remove per-file @OptIn annotations from 12 Compose screens.
Replace sessions.first() inside fsMutex.withLock with synchronous volatile
read to avoid potential deadlock. Wrap file deletions in IO dispatcher.
Log partial delete failures. Make pendingAutoZips thread-safe via
Collections.synchronizedSet. Add test for ext/cache same-basename IDs.