Bug: Recording State Not Synced When Screen Regains Focus
Date: 2026-05-15
Severity: High (User-facing error)
Status: Fixed
Bug Description
When the user is recording a session with glasses and navigates away from the recording screen (e.g., to chat) and comes back to the memory feed screen, the UI shows the "start recording" button instead of the "recording in progress" icon, even though the native recording session is still active. Tapping the button results in an error: "session is already running."
The root cause is a state synchronization issue between the JavaScript recording-control-orchestrator (which tracks UI state) and the native wearables module (which tracks actual recording state).
Root Cause Analysis
The recording-control-orchestrator.ts maintains a local snapshot of recording state that is updated when: 1. User taps the record button (startRecordingFromControl()) 2. User taps the stop button (stopRecordingFromControl()) 3. Native module sends an event (via listener)
However, when the screen regains focus after navigation, there is no mechanism to re-query the actual recording state from the native module. If the local snapshot is reset or out of sync (which can happen during app lifecycle transitions), the UI will show the wrong state while the native module continues recording.
Affected Code Paths
- Memory screen (
main/app/app/index.tsx): UsesuseRecordingControlSnapshot()hook to render UI - Recording-control-orchestrator (
main/app/lib/recording-control-orchestrator.ts): Maintains JS-side state - WearablesModule (Android/iOS): Maintains actual recording state (has audio/frame recording jobs running)
Why It Happens
- The orchestrator snapshot is not restored when the screen regains focus
- No mechanism to query "is native still recording?" from JavaScript
- The UI hook returns stale snapshot, not knowing that native is still active
Solution
Added a three-part fix:
1. New Native Query Method
Added isRecordingActive() method to WearablesModule that queries the true recording state:
Android (WearablesModule.kt):
AsyncFunction("isRecordingActive") {
(audioRecord != null && audioReadJob?.isActive == true) ||
(frameRecordingJob?.isActive == true)
}
iOS (WearablesModule.swift):
AsyncFunction("isRecordingActive") { () async -> Bool in
return (self.audioEngine != nil && self.audioChunkBuffer.count > 0) ||
(self.framesDirectory != nil && self.frameCount > 0)
}
TypeScript Interface (WearablesModule.ts):
2. State Sync Function
Added syncRecordingStateFromNative() to recording-control-orchestrator.ts: - Queries the native module to check if recording is active - If native says "recording" but JS says "idle", syncs to "recording" - If native says "idle" but JS says "recording", syncs to "idle" - Logs all sync events for debugging
3. Focus Hook Integration
Added useFocusEffect() hook to MemoriesScreenContent (app/index.tsx): - Calls syncRecordingStateFromNative() when screen regains focus - Ensures UI state matches reality after navigation
Files Modified
main/app/wearables-module/src/WearablesModule.ts— AddedisRecordingActive()to interfacemain/app/lib/recording-control-orchestrator.ts— AddedsyncRecordingStateFromNative()function and exportmain/app/app/index.tsx— AddeduseFocusEffect()hook to sync state on screen focusmain/app/wearables-module/android/src/main/java/expo/modules/wearablesmodule/WearablesModule.kt— ImplementedisRecordingActive()main/app/wearables-module/ios/WearablesModule.swift— ImplementedisRecordingActive()
Testing
Manual testing scenario: 1. Start recording on memory screen 2. Navigate to chat screen 3. Navigate back to memory screen 4. Verify UI shows "recording in progress" icon (not "start" button) 5. Verify tapping stop button works without error
Verification
The fix ensures that: - UI state is always consistent with native state when screen regains focus - User cannot accidentally try to start a session that's already recording - No "session already running" error when returning to recording screen - Minimal performance impact (one async query on screen focus)
Edge Cases Handled
- User navigates away and back quickly: State is checked and corrected
- Native session unexpectedly stopped: UI syncs to idle state
- Multiple rapid navigations: Each focus re-syncs the state
- Missing
isRecordingActiveon older native module: Graceful skip with log