@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.
Files changed (355) hide show
  1. package/README.md +42 -1
  2. package/build/engine.d.ts +11 -3
  3. package/build/engine.d.ts.map +1 -1
  4. package/build/engine.js +7 -2
  5. package/build/engine.js.map +1 -1
  6. package/build/facades/dev.d.ts.map +1 -1
  7. package/build/facades/dev.js +9 -5
  8. package/build/facades/dev.js.map +1 -1
  9. package/build/facades/gallery.js +1 -1
  10. package/build/facades/gallery.js.map +1 -1
  11. package/build/facades/glasses.d.ts +1 -0
  12. package/build/facades/glasses.d.ts.map +1 -1
  13. package/build/facades/glasses.js +19 -4
  14. package/build/facades/glasses.js.map +1 -1
  15. package/build/facades/glassesSettings.d.ts.map +1 -1
  16. package/build/facades/glassesSettings.js +1 -0
  17. package/build/facades/glassesSettings.js.map +1 -1
  18. package/build/facades/miniapps.d.ts +4 -0
  19. package/build/facades/miniapps.d.ts.map +1 -1
  20. package/build/facades/miniapps.js +5 -0
  21. package/build/facades/miniapps.js.map +1 -1
  22. package/build/facades/ota.d.ts +11 -2
  23. package/build/facades/ota.d.ts.map +1 -1
  24. package/build/facades/ota.js +19 -4
  25. package/build/facades/ota.js.map +1 -1
  26. package/build/facades/pairing.d.ts +4 -0
  27. package/build/facades/pairing.d.ts.map +1 -1
  28. package/build/facades/pairing.js +10 -8
  29. package/build/facades/pairing.js.map +1 -1
  30. package/build/facades/phoneNotifications.d.ts +0 -4
  31. package/build/facades/phoneNotifications.d.ts.map +1 -1
  32. package/build/facades/phoneNotifications.js +4 -14
  33. package/build/facades/phoneNotifications.js.map +1 -1
  34. package/build/index.d.ts +6 -3
  35. package/build/index.d.ts.map +1 -1
  36. package/build/index.js +4 -2
  37. package/build/index.js.map +1 -1
  38. package/build/internal.d.ts +2 -2
  39. package/build/internal.d.ts.map +1 -1
  40. package/build/internal.js +1 -1
  41. package/build/internal.js.map +1 -1
  42. package/build/react/MentraLiveOtaFlow.d.ts +38 -0
  43. package/build/react/MentraLiveOtaFlow.d.ts.map +1 -0
  44. package/build/react/MentraLiveOtaFlow.js +528 -0
  45. package/build/react/MentraLiveOtaFlow.js.map +1 -0
  46. package/build/react/index.d.ts +2 -0
  47. package/build/react/index.d.ts.map +1 -0
  48. package/build/react/index.js +2 -0
  49. package/build/react/index.js.map +1 -0
  50. package/build/react/useEngineSnapshot.d.ts +2 -0
  51. package/build/react/useEngineSnapshot.d.ts.map +1 -0
  52. package/build/react/useEngineSnapshot.js +14 -0
  53. package/build/react/useEngineSnapshot.js.map +1 -0
  54. package/build/runtime/bootstrap.d.ts +24 -1
  55. package/build/runtime/bootstrap.d.ts.map +1 -1
  56. package/build/runtime/bootstrap.js +17 -0
  57. package/build/runtime/bootstrap.js.map +1 -1
  58. package/build/runtime/scanQrSeam.d.ts +7 -0
  59. package/build/runtime/scanQrSeam.d.ts.map +1 -0
  60. package/build/runtime/scanQrSeam.js +16 -0
  61. package/build/runtime/scanQrSeam.js.map +1 -0
  62. package/build/services/AppRegistry.d.ts +18 -8
  63. package/build/services/AppRegistry.d.ts.map +1 -1
  64. package/build/services/AppRegistry.js +44 -45
  65. package/build/services/AppRegistry.js.map +1 -1
  66. package/build/services/AudioCloudUplink.d.ts +9 -3
  67. package/build/services/AudioCloudUplink.d.ts.map +1 -1
  68. package/build/services/AudioCloudUplink.js +9 -3
  69. package/build/services/AudioCloudUplink.js.map +1 -1
  70. package/build/services/AudioPlaybackService.d.ts +9 -1
  71. package/build/services/AudioPlaybackService.d.ts.map +1 -1
  72. package/build/services/AudioPlaybackService.js +77 -32
  73. package/build/services/AudioPlaybackService.js.map +1 -1
  74. package/build/services/BlobStore.d.ts +2 -1
  75. package/build/services/BlobStore.d.ts.map +1 -1
  76. package/build/services/BlobStore.js +17 -9
  77. package/build/services/BlobStore.js.map +1 -1
  78. package/build/services/CloudAudioSubscriptionSync.d.ts.map +1 -1
  79. package/build/services/CloudAudioSubscriptionSync.js +13 -1
  80. package/build/services/CloudAudioSubscriptionSync.js.map +1 -1
  81. package/build/services/CloudClientService.d.ts +15 -0
  82. package/build/services/CloudClientService.d.ts.map +1 -1
  83. package/build/services/CloudClientService.js +75 -0
  84. package/build/services/CloudClientService.js.map +1 -1
  85. package/build/services/ForegroundLocationPermission.d.ts +13 -0
  86. package/build/services/ForegroundLocationPermission.d.ts.map +1 -0
  87. package/build/services/ForegroundLocationPermission.js +12 -0
  88. package/build/services/ForegroundLocationPermission.js.map +1 -0
  89. package/build/services/GlassesSettingsSync.d.ts.map +1 -1
  90. package/build/services/GlassesSettingsSync.js +9 -3
  91. package/build/services/GlassesSettingsSync.js.map +1 -1
  92. package/build/services/GlassesStatusProjection.d.ts +2 -1
  93. package/build/services/GlassesStatusProjection.d.ts.map +1 -1
  94. package/build/services/GlassesStatusProjection.js +15 -8
  95. package/build/services/GlassesStatusProjection.js.map +1 -1
  96. package/build/services/HotspotOtaTransport.d.ts +29 -0
  97. package/build/services/HotspotOtaTransport.d.ts.map +1 -0
  98. package/build/services/HotspotOtaTransport.js +146 -0
  99. package/build/services/HotspotOtaTransport.js.map +1 -0
  100. package/build/services/HotspotShutdown.d.ts +17 -0
  101. package/build/services/HotspotShutdown.d.ts.map +1 -0
  102. package/build/services/HotspotShutdown.js +29 -0
  103. package/build/services/HotspotShutdown.js.map +1 -0
  104. package/build/services/LocalDisplayManager.d.ts +15 -0
  105. package/build/services/LocalDisplayManager.d.ts.map +1 -1
  106. package/build/services/LocalDisplayManager.js +106 -20
  107. package/build/services/LocalDisplayManager.js.map +1 -1
  108. package/build/services/LocalMiniappRuntime.d.ts +67 -1
  109. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  110. package/build/services/LocalMiniappRuntime.js +486 -126
  111. package/build/services/LocalMiniappRuntime.js.map +1 -1
  112. package/build/services/LocalMiniappStorage.d.ts +32 -0
  113. package/build/services/LocalMiniappStorage.d.ts.map +1 -0
  114. package/build/services/LocalMiniappStorage.js +66 -0
  115. package/build/services/LocalMiniappStorage.js.map +1 -0
  116. package/build/services/LocalMiniappUserIdentity.d.ts +20 -0
  117. package/build/services/LocalMiniappUserIdentity.d.ts.map +1 -0
  118. package/build/services/LocalMiniappUserIdentity.js +43 -0
  119. package/build/services/LocalMiniappUserIdentity.js.map +1 -0
  120. package/build/services/MicStateCoordinator.d.ts +42 -1
  121. package/build/services/MicStateCoordinator.d.ts.map +1 -1
  122. package/build/services/MicStateCoordinator.js +141 -3
  123. package/build/services/MicStateCoordinator.js.map +1 -1
  124. package/build/services/MiniappLauncher.d.ts.map +1 -1
  125. package/build/services/MiniappLauncher.js +23 -3
  126. package/build/services/MiniappLauncher.js.map +1 -1
  127. package/build/services/MiniappLiveness.d.ts +12 -0
  128. package/build/services/MiniappLiveness.d.ts.map +1 -0
  129. package/build/services/MiniappLiveness.js +16 -0
  130. package/build/services/MiniappLiveness.js.map +1 -0
  131. package/build/services/OtaArtifactDownloader.d.ts +65 -0
  132. package/build/services/OtaArtifactDownloader.d.ts.map +1 -0
  133. package/build/services/OtaArtifactDownloader.js +226 -0
  134. package/build/services/OtaArtifactDownloader.js.map +1 -0
  135. package/build/services/OtaAutoChain.d.ts +34 -0
  136. package/build/services/OtaAutoChain.d.ts.map +1 -0
  137. package/build/services/OtaAutoChain.js +83 -0
  138. package/build/services/OtaAutoChain.js.map +1 -0
  139. package/build/services/OtaErrorMapping.d.ts +18 -0
  140. package/build/services/OtaErrorMapping.d.ts.map +1 -0
  141. package/build/services/OtaErrorMapping.js +63 -0
  142. package/build/services/OtaErrorMapping.js.map +1 -0
  143. package/build/services/OtaInstallCoordinator.d.ts +94 -14
  144. package/build/services/OtaInstallCoordinator.d.ts.map +1 -1
  145. package/build/services/OtaInstallCoordinator.js +574 -130
  146. package/build/services/OtaInstallCoordinator.js.map +1 -1
  147. package/build/services/OtaService.d.ts.map +1 -1
  148. package/build/services/OtaService.js +8 -1
  149. package/build/services/OtaService.js.map +1 -1
  150. package/build/services/OtaUpdateCheckService.d.ts +38 -3
  151. package/build/services/OtaUpdateCheckService.d.ts.map +1 -1
  152. package/build/services/OtaUpdateCheckService.js +142 -22
  153. package/build/services/OtaUpdateCheckService.js.map +1 -1
  154. package/build/services/PhoneCalendarService.d.ts.map +1 -1
  155. package/build/services/PhoneCalendarService.js +22 -1
  156. package/build/services/PhoneCalendarService.js.map +1 -1
  157. package/build/services/PhoneCameraFovCoordinator.d.ts +2 -0
  158. package/build/services/PhoneCameraFovCoordinator.d.ts.map +1 -1
  159. package/build/services/PhoneCameraFovCoordinator.js +14 -0
  160. package/build/services/PhoneCameraFovCoordinator.js.map +1 -1
  161. package/build/services/PhoneNotificationsSync.d.ts.map +1 -1
  162. package/build/services/PhoneNotificationsSync.js +12 -10
  163. package/build/services/PhoneNotificationsSync.js.map +1 -1
  164. package/build/services/PhonePhotoCoordinator.d.ts +10 -3
  165. package/build/services/PhonePhotoCoordinator.d.ts.map +1 -1
  166. package/build/services/PhonePhotoCoordinator.js +21 -3
  167. package/build/services/PhonePhotoCoordinator.js.map +1 -1
  168. package/build/services/PhoneStreamCoordinator.d.ts +8 -1
  169. package/build/services/PhoneStreamCoordinator.d.ts.map +1 -1
  170. package/build/services/PhoneStreamCoordinator.js +125 -13
  171. package/build/services/PhoneStreamCoordinator.js.map +1 -1
  172. package/build/services/PhoneVideoCoordinator.d.ts +2 -0
  173. package/build/services/PhoneVideoCoordinator.d.ts.map +1 -1
  174. package/build/services/PhoneVideoCoordinator.js +10 -0
  175. package/build/services/PhoneVideoCoordinator.js.map +1 -1
  176. package/build/services/SceneRenderer.d.ts.map +1 -1
  177. package/build/services/SceneRenderer.js +15 -0
  178. package/build/services/SceneRenderer.js.map +1 -1
  179. package/build/services/SupportProfileSync.d.ts +22 -0
  180. package/build/services/SupportProfileSync.d.ts.map +1 -0
  181. package/build/services/SupportProfileSync.js +213 -0
  182. package/build/services/SupportProfileSync.js.map +1 -0
  183. package/build/services/TtsTextSanitizer.d.ts +5 -0
  184. package/build/services/TtsTextSanitizer.d.ts.map +1 -0
  185. package/build/services/TtsTextSanitizer.js +61 -0
  186. package/build/services/TtsTextSanitizer.js.map +1 -0
  187. package/build/services/WifiAdbRequest.d.ts +17 -0
  188. package/build/services/WifiAdbRequest.d.ts.map +1 -0
  189. package/build/services/WifiAdbRequest.js +35 -0
  190. package/build/services/WifiAdbRequest.js.map +1 -0
  191. package/build/services/asg/asgCameraApi.d.ts +3 -0
  192. package/build/services/asg/asgCameraApi.d.ts.map +1 -1
  193. package/build/services/asg/asgCameraApi.js +72 -2
  194. package/build/services/asg/asgCameraApi.js.map +1 -1
  195. package/build/services/asg/gallerySyncService.d.ts.map +1 -1
  196. package/build/services/asg/gallerySyncService.js +26 -14
  197. package/build/services/asg/gallerySyncService.js.map +1 -1
  198. package/build/services/asg/localNetworkTransport.d.ts +2 -2
  199. package/build/services/asg/localNetworkTransport.d.ts.map +1 -1
  200. package/build/services/asg/localNetworkTransport.js +6 -6
  201. package/build/services/asg/localNetworkTransport.js.map +1 -1
  202. package/build/services/audioPlaybackAssets.d.ts +8 -0
  203. package/build/services/audioPlaybackAssets.d.ts.map +1 -0
  204. package/build/services/audioPlaybackAssets.js +7 -0
  205. package/build/services/audioPlaybackAssets.js.map +1 -0
  206. package/build/services/glassesClockSync.js +2 -2
  207. package/build/services/glassesClockSync.js.map +1 -1
  208. package/build/services/manifestActions.d.ts +7 -0
  209. package/build/services/manifestActions.d.ts.map +1 -0
  210. package/build/services/manifestActions.js +25 -0
  211. package/build/services/manifestActions.js.map +1 -0
  212. package/build/services/otaDisplayState.d.ts +22 -11
  213. package/build/services/otaDisplayState.d.ts.map +1 -1
  214. package/build/services/otaDisplayState.js +36 -14
  215. package/build/services/otaDisplayState.js.map +1 -1
  216. package/build/services/otaInstallPolicy.d.ts +15 -3
  217. package/build/services/otaInstallPolicy.d.ts.map +1 -1
  218. package/build/services/otaInstallPolicy.js +24 -3
  219. package/build/services/otaInstallPolicy.js.map +1 -1
  220. package/build/services/otaManifestUrl.d.ts +2 -0
  221. package/build/services/otaManifestUrl.d.ts.map +1 -0
  222. package/build/services/{asgOtaVersionUrl.js → otaManifestUrl.js} +15 -5
  223. package/build/services/otaManifestUrl.js.map +1 -0
  224. package/build/services/slimStreamStatus.d.ts +11 -0
  225. package/build/services/slimStreamStatus.d.ts.map +1 -1
  226. package/build/services/slimStreamStatus.js +14 -4
  227. package/build/services/slimStreamStatus.js.map +1 -1
  228. package/build/stores/apps.d.ts.map +1 -1
  229. package/build/stores/apps.js +11 -0
  230. package/build/stores/apps.js.map +1 -1
  231. package/build/stores/bluetoothSettingKeys.d.ts.map +1 -1
  232. package/build/stores/bluetoothSettingKeys.js +2 -0
  233. package/build/stores/bluetoothSettingKeys.js.map +1 -1
  234. package/build/stores/gallerySync.d.ts +2 -0
  235. package/build/stores/gallerySync.d.ts.map +1 -1
  236. package/build/stores/gallerySync.js +2 -0
  237. package/build/stores/gallerySync.js.map +1 -1
  238. package/build/stores/glasses.d.ts.map +1 -1
  239. package/build/stores/glasses.js +3 -0
  240. package/build/stores/glasses.js.map +1 -1
  241. package/build/stores/settings.d.ts +17 -0
  242. package/build/stores/settings.d.ts.map +1 -1
  243. package/build/stores/settings.js +152 -19
  244. package/build/stores/settings.js.map +1 -1
  245. package/build/types/applet.d.ts +2 -0
  246. package/build/types/applet.d.ts.map +1 -1
  247. package/build/types/applet.js.map +1 -1
  248. package/build/types/enums.d.ts +1 -0
  249. package/build/types/enums.d.ts.map +1 -1
  250. package/build/types/enums.js +1 -0
  251. package/build/types/enums.js.map +1 -1
  252. package/build/types/hardware.d.ts +1 -0
  253. package/build/types/hardware.d.ts.map +1 -1
  254. package/build/types/hardware.js +14 -0
  255. package/build/types/hardware.js.map +1 -1
  256. package/build/utils/configuredDevHost.d.ts +6 -0
  257. package/build/utils/configuredDevHost.d.ts.map +1 -0
  258. package/build/utils/configuredDevHost.js +19 -0
  259. package/build/utils/configuredDevHost.js.map +1 -0
  260. package/build/utils/devMiniappLaunch.d.ts +19 -8
  261. package/build/utils/devMiniappLaunch.d.ts.map +1 -1
  262. package/build/utils/devMiniappLaunch.js +144 -21
  263. package/build/utils/devMiniappLaunch.js.map +1 -1
  264. package/build/utils/diagnosticContext.d.ts.map +1 -1
  265. package/build/utils/diagnosticContext.js +15 -17
  266. package/build/utils/diagnosticContext.js.map +1 -1
  267. package/build/utils/hardware/hardware.d.ts.map +1 -1
  268. package/build/utils/hardware/hardware.js +7 -3
  269. package/build/utils/hardware/hardware.js.map +1 -1
  270. package/build/utils/miniappDiagnostics.d.ts +64 -0
  271. package/build/utils/miniappDiagnostics.d.ts.map +1 -0
  272. package/build/utils/miniappDiagnostics.js +173 -0
  273. package/build/utils/miniappDiagnostics.js.map +1 -0
  274. package/build/utils/timers/timers.d.ts.map +1 -1
  275. package/build/utils/timers/timers.js +55 -18
  276. package/build/utils/timers/timers.js.map +1 -1
  277. package/package.json +27 -18
  278. package/src/assets/silence.wav +0 -0
  279. package/src/engine.ts +7 -2
  280. package/src/facades/dev.ts +9 -5
  281. package/src/facades/gallery.ts +1 -1
  282. package/src/facades/glasses.ts +17 -4
  283. package/src/facades/glassesSettings.ts +5 -0
  284. package/src/facades/miniapps.ts +7 -0
  285. package/src/facades/ota.ts +19 -4
  286. package/src/facades/pairing.ts +11 -7
  287. package/src/facades/phoneNotifications.ts +4 -13
  288. package/src/index.ts +26 -2
  289. package/src/internal.ts +2 -1
  290. package/src/react/MentraLiveOtaFlow.tsx +856 -0
  291. package/src/react/index.ts +7 -0
  292. package/src/react/useEngineSnapshot.ts +16 -0
  293. package/src/runtime/bootstrap.ts +33 -1
  294. package/src/runtime/scanQrSeam.ts +20 -0
  295. package/src/services/AppRegistry.ts +72 -48
  296. package/src/services/AudioCloudUplink.ts +9 -3
  297. package/src/services/AudioPlaybackService.ts +94 -34
  298. package/src/services/BlobStore.ts +22 -9
  299. package/src/services/CloudAudioSubscriptionSync.ts +13 -1
  300. package/src/services/CloudClientService.ts +74 -0
  301. package/src/services/ForegroundLocationPermission.ts +24 -0
  302. package/src/services/GlassesSettingsSync.ts +9 -3
  303. package/src/services/GlassesStatusProjection.ts +16 -8
  304. package/src/services/HotspotOtaTransport.ts +184 -0
  305. package/src/services/HotspotShutdown.ts +41 -0
  306. package/src/services/LocalDisplayManager.ts +112 -21
  307. package/src/services/LocalMiniappRuntime.ts +576 -130
  308. package/src/services/LocalMiniappStorage.ts +82 -0
  309. package/src/services/LocalMiniappUserIdentity.ts +46 -0
  310. package/src/services/MicStateCoordinator.ts +169 -4
  311. package/src/services/MiniappLauncher.ts +30 -3
  312. package/src/services/MiniappLiveness.ts +24 -0
  313. package/src/services/OtaArtifactDownloader.ts +312 -0
  314. package/src/services/OtaAutoChain.ts +106 -0
  315. package/src/services/OtaErrorMapping.ts +80 -0
  316. package/src/services/OtaInstallCoordinator.ts +645 -143
  317. package/src/services/OtaService.ts +11 -5
  318. package/src/services/OtaUpdateCheckService.ts +180 -20
  319. package/src/services/PhoneCalendarService.ts +27 -4
  320. package/src/services/PhoneCameraFovCoordinator.ts +15 -0
  321. package/src/services/PhoneNotificationsSync.ts +12 -10
  322. package/src/services/PhonePhotoCoordinator.ts +33 -7
  323. package/src/services/PhoneStreamCoordinator.ts +139 -25
  324. package/src/services/PhoneVideoCoordinator.ts +11 -0
  325. package/src/services/SceneRenderer.ts +16 -0
  326. package/src/services/SupportProfileSync.ts +218 -0
  327. package/src/services/TtsTextSanitizer.ts +66 -0
  328. package/src/services/WifiAdbRequest.ts +53 -0
  329. package/src/services/asg/asgCameraApi.ts +99 -8
  330. package/src/services/asg/gallerySyncService.ts +28 -19
  331. package/src/services/asg/localNetworkTransport.ts +7 -11
  332. package/src/services/audioPlaybackAssets.ts +8 -0
  333. package/src/services/glassesClockSync.ts +2 -2
  334. package/src/services/manifestActions.ts +30 -0
  335. package/src/services/otaDisplayState.ts +57 -16
  336. package/src/services/otaInstallPolicy.ts +29 -3
  337. package/src/services/{asgOtaVersionUrl.ts → otaManifestUrl.ts} +16 -4
  338. package/src/services/slimStreamStatus.ts +16 -5
  339. package/src/stores/apps.ts +12 -0
  340. package/src/stores/bluetoothSettingKeys.ts +2 -0
  341. package/src/stores/gallerySync.ts +4 -0
  342. package/src/stores/glasses.ts +3 -0
  343. package/src/stores/settings.ts +174 -21
  344. package/src/types/applet.ts +2 -0
  345. package/src/types/enums.ts +2 -1
  346. package/src/types/hardware.ts +17 -0
  347. package/src/utils/configuredDevHost.ts +17 -0
  348. package/src/utils/devMiniappLaunch.ts +151 -23
  349. package/src/utils/diagnosticContext.ts +17 -17
  350. package/src/utils/hardware/hardware.ts +7 -9
  351. package/src/utils/miniappDiagnostics.ts +226 -0
  352. package/src/utils/timers/timers.ts +57 -21
  353. package/build/services/asgOtaVersionUrl.d.ts +0 -2
  354. package/build/services/asgOtaVersionUrl.d.ts.map +0 -1
  355. 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
- /** packageName → committed usage bytes, cached; invalidated on commit/delete/clear. */
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 userId = sanitizeSegment(this.hooks.getUserId())
109
- const dir = new Directory(Paths.document, ROOT_DIR_NAME, userId, sanitizeSegment(packageName))
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.hooks.getUserId())}_${packageName}_`
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 cached = this.usageCache.get(packageName)
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(packageName, total)
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 cur = this.usageCache.get(packageName)
176
- if (cur !== undefined) this.usageCache.set(packageName, Math.max(0, cur + delta))
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
- if (nextKey === this.lastAppliedKey || nextKey === this.pendingKey) {
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
- void Promise.resolve(BluetoothSdk.updateBluetoothSettings(patch)).catch((error) => {
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
- await BluetoothSdk.updateBluetoothSettings(useSettingsStore.getState().getBluetoothSettings())
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
- await BluetoothSdk.updateBluetoothSettings(stripPairingIdentity(useSettingsStore.getState().getBluetoothSettings()))
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 (unsubs.length) return
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
- void BluetoothSdk.getBluetoothStatus()
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
- void BluetoothSdk.getGlassesStatus()
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 (+ forward to local miniapps; clear any stale
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
- localMiniappRuntime.forwardEvent("glasses_connection_state", changed)
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
+ }