Skip to content

Mobile app parity

The Apple and Android phone apps share one product experience. iOS is the UX reference; Android follows its task grouping, terminology and state semantics with native Compose controls. The source baseline reviewed here is landed main through a43d857 plus the 5 November 2026 ASK/bonded-only changes, rather than an old unmerged app branch.

Shared phone behaviour

Area Current contract
Navigation Control, Colours and Settings; frequent lighting controls on Control, palette management on Colours, advanced configuration/diagnostics in Settings.
Device state Fresh coherent state before edits; exact command confirmation; pending/applied/saving/save-failed and cached/live states remain distinct. Polling doesn't reset edits.
Catalogue Protocol 2, authenticated hash-checked descriptors, per-session property discovery, stable unsigned IDs, capability/binding gates. No guessed values for unavailable sensors or controls.
Lighting Same effects/palettes/overlays, temporary flashlight with independent brightness, output tuning, calibration and legacy/fixed-v2 wiring semantics. Clients send unscaled artistic RGB.
Diagnostics Matching power restrictions, motion backend, light/ear sensors, timed audio capture, five-second recordings, firmware warnings and explicit recent-log exports. Detailed pages are capability driven.
Audio export Explicit record/download/cancel/clear and native save/share of WAV plus JSON sidecar. No phone microphone access, implicit recording or automatic upload.
Local simulation Authenticated layout plus canonical C renderer; compact Control preview, phone/manual motion, Centre preview, pause/cache and isolated Audition. V1 uses the same band-mounted 85° apex, 5° short top edge and uniform LED pitch on both phones; v2 uses imported geometry. No headset frame stream or remote audition commands.
Connection Explicit bonded LE Secure Connections marker, OS numeric comparison and headset click; legacy Security 2 and unmarked plaintext rejected. One active controller, fresh state on reconnect and no ambiguous action replay.
Reconnection Opening the app reconnects to the saved ears. An explicit connection choice is remembered even if its first connection fails. iOS can restore a sole authorised accessory when its saved target is missing; it never guesses among several devices.
Cancellation Cancel ends pending connection/setup work and restores usable controls. It suppresses reconnect for the current visit; reopening after backgrounding or an explicit connection action permits another attempt. Saved metadata and OS bonds remain.
Updates Pages release selection or local application file, family/chip/slot checks, explicit install, confirmed write progress, cancellation and retry. Dedicated recovery routes directly to the phone updater.
AP setup Bonded normal/recovery password setup with transient fields, no password readback, storage-schema protection and OTA exclusion.
Support Offline policy/notices and explicit GitHub/email/Save/Copy routes from welcome, Settings and recovery. Matching bounded reports, default-off local counters and restrained suggestions; no automatic support uploader.

The detailed contracts live in local control, catalogue, BLE security, BLE OTA, simulation and support reporting. Firmware remains authoritative for validation, saved state, rendering and current limits. Both phones reject ESP32-C6 images and distinguish v2 8/16 MB families.

Intentional platform differences

  • Native widgets: Apple uses SwiftUI wheels/swipes and the system colour picker; Android uses Material selectors, explicit palette actions and an RGB hex/channel editor. Operations, constraints and confirmation semantics agree.
  • Apple windows and bars: iPhone/iPad and Catalyst use a single app window to preserve exclusive ownership of the shared Bluetooth session, accessory setup and Watch bridge. System multitasking and resizing remain available. Apple toolbar actions use native titles/symbols for horizontal and vertical bars; this changes presentation, not the shared phone tasks or control semantics.
  • Background connection: Apple retains an authenticated ordinary phone link while iOS permits, with interaction paused. Android normally releases BLE; an explicit notification-backed service opt-in can retain it and make at most three fresh reconnect attempts within 30 seconds. Neither guarantees process survival.
  • OTA lifecycle: Apple requires foreground. Notification-enabled Android OTA can continue through app switching/lock with a scoped wake lock; otherwise it is foreground-only. Android reconnects to check the running family/version after upload; Apple records selection and asks the user to reconnect. Selected bytes are never presented as a verified healthy boot.
  • Discovery and saved setup: iPhone/iPad use AccessorySetupKit with one-time saved-UUID migration before CoreBluetooth. Android prefers Companion Device Manager system setup, with explicit direct BLE scan fallback. Catalyst retains native discovery. CDM association is distinct from bonding/authenticated control; cancellation never starts fallback scanning. Existing Android background/ownership rules remain; no companion presence service or background privileges are requested. Both phones save non-secret device associations and rely on OS bonds. Android migrates old credential membership and removes obsolete encrypted strings; app backup/device transfer remain disabled. ASK authorisation is Apple-only. On iOS 26.1+, new ASK results use the advertised hardware name as the primary title; iOS 26.0 keeps the basic system picker. Existing OS-managed names are retained. Android keeps its native CDM chooser. Android bonding completes before reading protected endpoint descriptions; neither app accepts missing descriptions.
  • External handoffs: Apple uses MessageUI (subject-only mailto fallback), FileDocument and a local expiring clipboard. Android uses SENDTO, system document pickers and scoped content URIs. Exported copies have destination-owned lifetimes.
  • Watch: automatic non-secret selected-device context, direct compact control, independent pairing, WatchConnectivity ownership release and the static launcher are Apple-only. Detailed editors, full mounting calibration, simulation, recording, reporting and OTA stay on phone. No Wear OS app or cross-platform automatic handoff is implied. Setup arriving after the Watch app opens connects while active, or waits until active if dimmed; background delivery never acquires BLE. Ordinary dim/active transitions don't reacquire control after release.

See the Apple ownership guide and Android ownership guide.

Deferred ports and qualification

No phone implementation port is outstanding in the reviewed source. Physical acceptance is still required for pairing/bond persistence, CoC/GATT interoperability, Watch ownership, mounted sensing, provider exports and OTA/rollback. Source parity, compilation and emulator interactions establish different things.

Dated records retain the scope of each run, including older blocked Apple simulator checks and later successful reruns. Those older environment failures are not current implementation blockers:

Future deferred ports must record the source commit/paths, expected behaviour, affected counterpart, reason and required checks. Close them with implementation and validation evidence, or document an accepted permanent difference above. The repository policy requires inspecting both phones when changing shared behaviour; an old baseline is not proof of present parity.

Optional analytics and crash reporting

Both phone apps offer Allow analytics and crash reports? on first opening, with explicit Allow / Don't allow choices, collection off before a saved opt-in, offline policy access and one Share analytics and crash reports switch in Settings and disconnected Support. Unknown/corrupt choices never initialise Firebase. No custom user IDs, raw logs or accessory identity are attached. Both report the same two allowlisted connection/firmware result events with categorical timings; see the usage event contract. Generated SDK identifiers and diagnostics remain pseudonymous and require disclosure. Implementation: Android c802115 and Apple b03bb1a, based on landed main (0d2e9a9). Android paths are TelemetryConsent.kt, FirebaseTelemetry.kt, TelemetryScreen.kt and the app manifest; Apple paths are espurr/Telemetry.swift, EspurrControl/Sources/EspurrControl/TelemetryConsent.swift and phone/Catalyst Info plists.

Intentional platform differences:

  • Android includes Java/Kotlin and NDK Crashlytics with automatic reporting during consented runs. Revocation confirms Turn off and close, blocks new operations and exits the process after disabling collection/background upload entry points. The next declined launch never starts Firebase. This closes Android SDK workers whose collection-off setting otherwise applies only on a later run.
  • Apple phone/iPad/Catalyst use FirebaseAnalyticsCore without identity support and Crashlytics in permanent manual-upload mode. Only continuously allowed cold starts send previous reports; opting out disables Analytics and prevents later send calls, without terminating the app. Watch/widgets remain Firebase-free.
  • Apple firmware telemetry ends at selected; Android ends at post-reboot verified or an explicit uncertain/failure outcome. Those are different outcomes, not interchangeable success metrics. Demo/Watch/widget events remain excluded.
  • Both purge SDK-owned queues before a fresh consent period, keep failed cleanup pending across restarts, and cannot recall reports already transmitted/in flight. Advertising collection/consents and automatic screen reporting are disabled; Apple also disables IDFV. Apple's global developer-analytics switch governs the independent Apple pipeline and is not available as a Firebase consent API.

The complete data, revocation and store-disclosure contract is in support reporting.