@mentra/engine 0.1.0-dev.0 → 0.1.0-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -1
- package/build/engine.d.ts +11 -3
- package/build/engine.d.ts.map +1 -1
- package/build/engine.js +7 -2
- package/build/engine.js.map +1 -1
- package/build/facades/dev.d.ts +3 -2
- package/build/facades/dev.d.ts.map +1 -1
- package/build/facades/dev.js +18 -12
- package/build/facades/dev.js.map +1 -1
- package/build/facades/gallery.js +1 -1
- package/build/facades/gallery.js.map +1 -1
- package/build/facades/glasses.d.ts +1 -0
- package/build/facades/glasses.d.ts.map +1 -1
- package/build/facades/glasses.js +19 -4
- package/build/facades/glasses.js.map +1 -1
- package/build/facades/glassesSettings.d.ts.map +1 -1
- package/build/facades/glassesSettings.js +1 -0
- package/build/facades/glassesSettings.js.map +1 -1
- package/build/facades/miniapps.d.ts +4 -0
- package/build/facades/miniapps.d.ts.map +1 -1
- package/build/facades/miniapps.js +5 -0
- package/build/facades/miniapps.js.map +1 -1
- package/build/facades/ota.d.ts +11 -2
- package/build/facades/ota.d.ts.map +1 -1
- package/build/facades/ota.js +19 -4
- package/build/facades/ota.js.map +1 -1
- package/build/facades/pairing.d.ts +4 -0
- package/build/facades/pairing.d.ts.map +1 -1
- package/build/facades/pairing.js +10 -8
- package/build/facades/pairing.js.map +1 -1
- package/build/facades/permissions.d.ts +10 -0
- package/build/facades/permissions.d.ts.map +1 -1
- package/build/facades/permissions.js +10 -0
- package/build/facades/permissions.js.map +1 -1
- package/build/facades/phoneNotifications.d.ts +0 -4
- package/build/facades/phoneNotifications.d.ts.map +1 -1
- package/build/facades/phoneNotifications.js +4 -14
- package/build/facades/phoneNotifications.js.map +1 -1
- package/build/index.d.ts +6 -3
- package/build/index.d.ts.map +1 -1
- package/build/index.js +4 -2
- package/build/index.js.map +1 -1
- package/build/internal.d.ts +3 -2
- package/build/internal.d.ts.map +1 -1
- package/build/internal.js +2 -1
- package/build/internal.js.map +1 -1
- package/build/react/MentraLiveOtaFlow.d.ts +38 -0
- package/build/react/MentraLiveOtaFlow.d.ts.map +1 -0
- package/build/react/MentraLiveOtaFlow.js +528 -0
- package/build/react/MentraLiveOtaFlow.js.map +1 -0
- package/build/react/index.d.ts +2 -0
- package/build/react/index.d.ts.map +1 -0
- package/build/react/index.js +2 -0
- package/build/react/index.js.map +1 -0
- package/build/react/useEngineSnapshot.d.ts +2 -0
- package/build/react/useEngineSnapshot.d.ts.map +1 -0
- package/build/react/useEngineSnapshot.js +14 -0
- package/build/react/useEngineSnapshot.js.map +1 -0
- package/build/runtime/bootstrap.d.ts +24 -1
- package/build/runtime/bootstrap.d.ts.map +1 -1
- package/build/runtime/bootstrap.js +17 -0
- package/build/runtime/bootstrap.js.map +1 -1
- package/build/runtime/scanQrSeam.d.ts +7 -0
- package/build/runtime/scanQrSeam.d.ts.map +1 -0
- package/build/runtime/scanQrSeam.js +16 -0
- package/build/runtime/scanQrSeam.js.map +1 -0
- package/build/services/AppRegistry.d.ts +33 -57
- package/build/services/AppRegistry.d.ts.map +1 -1
- package/build/services/AppRegistry.js +193 -144
- package/build/services/AppRegistry.js.map +1 -1
- package/build/services/AudioCloudUplink.d.ts +12 -0
- package/build/services/AudioCloudUplink.d.ts.map +1 -1
- package/build/services/AudioCloudUplink.js +22 -1
- package/build/services/AudioCloudUplink.js.map +1 -1
- package/build/services/AudioPlaybackService.d.ts +41 -2
- package/build/services/AudioPlaybackService.d.ts.map +1 -1
- package/build/services/AudioPlaybackService.js +250 -33
- package/build/services/AudioPlaybackService.js.map +1 -1
- package/build/services/BlobStore.d.ts +2 -1
- package/build/services/BlobStore.d.ts.map +1 -1
- package/build/services/BlobStore.js +17 -9
- package/build/services/BlobStore.js.map +1 -1
- package/build/services/CloudAudioSubscriptionSync.d.ts.map +1 -1
- package/build/services/CloudAudioSubscriptionSync.js +13 -1
- package/build/services/CloudAudioSubscriptionSync.js.map +1 -1
- package/build/services/CloudClientService.d.ts +15 -0
- package/build/services/CloudClientService.d.ts.map +1 -1
- package/build/services/CloudClientService.js +93 -1
- package/build/services/CloudClientService.js.map +1 -1
- package/build/services/DeviceEventRouter.js +5 -5
- package/build/services/DeviceEventRouter.js.map +1 -1
- package/build/services/ForegroundLocationPermission.d.ts +13 -0
- package/build/services/ForegroundLocationPermission.d.ts.map +1 -0
- package/build/services/ForegroundLocationPermission.js +12 -0
- package/build/services/ForegroundLocationPermission.js.map +1 -0
- package/build/services/GlassesSettingsSync.d.ts.map +1 -1
- package/build/services/GlassesSettingsSync.js +9 -3
- package/build/services/GlassesSettingsSync.js.map +1 -1
- package/build/services/GlassesStatusProjection.d.ts +2 -1
- package/build/services/GlassesStatusProjection.d.ts.map +1 -1
- package/build/services/GlassesStatusProjection.js +15 -8
- package/build/services/GlassesStatusProjection.js.map +1 -1
- package/build/services/HotspotOtaTransport.d.ts +29 -0
- package/build/services/HotspotOtaTransport.d.ts.map +1 -0
- package/build/services/HotspotOtaTransport.js +146 -0
- package/build/services/HotspotOtaTransport.js.map +1 -0
- package/build/services/HotspotShutdown.d.ts +17 -0
- package/build/services/HotspotShutdown.d.ts.map +1 -0
- package/build/services/HotspotShutdown.js +29 -0
- package/build/services/HotspotShutdown.js.map +1 -0
- package/build/services/LocalDisplayManager.d.ts +15 -0
- package/build/services/LocalDisplayManager.d.ts.map +1 -1
- package/build/services/LocalDisplayManager.js +106 -20
- package/build/services/LocalDisplayManager.js.map +1 -1
- package/build/services/LocalMiniappRuntime.d.ts +74 -2
- package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
- package/build/services/LocalMiniappRuntime.js +785 -291
- package/build/services/LocalMiniappRuntime.js.map +1 -1
- package/build/services/LocalMiniappStorage.d.ts +32 -0
- package/build/services/LocalMiniappStorage.d.ts.map +1 -0
- package/build/services/LocalMiniappStorage.js +66 -0
- package/build/services/LocalMiniappStorage.js.map +1 -0
- package/build/services/LocalMiniappUserIdentity.d.ts +20 -0
- package/build/services/LocalMiniappUserIdentity.d.ts.map +1 -0
- package/build/services/LocalMiniappUserIdentity.js +43 -0
- package/build/services/LocalMiniappUserIdentity.js.map +1 -0
- package/build/services/LocalSttFallbackCoordinator.d.ts +9 -6
- package/build/services/LocalSttFallbackCoordinator.d.ts.map +1 -1
- package/build/services/LocalSttFallbackCoordinator.js +18 -12
- package/build/services/LocalSttFallbackCoordinator.js.map +1 -1
- package/build/services/MicStateCoordinator.d.ts +42 -1
- package/build/services/MicStateCoordinator.d.ts.map +1 -1
- package/build/services/MicStateCoordinator.js +141 -3
- package/build/services/MicStateCoordinator.js.map +1 -1
- package/build/services/MiniappLauncher.d.ts.map +1 -1
- package/build/services/MiniappLauncher.js +23 -3
- package/build/services/MiniappLauncher.js.map +1 -1
- package/build/services/MiniappLiveness.d.ts +12 -0
- package/build/services/MiniappLiveness.d.ts.map +1 -0
- package/build/services/MiniappLiveness.js +16 -0
- package/build/services/MiniappLiveness.js.map +1 -0
- package/build/services/NativeHttpResponse.d.ts +3 -0
- package/build/services/NativeHttpResponse.d.ts.map +1 -0
- package/build/services/NativeHttpResponse.js +6 -0
- package/build/services/NativeHttpResponse.js.map +1 -0
- package/build/services/OtaArtifactDownloader.d.ts +65 -0
- package/build/services/OtaArtifactDownloader.d.ts.map +1 -0
- package/build/services/OtaArtifactDownloader.js +226 -0
- package/build/services/OtaArtifactDownloader.js.map +1 -0
- package/build/services/OtaAutoChain.d.ts +34 -0
- package/build/services/OtaAutoChain.d.ts.map +1 -0
- package/build/services/OtaAutoChain.js +83 -0
- package/build/services/OtaAutoChain.js.map +1 -0
- package/build/services/OtaErrorMapping.d.ts +18 -0
- package/build/services/OtaErrorMapping.d.ts.map +1 -0
- package/build/services/OtaErrorMapping.js +63 -0
- package/build/services/OtaErrorMapping.js.map +1 -0
- package/build/services/OtaInstallCoordinator.d.ts +94 -14
- package/build/services/OtaInstallCoordinator.d.ts.map +1 -1
- package/build/services/OtaInstallCoordinator.js +574 -130
- package/build/services/OtaInstallCoordinator.js.map +1 -1
- package/build/services/OtaService.d.ts.map +1 -1
- package/build/services/OtaService.js +8 -1
- package/build/services/OtaService.js.map +1 -1
- package/build/services/OtaUpdateCheckService.d.ts +38 -3
- package/build/services/OtaUpdateCheckService.d.ts.map +1 -1
- package/build/services/OtaUpdateCheckService.js +142 -22
- package/build/services/OtaUpdateCheckService.js.map +1 -1
- package/build/services/PhoneCalendarHelpers.d.ts +32 -0
- package/build/services/PhoneCalendarHelpers.d.ts.map +1 -0
- package/build/services/PhoneCalendarHelpers.js +157 -0
- package/build/services/PhoneCalendarHelpers.js.map +1 -0
- package/build/services/PhoneCalendarService.d.ts +5 -0
- package/build/services/PhoneCalendarService.d.ts.map +1 -0
- package/build/services/PhoneCalendarService.js +45 -0
- package/build/services/PhoneCalendarService.js.map +1 -0
- package/build/services/PhoneCameraFovCoordinator.d.ts +30 -0
- package/build/services/PhoneCameraFovCoordinator.d.ts.map +1 -0
- package/build/services/PhoneCameraFovCoordinator.js +194 -0
- package/build/services/PhoneCameraFovCoordinator.js.map +1 -0
- package/build/services/PhoneNotificationsSync.d.ts.map +1 -1
- package/build/services/PhoneNotificationsSync.js +12 -10
- package/build/services/PhoneNotificationsSync.js.map +1 -1
- package/build/services/PhonePhotoCoordinator.d.ts +26 -2
- package/build/services/PhonePhotoCoordinator.d.ts.map +1 -1
- package/build/services/PhonePhotoCoordinator.js +79 -9
- package/build/services/PhonePhotoCoordinator.js.map +1 -1
- package/build/services/PhoneStreamCoordinator.d.ts +8 -1
- package/build/services/PhoneStreamCoordinator.d.ts.map +1 -1
- package/build/services/PhoneStreamCoordinator.js +125 -13
- package/build/services/PhoneStreamCoordinator.js.map +1 -1
- package/build/services/PhoneVideoCoordinator.d.ts +2 -0
- package/build/services/PhoneVideoCoordinator.d.ts.map +1 -1
- package/build/services/PhoneVideoCoordinator.js +10 -0
- package/build/services/PhoneVideoCoordinator.js.map +1 -1
- package/build/services/SceneRenderer.d.ts.map +1 -1
- package/build/services/SceneRenderer.js +15 -0
- package/build/services/SceneRenderer.js.map +1 -1
- package/build/services/SentenceTtsPipeline.d.ts +31 -0
- package/build/services/SentenceTtsPipeline.d.ts.map +1 -0
- package/build/services/SentenceTtsPipeline.js +67 -0
- package/build/services/SentenceTtsPipeline.js.map +1 -0
- package/build/services/SupportProfileSync.d.ts +22 -0
- package/build/services/SupportProfileSync.d.ts.map +1 -0
- package/build/services/SupportProfileSync.js +213 -0
- package/build/services/SupportProfileSync.js.map +1 -0
- package/build/services/TranscriptionRouting.d.ts +12 -0
- package/build/services/TranscriptionRouting.d.ts.map +1 -0
- package/build/services/TranscriptionRouting.js +24 -0
- package/build/services/TranscriptionRouting.js.map +1 -0
- package/build/services/TtsTextSanitizer.d.ts +5 -0
- package/build/services/TtsTextSanitizer.d.ts.map +1 -0
- package/build/services/TtsTextSanitizer.js +61 -0
- package/build/services/TtsTextSanitizer.js.map +1 -0
- package/build/services/WifiAdbRequest.d.ts +17 -0
- package/build/services/WifiAdbRequest.d.ts.map +1 -0
- package/build/services/WifiAdbRequest.js +35 -0
- package/build/services/WifiAdbRequest.js.map +1 -0
- package/build/services/asg/asgCameraApi.d.ts +43 -0
- package/build/services/asg/asgCameraApi.d.ts.map +1 -1
- package/build/services/asg/asgCameraApi.js +509 -26
- package/build/services/asg/asgCameraApi.js.map +1 -1
- package/build/services/asg/cameraRollExportCoordinator.d.ts +60 -0
- package/build/services/asg/cameraRollExportCoordinator.d.ts.map +1 -0
- package/build/services/asg/cameraRollExportCoordinator.js +618 -0
- package/build/services/asg/cameraRollExportCoordinator.js.map +1 -0
- package/build/services/asg/cameraRollExportLedger.d.ts +44 -0
- package/build/services/asg/cameraRollExportLedger.d.ts.map +1 -0
- package/build/services/asg/cameraRollExportLedger.js +78 -0
- package/build/services/asg/cameraRollExportLedger.js.map +1 -0
- package/build/services/asg/galleryNotices.d.ts +1 -1
- package/build/services/asg/galleryNotices.d.ts.map +1 -1
- package/build/services/asg/galleryNotices.js.map +1 -1
- package/build/services/asg/galleryPathMigration.d.ts +3 -0
- package/build/services/asg/galleryPathMigration.d.ts.map +1 -0
- package/build/services/asg/galleryPathMigration.js +26 -0
- package/build/services/asg/galleryPathMigration.js.map +1 -0
- package/build/services/asg/gallerySettingsService.d.ts +1 -0
- package/build/services/asg/gallerySettingsService.d.ts.map +1 -1
- package/build/services/asg/gallerySettingsService.js +2 -0
- package/build/services/asg/gallerySettingsService.js.map +1 -1
- package/build/services/asg/gallerySyncService.d.ts.map +1 -1
- package/build/services/asg/gallerySyncService.js +162 -56
- package/build/services/asg/gallerySyncService.js.map +1 -1
- package/build/services/asg/galleryTransferLedger.d.ts +58 -0
- package/build/services/asg/galleryTransferLedger.d.ts.map +1 -0
- package/build/services/asg/galleryTransferLedger.js +225 -0
- package/build/services/asg/galleryTransferLedger.js.map +1 -0
- package/build/services/asg/localNetworkTransport.d.ts +15 -0
- package/build/services/asg/localNetworkTransport.d.ts.map +1 -0
- package/build/services/asg/localNetworkTransport.js +144 -0
- package/build/services/asg/localNetworkTransport.js.map +1 -0
- package/build/services/asg/localStorageService.d.ts +18 -0
- package/build/services/asg/localStorageService.d.ts.map +1 -1
- package/build/services/asg/localStorageService.js +168 -19
- package/build/services/asg/localStorageService.js.map +1 -1
- package/build/services/asg/mediaProcessingQueue.d.ts +10 -0
- package/build/services/asg/mediaProcessingQueue.d.ts.map +1 -1
- package/build/services/asg/mediaProcessingQueue.js +193 -62
- package/build/services/asg/mediaProcessingQueue.js.map +1 -1
- package/build/services/audioPlaybackAssets.d.ts +8 -0
- package/build/services/audioPlaybackAssets.d.ts.map +1 -0
- package/build/services/audioPlaybackAssets.js +7 -0
- package/build/services/audioPlaybackAssets.js.map +1 -0
- package/build/services/gallerySyncClock.js +1 -1
- package/build/services/gallerySyncClock.js.map +1 -1
- package/build/services/glassesClockSync.js +2 -2
- package/build/services/glassesClockSync.js.map +1 -1
- package/build/services/manifestActions.d.ts +7 -0
- package/build/services/manifestActions.d.ts.map +1 -0
- package/build/services/manifestActions.js +25 -0
- package/build/services/manifestActions.js.map +1 -0
- package/build/services/otaDisplayState.d.ts +22 -11
- package/build/services/otaDisplayState.d.ts.map +1 -1
- package/build/services/otaDisplayState.js +36 -14
- package/build/services/otaDisplayState.js.map +1 -1
- package/build/services/otaInstallPolicy.d.ts +15 -3
- package/build/services/otaInstallPolicy.d.ts.map +1 -1
- package/build/services/otaInstallPolicy.js +24 -3
- package/build/services/otaInstallPolicy.js.map +1 -1
- package/build/services/otaManifestUrl.d.ts +2 -0
- package/build/services/otaManifestUrl.d.ts.map +1 -0
- package/build/services/{asgOtaVersionUrl.js → otaManifestUrl.js} +15 -5
- package/build/services/otaManifestUrl.js.map +1 -0
- package/build/services/slimStreamStatus.d.ts +11 -0
- package/build/services/slimStreamStatus.d.ts.map +1 -1
- package/build/services/slimStreamStatus.js +14 -4
- package/build/services/slimStreamStatus.js.map +1 -1
- package/build/stores/apps.d.ts.map +1 -1
- package/build/stores/apps.js +11 -0
- package/build/stores/apps.js.map +1 -1
- package/build/stores/bluetoothSettingKeys.d.ts.map +1 -1
- package/build/stores/bluetoothSettingKeys.js +2 -0
- package/build/stores/bluetoothSettingKeys.js.map +1 -1
- package/build/stores/gallerySync.d.ts +2 -0
- package/build/stores/gallerySync.d.ts.map +1 -1
- package/build/stores/gallerySync.js +2 -0
- package/build/stores/gallerySync.js.map +1 -1
- package/build/stores/glasses.d.ts.map +1 -1
- package/build/stores/glasses.js +3 -0
- package/build/stores/glasses.js.map +1 -1
- package/build/stores/settings.d.ts +17 -0
- package/build/stores/settings.d.ts.map +1 -1
- package/build/stores/settings.js +157 -17
- package/build/stores/settings.js.map +1 -1
- package/build/types/applet.d.ts +2 -0
- package/build/types/applet.d.ts.map +1 -1
- package/build/types/applet.js.map +1 -1
- package/build/types/asg.d.ts +7 -0
- package/build/types/asg.d.ts.map +1 -1
- package/build/types/asg.js.map +1 -1
- package/build/types/enums.d.ts +1 -0
- package/build/types/enums.d.ts.map +1 -1
- package/build/types/enums.js +1 -0
- package/build/types/enums.js.map +1 -1
- package/build/types/hardware.d.ts +1 -0
- package/build/types/hardware.d.ts.map +1 -1
- package/build/types/hardware.js +14 -0
- package/build/types/hardware.js.map +1 -1
- package/build/utils/configuredDevHost.d.ts +6 -0
- package/build/utils/configuredDevHost.d.ts.map +1 -0
- package/build/utils/configuredDevHost.js +19 -0
- package/build/utils/configuredDevHost.js.map +1 -0
- package/build/utils/devMiniappLaunch.d.ts +19 -8
- package/build/utils/devMiniappLaunch.d.ts.map +1 -1
- package/build/utils/devMiniappLaunch.js +144 -21
- package/build/utils/devMiniappLaunch.js.map +1 -1
- package/build/utils/diagnosticContext.d.ts.map +1 -1
- package/build/utils/diagnosticContext.js +15 -17
- package/build/utils/diagnosticContext.js.map +1 -1
- package/build/utils/hardware/hardware.d.ts.map +1 -1
- package/build/utils/hardware/hardware.js +7 -3
- package/build/utils/hardware/hardware.js.map +1 -1
- package/build/utils/miniappDiagnostics.d.ts +64 -0
- package/build/utils/miniappDiagnostics.d.ts.map +1 -0
- package/build/utils/miniappDiagnostics.js +173 -0
- package/build/utils/miniappDiagnostics.js.map +1 -0
- package/build/utils/permissions/MediaLibraryPermissions.d.ts +11 -0
- package/build/utils/permissions/MediaLibraryPermissions.d.ts.map +1 -1
- package/build/utils/permissions/MediaLibraryPermissions.js +27 -4
- package/build/utils/permissions/MediaLibraryPermissions.js.map +1 -1
- package/build/utils/timers/timers.d.ts.map +1 -1
- package/build/utils/timers/timers.js +55 -18
- package/build/utils/timers/timers.js.map +1 -1
- package/package.json +25 -15
- package/src/assets/silence.wav +0 -0
- package/src/engine.ts +7 -2
- package/src/facades/dev.ts +17 -12
- package/src/facades/gallery.ts +1 -1
- package/src/facades/glasses.ts +17 -4
- package/src/facades/glassesSettings.ts +5 -0
- package/src/facades/miniapps.ts +7 -0
- package/src/facades/ota.ts +19 -4
- package/src/facades/pairing.ts +11 -7
- package/src/facades/permissions.ts +14 -0
- package/src/facades/phoneNotifications.ts +4 -13
- package/src/index.ts +26 -2
- package/src/internal.ts +3 -1
- package/src/react/MentraLiveOtaFlow.tsx +856 -0
- package/src/react/index.ts +7 -0
- package/src/react/useEngineSnapshot.ts +16 -0
- package/src/runtime/bootstrap.ts +33 -1
- package/src/runtime/scanQrSeam.ts +20 -0
- package/src/services/AppRegistry.ts +224 -149
- package/src/services/AudioCloudUplink.ts +23 -1
- package/src/services/AudioPlaybackService.ts +287 -38
- package/src/services/BlobStore.ts +22 -9
- package/src/services/CloudAudioSubscriptionSync.ts +13 -1
- package/src/services/CloudClientService.ts +96 -1
- package/src/services/DeviceEventRouter.ts +5 -5
- package/src/services/ForegroundLocationPermission.ts +24 -0
- package/src/services/GlassesSettingsSync.ts +9 -3
- package/src/services/GlassesStatusProjection.ts +16 -8
- package/src/services/HotspotOtaTransport.ts +184 -0
- package/src/services/HotspotShutdown.ts +41 -0
- package/src/services/LocalDisplayManager.ts +112 -21
- package/src/services/LocalMiniappRuntime.ts +967 -346
- package/src/services/LocalMiniappStorage.ts +82 -0
- package/src/services/LocalMiniappUserIdentity.ts +46 -0
- package/src/services/LocalSttFallbackCoordinator.ts +19 -12
- package/src/services/MicStateCoordinator.ts +169 -4
- package/src/services/MiniappLauncher.ts +30 -3
- package/src/services/MiniappLiveness.ts +24 -0
- package/src/services/NativeHttpResponse.ts +6 -0
- package/src/services/OtaArtifactDownloader.ts +312 -0
- package/src/services/OtaAutoChain.ts +106 -0
- package/src/services/OtaErrorMapping.ts +80 -0
- package/src/services/OtaInstallCoordinator.ts +645 -143
- package/src/services/OtaService.ts +11 -5
- package/src/services/OtaUpdateCheckService.ts +180 -20
- package/src/services/PhoneCalendarHelpers.ts +212 -0
- package/src/services/PhoneCalendarService.ts +65 -0
- package/src/services/PhoneCameraFovCoordinator.ts +210 -0
- package/src/services/PhoneNotificationsSync.ts +12 -10
- package/src/services/PhonePhotoCoordinator.ts +116 -18
- package/src/services/PhoneStreamCoordinator.ts +139 -25
- package/src/services/PhoneVideoCoordinator.ts +11 -0
- package/src/services/SceneRenderer.ts +16 -0
- package/src/services/SentenceTtsPipeline.ts +106 -0
- package/src/services/SupportProfileSync.ts +218 -0
- package/src/services/TranscriptionRouting.ts +37 -0
- package/src/services/TtsTextSanitizer.ts +66 -0
- package/src/services/WifiAdbRequest.ts +53 -0
- package/src/services/asg/asgCameraApi.ts +615 -33
- package/src/services/asg/cameraRollExportCoordinator.ts +627 -0
- package/src/services/asg/cameraRollExportLedger.ts +122 -0
- package/src/services/asg/galleryNotices.ts +1 -0
- package/src/services/asg/galleryPathMigration.ts +22 -0
- package/src/services/asg/gallerySettingsService.ts +3 -0
- package/src/services/asg/gallerySyncService.ts +184 -73
- package/src/services/asg/galleryTransferLedger.ts +312 -0
- package/src/services/asg/localNetworkTransport.ts +169 -0
- package/src/services/asg/localStorageService.ts +179 -24
- package/src/services/asg/mediaProcessingQueue.ts +209 -62
- package/src/services/audioPlaybackAssets.ts +8 -0
- package/src/services/gallerySyncClock.ts +1 -1
- package/src/services/glassesClockSync.ts +2 -2
- package/src/services/manifestActions.ts +30 -0
- package/src/services/otaDisplayState.ts +57 -16
- package/src/services/otaInstallPolicy.ts +29 -3
- package/src/services/{asgOtaVersionUrl.ts → otaManifestUrl.ts} +16 -4
- package/src/services/slimStreamStatus.ts +16 -5
- package/src/stores/apps.ts +12 -0
- package/src/stores/bluetoothSettingKeys.ts +2 -0
- package/src/stores/gallerySync.ts +4 -0
- package/src/stores/glasses.ts +3 -0
- package/src/stores/settings.ts +179 -18
- package/src/types/applet.ts +2 -0
- package/src/types/asg.ts +7 -0
- package/src/types/enums.ts +2 -1
- package/src/types/hardware.ts +17 -0
- package/src/utils/configuredDevHost.ts +17 -0
- package/src/utils/devMiniappLaunch.ts +151 -23
- package/src/utils/diagnosticContext.ts +17 -17
- package/src/utils/hardware/hardware.ts +7 -9
- package/src/utils/miniappDiagnostics.ts +226 -0
- package/src/utils/permissions/MediaLibraryPermissions.ts +35 -4
- package/src/utils/timers/timers.ts +57 -21
- package/build/services/asgOtaVersionUrl.d.ts +0 -2
- package/build/services/asgOtaVersionUrl.d.ts.map +0 -1
- package/build/services/asgOtaVersionUrl.js.map +0 -1
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* This runtime handles request dispatch, stream fan-out, and lifecycle
|
|
9
9
|
* management (connect/disconnect/ping).
|
|
10
10
|
*/
|
|
11
|
-
import { Linking } from "react-native";
|
|
11
|
+
import { AppState, Linking } from "react-native";
|
|
12
12
|
import Share from "react-native-share";
|
|
13
13
|
import * as Battery from "expo-battery";
|
|
14
14
|
import * as Clipboard from "expo-clipboard";
|
|
@@ -32,18 +32,30 @@ import localSttFallbackCoordinator from "./LocalSttFallbackCoordinator";
|
|
|
32
32
|
import micStateCoordinator from "./MicStateCoordinator";
|
|
33
33
|
import { BlobStore } from "./BlobStore";
|
|
34
34
|
import { CloudAudioSubscriptionSync } from "./CloudAudioSubscriptionSync";
|
|
35
|
+
import { phoneCameraFovCoordinator } from "./PhoneCameraFovCoordinator";
|
|
35
36
|
import { phonePhotoCoordinator } from "./PhonePhotoCoordinator";
|
|
36
37
|
import { phoneStreamCoordinator } from "./PhoneStreamCoordinator";
|
|
37
38
|
import { phoneVideoCoordinator } from "./PhoneVideoCoordinator";
|
|
39
|
+
import { runSentenceTtsPipeline } from "./SentenceTtsPipeline";
|
|
40
|
+
import { summarizeTranscriptionRoutes, transcriptionDeliveryRoute } from "./TranscriptionRouting";
|
|
41
|
+
import { prepareTtsSentences } from "./TtsTextSanitizer";
|
|
42
|
+
import { handleWifiAdbRequest } from "./WifiAdbRequest";
|
|
38
43
|
import { cloudClientService } from "./CloudClientService";
|
|
39
44
|
import { miniappLauncher } from "./MiniappLauncher";
|
|
40
45
|
import { ISLAND_SETTINGS_KEYS, } from "../runtime/config";
|
|
41
46
|
import { getAnalytics, getUiSeams } from "../runtime/bootstrap";
|
|
47
|
+
import { invokeScanQrSeam } from "../runtime/scanQrSeam";
|
|
42
48
|
import { normalizeStreamAudioConfig, normalizeStreamVideoConfig } from "../runtime/streamConfig";
|
|
49
|
+
import { toLanguageHint } from "@mentra/cloud-protocol/languages";
|
|
50
|
+
import { buildMiniappManifestSnapshot } from "../utils/miniappDiagnostics";
|
|
43
51
|
import ttsModelManager from "./TTSModelManager";
|
|
44
52
|
import { NavigationHandlers } from "./NavigationHandlers";
|
|
45
53
|
import { useAppStatusStore } from "../stores/apps";
|
|
46
|
-
import {
|
|
54
|
+
import { getDevAppAttestation, getDevAppSourcePackage } from "./AppRegistry";
|
|
55
|
+
import { resolveForegroundLocationPermission } from "./ForegroundLocationPermission";
|
|
56
|
+
import { advanceMiniappPingLiveness } from "./MiniappLiveness";
|
|
57
|
+
import { listPhoneCalendarEvents, PhoneCalendarError } from "./PhoneCalendarService";
|
|
58
|
+
import { LocalMiniappStorage } from "./LocalMiniappStorage";
|
|
47
59
|
/**
|
|
48
60
|
* Strictness ordering for `location_stream` rate values. Higher index =
|
|
49
61
|
* higher accuracy and higher power cost. The aggregate tier applied to
|
|
@@ -76,6 +88,14 @@ function withTimeout(promise, timeoutMs) {
|
|
|
76
88
|
});
|
|
77
89
|
}
|
|
78
90
|
const LOG_TAG = "LOCAL_MINIAPP";
|
|
91
|
+
const DIAGNOSTIC_MAX_LIST_ITEMS = 100;
|
|
92
|
+
const DIAGNOSTIC_MAX_STRING_LENGTH = 512;
|
|
93
|
+
function diagnosticStringList(values) {
|
|
94
|
+
return [...values]
|
|
95
|
+
.map((value) => value.slice(0, DIAGNOSTIC_MAX_STRING_LENGTH))
|
|
96
|
+
.sort()
|
|
97
|
+
.slice(0, DIAGNOSTIC_MAX_LIST_ITEMS);
|
|
98
|
+
}
|
|
79
99
|
const SYSTEM_MINIAPP_PACKAGES = new Set([
|
|
80
100
|
"com.mentra.camera",
|
|
81
101
|
"com.mentra.gallery",
|
|
@@ -106,76 +126,32 @@ const REQUEST_WIFI_SETUP_TYPE = "miniapp_request_wifi_setup";
|
|
|
106
126
|
// respawn path wired (MentraJSRouter.start), a genuinely dead context still
|
|
107
127
|
// comes back automatically — this threshold only bounds how long that takes.
|
|
108
128
|
const PING_TIMEOUT_THRESHOLD = 6; // ~30s
|
|
109
|
-
// ===========================================================================
|
|
110
|
-
// BGCAP: temporary background-caption telemetry (DIAGNOSTIC — remove after the
|
|
111
|
-
// "captions fall behind then flood in waves" investigation lands a fix).
|
|
112
|
-
//
|
|
113
|
-
// Captions now run as a local miniapp inside the per-context JS engine
|
|
114
|
-
// (Crust: QuickJS on Android / JSC on iOS), driven off a single serial
|
|
115
|
-
// queue/executor with NO background awareness. The hypothesis is that the OS
|
|
116
|
-
// starves/freezes that queue when the screen is off (iOS app suspension /
|
|
117
|
-
// Android Doze), so inbound transcript deliveries pile up and drain in a burst
|
|
118
|
-
// on resume. These logs measure exactly that. All lines are prefixed "BGCAP:"
|
|
119
|
-
// so they're trivial to grep in incident logs and to delete. Cross-platform —
|
|
120
|
-
// host-side only, no native changes.
|
|
121
|
-
// ===========================================================================
|
|
122
|
-
const BGCAP_TELEMETRY = false;
|
|
123
129
|
const TRANSCRIPT_TIMING_TELEMETRY = globalThis.__DEV__ === true;
|
|
124
|
-
let bgcapHbTimer = null;
|
|
125
|
-
let bgcapHbLast = 0;
|
|
126
|
-
let bgcapTxCount = 0;
|
|
127
|
-
let bgcapTxLastFinalAt = 0;
|
|
128
|
-
let bgcapTxLastLogAt = 0;
|
|
129
|
-
function bgcapStartHeartbeat() {
|
|
130
|
-
if (!BGCAP_TELEMETRY || bgcapHbTimer !== null)
|
|
131
|
-
return;
|
|
132
|
-
bgcapHbLast = Date.now();
|
|
133
|
-
// PLAIN setInterval (NOT BgTimer): a plain JS timer only fires when the RN
|
|
134
|
-
// JS thread is actually executing. A gap >> 1s between ticks therefore proves
|
|
135
|
-
// the JS thread was suspended/throttled (iOS suspend / Android Doze) for that
|
|
136
|
-
// long — the smoking gun for the freeze. Silent when healthy; logs the stall
|
|
137
|
-
// duration when it happens. Pair with the "App state changed to:" lines.
|
|
138
|
-
bgcapHbTimer = setInterval(() => {
|
|
139
|
-
const now = Date.now();
|
|
140
|
-
const gap = now - bgcapHbLast;
|
|
141
|
-
bgcapHbLast = now;
|
|
142
|
-
if (gap > 2000) {
|
|
143
|
-
console.log(`BGCAP: host-js stall — no tick for ${gap}ms (RN JS thread frozen/throttled)`);
|
|
144
|
-
}
|
|
145
|
-
}, 1000);
|
|
146
|
-
}
|
|
147
|
-
function bgcapStopHeartbeat() {
|
|
148
|
-
if (bgcapHbTimer !== null) {
|
|
149
|
-
clearInterval(bgcapHbTimer);
|
|
150
|
-
bgcapHbTimer = null;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
// Rate-limited (1/s) summary of transcripts being FORWARDED to miniapps. Shows
|
|
154
|
-
// whether transcripts keep arriving + being handed to the captions miniapp
|
|
155
|
-
// during background. Compare its rate against the native "MAN: displayEvent"
|
|
156
|
-
// (render) rate: fwd keeps flowing but render lags => miniapp executor is
|
|
157
|
-
// starved; fwd itself stops => upstream (cloud delivery / RN thread) is the gap.
|
|
158
|
-
function bgcapNoteTranscriptForward(isFinal) {
|
|
159
|
-
if (!BGCAP_TELEMETRY)
|
|
160
|
-
return;
|
|
161
|
-
bgcapTxCount++;
|
|
162
|
-
if (isFinal)
|
|
163
|
-
bgcapTxLastFinalAt = Date.now();
|
|
164
|
-
const now = Date.now();
|
|
165
|
-
if (now - bgcapTxLastLogAt >= 1000) {
|
|
166
|
-
console.log(`BGCAP: tx forwarded=${bgcapTxCount} in last ${now - bgcapTxLastLogAt}ms (lastFinal ${bgcapTxLastFinalAt ? now - bgcapTxLastFinalAt : -1}ms ago)`);
|
|
167
|
-
bgcapTxCount = 0;
|
|
168
|
-
bgcapTxLastLogAt = now;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
130
|
const RGB_LED_ACTIONS = new Set(["on", "off"]);
|
|
172
131
|
const RGB_LED_COLORS = new Set(["red", "green", "blue", "orange", "white"]);
|
|
173
132
|
const CAMERA_FOV_MIN = 62;
|
|
174
133
|
const CAMERA_FOV_MAX = 118;
|
|
175
|
-
const CAMERA_FOV_DEFAULT =
|
|
176
|
-
const CAMERA_FOV_PRESETS = { narrow: 82, standard:
|
|
134
|
+
const CAMERA_FOV_DEFAULT = 118;
|
|
135
|
+
const CAMERA_FOV_PRESETS = { narrow: 82, standard: 102, wide: 118 };
|
|
177
136
|
const CAMERA_ROI_POSITION_BY_NAME = { center: "center", bottom: "bottom", top: "top" };
|
|
178
|
-
const
|
|
137
|
+
const localMiniappStorageBackend = {
|
|
138
|
+
get(key) {
|
|
139
|
+
const result = mmkvStorage.load(key);
|
|
140
|
+
return result.is_ok() ? result.value : null;
|
|
141
|
+
},
|
|
142
|
+
has(key) {
|
|
143
|
+
return mmkvStorage.load(key).is_ok();
|
|
144
|
+
},
|
|
145
|
+
set(key, value) {
|
|
146
|
+
mmkvStorage.save(key, value);
|
|
147
|
+
},
|
|
148
|
+
remove(key) {
|
|
149
|
+
mmkvStorage.remove(key);
|
|
150
|
+
},
|
|
151
|
+
keys() {
|
|
152
|
+
return mmkvStorage.getAllKeys();
|
|
153
|
+
},
|
|
154
|
+
};
|
|
179
155
|
// =============================================================================
|
|
180
156
|
// Declared-permission record helper (for CONNECT_ACK / PERMISSIONS_UPDATE)
|
|
181
157
|
// =============================================================================
|
|
@@ -292,10 +268,19 @@ class LocalMiniappRuntime {
|
|
|
292
268
|
connectWaiters = new Map();
|
|
293
269
|
/** Ref-counted stream subscriptions: stream → set of packageNames. */
|
|
294
270
|
streamSubscribers = new Map();
|
|
271
|
+
/** Host observers for the Mentra Live button-capture policy. */
|
|
272
|
+
buttonPressSubscriberListeners = new Set();
|
|
295
273
|
/** Guards one-time wiring of the cloud transcript/translation fan-out. */
|
|
296
274
|
cloudResultsWired = false;
|
|
297
275
|
cloudStatusWired = false;
|
|
298
276
|
cloudAudioSubscriptionSync = new CloudAudioSubscriptionSync();
|
|
277
|
+
/**
|
|
278
|
+
* Per-miniapp language hints from TRANSCRIPTION_CONFIG (issue 021 WP3).
|
|
279
|
+
* Bare ISO 639-1 codes, already registry-validated by the SDK and
|
|
280
|
+
* re-normalized here (defense in depth). Merged (union) into the auto-mode
|
|
281
|
+
* cloud transcription subscription in buildCloudAudioSubscriptions.
|
|
282
|
+
*/
|
|
283
|
+
transcriptionHintsByApp = new Map();
|
|
299
284
|
/** Ping interval handle. */
|
|
300
285
|
pingIntervalId = null;
|
|
301
286
|
foregroundProbeTimers = new Map();
|
|
@@ -310,12 +295,95 @@ class LocalMiniappRuntime {
|
|
|
310
295
|
onLivenessTimeout = null;
|
|
311
296
|
/** Pending cloud requests: requestId → packageName that originated the request. */
|
|
312
297
|
pendingCloudRequests = new Map();
|
|
298
|
+
speechRuns = new Map();
|
|
313
299
|
// Browser fallback token auth — HMAC-signed blob with a phone-local
|
|
314
300
|
// secret. Both issuer and verifier are the same process, so the
|
|
315
301
|
// secret never leaves the device.
|
|
316
302
|
localSecret = `miniapp_${Date.now()}_${Math.random().toString(36).slice(2, 14)}`;
|
|
317
303
|
usedTokens = new Set();
|
|
318
304
|
constructor() { }
|
|
305
|
+
/** Snapshot of miniapps actively subscribed to hardware button events. */
|
|
306
|
+
getButtonPressSubscribers() {
|
|
307
|
+
return [...(this.streamSubscribers.get(MiniappStreamType.BUTTON_PRESS) ?? [])].sort();
|
|
308
|
+
}
|
|
309
|
+
/** Observe active hardware-button subscriptions. */
|
|
310
|
+
onButtonPressSubscribersChanged(listener) {
|
|
311
|
+
this.buttonPressSubscriberListeners.add(listener);
|
|
312
|
+
return () => this.buttonPressSubscriberListeners.delete(listener);
|
|
313
|
+
}
|
|
314
|
+
/** Report-safe live miniapp/subscription/resource snapshot. */
|
|
315
|
+
getDiagnosticSnapshot(nowMs = Date.now()) {
|
|
316
|
+
const connectedApps = [...this.connectedApps.entries()]
|
|
317
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
318
|
+
.slice(0, DIAGNOSTIC_MAX_LIST_ITEMS)
|
|
319
|
+
.map(([packageName, app]) => ({
|
|
320
|
+
packageName: packageName.slice(0, DIAGNOSTIC_MAX_STRING_LENGTH),
|
|
321
|
+
handshakeComplete: this.handshookApps.has(packageName),
|
|
322
|
+
subscriptions: diagnosticStringList(app.subscriptions),
|
|
323
|
+
lastMessageAgeMs: Math.max(0, nowMs - app.lastPongAt),
|
|
324
|
+
requestedLocationRate: app.requestedLocationRate,
|
|
325
|
+
hasActiveSpeakerStream: app.activeSpeakerStreamId !== undefined,
|
|
326
|
+
...(app.installedManifest
|
|
327
|
+
? {
|
|
328
|
+
manifest: buildMiniappManifestSnapshot(app.installedManifest, {
|
|
329
|
+
packageName,
|
|
330
|
+
name: app.installedManifest.name,
|
|
331
|
+
version: app.installedManifest.version,
|
|
332
|
+
type: app.installedManifest.type,
|
|
333
|
+
}),
|
|
334
|
+
}
|
|
335
|
+
: {}),
|
|
336
|
+
}));
|
|
337
|
+
const subscriptionsByStream = Object.fromEntries([...this.streamSubscribers.entries()]
|
|
338
|
+
.filter(([, packageNames]) => packageNames.size > 0)
|
|
339
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
340
|
+
.slice(0, DIAGNOSTIC_MAX_LIST_ITEMS)
|
|
341
|
+
.map(([stream, packageNames]) => [
|
|
342
|
+
stream.slice(0, DIAGNOSTIC_MAX_STRING_LENGTH),
|
|
343
|
+
diagnosticStringList(packageNames),
|
|
344
|
+
]));
|
|
345
|
+
return {
|
|
346
|
+
connectedApps,
|
|
347
|
+
subscriptionsByStream,
|
|
348
|
+
resources: {
|
|
349
|
+
display: localDisplayManager.getDiagnosticSnapshot(),
|
|
350
|
+
camera: phonePhotoCoordinator.getDiagnosticSnapshot(),
|
|
351
|
+
video: phoneVideoCoordinator.getDiagnosticSnapshot(),
|
|
352
|
+
stream: phoneStreamCoordinator.getDiagnosticSnapshot(),
|
|
353
|
+
cameraFov: phoneCameraFovCoordinator.getDiagnosticSnapshot(),
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
replaceStreamSubscribers(packageName, previousStreams, nextStreams) {
|
|
358
|
+
const previousButtonSubscribers = this.getButtonPressSubscribers().join("\u0000");
|
|
359
|
+
for (const stream of previousStreams) {
|
|
360
|
+
const subscribers = this.streamSubscribers.get(stream);
|
|
361
|
+
if (!subscribers)
|
|
362
|
+
continue;
|
|
363
|
+
subscribers.delete(packageName);
|
|
364
|
+
if (subscribers.size === 0)
|
|
365
|
+
this.streamSubscribers.delete(stream);
|
|
366
|
+
}
|
|
367
|
+
for (const stream of nextStreams) {
|
|
368
|
+
let subscribers = this.streamSubscribers.get(stream);
|
|
369
|
+
if (!subscribers) {
|
|
370
|
+
subscribers = new Set();
|
|
371
|
+
this.streamSubscribers.set(stream, subscribers);
|
|
372
|
+
}
|
|
373
|
+
subscribers.add(packageName);
|
|
374
|
+
}
|
|
375
|
+
const nextButtonSubscribers = this.getButtonPressSubscribers();
|
|
376
|
+
if (previousButtonSubscribers !== nextButtonSubscribers.join("\u0000")) {
|
|
377
|
+
for (const listener of this.buttonPressSubscriberListeners) {
|
|
378
|
+
try {
|
|
379
|
+
listener(nextButtonSubscribers);
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
console.warn(`${LOG_TAG}: button press subscriber listener threw:`, error);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
319
387
|
/**
|
|
320
388
|
* Generate an HMAC-signed local session token for browser fallback auth.
|
|
321
389
|
* Token format: base64(JSON({userId, packageName, exp})).base64(HMAC-SHA256(payload, secret))
|
|
@@ -502,6 +570,7 @@ class LocalMiniappRuntime {
|
|
|
502
570
|
// dangling references. The WebView will re-subscribe after its CONNECT.
|
|
503
571
|
if (this.connectedApps.has(packageName)) {
|
|
504
572
|
const existing = this.connectedApps.get(packageName);
|
|
573
|
+
this.cancelSpeech(packageName);
|
|
505
574
|
// A crash respawn replaces the ConnectedMiniapp without going through
|
|
506
575
|
// unregisterApp(). Abort the old incarnation's native PCM player before
|
|
507
576
|
// its only stream id is lost. Target the exact stream rather than using
|
|
@@ -522,21 +591,17 @@ class LocalMiniappRuntime {
|
|
|
522
591
|
BgTimer.clearTimeout(existing.authRetryTimerId);
|
|
523
592
|
existing.authRetryTimerId = null;
|
|
524
593
|
}
|
|
525
|
-
|
|
526
|
-
const subs = this.streamSubscribers.get(stream);
|
|
527
|
-
if (subs) {
|
|
528
|
-
subs.delete(packageName);
|
|
529
|
-
if (subs.size === 0)
|
|
530
|
-
this.streamSubscribers.delete(stream);
|
|
531
|
-
}
|
|
532
|
-
}
|
|
594
|
+
this.replaceStreamSubscribers(packageName, existing.subscriptions, []);
|
|
533
595
|
this.recomputeMicRequirements();
|
|
534
596
|
this.updateCloudSubscriptions();
|
|
535
597
|
}
|
|
536
598
|
this.connectedApps.set(packageName, {
|
|
537
599
|
subscriptions: new Set(),
|
|
600
|
+
forceLocalTranscriptionStreams: new Set(),
|
|
601
|
+
cloudTranscriptionStreams: new Set(),
|
|
538
602
|
sendMessage: sendFn,
|
|
539
603
|
lastPongAt: Date.now(),
|
|
604
|
+
unansweredPingRounds: 0,
|
|
540
605
|
installedManifest,
|
|
541
606
|
authRefreshTimerId: null,
|
|
542
607
|
authRetryTimerId: null,
|
|
@@ -656,6 +721,7 @@ class LocalMiniappRuntime {
|
|
|
656
721
|
}
|
|
657
722
|
unregisterApp(packageName) {
|
|
658
723
|
console.log(`${LOG_TAG}: unregisterApp(${packageName})`);
|
|
724
|
+
const releasedMicGateOverride = micStateCoordinator.clearMiniappGateOverrides(packageName);
|
|
659
725
|
this.clearForegroundProbe(packageName);
|
|
660
726
|
this.clearMiniappAuthRefresh(packageName);
|
|
661
727
|
this.clearMiniappAuthDeliveryRetry(packageName);
|
|
@@ -664,21 +730,39 @@ class LocalMiniappRuntime {
|
|
|
664
730
|
// the early-return so it runs even if the connectedApps entry is already gone.
|
|
665
731
|
this.handshookApps.delete(packageName);
|
|
666
732
|
this.flushConnectWaiters(packageName, new Error(`${packageName} unregistered before connect`));
|
|
733
|
+
// Drop this app's transcription hints; the cloud subscription rebuild
|
|
734
|
+
// below (via replaceStreamSubscribers) re-derives the hint union without it.
|
|
735
|
+
this.transcriptionHintsByApp.delete(packageName);
|
|
736
|
+
// Warm-up is a request-owned camera lease. Cancel it even if the app record was already
|
|
737
|
+
// removed so a close racing the camera-open promise cannot leave the sensor running.
|
|
738
|
+
// Keep camera teardown ordered: restoring FOV can restart the HAL, so the warm-up lease must
|
|
739
|
+
// finish closing the sensor before FOV reconciliation begins. Continue to FOV cleanup even if
|
|
740
|
+
// warm-up cancellation fails; phone ownership remains retryable until the ASG lease TTL.
|
|
741
|
+
void phonePhotoCoordinator
|
|
742
|
+
.stopWarmUpForApp(packageName)
|
|
743
|
+
.catch((error) => {
|
|
744
|
+
console.warn(`${LOG_TAG}: failed to stop camera warm-up for ${packageName} on unregister`, error);
|
|
745
|
+
})
|
|
746
|
+
.then(() => phoneCameraFovCoordinator.releaseForApp(packageName))
|
|
747
|
+
.catch((error) => {
|
|
748
|
+
console.warn(`${LOG_TAG}: failed to release camera FOV override for ${packageName} on unregister`, error);
|
|
749
|
+
});
|
|
667
750
|
const app = this.connectedApps.get(packageName);
|
|
668
|
-
if (!app)
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
if (subs) {
|
|
674
|
-
subs.delete(packageName);
|
|
675
|
-
if (subs.size === 0) {
|
|
676
|
-
this.streamSubscribers.delete(stream);
|
|
677
|
-
}
|
|
751
|
+
if (!app) {
|
|
752
|
+
if (releasedMicGateOverride) {
|
|
753
|
+
void micStateCoordinator.syncEffectiveGatePolicy(this.getConfiguredMicGates()).catch((error) => {
|
|
754
|
+
console.warn(`${LOG_TAG}: failed to restore mic gate policy for ${packageName}`, error);
|
|
755
|
+
});
|
|
678
756
|
}
|
|
757
|
+
return;
|
|
679
758
|
}
|
|
759
|
+
// Remove from all stream subscriber sets
|
|
760
|
+
this.replaceStreamSubscribers(packageName, app.subscriptions, []);
|
|
680
761
|
// Reset per-session warning dedup so a relaunch surfaces issues again.
|
|
681
762
|
resetPermissionWarnings(packageName);
|
|
763
|
+
// Cancel lookahead/in-flight synthesis before stopping the current file so
|
|
764
|
+
// an interrupted sentence cannot advance and reclaim playback.
|
|
765
|
+
this.cancelSpeech(packageName);
|
|
682
766
|
// Stop audio for this app
|
|
683
767
|
audioPlaybackService.stopForApp(packageName);
|
|
684
768
|
// Tear down this app's blob state: abort in-flight uploads + close readers
|
|
@@ -728,6 +812,11 @@ class LocalMiniappRuntime {
|
|
|
728
812
|
localDisplayManager.onUnmount(packageName);
|
|
729
813
|
this.connectedApps.delete(packageName);
|
|
730
814
|
this.recomputeMicRequirements();
|
|
815
|
+
if (releasedMicGateOverride) {
|
|
816
|
+
void micStateCoordinator.syncEffectiveGatePolicy(this.getConfiguredMicGates()).catch((error) => {
|
|
817
|
+
console.warn(`${LOG_TAG}: failed to restore mic gate policy for ${packageName}`, error);
|
|
818
|
+
});
|
|
819
|
+
}
|
|
731
820
|
this.updateCloudSubscriptions();
|
|
732
821
|
if (this.connectedApps.size === 0) {
|
|
733
822
|
this.stopPingLoop();
|
|
@@ -824,6 +913,9 @@ class LocalMiniappRuntime {
|
|
|
824
913
|
case MiniappRequestType.LOCATION_POLL:
|
|
825
914
|
this.handleLocationPoll(packageName, requestId);
|
|
826
915
|
break;
|
|
916
|
+
case MiniappRequestType.CALENDAR_LIST_EVENTS:
|
|
917
|
+
void this.handleCalendarListEvents(packageName, payload, requestId);
|
|
918
|
+
break;
|
|
827
919
|
case MiniappRequestType.NAVIGATION_START:
|
|
828
920
|
this.navigationHandlers.handleStart(packageName, payload, requestId);
|
|
829
921
|
break;
|
|
@@ -890,9 +982,18 @@ class LocalMiniappRuntime {
|
|
|
890
982
|
case MiniappRequestType.CAMERA_FOV:
|
|
891
983
|
void this.handleCameraFov(packageName, payload, requestId);
|
|
892
984
|
break;
|
|
985
|
+
case MiniappRequestType.SET_WIFI_ADB_STATE:
|
|
986
|
+
void this.handleSetWifiAdbState(packageName, payload, requestId);
|
|
987
|
+
break;
|
|
893
988
|
case MiniappRequestType.IMU_SET_ENABLED:
|
|
894
989
|
this.handleImuSetEnabled(packageName, payload, requestId);
|
|
895
990
|
break;
|
|
991
|
+
case MiniappRequestType.MIC_SET_VAD_ENABLED:
|
|
992
|
+
void this.handleMicSetVadEnabled(packageName, payload, requestId);
|
|
993
|
+
break;
|
|
994
|
+
case MiniappRequestType.MIC_SET_LOUDNESS_GATE_ENABLED:
|
|
995
|
+
void this.handleMicSetLoudnessGateEnabled(packageName, payload, requestId);
|
|
996
|
+
break;
|
|
896
997
|
case MiniappRequestType.PING:
|
|
897
998
|
// SDK should handle this itself; reply PONG just in case
|
|
898
999
|
this.sendToMiniapp(packageName, { type: MiniappResponseType.PONG }, requestId);
|
|
@@ -900,23 +1001,6 @@ class LocalMiniappRuntime {
|
|
|
900
1001
|
case MiniappResponseType.PONG: {
|
|
901
1002
|
// Liveness already touched above for every inbound message; the
|
|
902
1003
|
// PONG carries no other action.
|
|
903
|
-
// BGCAP: measure PING→PONG round-trip ONLY here (a real pong), not in
|
|
904
|
-
// handlePong (which fires for every inbound envelope). RTT = how long
|
|
905
|
-
// the miniapp's own JS engine (Crust serial queue) took to answer — a
|
|
906
|
-
// large value means that queue was starved (the transcript→display
|
|
907
|
-
// layer). bgcapLastPingSentAt holds the OLDEST outstanding ping (see
|
|
908
|
-
// doPingRound), so a multi-round background stall reports its full
|
|
909
|
-
// duration on the first pong after resume.
|
|
910
|
-
if (BGCAP_TELEMETRY) {
|
|
911
|
-
const app = this.connectedApps.get(packageName);
|
|
912
|
-
if (app?.bgcapLastPingSentAt != null) {
|
|
913
|
-
const rtt = Date.now() - app.bgcapLastPingSentAt;
|
|
914
|
-
if (rtt > 1500) {
|
|
915
|
-
console.log(`BGCAP: ${packageName} pong rtt=${rtt}ms (miniapp JS engine was starved)`);
|
|
916
|
-
}
|
|
917
|
-
app.bgcapLastPingSentAt = undefined;
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
1004
|
break;
|
|
921
1005
|
}
|
|
922
1006
|
case MiniappRequestType.SHARE:
|
|
@@ -931,51 +1015,54 @@ class LocalMiniappRuntime {
|
|
|
931
1015
|
case MiniappRequestType.DOWNLOAD:
|
|
932
1016
|
this.handleDownload(packageName, payload, requestId);
|
|
933
1017
|
break;
|
|
1018
|
+
case MiniappRequestType.SCAN_QR:
|
|
1019
|
+
void this.handleScanQr(packageName, payload, requestId);
|
|
1020
|
+
break;
|
|
934
1021
|
// Persistent binary blob storage (session.blob)
|
|
935
1022
|
case MiniappRequestType.BLOB_CREATE:
|
|
936
|
-
this.blobStore.handleCreate(packageName, payload, requestId);
|
|
1023
|
+
this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleCreate(packageName, payload, requestId));
|
|
937
1024
|
break;
|
|
938
1025
|
case MiniappRequestType.BLOB_WRITE:
|
|
939
|
-
this.blobStore.handleWrite(packageName, payload, requestId);
|
|
1026
|
+
this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleWrite(packageName, payload, requestId));
|
|
940
1027
|
break;
|
|
941
1028
|
case MiniappRequestType.BLOB_COMMIT:
|
|
942
|
-
this.blobStore.handleCommit(packageName, payload, requestId);
|
|
1029
|
+
this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleCommit(packageName, payload, requestId));
|
|
943
1030
|
break;
|
|
944
1031
|
case MiniappRequestType.BLOB_ABORT:
|
|
945
|
-
this.blobStore.handleAbort(packageName, payload, requestId);
|
|
1032
|
+
this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleAbort(packageName, payload, requestId));
|
|
946
1033
|
break;
|
|
947
1034
|
case MiniappRequestType.BLOB_SET_FROM_URL:
|
|
948
|
-
|
|
1035
|
+
this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleSetFromUrl(packageName, payload, requestId));
|
|
949
1036
|
break;
|
|
950
1037
|
case MiniappRequestType.BLOB_IMPORT:
|
|
951
|
-
|
|
1038
|
+
this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleImport(packageName, payload, requestId));
|
|
952
1039
|
break;
|
|
953
1040
|
case MiniappRequestType.BLOB_GET:
|
|
954
|
-
this.blobStore.handleGet(packageName, payload, requestId);
|
|
1041
|
+
this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleGet(packageName, payload, requestId));
|
|
955
1042
|
break;
|
|
956
1043
|
case MiniappRequestType.BLOB_LIST:
|
|
957
|
-
this.blobStore.handleList(packageName, payload, requestId);
|
|
1044
|
+
this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleList(packageName, payload, requestId));
|
|
958
1045
|
break;
|
|
959
1046
|
case MiniappRequestType.BLOB_USAGE:
|
|
960
|
-
this.blobStore.handleUsage(packageName, payload, requestId);
|
|
1047
|
+
this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleUsage(packageName, payload, requestId));
|
|
961
1048
|
break;
|
|
962
1049
|
case MiniappRequestType.BLOB_DELETE:
|
|
963
|
-
this.blobStore.handleDelete(packageName, payload, requestId);
|
|
1050
|
+
this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleDelete(packageName, payload, requestId));
|
|
964
1051
|
break;
|
|
965
1052
|
case MiniappRequestType.BLOB_CLEAR:
|
|
966
|
-
this.blobStore.handleClear(packageName, payload, requestId);
|
|
1053
|
+
this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleClear(packageName, payload, requestId));
|
|
967
1054
|
break;
|
|
968
1055
|
case MiniappRequestType.BLOB_OPEN_READ:
|
|
969
|
-
this.blobStore.handleOpenRead(packageName, payload, requestId);
|
|
1056
|
+
this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleOpenRead(packageName, payload, requestId));
|
|
970
1057
|
break;
|
|
971
1058
|
case MiniappRequestType.BLOB_READ:
|
|
972
|
-
this.blobStore.handleRead(packageName, payload, requestId);
|
|
1059
|
+
this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleRead(packageName, payload, requestId));
|
|
973
1060
|
break;
|
|
974
1061
|
case MiniappRequestType.BLOB_CLOSE_READ:
|
|
975
|
-
this.blobStore.handleCloseRead(packageName, payload, requestId);
|
|
1062
|
+
this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleCloseRead(packageName, payload, requestId));
|
|
976
1063
|
break;
|
|
977
1064
|
case MiniappRequestType.BLOB_SHARE:
|
|
978
|
-
|
|
1065
|
+
this.enqueueBlobRequest(packageName, requestId, () => this.blobStore.handleShare(packageName, payload, requestId));
|
|
979
1066
|
break;
|
|
980
1067
|
// Cloud-coordinated features
|
|
981
1068
|
case MiniappRequestType.PHOTO:
|
|
@@ -1028,8 +1115,19 @@ class LocalMiniappRuntime {
|
|
|
1028
1115
|
message: "Dashboard API is deferred in v1",
|
|
1029
1116
|
});
|
|
1030
1117
|
break;
|
|
1118
|
+
case MiniappRequestType.TRANSCRIPTION_CONFIG:
|
|
1119
|
+
this.handleTranscriptionConfig(packageName, payload, requestId);
|
|
1120
|
+
break;
|
|
1031
1121
|
default:
|
|
1122
|
+
// NACK instead of silent drop (issue 021 S3): an SDK that sends a
|
|
1123
|
+
// request this runtime doesn't implement must get a rejected promise,
|
|
1124
|
+
// not a permanently-pending silent gap. One-shots (no requestId) can
|
|
1125
|
+
// only be logged.
|
|
1032
1126
|
console.warn(`${LOG_TAG}: Unknown request type from ${packageName}: ${requestType}`);
|
|
1127
|
+
this.sendResult(packageName, requestId, false, undefined, {
|
|
1128
|
+
code: MiniappErrorCode.NOT_IMPLEMENTED,
|
|
1129
|
+
message: `Unknown or unimplemented request type: ${requestType}`,
|
|
1130
|
+
});
|
|
1033
1131
|
break;
|
|
1034
1132
|
}
|
|
1035
1133
|
}
|
|
@@ -1046,6 +1144,7 @@ class LocalMiniappRuntime {
|
|
|
1046
1144
|
}
|
|
1047
1145
|
// Update lastPongAt so it doesn't time out right away
|
|
1048
1146
|
existing.lastPongAt = Date.now();
|
|
1147
|
+
existing.unansweredPingRounds = 0;
|
|
1049
1148
|
// Read current glasses capabilities from the settings store
|
|
1050
1149
|
const defaultWearable = useSettingsStore.getState().getSetting(ISLAND_SETTINGS_KEYS.defaultWearable);
|
|
1051
1150
|
const capabilities = getModelCapabilities(defaultWearable || DeviceTypes.NONE);
|
|
@@ -1091,10 +1190,12 @@ class LocalMiniappRuntime {
|
|
|
1091
1190
|
this.flushConnectWaiters(packageName);
|
|
1092
1191
|
}
|
|
1093
1192
|
async requestMiniappAuth(packageName, opts) {
|
|
1094
|
-
const authPackageName =
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1193
|
+
const authPackageName = getDevAppSourcePackage(packageName) ?? packageName;
|
|
1194
|
+
const devAttestation = getDevAppAttestation(packageName) ?? undefined;
|
|
1195
|
+
return cloudClientService.getMiniappAuthToken(authPackageName, {
|
|
1196
|
+
...opts,
|
|
1197
|
+
...(devAttestation ? { devAttestation } : {}),
|
|
1198
|
+
});
|
|
1098
1199
|
}
|
|
1099
1200
|
async handleAuthRefresh(packageName, payload, requestId) {
|
|
1100
1201
|
try {
|
|
@@ -1231,6 +1332,51 @@ class LocalMiniappRuntime {
|
|
|
1231
1332
|
this.deliverInitialMiniappAuth(packageName, undefined, 0);
|
|
1232
1333
|
}
|
|
1233
1334
|
}
|
|
1335
|
+
/**
|
|
1336
|
+
* TRANSCRIPTION_CONFIG (issue 021 WP3): store the miniapp's language hints
|
|
1337
|
+
* and rebuild the cloud subscription set so the auto-mode subscription
|
|
1338
|
+
* carries them. Replies with a REQUEST_RESULT so the SDK's configure()
|
|
1339
|
+
* promise settles: unknown hint values are an error, not a silent drop.
|
|
1340
|
+
*/
|
|
1341
|
+
handleTranscriptionConfig(packageName, payload, requestId) {
|
|
1342
|
+
const app = this.connectedApps.get(packageName);
|
|
1343
|
+
if (!app)
|
|
1344
|
+
return;
|
|
1345
|
+
const config = (payload.config ?? {});
|
|
1346
|
+
const rawHints = config.languageHints;
|
|
1347
|
+
if (rawHints !== undefined && !Array.isArray(rawHints)) {
|
|
1348
|
+
this.sendResult(packageName, requestId, false, undefined, {
|
|
1349
|
+
code: MiniappErrorCode.INTERNAL,
|
|
1350
|
+
message: "languageHints must be an array of language codes",
|
|
1351
|
+
});
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
if (rawHints === undefined || rawHints.length === 0) {
|
|
1355
|
+
this.transcriptionHintsByApp.delete(packageName);
|
|
1356
|
+
}
|
|
1357
|
+
else {
|
|
1358
|
+
// Defense in depth: the SDK already validated, but the wire is untyped.
|
|
1359
|
+
const hints = [];
|
|
1360
|
+
const invalid = [];
|
|
1361
|
+
for (const raw of rawHints) {
|
|
1362
|
+
const hint = typeof raw === "string" ? toLanguageHint(raw) : null;
|
|
1363
|
+
if (hint)
|
|
1364
|
+
hints.push(hint);
|
|
1365
|
+
else
|
|
1366
|
+
invalid.push(String(raw));
|
|
1367
|
+
}
|
|
1368
|
+
if (invalid.length > 0) {
|
|
1369
|
+
this.sendResult(packageName, requestId, false, undefined, {
|
|
1370
|
+
code: MiniappErrorCode.INTERNAL,
|
|
1371
|
+
message: `Unknown language hint(s): ${invalid.join(", ")}`,
|
|
1372
|
+
});
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
this.transcriptionHintsByApp.set(packageName, [...new Set(hints)].sort());
|
|
1376
|
+
}
|
|
1377
|
+
this.updateCloudSubscriptions();
|
|
1378
|
+
this.sendResult(packageName, requestId, true, { applied: true });
|
|
1379
|
+
}
|
|
1234
1380
|
handleSubscribe(packageName, payload, requestId) {
|
|
1235
1381
|
const app = this.connectedApps.get(packageName);
|
|
1236
1382
|
if (!app)
|
|
@@ -1241,6 +1387,8 @@ class LocalMiniappRuntime {
|
|
|
1241
1387
|
// The strictest rate wins when multiple `location_stream` entries appear
|
|
1242
1388
|
// in one SUBSCRIBE — the same rule we apply across apps.
|
|
1243
1389
|
let requestedLocationRate = null;
|
|
1390
|
+
const forceLocalTranscriptionStreams = new Set();
|
|
1391
|
+
const cloudTranscriptionStreams = new Set();
|
|
1244
1392
|
const streams = rawStreams?.map((s) => {
|
|
1245
1393
|
if (typeof s === "object" && s !== null) {
|
|
1246
1394
|
if (s.stream === "location_stream") {
|
|
@@ -1249,8 +1397,16 @@ class LocalMiniappRuntime {
|
|
|
1249
1397
|
}
|
|
1250
1398
|
return "location_update";
|
|
1251
1399
|
}
|
|
1400
|
+
if (s.forceLocal === true && s.stream.startsWith("transcription:")) {
|
|
1401
|
+
forceLocalTranscriptionStreams.add(s.stream);
|
|
1402
|
+
}
|
|
1403
|
+
if (s.stream.startsWith("transcription:") && (s.forceLocal !== true || s.includeCloud === true)) {
|
|
1404
|
+
cloudTranscriptionStreams.add(s.stream);
|
|
1405
|
+
}
|
|
1252
1406
|
return s.stream;
|
|
1253
1407
|
}
|
|
1408
|
+
if (s.startsWith("transcription:"))
|
|
1409
|
+
cloudTranscriptionStreams.add(s);
|
|
1254
1410
|
return s;
|
|
1255
1411
|
});
|
|
1256
1412
|
if (!Array.isArray(streams)) {
|
|
@@ -1276,8 +1432,6 @@ class LocalMiniappRuntime {
|
|
|
1276
1432
|
return "READ_NOTIFICATIONS";
|
|
1277
1433
|
if (s === "phone_notification_dismissed")
|
|
1278
1434
|
return "READ_NOTIFICATIONS";
|
|
1279
|
-
if (s === "calendar_event")
|
|
1280
|
-
return "CALENDAR";
|
|
1281
1435
|
return null;
|
|
1282
1436
|
};
|
|
1283
1437
|
for (const stream of streams) {
|
|
@@ -1295,26 +1449,12 @@ class LocalMiniappRuntime {
|
|
|
1295
1449
|
return;
|
|
1296
1450
|
}
|
|
1297
1451
|
}
|
|
1298
|
-
// Remove old subscriptions for this app
|
|
1299
|
-
for (const oldStream of app.subscriptions) {
|
|
1300
|
-
const subs = this.streamSubscribers.get(oldStream);
|
|
1301
|
-
if (subs) {
|
|
1302
|
-
subs.delete(packageName);
|
|
1303
|
-
if (subs.size === 0) {
|
|
1304
|
-
this.streamSubscribers.delete(oldStream);
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
}
|
|
1308
1452
|
// Add new subscriptions
|
|
1453
|
+
const previousSubscriptions = app.subscriptions;
|
|
1309
1454
|
app.subscriptions = new Set(streams);
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
subs = new Set();
|
|
1314
|
-
this.streamSubscribers.set(stream, subs);
|
|
1315
|
-
}
|
|
1316
|
-
subs.add(packageName);
|
|
1317
|
-
}
|
|
1455
|
+
app.forceLocalTranscriptionStreams = new Set([...forceLocalTranscriptionStreams].filter((stream) => app.subscriptions.has(stream)));
|
|
1456
|
+
app.cloudTranscriptionStreams = new Set([...cloudTranscriptionStreams].filter((stream) => app.subscriptions.has(stream)));
|
|
1457
|
+
this.replaceStreamSubscribers(packageName, previousSubscriptions, app.subscriptions);
|
|
1318
1458
|
this.recomputeMicRequirements();
|
|
1319
1459
|
this.updateCloudSubscriptions();
|
|
1320
1460
|
this.recomputeHeadingSubscription();
|
|
@@ -1329,6 +1469,30 @@ class LocalMiniappRuntime {
|
|
|
1329
1469
|
// to wait for the first change event.
|
|
1330
1470
|
this.emitInitialSnapshots(packageName, streams);
|
|
1331
1471
|
}
|
|
1472
|
+
async handleCalendarListEvents(packageName, payload, requestId) {
|
|
1473
|
+
const app = this.connectedApps.get(packageName);
|
|
1474
|
+
const declared = app?.installedManifest?.permissions?.some((permission) => permission.type?.toUpperCase() === "CALENDAR");
|
|
1475
|
+
if (!declared) {
|
|
1476
|
+
logPermissionNotDeclared(packageName, "CALENDAR", "to list calendar events", '{"type": "CALENDAR"}');
|
|
1477
|
+
this.sendResult(packageName, requestId, false, undefined, {
|
|
1478
|
+
code: MiniappErrorCode.PERMISSION_NOT_DECLARED,
|
|
1479
|
+
message: 'CALENDAR permission not declared in miniapp.json. Add {"type": "CALENDAR"} to the permissions array.',
|
|
1480
|
+
permission: "CALENDAR",
|
|
1481
|
+
});
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
try {
|
|
1485
|
+
const result = await listPhoneCalendarEvents(payload);
|
|
1486
|
+
this.sendResult(packageName, requestId, true, result);
|
|
1487
|
+
}
|
|
1488
|
+
catch (error) {
|
|
1489
|
+
const known = error instanceof PhoneCalendarError;
|
|
1490
|
+
this.sendResult(packageName, requestId, false, undefined, {
|
|
1491
|
+
code: known ? error.code : MiniappErrorCode.INTERNAL,
|
|
1492
|
+
message: error instanceof Error ? error.message : "Failed to read phone calendar events",
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1332
1496
|
/**
|
|
1333
1497
|
* For state-bearing streams (battery, connection), deliver the current value
|
|
1334
1498
|
* immediately on subscribe. Other streams (button press, transcription, etc.)
|
|
@@ -1518,6 +1682,7 @@ class LocalMiniappRuntime {
|
|
|
1518
1682
|
const audioRequestId = requestId || `local_${Date.now()}`;
|
|
1519
1683
|
const volume = typeof payload.volume === "number" ? payload.volume : 1.0;
|
|
1520
1684
|
const stopOtherAudio = payload.stopOtherAudio !== false;
|
|
1685
|
+
this.cancelSpeech(packageName);
|
|
1521
1686
|
this.setSpeakerState(packageName, "loading");
|
|
1522
1687
|
audioPlaybackService.play({ requestId: audioRequestId, audioUrl, appId: packageName, volume, stopOtherAudio }, (_respId, success, error, duration) => {
|
|
1523
1688
|
if (success) {
|
|
@@ -1544,6 +1709,7 @@ class LocalMiniappRuntime {
|
|
|
1544
1709
|
queueMicrotask(() => this.setSpeakerState(packageName, "playing"));
|
|
1545
1710
|
}
|
|
1546
1711
|
handleStopAudio(packageName, _payload, requestId) {
|
|
1712
|
+
this.cancelSpeech(packageName);
|
|
1547
1713
|
audioPlaybackService.stopForApp(packageName);
|
|
1548
1714
|
this.setSpeakerState(packageName, "stopped");
|
|
1549
1715
|
this.sendResult(packageName, requestId, true);
|
|
@@ -1730,84 +1896,184 @@ class LocalMiniappRuntime {
|
|
|
1730
1896
|
}
|
|
1731
1897
|
}
|
|
1732
1898
|
async handleSpeak(packageName, payload, requestId) {
|
|
1733
|
-
const
|
|
1734
|
-
|
|
1899
|
+
const rawText = payload.text;
|
|
1900
|
+
const rawSentences = typeof rawText === "string"
|
|
1901
|
+
? [rawText.trim()].filter(Boolean)
|
|
1902
|
+
: Array.isArray(rawText)
|
|
1903
|
+
? rawText
|
|
1904
|
+
.filter((sentence) => typeof sentence === "string")
|
|
1905
|
+
.map((sentence) => sentence.trim())
|
|
1906
|
+
.filter(Boolean)
|
|
1907
|
+
: [];
|
|
1908
|
+
const enableSanitization = payload.enableSanitization !== false;
|
|
1909
|
+
const sentences = prepareTtsSentences(rawSentences, enableSanitization);
|
|
1910
|
+
if (sentences.length === 0) {
|
|
1735
1911
|
this.sendResult(packageName, requestId, false, undefined, {
|
|
1736
1912
|
code: MiniappErrorCode.INTERNAL,
|
|
1737
|
-
message: "speak requires text",
|
|
1913
|
+
message: "speak requires text or a non-empty sentence list with speakable content",
|
|
1738
1914
|
});
|
|
1739
1915
|
return;
|
|
1740
1916
|
}
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1917
|
+
const cloudText = sentences.join(" ");
|
|
1918
|
+
const voice = (payload.voice_id ?? payload.voice) || "default";
|
|
1919
|
+
const audioRequestId = requestId || `tts_${Date.now()}`;
|
|
1920
|
+
const volume = typeof payload.volume === "number" ? payload.volume : 1.0;
|
|
1921
|
+
const stopOtherAudio = payload.stopOtherAudio !== false;
|
|
1922
|
+
const voiceSettings = payload.voice_settings && typeof payload.voice_settings === "object"
|
|
1923
|
+
? payload.voice_settings
|
|
1924
|
+
: undefined;
|
|
1925
|
+
const speed = typeof voiceSettings?.speed === "number" ? voiceSettings.speed : undefined;
|
|
1926
|
+
const run = { id: audioRequestId, cancelled: false };
|
|
1927
|
+
this.cancelSpeech(packageName);
|
|
1928
|
+
this.speechRuns.set(packageName, run);
|
|
1929
|
+
this.setSpeakerState(packageName, "loading");
|
|
1930
|
+
let terminalSent = false;
|
|
1931
|
+
const sendPlaybackResult = (success, error, duration, fallbackErrorMessage, completed = success, errorCode = MiniappErrorCode.TTS_UPSTREAM_ERROR) => {
|
|
1932
|
+
if (terminalSent)
|
|
1933
|
+
return;
|
|
1934
|
+
terminalSent = true;
|
|
1935
|
+
// A superseded run still owns its bridge request, but it no longer owns
|
|
1936
|
+
// the package's shared speaker state.
|
|
1937
|
+
const ownsSpeakerState = this.speechRuns.get(packageName) === run;
|
|
1938
|
+
if (ownsSpeakerState)
|
|
1939
|
+
this.speechRuns.delete(packageName);
|
|
1940
|
+
if (ownsSpeakerState) {
|
|
1761
1941
|
if (success) {
|
|
1762
1942
|
this.setSpeakerState(packageName, "stopped", { durationMs: duration ?? undefined });
|
|
1763
1943
|
}
|
|
1764
1944
|
else {
|
|
1765
1945
|
this.setSpeakerState(packageName, "error", {
|
|
1766
|
-
errorCode
|
|
1946
|
+
errorCode,
|
|
1767
1947
|
errorMessage: error ?? fallbackErrorMessage,
|
|
1768
1948
|
durationMs: duration ?? undefined,
|
|
1769
1949
|
});
|
|
1770
1950
|
}
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1951
|
+
}
|
|
1952
|
+
this.sendResult(packageName, requestId, success, { completed, duration }, error
|
|
1953
|
+
? {
|
|
1954
|
+
code: errorCode,
|
|
1955
|
+
message: error,
|
|
1956
|
+
}
|
|
1957
|
+
: undefined);
|
|
1958
|
+
};
|
|
1959
|
+
run.onCancelled = () => sendPlaybackResult(true, null, null, "tts playback stopped", false);
|
|
1960
|
+
try {
|
|
1961
|
+
const voiceExplicit = payload.voice_id !== undefined || payload.voice !== undefined;
|
|
1962
|
+
const offlineSupportsVoice = !voiceExplicit || voice === "default" || ttsModelManager.getAvailableLanguages().some((l) => l.code === voice);
|
|
1963
|
+
const modelId = typeof payload.model_id === "string" ? payload.model_id : undefined;
|
|
1964
|
+
const forceLocal = payload.forceLocal === true;
|
|
1965
|
+
const cloudConnected = cloudClientService.isConnected();
|
|
1966
|
+
console.log(`${LOG_TAG}: TTS decision for ${packageName}: cloudConnected=${cloudConnected}, runtimeTts=yes, offlineVoice=${offlineSupportsVoice}, forceLocal=${forceLocal}`);
|
|
1778
1967
|
const playOfflineTts = async (reason) => {
|
|
1968
|
+
if (run.cancelled)
|
|
1969
|
+
return true;
|
|
1779
1970
|
if (!offlineSupportsVoice || !(await ttsModelManager.isModelAvailable())) {
|
|
1780
1971
|
return false;
|
|
1781
1972
|
}
|
|
1973
|
+
if (run.cancelled)
|
|
1974
|
+
return true;
|
|
1975
|
+
const languageCode = ttsModelManager.getAvailableLanguages().some((l) => l.code === voice) ? voice : undefined;
|
|
1976
|
+
const offlineSentences = sentences;
|
|
1977
|
+
if (offlineSentences.length > 1) {
|
|
1978
|
+
let firstSentence;
|
|
1979
|
+
try {
|
|
1980
|
+
firstSentence = await ttsModelManager.synthesizeToFile(offlineSentences[0], { languageCode, speed });
|
|
1981
|
+
}
|
|
1982
|
+
catch (offlineErr) {
|
|
1983
|
+
if (run.cancelled)
|
|
1984
|
+
return true;
|
|
1985
|
+
console.warn(`${LOG_TAG}: offline sentence TTS synthesize failed${reason ? ` after ${reason}` : ""}:`, offlineErr);
|
|
1986
|
+
return false;
|
|
1987
|
+
}
|
|
1988
|
+
if (run.cancelled) {
|
|
1989
|
+
await Promise.resolve(firstSentence.cleanup?.());
|
|
1990
|
+
return true;
|
|
1991
|
+
}
|
|
1992
|
+
void runSentenceTtsPipeline({
|
|
1993
|
+
sentences: offlineSentences,
|
|
1994
|
+
firstAudio: firstSentence,
|
|
1995
|
+
run,
|
|
1996
|
+
synthesize: (sentence) => ttsModelManager.synthesizeToFile(sentence, { languageCode, speed }),
|
|
1997
|
+
play: (audio, index) => new Promise((resolve) => {
|
|
1998
|
+
const sentenceRequestId = `${audioRequestId}_sentence_${index}`;
|
|
1999
|
+
run.playbackRequestId = sentenceRequestId;
|
|
2000
|
+
audioPlaybackService.play({
|
|
2001
|
+
requestId: sentenceRequestId,
|
|
2002
|
+
audioUrl: audio.audioUrl,
|
|
2003
|
+
appId: packageName,
|
|
2004
|
+
volume,
|
|
2005
|
+
stopOtherAudio,
|
|
2006
|
+
}, (_responseId, success, error, duration, completionReason) => {
|
|
2007
|
+
if (run.playbackRequestId === sentenceRequestId)
|
|
2008
|
+
run.playbackRequestId = undefined;
|
|
2009
|
+
resolve({ success, completed: completionReason === "completed", error, duration });
|
|
2010
|
+
});
|
|
2011
|
+
}),
|
|
2012
|
+
onCleanupError: (cleanupError) => {
|
|
2013
|
+
console.warn(`${LOG_TAG}: sentence TTS cleanup failed`, cleanupError);
|
|
2014
|
+
},
|
|
2015
|
+
}).then(({ success, completed, error, duration }) => {
|
|
2016
|
+
sendPlaybackResult(success, error, duration, "offline sentence tts playback failed", completed);
|
|
2017
|
+
}, (pipelineError) => {
|
|
2018
|
+
const message = pipelineError instanceof Error ? pipelineError.message : String(pipelineError);
|
|
2019
|
+
sendPlaybackResult(false, message, null, "offline sentence tts playback failed", false);
|
|
2020
|
+
});
|
|
2021
|
+
queueMicrotask(() => {
|
|
2022
|
+
if (this.speechRuns.get(packageName) === run)
|
|
2023
|
+
this.setSpeakerState(packageName, "playing");
|
|
2024
|
+
});
|
|
2025
|
+
return true;
|
|
2026
|
+
}
|
|
1782
2027
|
let offlineGenerated;
|
|
1783
2028
|
try {
|
|
1784
|
-
|
|
1785
|
-
offlineGenerated = await ttsModelManager.synthesizeToFile(text, { languageCode, speed });
|
|
2029
|
+
offlineGenerated = await ttsModelManager.synthesizeToFile(offlineSentences[0], { languageCode, speed });
|
|
1786
2030
|
}
|
|
1787
2031
|
catch (offlineErr) {
|
|
2032
|
+
if (run.cancelled)
|
|
2033
|
+
return true;
|
|
1788
2034
|
console.warn(`${LOG_TAG}: offline TTS synthesize failed${reason ? ` after ${reason}` : ""}:`, offlineErr);
|
|
1789
2035
|
return false;
|
|
1790
2036
|
}
|
|
1791
2037
|
const generated = offlineGenerated;
|
|
1792
|
-
|
|
2038
|
+
if (run.cancelled) {
|
|
2039
|
+
await Promise.resolve(generated.cleanup?.());
|
|
2040
|
+
return true;
|
|
2041
|
+
}
|
|
2042
|
+
run.playbackRequestId = audioRequestId;
|
|
2043
|
+
audioPlaybackService.play({
|
|
2044
|
+
requestId: audioRequestId,
|
|
2045
|
+
audioUrl: generated.audioUrl,
|
|
2046
|
+
appId: packageName,
|
|
2047
|
+
volume,
|
|
2048
|
+
stopOtherAudio,
|
|
2049
|
+
}, (_respId, success, error, duration, completionReason) => {
|
|
2050
|
+
if (run.playbackRequestId === audioRequestId)
|
|
2051
|
+
run.playbackRequestId = undefined;
|
|
1793
2052
|
void Promise.resolve(generated.cleanup?.()).catch((cleanupError) => {
|
|
1794
2053
|
console.warn(`${LOG_TAG}: offline TTS cleanup failed`, cleanupError);
|
|
1795
2054
|
});
|
|
1796
|
-
sendPlaybackResult(success, error, duration, "offline tts playback failed");
|
|
2055
|
+
sendPlaybackResult(success, error, duration, "offline tts playback failed", completionReason === "completed");
|
|
2056
|
+
});
|
|
2057
|
+
queueMicrotask(() => {
|
|
2058
|
+
if (this.speechRuns.get(packageName) === run)
|
|
2059
|
+
this.setSpeakerState(packageName, "playing");
|
|
1797
2060
|
});
|
|
1798
|
-
queueMicrotask(() => this.setSpeakerState(packageName, "playing"));
|
|
1799
2061
|
return true;
|
|
1800
2062
|
};
|
|
1801
2063
|
const playCloudTts = async (fallbackToOffline) => {
|
|
2064
|
+
if (run.cancelled)
|
|
2065
|
+
return true;
|
|
1802
2066
|
let source;
|
|
1803
2067
|
try {
|
|
1804
|
-
source = await cloudClientService.tts.speak(
|
|
2068
|
+
source = await cloudClientService.tts.speak(cloudText, {
|
|
1805
2069
|
...(voiceExplicit && voice !== "default" ? { voice_id: voice } : {}),
|
|
1806
2070
|
...(modelId ? { model_id: modelId } : {}),
|
|
1807
2071
|
...(voiceSettings ? { voice_settings: voiceSettings } : {}),
|
|
1808
2072
|
});
|
|
1809
2073
|
}
|
|
1810
2074
|
catch (cloudErr) {
|
|
2075
|
+
if (run.cancelled)
|
|
2076
|
+
return true;
|
|
1811
2077
|
const error = cloudErr instanceof Error ? cloudErr.message : String(cloudErr);
|
|
1812
2078
|
console.warn(`${LOG_TAG}: cloud TTS source failed: ${error}`);
|
|
1813
2079
|
if (fallbackToOffline && (await playOfflineTts("cloud tts source failed"))) {
|
|
@@ -1816,7 +2082,18 @@ class LocalMiniappRuntime {
|
|
|
1816
2082
|
sendPlaybackResult(false, error, null, "tts failed");
|
|
1817
2083
|
return true;
|
|
1818
2084
|
}
|
|
1819
|
-
|
|
2085
|
+
if (run.cancelled)
|
|
2086
|
+
return true;
|
|
2087
|
+
run.playbackRequestId = audioRequestId;
|
|
2088
|
+
await Promise.resolve(audioPlaybackService.play({
|
|
2089
|
+
requestId: audioRequestId,
|
|
2090
|
+
audioUrl: source.audioUrl,
|
|
2091
|
+
appId: packageName,
|
|
2092
|
+
volume,
|
|
2093
|
+
stopOtherAudio,
|
|
2094
|
+
}, (_respId, success, error, duration, completionReason) => {
|
|
2095
|
+
if (run.playbackRequestId === audioRequestId)
|
|
2096
|
+
run.playbackRequestId = undefined;
|
|
1820
2097
|
if (!success && fallbackToOffline) {
|
|
1821
2098
|
void playOfflineTts("cloud tts playback failed").then((started) => {
|
|
1822
2099
|
if (!started) {
|
|
@@ -1825,11 +2102,24 @@ class LocalMiniappRuntime {
|
|
|
1825
2102
|
});
|
|
1826
2103
|
return;
|
|
1827
2104
|
}
|
|
1828
|
-
sendPlaybackResult(success, error, duration, "tts failed");
|
|
2105
|
+
sendPlaybackResult(success, error, duration, "tts failed", completionReason === "completed");
|
|
1829
2106
|
}));
|
|
1830
|
-
queueMicrotask(() =>
|
|
2107
|
+
queueMicrotask(() => {
|
|
2108
|
+
if (this.speechRuns.get(packageName) === run)
|
|
2109
|
+
this.setSpeakerState(packageName, "playing");
|
|
2110
|
+
});
|
|
1831
2111
|
return true;
|
|
1832
2112
|
};
|
|
2113
|
+
if (forceLocal) {
|
|
2114
|
+
if (await playOfflineTts()) {
|
|
2115
|
+
return;
|
|
2116
|
+
}
|
|
2117
|
+
if (run.cancelled)
|
|
2118
|
+
return;
|
|
2119
|
+
const message = "tts unavailable: forceLocal requested but offline TTS model is not available";
|
|
2120
|
+
sendPlaybackResult(false, message, null, message, false, MiniappErrorCode.TTS_LOCAL_UNAVAILABLE);
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
1833
2123
|
if (cloudConnected && (await playCloudTts(true))) {
|
|
1834
2124
|
return;
|
|
1835
2125
|
}
|
|
@@ -1842,28 +2132,24 @@ class LocalMiniappRuntime {
|
|
|
1842
2132
|
return;
|
|
1843
2133
|
}
|
|
1844
2134
|
const message = "tts unavailable: no cloud TTS URL or offline TTS model";
|
|
1845
|
-
|
|
1846
|
-
errorCode: MiniappErrorCode.TTS_UPSTREAM_ERROR,
|
|
1847
|
-
errorMessage: message,
|
|
1848
|
-
});
|
|
1849
|
-
this.sendResult(packageName, requestId, false, undefined, {
|
|
1850
|
-
code: MiniappErrorCode.TTS_UPSTREAM_ERROR,
|
|
1851
|
-
message,
|
|
1852
|
-
});
|
|
2135
|
+
sendPlaybackResult(false, message, null, message, false);
|
|
1853
2136
|
}
|
|
1854
2137
|
catch (err) {
|
|
1855
2138
|
console.error(`${LOG_TAG}: speak error:`, err);
|
|
1856
2139
|
const message = err instanceof Error ? err.message : "TTS error";
|
|
1857
|
-
|
|
1858
|
-
errorCode: MiniappErrorCode.TTS_UPSTREAM_ERROR,
|
|
1859
|
-
errorMessage: message,
|
|
1860
|
-
});
|
|
1861
|
-
this.sendResult(packageName, requestId, false, undefined, {
|
|
1862
|
-
code: MiniappErrorCode.TTS_UPSTREAM_ERROR,
|
|
1863
|
-
message,
|
|
1864
|
-
});
|
|
2140
|
+
sendPlaybackResult(false, message, null, "TTS error", false);
|
|
1865
2141
|
}
|
|
1866
2142
|
}
|
|
2143
|
+
cancelSpeech(packageName) {
|
|
2144
|
+
const run = this.speechRuns.get(packageName);
|
|
2145
|
+
if (!run)
|
|
2146
|
+
return;
|
|
2147
|
+
run.cancelled = true;
|
|
2148
|
+
this.speechRuns.delete(packageName);
|
|
2149
|
+
if (run.playbackRequestId)
|
|
2150
|
+
audioPlaybackService.cancelPlayback(run.playbackRequestId);
|
|
2151
|
+
run.onCancelled?.();
|
|
2152
|
+
}
|
|
1867
2153
|
async handleRgbLed(packageName, payload, requestId) {
|
|
1868
2154
|
const coerceNumber = (value, fallback) => {
|
|
1869
2155
|
const coerced = Number(value);
|
|
@@ -1885,7 +2171,15 @@ class LocalMiniappRuntime {
|
|
|
1885
2171
|
}
|
|
1886
2172
|
async handleLocationPoll(packageName, requestId) {
|
|
1887
2173
|
try {
|
|
1888
|
-
|
|
2174
|
+
// Expo's Android requestForegroundPermissionsAsync always delegates to
|
|
2175
|
+
// Activity.requestPermissions(), even when permission is already granted.
|
|
2176
|
+
// Calling it after the user presses Home therefore waits until the Activity
|
|
2177
|
+
// resumes. Read the current grant first, and only enter the prompt flow
|
|
2178
|
+
// while the Activity is actually foregrounded.
|
|
2179
|
+
const permissionStartedAt = Date.now();
|
|
2180
|
+
const permission = await resolveForegroundLocationPermission(Location, () => AppState.currentState);
|
|
2181
|
+
const { status } = permission;
|
|
2182
|
+
console.log(`${LOG_TAG}: location poll permission status=${status} appState=${AppState.currentState} elapsed=${Date.now() - permissionStartedAt}ms`);
|
|
1889
2183
|
if (status !== "granted") {
|
|
1890
2184
|
this.sendResult(packageName, requestId, false, undefined, {
|
|
1891
2185
|
code: MiniappErrorCode.PERMISSION_NOT_DECLARED,
|
|
@@ -1897,8 +2191,11 @@ class LocalMiniappRuntime {
|
|
|
1897
2191
|
// a fresh fix if the cache is empty. Use Low accuracy on the fresh
|
|
1898
2192
|
// path so we get a cell/wifi-tower fix in ~1s instead of waiting
|
|
1899
2193
|
// for full GPS warm-up.
|
|
2194
|
+
const locationStartedAt = Date.now();
|
|
1900
2195
|
const cached = await Location.getLastKnownPositionAsync({ maxAge: 60_000 });
|
|
2196
|
+
console.log(`${LOG_TAG}: location poll cache hit=${cached !== null} elapsed=${Date.now() - locationStartedAt}ms`);
|
|
1901
2197
|
const location = cached ?? (await Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.Low }));
|
|
2198
|
+
console.log(`${LOG_TAG}: location poll resolved elapsed=${Date.now() - locationStartedAt}ms`);
|
|
1902
2199
|
this.sendResult(packageName, requestId, true, {
|
|
1903
2200
|
lat: location.coords.latitude,
|
|
1904
2201
|
lng: location.coords.longitude,
|
|
@@ -1933,8 +2230,64 @@ class LocalMiniappRuntime {
|
|
|
1933
2230
|
*/
|
|
1934
2231
|
blobStore = new BlobStore({
|
|
1935
2232
|
sendResult: (packageName, requestId, ok, result, error) => this.sendResult(packageName, requestId, ok, result, error),
|
|
1936
|
-
getUserId: () =>
|
|
2233
|
+
getUserId: () => cloudClientService.getMentraUserId(),
|
|
2234
|
+
});
|
|
2235
|
+
blobRequestQueues = new Map();
|
|
2236
|
+
simpleStorage = new LocalMiniappStorage({
|
|
2237
|
+
backend: localMiniappStorageBackend,
|
|
2238
|
+
getUserId: () => cloudClientService.resolveMentraUserId(),
|
|
1937
2239
|
});
|
|
2240
|
+
/**
|
|
2241
|
+
* BlobStore's file helpers are synchronous once a user namespace is selected,
|
|
2242
|
+
* but first-boot Core auth is not. Queue requests per app behind stable
|
|
2243
|
+
* identity resolution so CREATE/WRITE/COMMIT ordering survives that wait.
|
|
2244
|
+
*/
|
|
2245
|
+
enqueueBlobRequest(packageName, requestId, operation) {
|
|
2246
|
+
const app = this.connectedApps.get(packageName);
|
|
2247
|
+
const previous = this.blobRequestQueues.get(packageName) ?? Promise.resolve();
|
|
2248
|
+
const queued = previous.then(async () => {
|
|
2249
|
+
try {
|
|
2250
|
+
await cloudClientService.resolveMentraUserId();
|
|
2251
|
+
}
|
|
2252
|
+
catch (err) {
|
|
2253
|
+
console.error(`${LOG_TAG}: blob identity error:`, err);
|
|
2254
|
+
this.sendResult(packageName, requestId, false, undefined, {
|
|
2255
|
+
code: MiniappErrorCode.NOT_CONNECTED,
|
|
2256
|
+
message: "Blob storage is unavailable before sign-in completes",
|
|
2257
|
+
});
|
|
2258
|
+
return;
|
|
2259
|
+
}
|
|
2260
|
+
// unregisterApp() or a crash-respawn may have replaced this app while
|
|
2261
|
+
// identity resolution was pending. Never replay an old context's request
|
|
2262
|
+
// against its replacement.
|
|
2263
|
+
if (!app || this.connectedApps.get(packageName) !== app)
|
|
2264
|
+
return;
|
|
2265
|
+
// Invoke in request order, but do not keep the queue occupied for the
|
|
2266
|
+
// lifetime of an async picker, download, or share sheet. CREATE/WRITE/
|
|
2267
|
+
// COMMIT are synchronous and therefore still dispatch strictly in order.
|
|
2268
|
+
try {
|
|
2269
|
+
const pending = operation();
|
|
2270
|
+
if (pending) {
|
|
2271
|
+
void pending.catch((err) => this.handleBlobRequestError(packageName, requestId, err));
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
catch (err) {
|
|
2275
|
+
this.handleBlobRequestError(packageName, requestId, err);
|
|
2276
|
+
}
|
|
2277
|
+
});
|
|
2278
|
+
this.blobRequestQueues.set(packageName, queued);
|
|
2279
|
+
void queued.finally(() => {
|
|
2280
|
+
if (this.blobRequestQueues.get(packageName) === queued)
|
|
2281
|
+
this.blobRequestQueues.delete(packageName);
|
|
2282
|
+
});
|
|
2283
|
+
}
|
|
2284
|
+
handleBlobRequestError(packageName, requestId, err) {
|
|
2285
|
+
console.error(`${LOG_TAG}: blob request error:`, err);
|
|
2286
|
+
this.sendResult(packageName, requestId, false, undefined, {
|
|
2287
|
+
code: MiniappErrorCode.INTERNAL,
|
|
2288
|
+
message: err instanceof Error ? err.message : "Blob storage error",
|
|
2289
|
+
});
|
|
2290
|
+
}
|
|
1938
2291
|
/**
|
|
1939
2292
|
* Heading is a sensor stream — start the native compass when any mini
|
|
1940
2293
|
* app is subscribed to "heading_update", stop it when none are.
|
|
@@ -2009,10 +2362,6 @@ class LocalMiniappRuntime {
|
|
|
2009
2362
|
// ---------------------------------------------------------------------------
|
|
2010
2363
|
// Storage helpers
|
|
2011
2364
|
// ---------------------------------------------------------------------------
|
|
2012
|
-
getStorageKeyPrefix(packageName) {
|
|
2013
|
-
const userId = useSettingsStore.getState().getSetting(ISLAND_SETTINGS_KEYS.coreToken) || "anonymous";
|
|
2014
|
-
return `mentraos_localstorage_${userId}_${packageName}_`;
|
|
2015
|
-
}
|
|
2016
2365
|
async handleStorageGet(packageName, payload, requestId) {
|
|
2017
2366
|
try {
|
|
2018
2367
|
const key = payload.key;
|
|
@@ -2023,9 +2372,8 @@ class LocalMiniappRuntime {
|
|
|
2023
2372
|
});
|
|
2024
2373
|
return;
|
|
2025
2374
|
}
|
|
2026
|
-
const
|
|
2027
|
-
|
|
2028
|
-
this.sendResult(packageName, requestId, true, { key, value: result.is_ok() ? result.value : null });
|
|
2375
|
+
const value = await this.simpleStorage.get(packageName, key);
|
|
2376
|
+
this.sendResult(packageName, requestId, true, { key, value });
|
|
2029
2377
|
}
|
|
2030
2378
|
catch (err) {
|
|
2031
2379
|
console.error(`${LOG_TAG}: storage_get error:`, err);
|
|
@@ -2045,8 +2393,7 @@ class LocalMiniappRuntime {
|
|
|
2045
2393
|
});
|
|
2046
2394
|
return;
|
|
2047
2395
|
}
|
|
2048
|
-
|
|
2049
|
-
mmkvStorage.save(fullKey, payload.value ?? null);
|
|
2396
|
+
await this.simpleStorage.set(packageName, key, payload.value ?? null);
|
|
2050
2397
|
this.sendResult(packageName, requestId, true);
|
|
2051
2398
|
}
|
|
2052
2399
|
catch (err) {
|
|
@@ -2067,8 +2414,7 @@ class LocalMiniappRuntime {
|
|
|
2067
2414
|
});
|
|
2068
2415
|
return;
|
|
2069
2416
|
}
|
|
2070
|
-
|
|
2071
|
-
mmkvStorage.remove(fullKey);
|
|
2417
|
+
await this.simpleStorage.delete(packageName, key);
|
|
2072
2418
|
this.sendResult(packageName, requestId, true);
|
|
2073
2419
|
}
|
|
2074
2420
|
catch (err) {
|
|
@@ -2081,9 +2427,7 @@ class LocalMiniappRuntime {
|
|
|
2081
2427
|
}
|
|
2082
2428
|
async handleStorageList(packageName, _payload, requestId) {
|
|
2083
2429
|
try {
|
|
2084
|
-
const
|
|
2085
|
-
const allKeys = mmkvStorage.getAllKeys();
|
|
2086
|
-
const keys = allKeys.filter((k) => k.startsWith(prefix)).map((k) => k.slice(prefix.length));
|
|
2430
|
+
const keys = await this.simpleStorage.keys(packageName);
|
|
2087
2431
|
this.sendResult(packageName, requestId, true, { keys });
|
|
2088
2432
|
}
|
|
2089
2433
|
catch (err) {
|
|
@@ -2096,12 +2440,7 @@ class LocalMiniappRuntime {
|
|
|
2096
2440
|
}
|
|
2097
2441
|
async handleStorageClear(packageName, requestId) {
|
|
2098
2442
|
try {
|
|
2099
|
-
|
|
2100
|
-
const allKeys = mmkvStorage.getAllKeys();
|
|
2101
|
-
for (const k of allKeys) {
|
|
2102
|
-
if (k.startsWith(prefix))
|
|
2103
|
-
mmkvStorage.remove(k);
|
|
2104
|
-
}
|
|
2443
|
+
await this.simpleStorage.clear(packageName);
|
|
2105
2444
|
this.sendResult(packageName, requestId, true);
|
|
2106
2445
|
}
|
|
2107
2446
|
catch (err) {
|
|
@@ -2122,9 +2461,8 @@ class LocalMiniappRuntime {
|
|
|
2122
2461
|
});
|
|
2123
2462
|
return;
|
|
2124
2463
|
}
|
|
2125
|
-
const
|
|
2126
|
-
|
|
2127
|
-
this.sendResult(packageName, requestId, true, { has: result.is_ok() });
|
|
2464
|
+
const has = await this.simpleStorage.has(packageName, key);
|
|
2465
|
+
this.sendResult(packageName, requestId, true, { has });
|
|
2128
2466
|
}
|
|
2129
2467
|
catch (err) {
|
|
2130
2468
|
console.error(`${LOG_TAG}: storage_has error:`, err);
|
|
@@ -2136,18 +2474,7 @@ class LocalMiniappRuntime {
|
|
|
2136
2474
|
}
|
|
2137
2475
|
async handleStorageGetAll(packageName, requestId) {
|
|
2138
2476
|
try {
|
|
2139
|
-
const
|
|
2140
|
-
const allKeys = mmkvStorage.getAllKeys();
|
|
2141
|
-
const values = {};
|
|
2142
|
-
for (const k of allKeys) {
|
|
2143
|
-
if (!k.startsWith(prefix))
|
|
2144
|
-
continue;
|
|
2145
|
-
const r = mmkvStorage.load(k);
|
|
2146
|
-
if (r.is_ok()) {
|
|
2147
|
-
const v = r.value;
|
|
2148
|
-
values[k.slice(prefix.length)] = typeof v === "string" ? v : String(v ?? "");
|
|
2149
|
-
}
|
|
2150
|
-
}
|
|
2477
|
+
const values = await this.simpleStorage.getAll(packageName);
|
|
2151
2478
|
this.sendResult(packageName, requestId, true, { values });
|
|
2152
2479
|
}
|
|
2153
2480
|
catch (err) {
|
|
@@ -2168,10 +2495,7 @@ class LocalMiniappRuntime {
|
|
|
2168
2495
|
});
|
|
2169
2496
|
return;
|
|
2170
2497
|
}
|
|
2171
|
-
|
|
2172
|
-
for (const [key, value] of Object.entries(values)) {
|
|
2173
|
-
mmkvStorage.save(prefix + key, value ?? null);
|
|
2174
|
-
}
|
|
2498
|
+
await this.simpleStorage.setMultiple(packageName, values);
|
|
2175
2499
|
this.sendResult(packageName, requestId, true);
|
|
2176
2500
|
}
|
|
2177
2501
|
catch (err) {
|
|
@@ -2183,6 +2507,29 @@ class LocalMiniappRuntime {
|
|
|
2183
2507
|
}
|
|
2184
2508
|
}
|
|
2185
2509
|
// ---------------------------------------------------------------------------
|
|
2510
|
+
// Wi-Fi ADB
|
|
2511
|
+
// ---------------------------------------------------------------------------
|
|
2512
|
+
async handleSetWifiAdbState(packageName, payload, requestId) {
|
|
2513
|
+
const result = await handleWifiAdbRequest(packageName, payload, {
|
|
2514
|
+
isSystemPackage: (candidate) => this.isSystemPackage(candidate),
|
|
2515
|
+
setWifiAdbState: async (enabled) => {
|
|
2516
|
+
console.log(`${LOG_TAG}: set_wifi_adb_state enabled=${enabled} from ${packageName}`);
|
|
2517
|
+
try {
|
|
2518
|
+
await BluetoothSdk.setWifiAdbState(enabled);
|
|
2519
|
+
}
|
|
2520
|
+
catch (error) {
|
|
2521
|
+
console.error(`${LOG_TAG}: set_wifi_adb_state error:`, error);
|
|
2522
|
+
throw error;
|
|
2523
|
+
}
|
|
2524
|
+
},
|
|
2525
|
+
});
|
|
2526
|
+
if (result.ok) {
|
|
2527
|
+
this.sendResult(packageName, requestId, true);
|
|
2528
|
+
return;
|
|
2529
|
+
}
|
|
2530
|
+
this.sendResult(packageName, requestId, false, undefined, result.error);
|
|
2531
|
+
}
|
|
2532
|
+
// ---------------------------------------------------------------------------
|
|
2186
2533
|
// Camera FOV
|
|
2187
2534
|
// ---------------------------------------------------------------------------
|
|
2188
2535
|
async handleCameraFov(packageName, payload, requestId) {
|
|
@@ -2202,12 +2549,7 @@ class LocalMiniappRuntime {
|
|
|
2202
2549
|
const request = normalizeCameraFovPayload(payload);
|
|
2203
2550
|
const description = "preset" in request ? `preset=${request.preset}` : `fov=${request.fov} roi=${request.roiPosition ?? "center"}`;
|
|
2204
2551
|
console.log(`${LOG_TAG}: camera_fov_set ${description}`);
|
|
2205
|
-
|
|
2206
|
-
// runtime hook — a pure BluetoothSdk.setCameraFov passthrough with no host coupling).
|
|
2207
|
-
const result = await BluetoothSdk.setCameraFov(request);
|
|
2208
|
-
useSettingsStore
|
|
2209
|
-
.getState()
|
|
2210
|
-
.setSetting(ISLAND_SETTINGS_KEYS.cameraFov, { fov: result.fov, roi_position: CAMERA_ROI_POSITION_VALUES[result.roiPosition] }, false);
|
|
2552
|
+
const result = await phoneCameraFovCoordinator.setOverride(packageName, request);
|
|
2211
2553
|
this.sendResult(packageName, requestId, true, result);
|
|
2212
2554
|
}
|
|
2213
2555
|
catch (err) {
|
|
@@ -2244,6 +2586,103 @@ class LocalMiniappRuntime {
|
|
|
2244
2586
|
});
|
|
2245
2587
|
}
|
|
2246
2588
|
}
|
|
2589
|
+
/**
|
|
2590
|
+
* Lifecycle-scoped glasses-side VAD override from a miniapp
|
|
2591
|
+
* (session.mic.setVoiceActivityDetectionEnabled). Requires MICROPHONE in the
|
|
2592
|
+
* miniapp manifest. Mentra Live only today — other models no-op via DeviceStore.
|
|
2593
|
+
*/
|
|
2594
|
+
async handleMicSetVadEnabled(packageName, payload, requestId) {
|
|
2595
|
+
const app = this.connectedApps.get(packageName);
|
|
2596
|
+
const hasMicPermission = app?.installedManifest?.permissions?.some((p) => p.type === "MICROPHONE");
|
|
2597
|
+
if (!hasMicPermission) {
|
|
2598
|
+
logPermissionNotDeclared(packageName, "MICROPHONE", "to set VAD enabled", `{"type": "MICROPHONE"}`);
|
|
2599
|
+
this.sendResult(packageName, requestId, false, undefined, {
|
|
2600
|
+
code: MiniappErrorCode.PERMISSION_NOT_DECLARED,
|
|
2601
|
+
message: `MICROPHONE permission not declared in miniapp.json. Add {"type": "MICROPHONE"} to the "permissions" array.`,
|
|
2602
|
+
permission: "MICROPHONE",
|
|
2603
|
+
operation: MiniappRequestType.MIC_SET_VAD_ENABLED,
|
|
2604
|
+
});
|
|
2605
|
+
return;
|
|
2606
|
+
}
|
|
2607
|
+
if (typeof payload.enabled !== "boolean") {
|
|
2608
|
+
this.sendResult(packageName, requestId, false, undefined, {
|
|
2609
|
+
code: MiniappErrorCode.INVALID_ARGUMENT,
|
|
2610
|
+
message: "enabled must be a boolean",
|
|
2611
|
+
});
|
|
2612
|
+
return;
|
|
2613
|
+
}
|
|
2614
|
+
try {
|
|
2615
|
+
const enabled = payload.enabled;
|
|
2616
|
+
console.log(`${LOG_TAG}: mic_set_vad_enabled ${enabled} (by ${packageName})`);
|
|
2617
|
+
await micStateCoordinator.setMiniappGateOverride(packageName, "vad", enabled, this.getConfiguredMicGates());
|
|
2618
|
+
this.sendResult(packageName, requestId, true);
|
|
2619
|
+
}
|
|
2620
|
+
catch (err) {
|
|
2621
|
+
console.error(`${LOG_TAG}: mic_set_vad_enabled error:`, err);
|
|
2622
|
+
this.sendResult(packageName, requestId, false, undefined, {
|
|
2623
|
+
code: MiniappErrorCode.INTERNAL,
|
|
2624
|
+
message: err instanceof Error ? err.message : "VAD set-enabled error",
|
|
2625
|
+
});
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
/**
|
|
2629
|
+
* Lifecycle-scoped center-mic loudness gate ("Barrier") override from a miniapp
|
|
2630
|
+
* (session.mic.setLoudnessGateEnabled). Requires MICROPHONE in the miniapp
|
|
2631
|
+
* manifest. Mentra Live only today — other models no-op via DeviceStore.
|
|
2632
|
+
*/
|
|
2633
|
+
async handleMicSetLoudnessGateEnabled(packageName, payload, requestId) {
|
|
2634
|
+
const app = this.connectedApps.get(packageName);
|
|
2635
|
+
const hasMicPermission = app?.installedManifest?.permissions?.some((p) => p.type === "MICROPHONE");
|
|
2636
|
+
if (!hasMicPermission) {
|
|
2637
|
+
logPermissionNotDeclared(packageName, "MICROPHONE", "to set loudness gate enabled", `{"type": "MICROPHONE"}`);
|
|
2638
|
+
this.sendResult(packageName, requestId, false, undefined, {
|
|
2639
|
+
code: MiniappErrorCode.PERMISSION_NOT_DECLARED,
|
|
2640
|
+
message: `MICROPHONE permission not declared in miniapp.json. Add {"type": "MICROPHONE"} to the "permissions" array.`,
|
|
2641
|
+
permission: "MICROPHONE",
|
|
2642
|
+
operation: MiniappRequestType.MIC_SET_LOUDNESS_GATE_ENABLED,
|
|
2643
|
+
});
|
|
2644
|
+
return;
|
|
2645
|
+
}
|
|
2646
|
+
if (typeof payload.enabled !== "boolean") {
|
|
2647
|
+
this.sendResult(packageName, requestId, false, undefined, {
|
|
2648
|
+
code: MiniappErrorCode.INVALID_ARGUMENT,
|
|
2649
|
+
message: "enabled must be a boolean",
|
|
2650
|
+
});
|
|
2651
|
+
return;
|
|
2652
|
+
}
|
|
2653
|
+
try {
|
|
2654
|
+
const enabled = payload.enabled;
|
|
2655
|
+
console.log(`${LOG_TAG}: mic_set_loudness_gate_enabled ${enabled} (by ${packageName})`);
|
|
2656
|
+
await micStateCoordinator.setMiniappGateOverride(packageName, "loudnessGate", enabled, this.getConfiguredMicGates());
|
|
2657
|
+
this.sendResult(packageName, requestId, true);
|
|
2658
|
+
}
|
|
2659
|
+
catch (err) {
|
|
2660
|
+
console.error(`${LOG_TAG}: mic_set_loudness_gate_enabled error:`, err);
|
|
2661
|
+
this.sendResult(packageName, requestId, false, undefined, {
|
|
2662
|
+
code: MiniappErrorCode.INTERNAL,
|
|
2663
|
+
message: err instanceof Error ? err.message : "Loudness gate set-enabled error",
|
|
2664
|
+
});
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
/**
|
|
2668
|
+
* session.system.scanQr — host camera overlay. Must not clear miniapp
|
|
2669
|
+
* foreground; the host seam is responsible for presenting a Modal on top.
|
|
2670
|
+
*/
|
|
2671
|
+
async handleScanQr(packageName, payload, requestId) {
|
|
2672
|
+
try {
|
|
2673
|
+
const result = await invokeScanQrSeam(getUiSeams().scanQr, payload);
|
|
2674
|
+
this.sendResult(packageName, requestId, true, result);
|
|
2675
|
+
}
|
|
2676
|
+
catch (err) {
|
|
2677
|
+
if (!getUiSeams().scanQr) {
|
|
2678
|
+
console.warn(`${LOG_TAG}: scanQr seam missing — QR overlay is not registered`);
|
|
2679
|
+
}
|
|
2680
|
+
this.sendResult(packageName, requestId, false, undefined, {
|
|
2681
|
+
code: err?.code || MiniappErrorCode.INTERNAL,
|
|
2682
|
+
message: err instanceof Error ? err.message : "QR scan failed",
|
|
2683
|
+
});
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2247
2686
|
// ---------------------------------------------------------------------------
|
|
2248
2687
|
// System utilities (share, open URL, clipboard, download)
|
|
2249
2688
|
// ---------------------------------------------------------------------------
|
|
@@ -2371,10 +2810,20 @@ class LocalMiniappRuntime {
|
|
|
2371
2810
|
const result = await phonePhotoCoordinator.takePhoto(packageName, {
|
|
2372
2811
|
size: payload.size,
|
|
2373
2812
|
mode: payload.mode,
|
|
2813
|
+
transferMethod: payload.transferMethod,
|
|
2374
2814
|
compress: payload.compress,
|
|
2375
2815
|
sound: payload.sound,
|
|
2376
2816
|
saveToGallery: payload.saveToGallery,
|
|
2377
2817
|
exposureTimeNs: payload.exposureTimeNs,
|
|
2818
|
+
iso: payload.iso,
|
|
2819
|
+
aeExposureDivisor: payload.aeExposureDivisor,
|
|
2820
|
+
isoCap: payload.isoCap,
|
|
2821
|
+
noiseReduction: payload.noiseReduction,
|
|
2822
|
+
edgeEnhancement: payload.edgeEnhancement,
|
|
2823
|
+
zsl: payload.zsl,
|
|
2824
|
+
mfnr: payload.mfnr,
|
|
2825
|
+
ispDigitalGain: payload.ispDigitalGain,
|
|
2826
|
+
ispAnalogGain: payload.ispAnalogGain,
|
|
2378
2827
|
});
|
|
2379
2828
|
this.sendResult(packageName, requestId, true, result);
|
|
2380
2829
|
}
|
|
@@ -2404,8 +2853,11 @@ class LocalMiniappRuntime {
|
|
|
2404
2853
|
try {
|
|
2405
2854
|
await phonePhotoCoordinator.warmUpCamera(packageName, {
|
|
2406
2855
|
size: payload.size,
|
|
2856
|
+
mode: payload.mode,
|
|
2407
2857
|
exposureTimeNs: payload.exposureTimeNs,
|
|
2408
2858
|
durationMs: payload.durationMs,
|
|
2859
|
+
zsl: payload.zsl,
|
|
2860
|
+
mfnr: payload.mfnr,
|
|
2409
2861
|
});
|
|
2410
2862
|
this.sendResult(packageName, requestId, true);
|
|
2411
2863
|
}
|
|
@@ -2485,6 +2937,7 @@ class LocalMiniappRuntime {
|
|
|
2485
2937
|
video: normalizeStreamVideoConfig(payload.video),
|
|
2486
2938
|
audio: normalizeStreamAudioConfig(payload.audio),
|
|
2487
2939
|
sound: payload.sound,
|
|
2940
|
+
authToken: typeof payload.authToken === "string" ? payload.authToken : undefined,
|
|
2488
2941
|
});
|
|
2489
2942
|
this.sendResult(packageName, requestId, true, result);
|
|
2490
2943
|
}
|
|
@@ -2563,7 +3016,7 @@ class LocalMiniappRuntime {
|
|
|
2563
3016
|
return;
|
|
2564
3017
|
}
|
|
2565
3018
|
try {
|
|
2566
|
-
await requestWifiSetup(payload.reason);
|
|
3019
|
+
await requestWifiSetup(payload.reason, packageName);
|
|
2567
3020
|
this.sendResult(packageName, requestId, true);
|
|
2568
3021
|
}
|
|
2569
3022
|
catch (err) {
|
|
@@ -2622,26 +3075,12 @@ class LocalMiniappRuntime {
|
|
|
2622
3075
|
return { ok: false, error: "MICROPHONE permission not declared in miniapp.json" };
|
|
2623
3076
|
}
|
|
2624
3077
|
}
|
|
2625
|
-
// Remove old subscriptions for this app
|
|
2626
|
-
for (const oldStream of app.subscriptions) {
|
|
2627
|
-
const subs = this.streamSubscribers.get(oldStream);
|
|
2628
|
-
if (subs) {
|
|
2629
|
-
subs.delete(packageName);
|
|
2630
|
-
if (subs.size === 0) {
|
|
2631
|
-
this.streamSubscribers.delete(oldStream);
|
|
2632
|
-
}
|
|
2633
|
-
}
|
|
2634
|
-
}
|
|
2635
3078
|
// Add new subscriptions
|
|
3079
|
+
const previousSubscriptions = app.subscriptions;
|
|
2636
3080
|
app.subscriptions = new Set(streams);
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
subs = new Set();
|
|
2641
|
-
this.streamSubscribers.set(stream, subs);
|
|
2642
|
-
}
|
|
2643
|
-
subs.add(packageName);
|
|
2644
|
-
}
|
|
3081
|
+
app.forceLocalTranscriptionStreams.clear();
|
|
3082
|
+
app.cloudTranscriptionStreams = new Set(streams.filter((stream) => stream.startsWith("transcription:")));
|
|
3083
|
+
this.replaceStreamSubscribers(packageName, previousSubscriptions, app.subscriptions);
|
|
2645
3084
|
this.recomputeMicRequirements();
|
|
2646
3085
|
this.updateCloudSubscriptions();
|
|
2647
3086
|
return { ok: true };
|
|
@@ -2649,6 +3088,15 @@ class LocalMiniappRuntime {
|
|
|
2649
3088
|
// ===========================================================================
|
|
2650
3089
|
// Mic requirements
|
|
2651
3090
|
// ===========================================================================
|
|
3091
|
+
getConfiguredMicGates() {
|
|
3092
|
+
const bluetoothSettings = useSettingsStore.getState().getBluetoothSettings();
|
|
3093
|
+
const configuredVad = bluetoothSettings.voice_activity_detection_enabled;
|
|
3094
|
+
const configuredLoudnessGate = bluetoothSettings.loudness_gate_enabled;
|
|
3095
|
+
return {
|
|
3096
|
+
vadEnabled: typeof configuredVad === "boolean" ? configuredVad : null,
|
|
3097
|
+
loudnessGateEnabled: typeof configuredLoudnessGate === "boolean" ? configuredLoudnessGate : null,
|
|
3098
|
+
};
|
|
3099
|
+
}
|
|
2652
3100
|
recomputeMicRequirements() {
|
|
2653
3101
|
let anyPcm = false;
|
|
2654
3102
|
let anyLc3 = false;
|
|
@@ -2660,7 +3108,11 @@ class LocalMiniappRuntime {
|
|
|
2660
3108
|
if (stream.startsWith("transcription:") || stream.startsWith("translation:") || stream === "vad")
|
|
2661
3109
|
anyLc3 = true;
|
|
2662
3110
|
}
|
|
2663
|
-
micStateCoordinator.setLocalRequirements({
|
|
3111
|
+
micStateCoordinator.setLocalRequirements({
|
|
3112
|
+
pcm: anyPcm,
|
|
3113
|
+
lc3: anyLc3,
|
|
3114
|
+
...this.getConfiguredMicGates(),
|
|
3115
|
+
});
|
|
2664
3116
|
}
|
|
2665
3117
|
/**
|
|
2666
3118
|
* Recompute the aggregated v2 cloud subscription list across all local miniapps.
|
|
@@ -2672,20 +3124,31 @@ class LocalMiniappRuntime {
|
|
|
2672
3124
|
updateCloudSubscriptions() {
|
|
2673
3125
|
const cloudStreams = new Set();
|
|
2674
3126
|
let transcriptionLang = null;
|
|
3127
|
+
let forceLocalTranscriptionLang = null;
|
|
3128
|
+
let hasForceLocalTranscription = false;
|
|
2675
3129
|
for (const [stream, subscribers] of this.streamSubscribers) {
|
|
2676
3130
|
if (subscribers.size === 0)
|
|
2677
3131
|
continue;
|
|
2678
|
-
// Only transcription / translation need cloud delivery. Location
|
|
2679
|
-
// notifications
|
|
2680
|
-
|
|
2681
|
-
if (stream.startsWith("transcription:") || stream.startsWith("translation:")) {
|
|
3132
|
+
// Only transcription / translation need cloud delivery. Location and
|
|
3133
|
+
// notifications are sourced natively on the phone — no cloud hop.
|
|
3134
|
+
if (stream.startsWith("translation:")) {
|
|
2682
3135
|
cloudStreams.add(stream);
|
|
2683
3136
|
}
|
|
2684
|
-
if (stream.startsWith("transcription:")
|
|
2685
|
-
|
|
3137
|
+
if (stream.startsWith("transcription:")) {
|
|
3138
|
+
const language = stream.substring("transcription:".length);
|
|
3139
|
+
if (transcriptionLang === null)
|
|
3140
|
+
transcriptionLang = language;
|
|
3141
|
+
const routes = summarizeTranscriptionRoutes(subscribers, (packageName) => this.appTranscriptionRoutesForSubscription(packageName, stream));
|
|
3142
|
+
if (routes.hasForceLocalSubscriber) {
|
|
3143
|
+
hasForceLocalTranscription = true;
|
|
3144
|
+
if (forceLocalTranscriptionLang === null)
|
|
3145
|
+
forceLocalTranscriptionLang = language;
|
|
3146
|
+
}
|
|
3147
|
+
if (routes.hasCloudSubscriber)
|
|
3148
|
+
cloudStreams.add(stream);
|
|
2686
3149
|
}
|
|
2687
3150
|
}
|
|
2688
|
-
localSttFallbackCoordinator.onSubscriptionChange(transcriptionLang !== null, transcriptionLang);
|
|
3151
|
+
localSttFallbackCoordinator.onSubscriptionChange(transcriptionLang !== null, forceLocalTranscriptionLang ?? transcriptionLang, hasForceLocalTranscription);
|
|
2689
3152
|
// Mirror the same set as typed AudioSubscription[] and push it to the cloud
|
|
2690
3153
|
// runtime.
|
|
2691
3154
|
this.ensureCloudResultsWired();
|
|
@@ -2720,11 +3183,20 @@ class LocalMiniappRuntime {
|
|
|
2720
3183
|
*/
|
|
2721
3184
|
buildCloudAudioSubscriptions(cloudStreams) {
|
|
2722
3185
|
const subs = [];
|
|
3186
|
+
// Union of every connected app's TRANSCRIPTION_CONFIG hints. Hints only
|
|
3187
|
+
// apply to the auto-mode TRANSCRIPTION subscription (the wire schema has
|
|
3188
|
+
// no hints field on specific-language sources — the language IS the hint
|
|
3189
|
+
// there), and are deliberately NOT attached to translation sources.
|
|
3190
|
+
const hintUnion = [...new Set([...this.transcriptionHintsByApp.values()].flat())].sort();
|
|
2723
3191
|
const langSource = (code) => code === "auto" || code === "*" ? { mode: "auto" } : { mode: "specific", code };
|
|
2724
3192
|
for (const stream of cloudStreams) {
|
|
2725
3193
|
if (stream.startsWith("transcription:")) {
|
|
2726
3194
|
const lang = stream.substring("transcription:".length);
|
|
2727
|
-
|
|
3195
|
+
const source = langSource(lang);
|
|
3196
|
+
subs.push({
|
|
3197
|
+
kind: "transcription",
|
|
3198
|
+
language: source.mode === "auto" && hintUnion.length > 0 ? { mode: "auto", hints: hintUnion } : source,
|
|
3199
|
+
});
|
|
2728
3200
|
}
|
|
2729
3201
|
else if (stream.startsWith("translation:")) {
|
|
2730
3202
|
const parts = stream.split(":");
|
|
@@ -2787,7 +3259,7 @@ class LocalMiniappRuntime {
|
|
|
2787
3259
|
provider: d.provider,
|
|
2788
3260
|
confidence: d.confidence,
|
|
2789
3261
|
__hostReceivedAt: receivedAt,
|
|
2790
|
-
});
|
|
3262
|
+
}, "cloud");
|
|
2791
3263
|
});
|
|
2792
3264
|
cloudClientService.onTranslation((d) => {
|
|
2793
3265
|
this.forwardEvent(`translation:${d.source.language}:${d.target.language}`, {
|
|
@@ -2858,7 +3330,7 @@ class LocalMiniappRuntime {
|
|
|
2858
3330
|
* - Cloud sends "head_up" → miniapp protocol uses "head_position" (HEAD_POSITION)
|
|
2859
3331
|
* - Cloud sends "VAD" (uppercase) → miniapp protocol uses "vad" (lowercase)
|
|
2860
3332
|
*/
|
|
2861
|
-
forwardEvent(streamType, data) {
|
|
3333
|
+
forwardEvent(streamType, data, transcriptionSource) {
|
|
2862
3334
|
// Translate cloud event names to miniapp protocol stream types
|
|
2863
3335
|
const normalizedStream = this.normalizeStreamType(streamType);
|
|
2864
3336
|
// Collect all subscribers: exact match, plus wildcard matches for streams
|
|
@@ -2938,15 +3410,15 @@ class LocalMiniappRuntime {
|
|
|
2938
3410
|
}
|
|
2939
3411
|
if (matchedSubs.size === 0 && !perGestureStream)
|
|
2940
3412
|
return;
|
|
2941
|
-
// BGCAP: count transcripts only once we know they're actually being handed
|
|
2942
|
-
// to a subscriber. If the captions miniapp is unregistered/respawning (or
|
|
2943
|
-
// hasn't subscribed yet) the cloud may still push transcripts that match no
|
|
2944
|
-
// one — counting those would falsely show "forwarded" during the exact gap
|
|
2945
|
-
// the diagnostic is meant to expose.
|
|
2946
|
-
if (BGCAP_TELEMETRY && normalizedStream.startsWith("transcription:")) {
|
|
2947
|
-
bgcapNoteTranscriptForward(!!data?.isFinal);
|
|
2948
|
-
}
|
|
2949
3413
|
for (const packageName of matchedSubs) {
|
|
3414
|
+
let transcriptionRoute;
|
|
3415
|
+
if (normalizedStream.startsWith("transcription:") && transcriptionSource) {
|
|
3416
|
+
const routes = this.appTranscriptionRoutesForEvent(packageName, normalizedStream);
|
|
3417
|
+
transcriptionRoute =
|
|
3418
|
+
transcriptionDeliveryRoute(transcriptionSource, routes, cloudClientService.isConnected()) ?? undefined;
|
|
3419
|
+
if (!transcriptionRoute)
|
|
3420
|
+
continue;
|
|
3421
|
+
}
|
|
2950
3422
|
if (TRANSCRIPT_TIMING_TELEMETRY && normalizedStream.startsWith("transcription:")) {
|
|
2951
3423
|
const transcript = data;
|
|
2952
3424
|
const now = Date.now();
|
|
@@ -2964,6 +3436,7 @@ class LocalMiniappRuntime {
|
|
|
2964
3436
|
type: MiniappResponseType.EVENT,
|
|
2965
3437
|
streamType: normalizedStream,
|
|
2966
3438
|
data: outboundData,
|
|
3439
|
+
...(transcriptionRoute ? { transcriptionRoute } : {}),
|
|
2967
3440
|
});
|
|
2968
3441
|
}
|
|
2969
3442
|
// Send a separate envelope to per-gesture subscribers tagged with the
|
|
@@ -2981,6 +3454,22 @@ class LocalMiniappRuntime {
|
|
|
2981
3454
|
}
|
|
2982
3455
|
}
|
|
2983
3456
|
}
|
|
3457
|
+
appTranscriptionRoutesForSubscription(packageName, stream) {
|
|
3458
|
+
const app = this.connectedApps.get(packageName);
|
|
3459
|
+
return {
|
|
3460
|
+
cloud: app?.cloudTranscriptionStreams.has(stream) === true,
|
|
3461
|
+
forceLocal: app?.forceLocalTranscriptionStreams.has(stream) === true,
|
|
3462
|
+
};
|
|
3463
|
+
}
|
|
3464
|
+
appTranscriptionRoutesForEvent(packageName, stream) {
|
|
3465
|
+
const app = this.connectedApps.get(packageName);
|
|
3466
|
+
const autoStream = `${MiniappStreamType.TRANSCRIPTION}:auto`;
|
|
3467
|
+
return {
|
|
3468
|
+
cloud: app?.cloudTranscriptionStreams.has(stream) === true || app?.cloudTranscriptionStreams.has(autoStream) === true,
|
|
3469
|
+
forceLocal: app?.forceLocalTranscriptionStreams.has(stream) === true ||
|
|
3470
|
+
app?.forceLocalTranscriptionStreams.has(autoStream) === true,
|
|
3471
|
+
};
|
|
3472
|
+
}
|
|
2984
3473
|
/**
|
|
2985
3474
|
* Translate cloud event names to miniapp stream type values.
|
|
2986
3475
|
*/
|
|
@@ -3469,7 +3958,6 @@ class LocalMiniappRuntime {
|
|
|
3469
3958
|
// Ping / pong liveness
|
|
3470
3959
|
// ===========================================================================
|
|
3471
3960
|
ensurePingLoop() {
|
|
3472
|
-
bgcapStartHeartbeat(); // BGCAP diagnostic
|
|
3473
3961
|
if (this.pingIntervalId !== null)
|
|
3474
3962
|
return;
|
|
3475
3963
|
this.pingIntervalId = BgTimer.setInterval(() => {
|
|
@@ -3477,28 +3965,22 @@ class LocalMiniappRuntime {
|
|
|
3477
3965
|
}, PING_INTERVAL_MS);
|
|
3478
3966
|
}
|
|
3479
3967
|
stopPingLoop() {
|
|
3480
|
-
bgcapStopHeartbeat(); // BGCAP diagnostic
|
|
3481
3968
|
if (this.pingIntervalId !== null) {
|
|
3482
3969
|
BgTimer.clearInterval(this.pingIntervalId);
|
|
3483
3970
|
this.pingIntervalId = null;
|
|
3484
3971
|
}
|
|
3485
3972
|
}
|
|
3486
3973
|
doPingRound() {
|
|
3487
|
-
const now = Date.now();
|
|
3488
|
-
const staleThreshold = PING_INTERVAL_MS * PING_TIMEOUT_THRESHOLD;
|
|
3489
3974
|
const toRemove = [];
|
|
3490
3975
|
for (const [packageName, app] of this.connectedApps) {
|
|
3491
|
-
|
|
3976
|
+
const liveness = advanceMiniappPingLiveness(app.unansweredPingRounds, PING_TIMEOUT_THRESHOLD);
|
|
3977
|
+
if (liveness.shouldUnregister) {
|
|
3492
3978
|
console.warn(`${LOG_TAG}: ${packageName} missed ${PING_TIMEOUT_THRESHOLD} pings, unregistering`);
|
|
3493
3979
|
toRemove.push(packageName);
|
|
3494
3980
|
continue;
|
|
3495
3981
|
}
|
|
3982
|
+
app.unansweredPingRounds = liveness.unansweredPingRounds;
|
|
3496
3983
|
// Send PING — SDK auto-replies with PONG
|
|
3497
|
-
// BGCAP: stamp the OLDEST outstanding ping only — don't overwrite, or a
|
|
3498
|
-
// background stall (pings keep firing on the native BgTimer while the
|
|
3499
|
-
// miniapp engine is frozen) would reset the clock and hide its duration.
|
|
3500
|
-
if (BGCAP_TELEMETRY && app.bgcapLastPingSentAt == null)
|
|
3501
|
-
app.bgcapLastPingSentAt = now;
|
|
3502
3984
|
this.sendToMiniapp(packageName, {
|
|
3503
3985
|
type: MiniappRequestType.PING,
|
|
3504
3986
|
});
|
|
@@ -3518,6 +4000,7 @@ class LocalMiniappRuntime {
|
|
|
3518
4000
|
const app = this.connectedApps.get(packageName);
|
|
3519
4001
|
if (app) {
|
|
3520
4002
|
app.lastPongAt = Date.now();
|
|
4003
|
+
app.unansweredPingRounds = 0;
|
|
3521
4004
|
this.clearForegroundProbe(packageName);
|
|
3522
4005
|
}
|
|
3523
4006
|
}
|
|
@@ -3541,7 +4024,18 @@ class LocalMiniappRuntime {
|
|
|
3541
4024
|
}
|
|
3542
4025
|
// Belt-and-suspenders: clear any remaining state
|
|
3543
4026
|
this.pendingCloudRequests.clear();
|
|
4027
|
+
const hadButtonPressSubscribers = this.getButtonPressSubscribers().length > 0;
|
|
3544
4028
|
this.streamSubscribers.clear();
|
|
4029
|
+
if (hadButtonPressSubscribers) {
|
|
4030
|
+
for (const listener of this.buttonPressSubscriberListeners) {
|
|
4031
|
+
try {
|
|
4032
|
+
listener([]);
|
|
4033
|
+
}
|
|
4034
|
+
catch (error) {
|
|
4035
|
+
console.warn(`${LOG_TAG}: button press subscriber listener threw during cleanup:`, error);
|
|
4036
|
+
}
|
|
4037
|
+
}
|
|
4038
|
+
}
|
|
3545
4039
|
this.connectedApps.clear();
|
|
3546
4040
|
for (const timerId of this.foregroundProbeTimers.values()) {
|
|
3547
4041
|
BgTimer.clearTimeout(timerId);
|