fix(ios): surface devices after camera-permission grant; Android parity#14
Conversation
The plugin only ever observed the device selector's active-device stream, never the SDK device list itself. On iOS, Wearables.shared.devices is a cold snapshot that stays warm only while something subscribes to devicesStream(), and glasses surface in that flow only *after* a camera-permission grant. With no subscriber, the pre-registration AutoDeviceSelector stayed permanently blind and the snapshot read empty, so startStreamSession failed with noEligibleDevice and getDevices() returned 0 even with paired, worn glasses. iOS: - Keep a lifetime devicesStream() subscription (Meta's canonical pattern, started right after configure()) that warms a device-list cache and deterministically re-warms a blind selector when devices appear. - Relaunch that subscription on the disconnect/re-register recovery path. - requestCameraPermission now waits (bounded) for a device to resolve on a grant, so an immediately-following startStreamSession / getDevices doesn't race discovery; returns early on a routine re-check with no device present. - getDevices() reads the warm cache (snapshot fallback only pre-first-emit). - Add a re-entrancy guard to rebuildDeviceSelectorIfBlind (now has concurrent callers). Android (parity): requestCameraPermission re-kicks monitoring and bounded- waits for the selector to resolve. Android's device list is a hot StateFlow that stays warm and the selector tracks it, so no device-list subscription is needed there. Docs: requestCameraPermission documents the bounded post-grant wait. Bumps to 0.6.1. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses PR review (P1): on the already-granted path, awaitDeviceAfterPermissionGrant(returnEarlyIfNoDevices: true) used knownDeviceIds.isEmpty as "no device", but an empty cache before the devicesStream() subscription delivers its first value means "unknown", not "none". That let the fast path return before the device list arrived and the selector warmed, racing the first emission. Track didReceiveDeviceListEmission (set on each emission, reset when the subscription is relaunched) and gate the early return on it. A pre-first- emission empty cache now falls through to the bounded wait instead of returning. Fresh-grant path is unaffected (it never early-returns). Android needs no change: it reads Wearables.devices.value, a hot StateFlow that always carries a current confirmed value. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Good catch on the P1 — fixed in 2509544.
The fresh-grant path is unaffected (it never early-returns), and Android needs no equivalent change — it reads Verified |
Problem
Reproduced on iOS (0.6.0, real glasses, 2 paired+worn pairs): after the user accepts the Meta AI camera-access prompt, the SDK reports no eligible device —
startStreamSessionfails withnoEligibleDevice,activeDeviceStreamnever emits a non-nil device, andgetDevices()returns0.Root cause
The plugin only ever observed the
AutoDeviceSelector'sactiveDeviceStream()— it never subscribed toWearables.shared.devicesStream()/addDevicesListener. On iOS,Wearables.shared.devicesis a cold snapshot that stays warm only while something is actively collectingdevicesStream(), and glasses surface in that flow only after a camera-permission grant. The selector is created at registrar time (pre-registration) and can stay blind; with nothing watching the device flow, the post-grant emission was observed by nothing, so the selector never resolved and the snapshot read empty. Meta's canonical iOS sample keeps adevicesStream()subscription alive fromconfigure()onward — that was the missing piece.Android is structurally different:
Wearables.devicesis a hotStateFlow(confirmed against the decompiledmwdat-core-0.7.0.aar) andDeviceSelectorBasederives itsactiveDeviceStateas aStateFlowfrom it, so the selector self-resolves and never goes permanently blind. The cold-snapshot bug doesn't exist there — only the post-grant timing needs parity.Changes
iOS (
MetaWearablesDatPlugin.swift)startDeviceListMonitoring()— lifetimedevicesStream()subscription started right afterconfigure(); warms aknownDeviceIdscache and deterministically re-warms a blind selector when devices appear.restartActiveDeviceMonitoring(disconnect/re-register) recovery path.requestCameraPermissionwaits (bounded, 8s) for a device to resolve on a grant; returns early on a routine re-check when no device is present.getDevices()reads the warm cache (snapshot fallback only before the first emission).rebuildDeviceSelectorIfBlind(now has concurrent callers).Android (
MetaWearablesDatPlugin.kt) — parity:requestCameraPermissionre-kicks active-device/device-state monitoring and bounded-waits for the selector to resolve. No device-list subscription needed (hot StateFlow).Dart —
requestCameraPermissiondocuments the bounded post-grant wait.Bumps to 0.6.1 + CHANGELOG.
Verification
flutter build ios --no-codesign→ ✅ built:flutter_meta_wearables_dat:compileDebugKotlin→ ✅ successdart analyze→ no issuesNot yet validated
On-device check with 2 paired+worn pairs (accept prompt →
activeDevicenon-nil in seconds →startStreamSession(null)succeeds →getDevices()returns 2) — needs a maintainer with hardware.🤖 Generated with Claude Code