Wearables Helpers
System Intent
- What this is:
WearablesHelpers is a stateless utility object (Android: object WearablesHelpers in WearablesHelpers.kt; iOS: class WearablesHelpers in WearablesHelpers.swift) that wraps the Meta Wearables SDK for two purposes: (1) answering whether any device is registered (in the Meta SDK device list), and (2) answering whether any registered device is available (has an active Bluetooth connection). These two questions read from different SDK state sources and must never be conflated.
Mermaid Diagram
flowchart TD
subgraph "Meta SDK State (Android)"
devices["Wearables.devices\n(StateFlow of List)\nCleared on unregistration"]
metadata["Wearables.devicesMetadata\n(FlowMap keyed by device ID)\nPersists on unregistration,\nonly LinkState changes"]
end
subgraph "WearablesHelpers API"
isReg["isAnyDeviceRegisted()\nReads: Wearables.devices.value.size > 0\nAnswers: is any device in the SDK device list?"]
isAvail["isAnyDeviceAvailable()\nReads: devicesMetadata.any { LinkState.CONNECTED }\nAnswers: is any device actively connected?"]
end
subgraph "Callers"
settings["Settings screen\nregistration toggle UI"]
picker["Device picker\nrecording-device-preference hydration"]
captureSession["capture-session.ts\nstartStreamSession()"]
end
devices -->|"authoritative for registration"| isReg
metadata -->|"authoritative for connection state"| isAvail
isReg --> settings
isAvail --> picker
isAvail --> captureSession
Flows
Flow: isAnyDeviceRegisted
- Core files:
- Android:
main/app/wearables-module/android/src/main/java/expo/modules/wearablesmodule/WearablesHelpers.kt - iOS:
main/app/wearables-module/ios/WearablesHelpers.swift - Test files: none (called by
WearablesModule.isAnyDeviceRegisted() which is tested via settings-screen tests)
Types
isAnyDeviceRegisted() -> Boolean
Synchronous. No arguments.
Returns true if the Meta SDK device list contains at least one device.
Returns false on any exception (defensive — Meta SDK state access can throw before configure()).
Paths
| path | input | output | path-type | notes |
isAnyDeviceRegisted.has-devices | Wearables.devices.value is non-empty | true | happy path | At least one device has been registered via Meta SDK |
isAnyDeviceRegisted.no-devices | Wearables.devices.value is empty | false | happy path | SDK device list empty (never registered, or unregistration completed) |
isAnyDeviceRegisted.sdk-throws | SDK access throws (e.g. before Wearables.configure()) | false | error | Try-catch returns false as safe default |
Pseudocode
// Android
isAnyDeviceRegisted():
try:
return Wearables.devices.value.size > 0
catch:
return false
// iOS (uses registrationState property, not device list directly)
isAnyDeviceRegisted():
guard sdkConfigured else: return false
return Wearables.shared.registrationState == .registered
Critical invariant — Android: Wearables.devices is the correct source. Wearables.devicesMetadata MUST NOT be used for registration state. When the Meta SDK processes an unregistration, it clears Wearables.devices but metadata entries persist with an updated LinkState. Checking devicesMetadata.any { != null } would return true for an unregistered device that still has a metadata entry. See bug log: docs/bugs/2026-05-10-glasses-registration-state-mismatch-after-unregister.md.
Flow: isAnyDeviceAvailable
- Core files:
- Android:
main/app/wearables-module/android/src/main/java/expo/modules/wearablesmodule/WearablesHelpers.kt - iOS:
main/app/wearables-module/ios/WearablesHelpers.swift
Types
isAnyDeviceAvailable() -> Boolean
Synchronous. No arguments.
Returns true if any device metadata entry has LinkState == CONNECTED (Android) or linkState == .connected (iOS).
Paths
| path | input | output | path-type | notes |
isAnyDeviceAvailable.connected | Any metadata entry has LinkState.CONNECTED | true | happy path | Glasses are Bluetooth-connected and ready for capture |
isAnyDeviceAvailable.not-connected | No metadata entry has LinkState.CONNECTED | false | happy path | No active Bluetooth connection |
Pseudocode
// Android
isAnyDeviceAvailable():
return Wearables.devicesMetadata.values.any { flow ->
flow.value?.linkState == LinkState.CONNECTED
}
// iOS
isAnyDeviceAvailable():
guard sdkConfigured else: return false
for deviceId in Wearables.shared.devices:
if let device = Wearables.shared.deviceForIdentifier(deviceId):
if device.linkState == .connected: return true
return false
Critical invariant: devicesMetadata is the correct source for connection state. It is NOT cleared on unregistration — entries persist with LinkState updated (e.g. to DISCONNECTED or UNKNOWN). This means isAnyDeviceAvailable() correctly returns false after unregistration even though metadata entries still exist, because LinkState will no longer be CONNECTED.
SDK State Source Reference
| Question | Authoritative Source | Notes |
| Is any device registered? | Wearables.devices (Android), Wearables.shared.registrationState (iOS) | Cleared/updated to unregistered on startUnregistration() |
| Is any device connected? | Wearables.devicesMetadata (both platforms) | Persists on unregistration; LinkState changes to DISCONNECTED/UNKNOWN |
Do not use devicesMetadata to answer registration questions. A metadata entry exists as long as the device has ever been paired, regardless of current registration status.
Logs
| Source | Location |
| Android | Logcat tag WearablesModule (called via WearablesModule.isAnyDeviceRegisted() / isAnyDeviceAvailable()) |
Deployment
- Mechanism:
local only (compiled into the iOS/Android app as a native module) - Deploy command:
# iOS
npx expo run:ios
# Android
npx expo run:android
- Notes: Android
isAnyDeviceRegisted() wraps the call in a try-catch because Wearables.devices can throw if the SDK is accessed before Wearables.configure() is called. iOS guards with sdkConfigured flag (set by WearablesHelpers.markConfigured() after SDK initialization) for the same reason. The mock state (mockRegisteredDevices, mockAvailableDevices) on iOS is used only when the MWDAT_SDK_AVAILABLE compile flag is absent (test builds without the SDK).