@mentra/engine 0.1.0-beta.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.map +1 -1
- package/build/facades/dev.js +9 -5
- 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/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 +2 -2
- package/build/internal.d.ts.map +1 -1
- package/build/internal.js +1 -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 +18 -8
- package/build/services/AppRegistry.d.ts.map +1 -1
- package/build/services/AppRegistry.js +44 -45
- package/build/services/AppRegistry.js.map +1 -1
- package/build/services/AudioCloudUplink.d.ts +9 -3
- package/build/services/AudioCloudUplink.d.ts.map +1 -1
- package/build/services/AudioCloudUplink.js +9 -3
- package/build/services/AudioCloudUplink.js.map +1 -1
- package/build/services/AudioPlaybackService.d.ts +9 -1
- package/build/services/AudioPlaybackService.d.ts.map +1 -1
- package/build/services/AudioPlaybackService.js +77 -32
- 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 +75 -0
- package/build/services/CloudClientService.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 +67 -1
- package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
- package/build/services/LocalMiniappRuntime.js +486 -126
- 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/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/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/PhoneCalendarService.d.ts.map +1 -1
- package/build/services/PhoneCalendarService.js +22 -1
- package/build/services/PhoneCalendarService.js.map +1 -1
- package/build/services/PhoneCameraFovCoordinator.d.ts +2 -0
- package/build/services/PhoneCameraFovCoordinator.d.ts.map +1 -1
- package/build/services/PhoneCameraFovCoordinator.js +14 -0
- package/build/services/PhoneCameraFovCoordinator.js.map +1 -1
- 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 +10 -3
- package/build/services/PhonePhotoCoordinator.d.ts.map +1 -1
- package/build/services/PhonePhotoCoordinator.js +21 -3
- 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/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/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 +3 -0
- package/build/services/asg/asgCameraApi.d.ts.map +1 -1
- package/build/services/asg/asgCameraApi.js +72 -2
- package/build/services/asg/asgCameraApi.js.map +1 -1
- package/build/services/asg/gallerySyncService.d.ts.map +1 -1
- package/build/services/asg/gallerySyncService.js +26 -14
- package/build/services/asg/gallerySyncService.js.map +1 -1
- package/build/services/asg/localNetworkTransport.d.ts +2 -2
- package/build/services/asg/localNetworkTransport.d.ts.map +1 -1
- package/build/services/asg/localNetworkTransport.js +6 -6
- package/build/services/asg/localNetworkTransport.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/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 +152 -19
- 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/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/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 +27 -18
- package/src/assets/silence.wav +0 -0
- package/src/engine.ts +7 -2
- package/src/facades/dev.ts +9 -5
- 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/phoneNotifications.ts +4 -13
- package/src/index.ts +26 -2
- package/src/internal.ts +2 -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 +72 -48
- package/src/services/AudioCloudUplink.ts +9 -3
- package/src/services/AudioPlaybackService.ts +94 -34
- package/src/services/BlobStore.ts +22 -9
- package/src/services/CloudAudioSubscriptionSync.ts +13 -1
- package/src/services/CloudClientService.ts +74 -0
- 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 +576 -130
- package/src/services/LocalMiniappStorage.ts +82 -0
- package/src/services/LocalMiniappUserIdentity.ts +46 -0
- package/src/services/MicStateCoordinator.ts +169 -4
- package/src/services/MiniappLauncher.ts +30 -3
- package/src/services/MiniappLiveness.ts +24 -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/PhoneCalendarService.ts +27 -4
- package/src/services/PhoneCameraFovCoordinator.ts +15 -0
- package/src/services/PhoneNotificationsSync.ts +12 -10
- package/src/services/PhonePhotoCoordinator.ts +33 -7
- package/src/services/PhoneStreamCoordinator.ts +139 -25
- package/src/services/PhoneVideoCoordinator.ts +11 -0
- package/src/services/SceneRenderer.ts +16 -0
- package/src/services/SupportProfileSync.ts +218 -0
- package/src/services/TtsTextSanitizer.ts +66 -0
- package/src/services/WifiAdbRequest.ts +53 -0
- package/src/services/asg/asgCameraApi.ts +99 -8
- package/src/services/asg/gallerySyncService.ts +28 -19
- package/src/services/asg/localNetworkTransport.ts +7 -11
- package/src/services/audioPlaybackAssets.ts +8 -0
- 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 +174 -21
- package/src/types/applet.ts +2 -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/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
|
@@ -97,22 +97,33 @@ interface ActiveReader {
|
|
|
97
97
|
export class BlobStore {
|
|
98
98
|
private readonly uploads = new Map<string, ActiveUpload>() // key: `${pkg} ${key}`
|
|
99
99
|
private readonly readers = new Map<string, ActiveReader>() // key: host handle id
|
|
100
|
-
/**
|
|
100
|
+
/** user + package → committed usage bytes, cached; invalidated on commit/delete/clear. */
|
|
101
101
|
private readonly usageCache = new Map<string, number>()
|
|
102
102
|
|
|
103
103
|
constructor(private readonly hooks: BlobStoreHooks) {}
|
|
104
104
|
|
|
105
105
|
// ---- path / meta helpers -------------------------------------------------
|
|
106
106
|
|
|
107
|
+
private currentIdentity(packageName: string): {userId: string; scope: string} {
|
|
108
|
+
const userId = this.hooks.getUserId().trim()
|
|
109
|
+
if (!userId) throw new Error("Mentra user identity is unavailable")
|
|
110
|
+
const scope = `${userId}\0${packageName}`
|
|
111
|
+
return {userId, scope}
|
|
112
|
+
}
|
|
113
|
+
|
|
107
114
|
private dirFor(packageName: string): Directory {
|
|
108
|
-
const
|
|
109
|
-
|
|
115
|
+
const dir = new Directory(
|
|
116
|
+
Paths.document,
|
|
117
|
+
ROOT_DIR_NAME,
|
|
118
|
+
sanitizeSegment(this.currentIdentity(packageName).userId),
|
|
119
|
+
sanitizeSegment(packageName),
|
|
120
|
+
)
|
|
110
121
|
if (!dir.exists) dir.create({intermediates: true})
|
|
111
122
|
return dir
|
|
112
123
|
}
|
|
113
124
|
|
|
114
125
|
private metaPrefix(packageName: string): string {
|
|
115
|
-
return `${META_KEY_ROOT}${sanitizeSegment(this.
|
|
126
|
+
return `${META_KEY_ROOT}${sanitizeSegment(this.currentIdentity(packageName).userId)}_${packageName}_`
|
|
116
127
|
}
|
|
117
128
|
|
|
118
129
|
private metaKey(packageName: string, key: string): string {
|
|
@@ -163,17 +174,19 @@ export class BlobStore {
|
|
|
163
174
|
}
|
|
164
175
|
|
|
165
176
|
private committedUsage(packageName: string): number {
|
|
166
|
-
const
|
|
177
|
+
const {scope} = this.currentIdentity(packageName)
|
|
178
|
+
const cached = this.usageCache.get(scope)
|
|
167
179
|
if (cached !== undefined) return cached
|
|
168
180
|
let total = 0
|
|
169
181
|
for (const m of this.allMeta(packageName)) total += m.bytes || 0
|
|
170
|
-
this.usageCache.set(
|
|
182
|
+
this.usageCache.set(scope, total)
|
|
171
183
|
return total
|
|
172
184
|
}
|
|
173
185
|
|
|
174
186
|
private bumpUsage(packageName: string, delta: number): void {
|
|
175
|
-
const
|
|
176
|
-
|
|
187
|
+
const {scope} = this.currentIdentity(packageName)
|
|
188
|
+
const cur = this.usageCache.get(scope)
|
|
189
|
+
if (cur !== undefined) this.usageCache.set(scope, Math.max(0, cur + delta))
|
|
177
190
|
}
|
|
178
191
|
|
|
179
192
|
private safeMd5(file: File, bytes: number): string | undefined {
|
|
@@ -509,7 +522,7 @@ export class BlobStore {
|
|
|
509
522
|
} catch {
|
|
510
523
|
/* best-effort */
|
|
511
524
|
}
|
|
512
|
-
this.usageCache.set(packageName, 0)
|
|
525
|
+
this.usageCache.set(this.currentIdentity(packageName).scope, 0)
|
|
513
526
|
this.hooks.sendResult(packageName, requestId, true)
|
|
514
527
|
} catch (err) {
|
|
515
528
|
this.fail(packageName, requestId, MiniappErrorCode.INTERNAL, err)
|
|
@@ -19,7 +19,19 @@ export class CloudAudioSubscriptionSync {
|
|
|
19
19
|
private pendingKey = ""
|
|
20
20
|
|
|
21
21
|
public begin(nextKey: string): boolean {
|
|
22
|
-
|
|
22
|
+
// Exact duplicate of the write already in flight — nothing to do.
|
|
23
|
+
if (nextKey === this.pendingKey) {
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
26
|
+
// Matches the last successfully applied set, but ONLY skip when nothing
|
|
27
|
+
// else is in flight. If a different write is pending (e.g. desired went
|
|
28
|
+
// fr-FR -> [] -> fr-FR while the [] write is still on the wire),
|
|
29
|
+
// lastAppliedKey is stale: it names what was applied BEFORE the in-flight
|
|
30
|
+
// write lands. Skipping here would drop the re-subscribe and strand the
|
|
31
|
+
// cloud on the intermediate (empty) set — captions silently die on any
|
|
32
|
+
// rapid A -> B -> A subscription churn. The cloud's own version counter
|
|
33
|
+
// resolves ordering between the concurrent writes, so sending both is safe.
|
|
34
|
+
if (nextKey === this.lastAppliedKey && this.pendingKey === "") {
|
|
23
35
|
return false
|
|
24
36
|
}
|
|
25
37
|
this.pendingKey = nextKey
|
|
@@ -26,10 +26,12 @@ import {useSettingsStore, SETTINGS} from "../stores/settings"
|
|
|
26
26
|
import {type CloudClientStatusSnapshot, type MiniappAuthToken} from "../runtime/config"
|
|
27
27
|
import {createCloudUdpSocket} from "../utils/cloudClient/RnUdpAdapter"
|
|
28
28
|
import {cloudSecureStore} from "../utils/cloudClient/cloudSecureStore"
|
|
29
|
+
import {storage as mmkvStorage} from "../utils/storage"
|
|
29
30
|
import {useCloudClientStatusStore} from "../stores/cloudClientStatus"
|
|
30
31
|
import {islandNotifications} from "./NotificationsEmitter"
|
|
31
32
|
import {BgTimer} from "../utils/timers"
|
|
32
33
|
import {logCloudV2TranscriptMetric} from "./CloudTranscriptE2EMetrics"
|
|
34
|
+
import {LocalMiniappUserIdentity} from "./LocalMiniappUserIdentity"
|
|
33
35
|
import {nativeHttpResponseBody} from "./NativeHttpResponse"
|
|
34
36
|
|
|
35
37
|
const LOG_TAG = "cloudClient"
|
|
@@ -40,6 +42,7 @@ type CloudCore = NonNullable<CloudClient["core"]>
|
|
|
40
42
|
// been continuously NOT connected for this long. A quick reconnect cancels it; brief
|
|
41
43
|
// flaps that recover don't fire. Raised as engine.notifications(connection_failed_persistent).
|
|
42
44
|
const CLOUD_PERSISTENT_FAILURE_MS = 60_000
|
|
45
|
+
const LOCAL_MINIAPP_USER_ID_KEY = "mentra.localMiniapp.userId"
|
|
43
46
|
|
|
44
47
|
/** Cancel the pending persistent-failure alarm + re-arm for the next outage. */
|
|
45
48
|
function clearPersistentFailureAlarm(): void {
|
|
@@ -75,6 +78,21 @@ let localDevRuntimeToken: {runtimeUrl: string; token: string; expiresAtMs: numbe
|
|
|
75
78
|
let lc3FrameSizeUnsubscribe: (() => void) | null = null
|
|
76
79
|
let coreTokenSyncPromise: Promise<string> | null = null
|
|
77
80
|
|
|
81
|
+
const localMiniappUserIdentity = new LocalMiniappUserIdentity({
|
|
82
|
+
get(): string | null {
|
|
83
|
+
const result = mmkvStorage.load<string>(LOCAL_MINIAPP_USER_ID_KEY)
|
|
84
|
+
return result.is_ok() ? result.value : null
|
|
85
|
+
},
|
|
86
|
+
set(userId: string): void {
|
|
87
|
+
const result = mmkvStorage.save(LOCAL_MINIAPP_USER_ID_KEY, userId)
|
|
88
|
+
if (result.is_error()) throw result.error
|
|
89
|
+
},
|
|
90
|
+
remove(): void {
|
|
91
|
+
const result = mmkvStorage.remove(LOCAL_MINIAPP_USER_ID_KEY)
|
|
92
|
+
if (result.is_error()) throw result.error
|
|
93
|
+
},
|
|
94
|
+
})
|
|
95
|
+
|
|
78
96
|
const transcriptListeners = new Set<(d: TranscriptionData) => void>()
|
|
79
97
|
const translationListeners = new Set<(d: TranslationData) => void>()
|
|
80
98
|
const statusListeners = new Set<(snapshot: CloudClientStatusSnapshot) => void>()
|
|
@@ -114,6 +132,7 @@ async function syncCoreAccessTokenToBluetoothInternal(): Promise<string> {
|
|
|
114
132
|
if (c !== client) {
|
|
115
133
|
throw new Error("cloud client changed while syncing core token")
|
|
116
134
|
}
|
|
135
|
+
localMiniappUserIdentity.remember(c.auth.identity.mentraUserId)
|
|
117
136
|
const result = await useSettingsStore.getState().setSetting(SETTINGS.core_token.key, token, false)
|
|
118
137
|
if (result.is_error()) {
|
|
119
138
|
throw result.error
|
|
@@ -264,6 +283,13 @@ function ensureAuthWatch(): void {
|
|
|
264
283
|
let reconnectPending = false
|
|
265
284
|
try {
|
|
266
285
|
const sub = auth.onStateChange((event, session) => {
|
|
286
|
+
if (event === "SIGNED_OUT") {
|
|
287
|
+
localMiniappUserIdentity.forget()
|
|
288
|
+
return
|
|
289
|
+
}
|
|
290
|
+
// A new sign-in may belong to a different account. Force the next
|
|
291
|
+
// storage request to resolve and persist that account's Core identity.
|
|
292
|
+
if (event === "SIGNED_IN") localMiniappUserIdentity.forget()
|
|
267
293
|
if (!session?.token) return
|
|
268
294
|
if (connected || reconnectPending) return
|
|
269
295
|
|
|
@@ -396,6 +422,12 @@ function construct(): void {
|
|
|
396
422
|
// LC3 at 16 kHz with the frame size the encoder emits.
|
|
397
423
|
audio: {codec: "lc3", sampleRate: 16000, frameSizeBytes: frameSizeBytes()},
|
|
398
424
|
auth,
|
|
425
|
+
timers: {
|
|
426
|
+
setTimeout: (callback, delayMs) => BgTimer.setTimeout(callback, delayMs),
|
|
427
|
+
clearTimeout: (handle) => BgTimer.clearTimeout(handle as number),
|
|
428
|
+
setInterval: (callback, intervalMs) => BgTimer.setInterval(callback, intervalMs),
|
|
429
|
+
clearInterval: (handle) => BgTimer.clearInterval(handle as number),
|
|
430
|
+
},
|
|
399
431
|
logger: cloudLogger,
|
|
400
432
|
})
|
|
401
433
|
|
|
@@ -420,6 +452,7 @@ function construct(): void {
|
|
|
420
452
|
if (c !== client) return
|
|
421
453
|
connected = true
|
|
422
454
|
console.log(`${LOG_TAG}: runtime connected`)
|
|
455
|
+
console.log(`${LOG_TAG}: debug: ws-session-debug connected`)
|
|
423
456
|
// Cloud is up — cancel any pending persistent-failure alarm and re-arm for the
|
|
424
457
|
// next outage.
|
|
425
458
|
clearPersistentFailureAlarm()
|
|
@@ -436,6 +469,9 @@ function construct(): void {
|
|
|
436
469
|
if (c !== client) return
|
|
437
470
|
connected = false
|
|
438
471
|
console.log(`${LOG_TAG}: runtime disconnected (${info.reason})`)
|
|
472
|
+
console.log(
|
|
473
|
+
`${LOG_TAG}: debug: ws-session-debug disconnected reason=${info.reason} willStayDown=${/superseded by newer session/i.test(info.reason)}`,
|
|
474
|
+
)
|
|
439
475
|
// Arm the persistent-failure alarm once; if we're still down when it fires, raise
|
|
440
476
|
// the notification. A reconnect within the window cancels it (onConnected above).
|
|
441
477
|
if (!persistentFailureTimer && !persistentFailureNotified) {
|
|
@@ -518,6 +554,34 @@ export const cloudClientService = {
|
|
|
518
554
|
return c.core.miniapps.getRegistry()
|
|
519
555
|
},
|
|
520
556
|
|
|
557
|
+
/**
|
|
558
|
+
* Resolve the stable Core-owned Mentra user id.
|
|
559
|
+
*
|
|
560
|
+
* Storage must use this identity rather than the short-lived Core access
|
|
561
|
+
* token. A previously verified id is restored from MMKV so local miniapps
|
|
562
|
+
* remain available after an offline process restart. On first sign-in,
|
|
563
|
+
* callers join the auth module's single-flight exchange/refresh instead of
|
|
564
|
+
* falling back to an anonymous namespace.
|
|
565
|
+
*/
|
|
566
|
+
async resolveMentraUserId(): Promise<string> {
|
|
567
|
+
return localMiniappUserIdentity.resolve(async () => {
|
|
568
|
+
if (!client) this.init()
|
|
569
|
+
const c = client
|
|
570
|
+
if (!c) throw new Error("cloud client not initialized")
|
|
571
|
+
|
|
572
|
+
await c.auth.getCoreToken()
|
|
573
|
+
if (c !== client) throw new Error("cloud client changed while resolving user identity")
|
|
574
|
+
return c.auth.identity.mentraUserId
|
|
575
|
+
})
|
|
576
|
+
},
|
|
577
|
+
|
|
578
|
+
/** Read the stable Mentra user id after async identity resolution. */
|
|
579
|
+
getMentraUserId(): string {
|
|
580
|
+
const userId = localMiniappUserIdentity.get()
|
|
581
|
+
if (!userId) throw new Error("Mentra user identity is unavailable")
|
|
582
|
+
return userId
|
|
583
|
+
},
|
|
584
|
+
|
|
521
585
|
async getMiniappAuthToken(
|
|
522
586
|
packageName: string,
|
|
523
587
|
opts?: {minTtlMs?: number; devAttestation?: string},
|
|
@@ -549,6 +613,11 @@ export const cloudClientService = {
|
|
|
549
613
|
clearPersistentFailureAlarm()
|
|
550
614
|
const wasConnected = connected
|
|
551
615
|
client = null
|
|
616
|
+
// stop() is part of the logout lifecycle and runs before the host emits
|
|
617
|
+
// SIGNED_OUT. Remove the persisted namespace owner here so a later account
|
|
618
|
+
// can never inherit it. A force-stop/process kill does not call stop(), so
|
|
619
|
+
// the cached id still survives the offline cold-start case.
|
|
620
|
+
localMiniappUserIdentity.forget()
|
|
552
621
|
localDevRuntimeToken = null
|
|
553
622
|
connected = false
|
|
554
623
|
resetRuntimeStatus()
|
|
@@ -681,6 +750,11 @@ export const cloudClientService = {
|
|
|
681
750
|
return getCoreClient().reports.complete(...args)
|
|
682
751
|
},
|
|
683
752
|
},
|
|
753
|
+
supportProfile: {
|
|
754
|
+
update(...args: Parameters<CloudCore["supportProfile"]["update"]>) {
|
|
755
|
+
return getCoreClient().supportProfile.update(...args)
|
|
756
|
+
},
|
|
757
|
+
},
|
|
684
758
|
},
|
|
685
759
|
|
|
686
760
|
hasAudioSubscriptions(): boolean {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface ForegroundLocationPermissionResponse {
|
|
2
|
+
status: string
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export interface ForegroundLocationPermissionClient {
|
|
6
|
+
getForegroundPermissionsAsync(): Promise<ForegroundLocationPermissionResponse>
|
|
7
|
+
requestForegroundPermissionsAsync(): Promise<ForegroundLocationPermissionResponse>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Resolve foreground location permission without invoking an Activity-bound
|
|
12
|
+
* Android permission request while the app is backgrounded.
|
|
13
|
+
*/
|
|
14
|
+
export async function resolveForegroundLocationPermission(
|
|
15
|
+
client: ForegroundLocationPermissionClient,
|
|
16
|
+
getAppState: () => string,
|
|
17
|
+
): Promise<ForegroundLocationPermissionResponse> {
|
|
18
|
+
const current = await client.getForegroundPermissionsAsync()
|
|
19
|
+
if (current.status === "granted" || getAppState() !== "active") {
|
|
20
|
+
return current
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return client.requestForegroundPermissionsAsync()
|
|
24
|
+
}
|
|
@@ -19,6 +19,7 @@ import {useSettingsStore, PAIRING_IDENTITY_KEYS} from "../stores/settings"
|
|
|
19
19
|
import {useGlassesStore} from "../stores/glasses"
|
|
20
20
|
import {createDebouncedPatchFlusher} from "../utils/debouncedPatch"
|
|
21
21
|
import {isGlassesConnected} from "./GlassesReadiness"
|
|
22
|
+
import micStateCoordinator from "./MicStateCoordinator"
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Change-pushes are debounced (300ms) and merged so a burst of setSetting
|
|
@@ -28,7 +29,10 @@ import {isGlassesConnected} from "./GlassesReadiness"
|
|
|
28
29
|
const flushBluetoothSettingsPatch = createDebouncedPatchFlusher<Record<string, unknown>>((patch) => {
|
|
29
30
|
// Settings can change while the glasses are disconnected — a native
|
|
30
31
|
// rejection must not surface as an unhandled promise rejection.
|
|
31
|
-
|
|
32
|
+
// Apply mic overrides at flush time, not enqueue time: raw PCM can stop
|
|
33
|
+
// during the debounce window and a captured VAD=false would then be stale.
|
|
34
|
+
const runtimePatch = micStateCoordinator.applyRuntimeOverrides(patch)
|
|
35
|
+
void Promise.resolve(BluetoothSdk.updateBluetoothSettings(runtimePatch)).catch((error) => {
|
|
32
36
|
console.warn("GlassesSettingsSync: updateBluetoothSettings failed:", error)
|
|
33
37
|
})
|
|
34
38
|
}, 300)
|
|
@@ -79,7 +83,8 @@ export async function pushAllBluetoothSettings(): Promise<void> {
|
|
|
79
83
|
// Returns the native write promise so callers can await the seed before the
|
|
80
84
|
// connect handshake replays settings to the glasses (otherwise the handshake
|
|
81
85
|
// can race ahead and replay stale native settings).
|
|
82
|
-
|
|
86
|
+
const settings = useSettingsStore.getState().getBluetoothSettings()
|
|
87
|
+
await BluetoothSdk.updateBluetoothSettings(micStateCoordinator.applyRuntimeOverrides(settings))
|
|
83
88
|
}
|
|
84
89
|
|
|
85
90
|
/**
|
|
@@ -91,7 +96,8 @@ export async function pushAllBluetoothSettings(): Promise<void> {
|
|
|
91
96
|
* with pre-promotion empties (wiping the pairing it had just made).
|
|
92
97
|
*/
|
|
93
98
|
export async function pushDeviceSettingsOnConnect(): Promise<void> {
|
|
94
|
-
|
|
99
|
+
const settings = stripPairingIdentity(useSettingsStore.getState().getBluetoothSettings())
|
|
100
|
+
await BluetoothSdk.updateBluetoothSettings(micStateCoordinator.applyRuntimeOverrides(settings))
|
|
95
101
|
}
|
|
96
102
|
|
|
97
103
|
export function startGlassesSettingsSync(): void {
|
|
@@ -12,21 +12,24 @@
|
|
|
12
12
|
* Started by `engine.start()`. Idempotent.
|
|
13
13
|
*/
|
|
14
14
|
import BluetoothSdk from "@mentra/bluetooth-sdk/internal"
|
|
15
|
+
import type {GlassesStatus} from "@mentra/bluetooth-sdk/internal"
|
|
15
16
|
import {useCoreStore} from "../stores/core"
|
|
16
17
|
import {useGlassesStore} from "../stores/glasses"
|
|
17
|
-
import localMiniappRuntime from "./LocalMiniappRuntime"
|
|
18
18
|
|
|
19
19
|
let unsubs: Array<() => void> = []
|
|
20
20
|
let projectionRunId = 0
|
|
21
|
+
let glassesStatusForwarder: ((status: Partial<GlassesStatus>) => void) | null = null
|
|
22
|
+
let hydrationPromise: Promise<void> | null = null
|
|
21
23
|
|
|
22
|
-
export function startGlassesStatusProjection(): void {
|
|
23
|
-
if (
|
|
24
|
+
export function startGlassesStatusProjection(forwarder?: (status: Partial<GlassesStatus>) => void): Promise<void> {
|
|
25
|
+
if (forwarder) glassesStatusForwarder = forwarder
|
|
26
|
+
if (unsubs.length) return hydrationPromise ?? Promise.resolve()
|
|
24
27
|
|
|
25
28
|
const runId = ++projectionRunId
|
|
26
29
|
let bluetoothEventSeen = false
|
|
27
30
|
let glassesEventSeen = false
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
const bluetoothHydration = BluetoothSdk.getBluetoothStatus()
|
|
30
33
|
.then((status) => {
|
|
31
34
|
if (runId !== projectionRunId || bluetoothEventSeen) return
|
|
32
35
|
useCoreStore.getState().setCoreInfo(status)
|
|
@@ -35,7 +38,7 @@ export function startGlassesStatusProjection(): void {
|
|
|
35
38
|
console.warn("GlassesStatusProjection: getBluetoothStatus failed", error)
|
|
36
39
|
})
|
|
37
40
|
|
|
38
|
-
|
|
41
|
+
const glassesHydration = BluetoothSdk.getGlassesStatus()
|
|
39
42
|
.then((status) => {
|
|
40
43
|
if (runId !== projectionRunId || glassesEventSeen) return
|
|
41
44
|
useGlassesStore.getState().setGlassesInfo(status)
|
|
@@ -52,22 +55,27 @@ export function startGlassesStatusProjection(): void {
|
|
|
52
55
|
}),
|
|
53
56
|
)
|
|
54
57
|
|
|
55
|
-
// Glasses status -> glasses store (+
|
|
56
|
-
// OTA-available flag on disconnect).
|
|
58
|
+
// Glasses status -> glasses store (+ optional full-runtime forwarding to local
|
|
59
|
+
// miniapps; clear any stale OTA-available flag on disconnect).
|
|
57
60
|
unsubs.push(
|
|
58
61
|
BluetoothSdk.onGlassesStatus((changed) => {
|
|
59
62
|
glassesEventSeen = true
|
|
60
63
|
useGlassesStore.getState().setGlassesInfo(changed)
|
|
61
|
-
|
|
64
|
+
glassesStatusForwarder?.(changed)
|
|
62
65
|
if (changed.connection?.state === "disconnected") {
|
|
63
66
|
useGlassesStore.getState().setOtaUpdateAvailable(null)
|
|
64
67
|
}
|
|
65
68
|
}),
|
|
66
69
|
)
|
|
70
|
+
|
|
71
|
+
hydrationPromise = Promise.allSettled([bluetoothHydration, glassesHydration]).then(() => undefined)
|
|
72
|
+
return hydrationPromise
|
|
67
73
|
}
|
|
68
74
|
|
|
69
75
|
export function stopGlassesStatusProjection(): void {
|
|
70
76
|
projectionRunId++
|
|
71
77
|
unsubs.forEach((unsub) => unsub())
|
|
72
78
|
unsubs = []
|
|
79
|
+
glassesStatusForwarder = null
|
|
80
|
+
hydrationPromise = null
|
|
73
81
|
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import BluetoothSdk, {isEnabledHotspotStatus, MentraOtaServer} from "@mentra/bluetooth-sdk/internal"
|
|
2
|
+
// One coordinator selects the native Android or iOS staging implementation at runtime.
|
|
3
|
+
// eslint-disable-next-line react-native/split-platform-components
|
|
4
|
+
import {PermissionsAndroid, Platform} from "react-native"
|
|
5
|
+
import type {OtaCheckCurrentGlassesResult} from "./OtaUpdateCheckService"
|
|
6
|
+
import {
|
|
7
|
+
cleanupArtifacts,
|
|
8
|
+
OtaArtifactError,
|
|
9
|
+
planArtifacts,
|
|
10
|
+
prepareArtifacts,
|
|
11
|
+
rewriteManifestForLocalServer,
|
|
12
|
+
type OtaArtifactDownloadProgress,
|
|
13
|
+
type PreparedOtaArtifact,
|
|
14
|
+
} from "./OtaArtifactDownloader"
|
|
15
|
+
import {disableHotspotWithRetry} from "./HotspotShutdown"
|
|
16
|
+
import {localNetworkTransport} from "./asg/localNetworkTransport"
|
|
17
|
+
|
|
18
|
+
export type HotspotOtaPhase = "idle" | "downloading" | "starting_hotspot" | "joining_hotspot" | "serving"
|
|
19
|
+
|
|
20
|
+
export type HotspotOtaProgress = {
|
|
21
|
+
phase: HotspotOtaPhase
|
|
22
|
+
artifact?: OtaArtifactDownloadProgress
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type HotspotOtaErrorCode =
|
|
26
|
+
| "hotspot_wifi_permission_denied"
|
|
27
|
+
| "hotspot_start_failed"
|
|
28
|
+
| "hotspot_join_failed"
|
|
29
|
+
| "hotspot_server_failed"
|
|
30
|
+
|
|
31
|
+
export class HotspotOtaTransportError extends Error {
|
|
32
|
+
constructor(
|
|
33
|
+
public readonly code: HotspotOtaErrorCode,
|
|
34
|
+
message: string,
|
|
35
|
+
) {
|
|
36
|
+
super(message)
|
|
37
|
+
this.name = "HotspotOtaTransportError"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Owns the single phone-side endpoint and network lease for one hotspot OTA attempt. */
|
|
42
|
+
class HotspotOtaTransport {
|
|
43
|
+
private active = false
|
|
44
|
+
private prepared: PreparedOtaArtifact[] = []
|
|
45
|
+
private hotspotRequested = false
|
|
46
|
+
private localNetworkConnected = false
|
|
47
|
+
private serverStarted = false
|
|
48
|
+
private teardownPromise: Promise<void> | null = null
|
|
49
|
+
|
|
50
|
+
async prepare(
|
|
51
|
+
checkResult: OtaCheckCurrentGlassesResult,
|
|
52
|
+
onProgress?: (progress: HotspotOtaProgress) => void,
|
|
53
|
+
): Promise<string> {
|
|
54
|
+
if (this.teardownPromise) await this.teardownPromise
|
|
55
|
+
if (this.active) {
|
|
56
|
+
throw new Error("A hotspot OTA transport is already active")
|
|
57
|
+
}
|
|
58
|
+
if (!checkResult.manifestBody) {
|
|
59
|
+
throw new Error("The selected OTA check has no manifest body")
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let phase: HotspotOtaPhase = "downloading"
|
|
63
|
+
try {
|
|
64
|
+
await this.ensureAndroidWifiPermission()
|
|
65
|
+
onProgress?.({phase: "downloading"})
|
|
66
|
+
this.prepared = await prepareArtifacts(
|
|
67
|
+
planArtifacts(checkResult),
|
|
68
|
+
(artifact) => onProgress?.({phase: "downloading", artifact}),
|
|
69
|
+
Platform.OS === "ios" ? this.downloadIosArtifact : undefined,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
phase = "starting_hotspot"
|
|
73
|
+
onProgress?.({phase: "starting_hotspot"})
|
|
74
|
+
this.hotspotRequested = true
|
|
75
|
+
const hotspot = await BluetoothSdk.setHotspotState(true)
|
|
76
|
+
if (!isEnabledHotspotStatus(hotspot)) {
|
|
77
|
+
throw new HotspotOtaTransportError("hotspot_start_failed", "Mentra Live did not return hotspot credentials")
|
|
78
|
+
}
|
|
79
|
+
phase = "joining_hotspot"
|
|
80
|
+
onProgress?.({phase: "joining_hotspot"})
|
|
81
|
+
const scopedAddress = await localNetworkTransport.connect(hotspot.ssid, hotspot.password)
|
|
82
|
+
this.localNetworkConnected = true
|
|
83
|
+
let localAddress = scopedAddress
|
|
84
|
+
if (Platform.OS === "ios") {
|
|
85
|
+
try {
|
|
86
|
+
localAddress = await MentraOtaServer.waitForWifiAddress(hotspot.localIp, 15_000)
|
|
87
|
+
} catch (error) {
|
|
88
|
+
throw new HotspotOtaTransportError(
|
|
89
|
+
"hotspot_join_failed",
|
|
90
|
+
error instanceof Error ? error.message : String(error),
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const artifactPaths = Object.fromEntries(this.prepared.map((artifact) => [artifact.sha256, artifact.filePath]))
|
|
95
|
+
// Start exactly one native listener to learn its selected port, then atomically replace
|
|
96
|
+
// the placeholder with the immutable rewritten manifest before ota_start is sent.
|
|
97
|
+
const server = await MentraOtaServer.startOtaServer("{}", artifactPaths, localAddress)
|
|
98
|
+
this.serverStarted = true
|
|
99
|
+
const manifest = rewriteManifestForLocalServer(checkResult.manifestBody, this.prepared, server.baseUrl)
|
|
100
|
+
const published = await MentraOtaServer.startOtaServer(manifest, artifactPaths, server.host)
|
|
101
|
+
if (published.manifestUrl !== server.manifestUrl) {
|
|
102
|
+
throw new Error("Local OTA server endpoint changed while publishing the manifest")
|
|
103
|
+
}
|
|
104
|
+
this.active = true
|
|
105
|
+
onProgress?.({phase: "serving"})
|
|
106
|
+
return published.manifestUrl
|
|
107
|
+
} catch (error) {
|
|
108
|
+
const joined = this.localNetworkConnected
|
|
109
|
+
await this.teardown()
|
|
110
|
+
if (error instanceof OtaArtifactError || error instanceof HotspotOtaTransportError) {
|
|
111
|
+
throw error
|
|
112
|
+
}
|
|
113
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
114
|
+
if (phase === "starting_hotspot") {
|
|
115
|
+
throw new HotspotOtaTransportError("hotspot_start_failed", message)
|
|
116
|
+
}
|
|
117
|
+
if (phase === "joining_hotspot" && !joined) {
|
|
118
|
+
throw new HotspotOtaTransportError("hotspot_join_failed", message)
|
|
119
|
+
}
|
|
120
|
+
throw new HotspotOtaTransportError("hotspot_server_failed", message)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Android 13+ requires the nearby-WiFi runtime grant before WifiNetworkSpecifier is usable. */
|
|
125
|
+
private async ensureAndroidWifiPermission(): Promise<void> {
|
|
126
|
+
if (Platform.OS !== "android") return
|
|
127
|
+
|
|
128
|
+
const permission = PermissionsAndroid.PERMISSIONS.NEARBY_WIFI_DEVICES
|
|
129
|
+
if (await PermissionsAndroid.check(permission)) return
|
|
130
|
+
const result = await PermissionsAndroid.request(permission)
|
|
131
|
+
if (result !== PermissionsAndroid.RESULTS.GRANTED) {
|
|
132
|
+
throw new HotspotOtaTransportError(
|
|
133
|
+
"hotspot_wifi_permission_denied",
|
|
134
|
+
"Nearby devices permission is required to connect to the glasses hotspot",
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private downloadIosArtifact = async (
|
|
140
|
+
entry: {url: string},
|
|
141
|
+
destination: string,
|
|
142
|
+
onProgress?: (bytesWritten: number, contentLength: number) => void,
|
|
143
|
+
): Promise<{statusCode: number}> => {
|
|
144
|
+
const subscription = MentraOtaServer.addListener("artifactDownloadProgress", (event) => {
|
|
145
|
+
if (event.destination === destination) onProgress?.(event.bytesWritten, event.contentLength)
|
|
146
|
+
})
|
|
147
|
+
try {
|
|
148
|
+
return await MentraOtaServer.downloadArtifact(entry.url, destination)
|
|
149
|
+
} finally {
|
|
150
|
+
subscription.remove()
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async teardown(): Promise<void> {
|
|
155
|
+
if (this.teardownPromise) return this.teardownPromise
|
|
156
|
+
this.teardownPromise = (async () => {
|
|
157
|
+
if (this.hotspotRequested) {
|
|
158
|
+
const hotspotStopped = await disableHotspotWithRetry(() => BluetoothSdk.setHotspotState(false), {
|
|
159
|
+
// A completed APK step has just replaced ASG. Let the new command path
|
|
160
|
+
// settle before asking it to tear down the SystemUI-owned access point.
|
|
161
|
+
initialDelayMs: this.serverStarted ? 750 : 0,
|
|
162
|
+
})
|
|
163
|
+
if (!hotspotStopped) {
|
|
164
|
+
console.warn("[OTA_PROGRESS] glasses hotspot shutdown was not confirmed after bounded retries")
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (this.serverStarted) await MentraOtaServer.stopOtaServer().catch(() => {})
|
|
168
|
+
if (this.localNetworkConnected) await localNetworkTransport.disconnect().catch(() => {})
|
|
169
|
+
await cleanupArtifacts().catch(() => {})
|
|
170
|
+
this.prepared = []
|
|
171
|
+
this.active = false
|
|
172
|
+
this.serverStarted = false
|
|
173
|
+
this.localNetworkConnected = false
|
|
174
|
+
this.hotspotRequested = false
|
|
175
|
+
})()
|
|
176
|
+
try {
|
|
177
|
+
await this.teardownPromise
|
|
178
|
+
} finally {
|
|
179
|
+
this.teardownPromise = null
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export const hotspotOtaTransport = new HotspotOtaTransport()
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
type HotspotState = {state: "disabled" | "enabled"}
|
|
2
|
+
|
|
3
|
+
type HotspotShutdownOptions = {
|
|
4
|
+
attempts?: number
|
|
5
|
+
initialDelayMs?: number
|
|
6
|
+
retryDelayMs?: number
|
|
7
|
+
sleep?: (delayMs: number) => Promise<void>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const sleep = (delayMs: number) => new Promise<void>((resolve) => setTimeout(resolve, delayMs))
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Disable a glasses hotspot with a bounded retry. ASG APK replacement keeps the
|
|
14
|
+
* BLE link alive through BES, but the first command after the new ASG process
|
|
15
|
+
* appears can still land while its command path is settling.
|
|
16
|
+
*/
|
|
17
|
+
export async function disableHotspotWithRetry(
|
|
18
|
+
requestDisabled: () => Promise<HotspotState>,
|
|
19
|
+
options: HotspotShutdownOptions = {},
|
|
20
|
+
): Promise<boolean> {
|
|
21
|
+
const attempts = Math.max(1, options.attempts ?? 2)
|
|
22
|
+
const initialDelayMs = Math.max(0, options.initialDelayMs ?? 0)
|
|
23
|
+
const retryDelayMs = Math.max(0, options.retryDelayMs ?? 500)
|
|
24
|
+
const wait = options.sleep ?? sleep
|
|
25
|
+
|
|
26
|
+
if (initialDelayMs > 0) await wait(initialDelayMs)
|
|
27
|
+
|
|
28
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
29
|
+
try {
|
|
30
|
+
const status = await requestDisabled()
|
|
31
|
+
if (status.state === "disabled") return true
|
|
32
|
+
} catch {
|
|
33
|
+
// A lost command is indistinguishable from a response timeout here. Retry
|
|
34
|
+
// while the phone still owns the hotspot network and local server.
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (attempt < attempts && retryDelayMs > 0) await wait(retryDelayMs)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return false
|
|
41
|
+
}
|