Skip to content

Recording Control Notifications

Metadata

  • System type: library

System Intent

  • What this is: A platform-forked notification system that shows a persistent, high-importance recording-controls notification while a capture session is active. On iOS it uses expo-notifications; on Android it uses a native foreground service (RecordingControlService). The notification surfaces Start/Stop action buttons so the user can control recording without opening the app.

Mermaid Diagram

flowchart TD
    Init[initializeRecordingControlOrchestrator] -->|iOS| IOSInit[initializeIOSNotifications]
    Init -->|Android| AndroidInit[WearablesModule.initializeRecordingControls]

    IOSInit --> Handler[setNotificationHandler\nshouldShowBanner: true\nshouldShowList: true]
    IOSInit --> Categories[setNotificationCategoryAsync\nidle + active categories]
    IOSInit --> Perms[requestPermissionsAsync]

    AndroidInit --> Channel[ensureNotificationChannel\nIMPORTANCE_HIGH\nencache_recording_controls_v6]

    StateChange[syncNativeControlState] -->|iOS| UpdateIOS[scheduleNotificationAsync\ntrigger: null]
    StateChange -->|Android| UpdateAndroid[RecordingControlService.startOrUpdate\nvia startForegroundService]

    UpdateAndroid --> Builder[NotificationCompat.Builder\nPRIORITY_HIGH\nFOREGROUND_SERVICE_IMMEDIATE]

Flows

Flow: initializeIOSNotifications

  • Core files: main/app/lib/recording-control-orchestrator.ts

Types

NotificationHandlerConfig {
  shouldShowBanner: true     (required — shows the banner in foreground)
  shouldShowList: true       (required — retains entry in Notification Center)
  shouldPlaySound: false
  shouldSetBadge: false
}

NotificationCategory {
  identifier: string         ("encache-recording-idle" | "encache-recording-active")
  actions: NotificationAction[]
}

Paths

path input output path-type notes
initializeIOSNotifications.success none handler registered, categories set, permission granted happy path iosPermissionGranted = true
initializeIOSNotifications.permission-denied none handler registered, categories set degraded iosPermissionGranted = false; no notifications shown
initializeIOSNotifications.category-error none error logged, execution continues error category registration failure is non-fatal

Pseudocode

setNotificationHandler({ shouldShowBanner: true, shouldShowList: true, shouldPlaySound: false, shouldSetBadge: false })
setNotificationCategoryAsync(RECORDING_IDLE_CATEGORY, [{ id:"start", opensAppToForeground:true }])
setNotificationCategoryAsync(RECORDING_ACTIVE_CATEGORY, [{ id:"stop", opensAppToForeground:true }])
{ status } = await requestPermissionsAsync()
iosPermissionGranted = status === "granted"
addNotificationResponseReceivedListener → applyExternalAction

Flow: updateIOSNotification

  • Core files: main/app/lib/recording-control-orchestrator.ts

Types

RecordingControlNotificationContent {
  title: string
  body: string
  actions: Array<{ id: "start" | "stop"; label: string }>
}

Paths

path input output path-type notes
updateIOSNotification.success RecordingControlNotificationContent notification rescheduled happy path old notification dismissed before new one scheduled
updateIOSNotification.no-permission RecordingControlNotificationContent no-op guard returns early if iosPermissionGranted === false

Pseudocode

if (!iosPermissionGranted) return
dismiss + cancel old notification (swallow errors)
scheduleNotificationAsync({ identifier: RECORDING_NOTIFICATION_ID, content: { title, body, categoryIdentifier, sound:false }, trigger: null })

Flow: ensureNotificationChannel (Android)

  • Core files: main/app/wearables-module/android/src/main/java/expo/modules/wearablesmodule/RecordingControlService.kt

Types

NotificationChannel {
  id: "encache_recording_controls_v6"
  name: "Recording Controls"
  importance: IMPORTANCE_HIGH      (heads-up / peek display)
  vibration: false
  sound: null
  badge: false
  lockscreenVisibility: VISIBILITY_PUBLIC
}

Paths

path input output path-type notes
ensureNotificationChannel.created none channel created with IMPORTANCE_HIGH happy path only runs on API 26+
ensureNotificationChannel.already-exists none no-op guard channel already registered for this ID
ensureNotificationChannel.pre-oreo none no-op guard Build.VERSION.SDK_INT < O

Flow: buildNotification (Android)

  • Core files: main/app/wearables-module/android/src/main/java/expo/modules/wearablesmodule/RecordingControlService.kt

Types

NotificationBuilderConfig {
  priority: PRIORITY_HIGH
  foregroundServiceBehavior: FOREGROUND_SERVICE_IMMEDIATE
  ongoing: true
  silent: true
  onlyAlertOnce: true
  visibility: VISIBILITY_PUBLIC
  category: CATEGORY_SERVICE
}

Paths

path input output path-type notes
buildNotification.with-actions title, body, actionIds[], actionLabels[] Notification with action buttons happy path action intents sent to RecordingActionReceiver
buildNotification.no-actions title, body, empty arrays Notification without action buttons valid idle state has a Start button; recording has a Stop button

Pseudocode

NotificationCompat.Builder(CHANNEL_ID)
  .setPriority(PRIORITY_HIGH)
  .setForegroundServiceBehavior(FOREGROUND_SERVICE_IMMEDIATE)
  .setOngoing(true)
  .setSilent(true)
  for each action → addAction(label, PendingIntent → RecordingActionReceiver)
return builder.build()

Flow: RecordingControlService.onStartCommand (Android)

  • Core files: main/app/wearables-module/android/src/main/java/expo/modules/wearablesmodule/RecordingControlService.kt

Paths

path input output path-type notes
onStartCommand.first-start Intent with title/body/actions startForeground(NOTIFICATION_ID, notification) happy path isForegroundStarted flipped to true
onStartCommand.update Intent with title/body/actions NotificationManagerCompat.notify(NOTIFICATION_ID, notification) happy path subsequent calls when service already running

Notification Channel History

Channel ID Importance Notes
encache_recording_controls_v5 IMPORTANCE_LOW Original — no heads-up display. Retired.
encache_recording_controls_v6 IMPORTANCE_HIGH Current — shows heads-up banner. Channel ID bumped because Android caches importance at channel creation time; existing installs with v5 would ignore an importance upgrade without a new ID.

Logs

Source Location
iOS orchestrator createFlowLogger("recording-control") — step keys: recording_control_ios_initialized, recording_control_ios_notification_update_failed, recording_control_ios_teardown_succeeded
Android orchestrator step keys: recording_control_native_initialize_succeeded, recording_control_native_update_succeeded, recording_control_native_teardown_succeeded

Deployment

  • Mechanism: local only (shipped as part of the React Native app bundle and native Android module)
  • Deploy command:
    # iOS
    npx expo run:ios
    
    # Android
    npx expo run:android
    
  • Notes: The Android NotificationChannel is created once per install per channel ID. Changing IMPORTANCE_HIGH back to a lower value on an existing install requires the user to uninstall or manually reconfigure the channel in system settings. Always bump the channel ID suffix when changing importance.