Recording Device Preference
Metadata
- System type:
library
System Intent
- What this is: A singleton in-memory store and persistence layer for the user's recording device preference (
"glasses","glasses-audio", or"phone"). It exposes React hooks for reading the preference reactively, imperative getters/setters for non-React consumers, and a hydration function that runs once on app launch to restore the saved preference fromAsyncStorage. On hydration, if a glasses-mode preference is stored but no glasses device is currently connected, the preference is overridden to"phone"in-memory only — the stored value is left intact so the glasses preference is restored on the next launch when glasses are connected.
Mermaid Diagram
flowchart TD
AppInit["AppInit (component)"] -->|"calls on init"| Hydrate["hydrateRecordingDevicePreference()"]
Hydrate -->|"getItem(STORAGE_KEY)"| AsyncStorage["AsyncStorage"]
AsyncStorage -->|"storedDevice | null"| Hydrate
Hydrate -->|"isAnyDeviceAvailable()"| WearablesModule["WearablesModule (native bridge)"]
WearablesModule -->|"isConnected: bool"| Hydrate
Hydrate -->|"setSelectedRecordingDeviceValue(device)"| Store["selectedRecordingDevice (in-memory)"]
Store -->|"emit()"| Listeners["subscribed React components"]
SettingsScreen["Settings Screen"] -->|"setSelectedRecordingDevice(device)"| Store
SettingsScreen -->|"setItem(STORAGE_KEY, device)"| AsyncStorage Flows
Global Types
RecordingDevice = "glasses" | "glasses-audio" | "phone"
StandardError {
message: string (human-readable description of what went wrong)
}
Flow: hydrateRecordingDevicePreference
- Core files:
main/app/lib/recording-device-preference.ts - Test files:
main/app/__tests__/recording-device-preference.test.ts
Types
HydrateInput {
// no arguments — reads AsyncStorage and WearablesModule internally
}
HydrateOutput {
// void — side-effects only: updates selectedRecordingDevice in-memory and emits to listeners
}
Paths
| path | input | output | path-type | notes |
|---|---|---|---|---|
hydrate.glasses-connected-saved-glasses | stored = "glasses", isAnyDeviceAvailable = true | selectedRecordingDevice = "glasses" | happy path | Glasses connected; stored glasses preference honoured |
hydrate.glasses-connected-saved-glasses-audio | stored = "glasses-audio", isAnyDeviceAvailable = true | selectedRecordingDevice = "glasses-audio" | happy path | Glasses connected; stored glasses-audio preference honoured |
hydrate.glasses-connected-saved-phone | stored = "phone", isAnyDeviceAvailable = true | selectedRecordingDevice = "phone" | happy path | User explicitly chose phone; kept even when glasses are connected |
hydrate.glasses-not-connected-saved-glasses | stored = "glasses", isAnyDeviceAvailable = false | selectedRecordingDevice = "phone" | happy path | Glasses not connected; override to phone in-memory only; AsyncStorage not updated |
hydrate.glasses-not-connected-saved-glasses-audio | stored = "glasses-audio", isAnyDeviceAvailable = false | selectedRecordingDevice = "phone" | happy path | Glasses not connected; override to phone in-memory only; AsyncStorage not updated |
hydrate.glasses-not-connected-saved-phone | stored = "phone", isAnyDeviceAvailable = false | selectedRecordingDevice = "phone" | happy path | Already phone; no change |
hydrate.no-saved-preference-glasses-connected | stored = null, isAnyDeviceAvailable = true | selectedRecordingDevice = "glasses" | happy path | No stored preference; default is glasses when connected |
hydrate.no-saved-preference-glasses-not-connected | stored = null, isAnyDeviceAvailable = false | selectedRecordingDevice = "phone" | happy path | No stored preference; default is phone when not connected |
hydrate.storage-read-error | AsyncStorage throws, isAnyDeviceAvailable = false | selectedRecordingDevice = "phone" | error | Error logged; falls through to connection-aware default |
hydrate.storage-read-error-glasses-connected | AsyncStorage throws, isAnyDeviceAvailable = true | selectedRecordingDevice = "glasses" | error | Error logged; falls through to connection-aware default |
Pseudocode
hydrateRecordingDevicePreference():
storedDevice = await AsyncStorage.getItem(STORAGE_KEY)
if isRecordingDevice(storedDevice):
isConnected = isGlassesDeviceAvailable()
// If stored preference is a glasses mode but glasses are not connected,
// override to phone for this session only. AsyncStorage is NOT updated —
// the user's stored preference is preserved for the next launch.
if (storedDevice === "glasses" || storedDevice === "glasses-audio") && !isConnected:
log recording_device_glasses_not_connected_override { storedDevice, override: "phone" }
setSelectedRecordingDeviceValue("phone")
return
setSelectedRecordingDeviceValue(storedDevice)
return
// No stored preference or read error — fall through to connection-aware default
fallbackDevice = getDefaultRecordingDevice() // calls isAnyDeviceAvailable()
if selectedRecordingDevice !== fallbackDevice:
selectedRecordingDevice = fallbackDevice
emit()
Flow: setSelectedRecordingDevice
- Core files:
main/app/lib/recording-device-preference.ts
Types
SetDeviceInput {
device: RecordingDevice
}
SetDeviceOutput {
// void — updates in-memory store, emits to listeners, persists to AsyncStorage
}
Paths
| path | input | output | path-type | notes |
|---|---|---|---|---|
setDevice.success | device: RecordingDevice | in-memory updated, listeners notified, AsyncStorage updated | happy path | Called from Settings when user changes preference |
setDevice.write-error | AsyncStorage.setItem throws | in-memory still updated; error logged; no throw | error | Preference change takes effect for the session even if persistence fails |
Flow: getSelectedRecordingDevice / useSelectedRecordingDevice
- Core files:
main/app/lib/recording-device-preference.ts
Types
Paths
| path | input | output | path-type | notes |
|---|---|---|---|---|
getDevice.sync | void | current selectedRecordingDevice | happy path | Imperative getter; safe to call from non-React code |
getDevice.hook | void (React component) | current selectedRecordingDevice, re-renders on change | happy path | Uses useSyncExternalStore; subscribes via subscribeRecordingDevicePreference |
Logs
| Source | Location |
|---|---|
| App (JS) | createFlowLogger("recording-device") |
Log steps:
| step | when |
|---|---|
recording_device_glasses_not_connected_override | Glasses-mode preference stored but glasses unavailable at hydration; in-memory override to phone. Payload: { storedDevice, override: "phone" } |
recording_device_preference_read_failed | AsyncStorage.getItem throws during hydration. Payload: { error } |
recording_device_preference_write_failed | AsyncStorage.setItem throws during setSelectedRecordingDevice. Payload: { error, device } |
Deployment
- Mechanism:
local only(shipped as part of the React Native app bundle) - Deploy command:
- Notes: JS-only module; no native rebuild required. The in-memory override on hydration is intentionally not persisted —
AsyncStorageretains the user's glasses preference so it is restored automatically on the next launch when glasses are connected.