@mentra/engine 0.1.0-dev.0 → 0.1.0-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (441) 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 +3 -2
  7. package/build/facades/dev.d.ts.map +1 -1
  8. package/build/facades/dev.js +18 -12
  9. package/build/facades/dev.js.map +1 -1
  10. package/build/facades/gallery.js +1 -1
  11. package/build/facades/gallery.js.map +1 -1
  12. package/build/facades/glasses.d.ts +1 -0
  13. package/build/facades/glasses.d.ts.map +1 -1
  14. package/build/facades/glasses.js +19 -4
  15. package/build/facades/glasses.js.map +1 -1
  16. package/build/facades/glassesSettings.d.ts.map +1 -1
  17. package/build/facades/glassesSettings.js +1 -0
  18. package/build/facades/glassesSettings.js.map +1 -1
  19. package/build/facades/miniapps.d.ts +4 -0
  20. package/build/facades/miniapps.d.ts.map +1 -1
  21. package/build/facades/miniapps.js +5 -0
  22. package/build/facades/miniapps.js.map +1 -1
  23. package/build/facades/ota.d.ts +11 -2
  24. package/build/facades/ota.d.ts.map +1 -1
  25. package/build/facades/ota.js +19 -4
  26. package/build/facades/ota.js.map +1 -1
  27. package/build/facades/pairing.d.ts +4 -0
  28. package/build/facades/pairing.d.ts.map +1 -1
  29. package/build/facades/pairing.js +10 -8
  30. package/build/facades/pairing.js.map +1 -1
  31. package/build/facades/permissions.d.ts +10 -0
  32. package/build/facades/permissions.d.ts.map +1 -1
  33. package/build/facades/permissions.js +10 -0
  34. package/build/facades/permissions.js.map +1 -1
  35. package/build/facades/phoneNotifications.d.ts +0 -4
  36. package/build/facades/phoneNotifications.d.ts.map +1 -1
  37. package/build/facades/phoneNotifications.js +4 -14
  38. package/build/facades/phoneNotifications.js.map +1 -1
  39. package/build/index.d.ts +6 -3
  40. package/build/index.d.ts.map +1 -1
  41. package/build/index.js +4 -2
  42. package/build/index.js.map +1 -1
  43. package/build/internal.d.ts +3 -2
  44. package/build/internal.d.ts.map +1 -1
  45. package/build/internal.js +2 -1
  46. package/build/internal.js.map +1 -1
  47. package/build/react/MentraLiveOtaFlow.d.ts +38 -0
  48. package/build/react/MentraLiveOtaFlow.d.ts.map +1 -0
  49. package/build/react/MentraLiveOtaFlow.js +528 -0
  50. package/build/react/MentraLiveOtaFlow.js.map +1 -0
  51. package/build/react/index.d.ts +2 -0
  52. package/build/react/index.d.ts.map +1 -0
  53. package/build/react/index.js +2 -0
  54. package/build/react/index.js.map +1 -0
  55. package/build/react/useEngineSnapshot.d.ts +2 -0
  56. package/build/react/useEngineSnapshot.d.ts.map +1 -0
  57. package/build/react/useEngineSnapshot.js +14 -0
  58. package/build/react/useEngineSnapshot.js.map +1 -0
  59. package/build/runtime/bootstrap.d.ts +24 -1
  60. package/build/runtime/bootstrap.d.ts.map +1 -1
  61. package/build/runtime/bootstrap.js +17 -0
  62. package/build/runtime/bootstrap.js.map +1 -1
  63. package/build/runtime/scanQrSeam.d.ts +7 -0
  64. package/build/runtime/scanQrSeam.d.ts.map +1 -0
  65. package/build/runtime/scanQrSeam.js +16 -0
  66. package/build/runtime/scanQrSeam.js.map +1 -0
  67. package/build/services/AppRegistry.d.ts +33 -57
  68. package/build/services/AppRegistry.d.ts.map +1 -1
  69. package/build/services/AppRegistry.js +193 -144
  70. package/build/services/AppRegistry.js.map +1 -1
  71. package/build/services/AudioCloudUplink.d.ts +12 -0
  72. package/build/services/AudioCloudUplink.d.ts.map +1 -1
  73. package/build/services/AudioCloudUplink.js +22 -1
  74. package/build/services/AudioCloudUplink.js.map +1 -1
  75. package/build/services/AudioPlaybackService.d.ts +41 -2
  76. package/build/services/AudioPlaybackService.d.ts.map +1 -1
  77. package/build/services/AudioPlaybackService.js +250 -33
  78. package/build/services/AudioPlaybackService.js.map +1 -1
  79. package/build/services/BlobStore.d.ts +2 -1
  80. package/build/services/BlobStore.d.ts.map +1 -1
  81. package/build/services/BlobStore.js +17 -9
  82. package/build/services/BlobStore.js.map +1 -1
  83. package/build/services/CloudAudioSubscriptionSync.d.ts.map +1 -1
  84. package/build/services/CloudAudioSubscriptionSync.js +13 -1
  85. package/build/services/CloudAudioSubscriptionSync.js.map +1 -1
  86. package/build/services/CloudClientService.d.ts +15 -0
  87. package/build/services/CloudClientService.d.ts.map +1 -1
  88. package/build/services/CloudClientService.js +93 -1
  89. package/build/services/CloudClientService.js.map +1 -1
  90. package/build/services/DeviceEventRouter.js +5 -5
  91. package/build/services/DeviceEventRouter.js.map +1 -1
  92. package/build/services/ForegroundLocationPermission.d.ts +13 -0
  93. package/build/services/ForegroundLocationPermission.d.ts.map +1 -0
  94. package/build/services/ForegroundLocationPermission.js +12 -0
  95. package/build/services/ForegroundLocationPermission.js.map +1 -0
  96. package/build/services/GlassesSettingsSync.d.ts.map +1 -1
  97. package/build/services/GlassesSettingsSync.js +9 -3
  98. package/build/services/GlassesSettingsSync.js.map +1 -1
  99. package/build/services/GlassesStatusProjection.d.ts +2 -1
  100. package/build/services/GlassesStatusProjection.d.ts.map +1 -1
  101. package/build/services/GlassesStatusProjection.js +15 -8
  102. package/build/services/GlassesStatusProjection.js.map +1 -1
  103. package/build/services/HotspotOtaTransport.d.ts +29 -0
  104. package/build/services/HotspotOtaTransport.d.ts.map +1 -0
  105. package/build/services/HotspotOtaTransport.js +146 -0
  106. package/build/services/HotspotOtaTransport.js.map +1 -0
  107. package/build/services/HotspotShutdown.d.ts +17 -0
  108. package/build/services/HotspotShutdown.d.ts.map +1 -0
  109. package/build/services/HotspotShutdown.js +29 -0
  110. package/build/services/HotspotShutdown.js.map +1 -0
  111. package/build/services/LocalDisplayManager.d.ts +15 -0
  112. package/build/services/LocalDisplayManager.d.ts.map +1 -1
  113. package/build/services/LocalDisplayManager.js +106 -20
  114. package/build/services/LocalDisplayManager.js.map +1 -1
  115. package/build/services/LocalMiniappRuntime.d.ts +74 -2
  116. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  117. package/build/services/LocalMiniappRuntime.js +785 -291
  118. package/build/services/LocalMiniappRuntime.js.map +1 -1
  119. package/build/services/LocalMiniappStorage.d.ts +32 -0
  120. package/build/services/LocalMiniappStorage.d.ts.map +1 -0
  121. package/build/services/LocalMiniappStorage.js +66 -0
  122. package/build/services/LocalMiniappStorage.js.map +1 -0
  123. package/build/services/LocalMiniappUserIdentity.d.ts +20 -0
  124. package/build/services/LocalMiniappUserIdentity.d.ts.map +1 -0
  125. package/build/services/LocalMiniappUserIdentity.js +43 -0
  126. package/build/services/LocalMiniappUserIdentity.js.map +1 -0
  127. package/build/services/LocalSttFallbackCoordinator.d.ts +9 -6
  128. package/build/services/LocalSttFallbackCoordinator.d.ts.map +1 -1
  129. package/build/services/LocalSttFallbackCoordinator.js +18 -12
  130. package/build/services/LocalSttFallbackCoordinator.js.map +1 -1
  131. package/build/services/MicStateCoordinator.d.ts +42 -1
  132. package/build/services/MicStateCoordinator.d.ts.map +1 -1
  133. package/build/services/MicStateCoordinator.js +141 -3
  134. package/build/services/MicStateCoordinator.js.map +1 -1
  135. package/build/services/MiniappLauncher.d.ts.map +1 -1
  136. package/build/services/MiniappLauncher.js +23 -3
  137. package/build/services/MiniappLauncher.js.map +1 -1
  138. package/build/services/MiniappLiveness.d.ts +12 -0
  139. package/build/services/MiniappLiveness.d.ts.map +1 -0
  140. package/build/services/MiniappLiveness.js +16 -0
  141. package/build/services/MiniappLiveness.js.map +1 -0
  142. package/build/services/NativeHttpResponse.d.ts +3 -0
  143. package/build/services/NativeHttpResponse.d.ts.map +1 -0
  144. package/build/services/NativeHttpResponse.js +6 -0
  145. package/build/services/NativeHttpResponse.js.map +1 -0
  146. package/build/services/OtaArtifactDownloader.d.ts +65 -0
  147. package/build/services/OtaArtifactDownloader.d.ts.map +1 -0
  148. package/build/services/OtaArtifactDownloader.js +226 -0
  149. package/build/services/OtaArtifactDownloader.js.map +1 -0
  150. package/build/services/OtaAutoChain.d.ts +34 -0
  151. package/build/services/OtaAutoChain.d.ts.map +1 -0
  152. package/build/services/OtaAutoChain.js +83 -0
  153. package/build/services/OtaAutoChain.js.map +1 -0
  154. package/build/services/OtaErrorMapping.d.ts +18 -0
  155. package/build/services/OtaErrorMapping.d.ts.map +1 -0
  156. package/build/services/OtaErrorMapping.js +63 -0
  157. package/build/services/OtaErrorMapping.js.map +1 -0
  158. package/build/services/OtaInstallCoordinator.d.ts +94 -14
  159. package/build/services/OtaInstallCoordinator.d.ts.map +1 -1
  160. package/build/services/OtaInstallCoordinator.js +574 -130
  161. package/build/services/OtaInstallCoordinator.js.map +1 -1
  162. package/build/services/OtaService.d.ts.map +1 -1
  163. package/build/services/OtaService.js +8 -1
  164. package/build/services/OtaService.js.map +1 -1
  165. package/build/services/OtaUpdateCheckService.d.ts +38 -3
  166. package/build/services/OtaUpdateCheckService.d.ts.map +1 -1
  167. package/build/services/OtaUpdateCheckService.js +142 -22
  168. package/build/services/OtaUpdateCheckService.js.map +1 -1
  169. package/build/services/PhoneCalendarHelpers.d.ts +32 -0
  170. package/build/services/PhoneCalendarHelpers.d.ts.map +1 -0
  171. package/build/services/PhoneCalendarHelpers.js +157 -0
  172. package/build/services/PhoneCalendarHelpers.js.map +1 -0
  173. package/build/services/PhoneCalendarService.d.ts +5 -0
  174. package/build/services/PhoneCalendarService.d.ts.map +1 -0
  175. package/build/services/PhoneCalendarService.js +45 -0
  176. package/build/services/PhoneCalendarService.js.map +1 -0
  177. package/build/services/PhoneCameraFovCoordinator.d.ts +30 -0
  178. package/build/services/PhoneCameraFovCoordinator.d.ts.map +1 -0
  179. package/build/services/PhoneCameraFovCoordinator.js +194 -0
  180. package/build/services/PhoneCameraFovCoordinator.js.map +1 -0
  181. package/build/services/PhoneNotificationsSync.d.ts.map +1 -1
  182. package/build/services/PhoneNotificationsSync.js +12 -10
  183. package/build/services/PhoneNotificationsSync.js.map +1 -1
  184. package/build/services/PhonePhotoCoordinator.d.ts +26 -2
  185. package/build/services/PhonePhotoCoordinator.d.ts.map +1 -1
  186. package/build/services/PhonePhotoCoordinator.js +79 -9
  187. package/build/services/PhonePhotoCoordinator.js.map +1 -1
  188. package/build/services/PhoneStreamCoordinator.d.ts +8 -1
  189. package/build/services/PhoneStreamCoordinator.d.ts.map +1 -1
  190. package/build/services/PhoneStreamCoordinator.js +125 -13
  191. package/build/services/PhoneStreamCoordinator.js.map +1 -1
  192. package/build/services/PhoneVideoCoordinator.d.ts +2 -0
  193. package/build/services/PhoneVideoCoordinator.d.ts.map +1 -1
  194. package/build/services/PhoneVideoCoordinator.js +10 -0
  195. package/build/services/PhoneVideoCoordinator.js.map +1 -1
  196. package/build/services/SceneRenderer.d.ts.map +1 -1
  197. package/build/services/SceneRenderer.js +15 -0
  198. package/build/services/SceneRenderer.js.map +1 -1
  199. package/build/services/SentenceTtsPipeline.d.ts +31 -0
  200. package/build/services/SentenceTtsPipeline.d.ts.map +1 -0
  201. package/build/services/SentenceTtsPipeline.js +67 -0
  202. package/build/services/SentenceTtsPipeline.js.map +1 -0
  203. package/build/services/SupportProfileSync.d.ts +22 -0
  204. package/build/services/SupportProfileSync.d.ts.map +1 -0
  205. package/build/services/SupportProfileSync.js +213 -0
  206. package/build/services/SupportProfileSync.js.map +1 -0
  207. package/build/services/TranscriptionRouting.d.ts +12 -0
  208. package/build/services/TranscriptionRouting.d.ts.map +1 -0
  209. package/build/services/TranscriptionRouting.js +24 -0
  210. package/build/services/TranscriptionRouting.js.map +1 -0
  211. package/build/services/TtsTextSanitizer.d.ts +5 -0
  212. package/build/services/TtsTextSanitizer.d.ts.map +1 -0
  213. package/build/services/TtsTextSanitizer.js +61 -0
  214. package/build/services/TtsTextSanitizer.js.map +1 -0
  215. package/build/services/WifiAdbRequest.d.ts +17 -0
  216. package/build/services/WifiAdbRequest.d.ts.map +1 -0
  217. package/build/services/WifiAdbRequest.js +35 -0
  218. package/build/services/WifiAdbRequest.js.map +1 -0
  219. package/build/services/asg/asgCameraApi.d.ts +43 -0
  220. package/build/services/asg/asgCameraApi.d.ts.map +1 -1
  221. package/build/services/asg/asgCameraApi.js +509 -26
  222. package/build/services/asg/asgCameraApi.js.map +1 -1
  223. package/build/services/asg/cameraRollExportCoordinator.d.ts +60 -0
  224. package/build/services/asg/cameraRollExportCoordinator.d.ts.map +1 -0
  225. package/build/services/asg/cameraRollExportCoordinator.js +618 -0
  226. package/build/services/asg/cameraRollExportCoordinator.js.map +1 -0
  227. package/build/services/asg/cameraRollExportLedger.d.ts +44 -0
  228. package/build/services/asg/cameraRollExportLedger.d.ts.map +1 -0
  229. package/build/services/asg/cameraRollExportLedger.js +78 -0
  230. package/build/services/asg/cameraRollExportLedger.js.map +1 -0
  231. package/build/services/asg/galleryNotices.d.ts +1 -1
  232. package/build/services/asg/galleryNotices.d.ts.map +1 -1
  233. package/build/services/asg/galleryNotices.js.map +1 -1
  234. package/build/services/asg/galleryPathMigration.d.ts +3 -0
  235. package/build/services/asg/galleryPathMigration.d.ts.map +1 -0
  236. package/build/services/asg/galleryPathMigration.js +26 -0
  237. package/build/services/asg/galleryPathMigration.js.map +1 -0
  238. package/build/services/asg/gallerySettingsService.d.ts +1 -0
  239. package/build/services/asg/gallerySettingsService.d.ts.map +1 -1
  240. package/build/services/asg/gallerySettingsService.js +2 -0
  241. package/build/services/asg/gallerySettingsService.js.map +1 -1
  242. package/build/services/asg/gallerySyncService.d.ts.map +1 -1
  243. package/build/services/asg/gallerySyncService.js +162 -56
  244. package/build/services/asg/gallerySyncService.js.map +1 -1
  245. package/build/services/asg/galleryTransferLedger.d.ts +58 -0
  246. package/build/services/asg/galleryTransferLedger.d.ts.map +1 -0
  247. package/build/services/asg/galleryTransferLedger.js +225 -0
  248. package/build/services/asg/galleryTransferLedger.js.map +1 -0
  249. package/build/services/asg/localNetworkTransport.d.ts +15 -0
  250. package/build/services/asg/localNetworkTransport.d.ts.map +1 -0
  251. package/build/services/asg/localNetworkTransport.js +144 -0
  252. package/build/services/asg/localNetworkTransport.js.map +1 -0
  253. package/build/services/asg/localStorageService.d.ts +18 -0
  254. package/build/services/asg/localStorageService.d.ts.map +1 -1
  255. package/build/services/asg/localStorageService.js +168 -19
  256. package/build/services/asg/localStorageService.js.map +1 -1
  257. package/build/services/asg/mediaProcessingQueue.d.ts +10 -0
  258. package/build/services/asg/mediaProcessingQueue.d.ts.map +1 -1
  259. package/build/services/asg/mediaProcessingQueue.js +193 -62
  260. package/build/services/asg/mediaProcessingQueue.js.map +1 -1
  261. package/build/services/audioPlaybackAssets.d.ts +8 -0
  262. package/build/services/audioPlaybackAssets.d.ts.map +1 -0
  263. package/build/services/audioPlaybackAssets.js +7 -0
  264. package/build/services/audioPlaybackAssets.js.map +1 -0
  265. package/build/services/gallerySyncClock.js +1 -1
  266. package/build/services/gallerySyncClock.js.map +1 -1
  267. package/build/services/glassesClockSync.js +2 -2
  268. package/build/services/glassesClockSync.js.map +1 -1
  269. package/build/services/manifestActions.d.ts +7 -0
  270. package/build/services/manifestActions.d.ts.map +1 -0
  271. package/build/services/manifestActions.js +25 -0
  272. package/build/services/manifestActions.js.map +1 -0
  273. package/build/services/otaDisplayState.d.ts +22 -11
  274. package/build/services/otaDisplayState.d.ts.map +1 -1
  275. package/build/services/otaDisplayState.js +36 -14
  276. package/build/services/otaDisplayState.js.map +1 -1
  277. package/build/services/otaInstallPolicy.d.ts +15 -3
  278. package/build/services/otaInstallPolicy.d.ts.map +1 -1
  279. package/build/services/otaInstallPolicy.js +24 -3
  280. package/build/services/otaInstallPolicy.js.map +1 -1
  281. package/build/services/otaManifestUrl.d.ts +2 -0
  282. package/build/services/otaManifestUrl.d.ts.map +1 -0
  283. package/build/services/{asgOtaVersionUrl.js → otaManifestUrl.js} +15 -5
  284. package/build/services/otaManifestUrl.js.map +1 -0
  285. package/build/services/slimStreamStatus.d.ts +11 -0
  286. package/build/services/slimStreamStatus.d.ts.map +1 -1
  287. package/build/services/slimStreamStatus.js +14 -4
  288. package/build/services/slimStreamStatus.js.map +1 -1
  289. package/build/stores/apps.d.ts.map +1 -1
  290. package/build/stores/apps.js +11 -0
  291. package/build/stores/apps.js.map +1 -1
  292. package/build/stores/bluetoothSettingKeys.d.ts.map +1 -1
  293. package/build/stores/bluetoothSettingKeys.js +2 -0
  294. package/build/stores/bluetoothSettingKeys.js.map +1 -1
  295. package/build/stores/gallerySync.d.ts +2 -0
  296. package/build/stores/gallerySync.d.ts.map +1 -1
  297. package/build/stores/gallerySync.js +2 -0
  298. package/build/stores/gallerySync.js.map +1 -1
  299. package/build/stores/glasses.d.ts.map +1 -1
  300. package/build/stores/glasses.js +3 -0
  301. package/build/stores/glasses.js.map +1 -1
  302. package/build/stores/settings.d.ts +17 -0
  303. package/build/stores/settings.d.ts.map +1 -1
  304. package/build/stores/settings.js +157 -17
  305. package/build/stores/settings.js.map +1 -1
  306. package/build/types/applet.d.ts +2 -0
  307. package/build/types/applet.d.ts.map +1 -1
  308. package/build/types/applet.js.map +1 -1
  309. package/build/types/asg.d.ts +7 -0
  310. package/build/types/asg.d.ts.map +1 -1
  311. package/build/types/asg.js.map +1 -1
  312. package/build/types/enums.d.ts +1 -0
  313. package/build/types/enums.d.ts.map +1 -1
  314. package/build/types/enums.js +1 -0
  315. package/build/types/enums.js.map +1 -1
  316. package/build/types/hardware.d.ts +1 -0
  317. package/build/types/hardware.d.ts.map +1 -1
  318. package/build/types/hardware.js +14 -0
  319. package/build/types/hardware.js.map +1 -1
  320. package/build/utils/configuredDevHost.d.ts +6 -0
  321. package/build/utils/configuredDevHost.d.ts.map +1 -0
  322. package/build/utils/configuredDevHost.js +19 -0
  323. package/build/utils/configuredDevHost.js.map +1 -0
  324. package/build/utils/devMiniappLaunch.d.ts +19 -8
  325. package/build/utils/devMiniappLaunch.d.ts.map +1 -1
  326. package/build/utils/devMiniappLaunch.js +144 -21
  327. package/build/utils/devMiniappLaunch.js.map +1 -1
  328. package/build/utils/diagnosticContext.d.ts.map +1 -1
  329. package/build/utils/diagnosticContext.js +15 -17
  330. package/build/utils/diagnosticContext.js.map +1 -1
  331. package/build/utils/hardware/hardware.d.ts.map +1 -1
  332. package/build/utils/hardware/hardware.js +7 -3
  333. package/build/utils/hardware/hardware.js.map +1 -1
  334. package/build/utils/miniappDiagnostics.d.ts +64 -0
  335. package/build/utils/miniappDiagnostics.d.ts.map +1 -0
  336. package/build/utils/miniappDiagnostics.js +173 -0
  337. package/build/utils/miniappDiagnostics.js.map +1 -0
  338. package/build/utils/permissions/MediaLibraryPermissions.d.ts +11 -0
  339. package/build/utils/permissions/MediaLibraryPermissions.d.ts.map +1 -1
  340. package/build/utils/permissions/MediaLibraryPermissions.js +27 -4
  341. package/build/utils/permissions/MediaLibraryPermissions.js.map +1 -1
  342. package/build/utils/timers/timers.d.ts.map +1 -1
  343. package/build/utils/timers/timers.js +55 -18
  344. package/build/utils/timers/timers.js.map +1 -1
  345. package/package.json +25 -15
  346. package/src/assets/silence.wav +0 -0
  347. package/src/engine.ts +7 -2
  348. package/src/facades/dev.ts +17 -12
  349. package/src/facades/gallery.ts +1 -1
  350. package/src/facades/glasses.ts +17 -4
  351. package/src/facades/glassesSettings.ts +5 -0
  352. package/src/facades/miniapps.ts +7 -0
  353. package/src/facades/ota.ts +19 -4
  354. package/src/facades/pairing.ts +11 -7
  355. package/src/facades/permissions.ts +14 -0
  356. package/src/facades/phoneNotifications.ts +4 -13
  357. package/src/index.ts +26 -2
  358. package/src/internal.ts +3 -1
  359. package/src/react/MentraLiveOtaFlow.tsx +856 -0
  360. package/src/react/index.ts +7 -0
  361. package/src/react/useEngineSnapshot.ts +16 -0
  362. package/src/runtime/bootstrap.ts +33 -1
  363. package/src/runtime/scanQrSeam.ts +20 -0
  364. package/src/services/AppRegistry.ts +224 -149
  365. package/src/services/AudioCloudUplink.ts +23 -1
  366. package/src/services/AudioPlaybackService.ts +287 -38
  367. package/src/services/BlobStore.ts +22 -9
  368. package/src/services/CloudAudioSubscriptionSync.ts +13 -1
  369. package/src/services/CloudClientService.ts +96 -1
  370. package/src/services/DeviceEventRouter.ts +5 -5
  371. package/src/services/ForegroundLocationPermission.ts +24 -0
  372. package/src/services/GlassesSettingsSync.ts +9 -3
  373. package/src/services/GlassesStatusProjection.ts +16 -8
  374. package/src/services/HotspotOtaTransport.ts +184 -0
  375. package/src/services/HotspotShutdown.ts +41 -0
  376. package/src/services/LocalDisplayManager.ts +112 -21
  377. package/src/services/LocalMiniappRuntime.ts +967 -346
  378. package/src/services/LocalMiniappStorage.ts +82 -0
  379. package/src/services/LocalMiniappUserIdentity.ts +46 -0
  380. package/src/services/LocalSttFallbackCoordinator.ts +19 -12
  381. package/src/services/MicStateCoordinator.ts +169 -4
  382. package/src/services/MiniappLauncher.ts +30 -3
  383. package/src/services/MiniappLiveness.ts +24 -0
  384. package/src/services/NativeHttpResponse.ts +6 -0
  385. package/src/services/OtaArtifactDownloader.ts +312 -0
  386. package/src/services/OtaAutoChain.ts +106 -0
  387. package/src/services/OtaErrorMapping.ts +80 -0
  388. package/src/services/OtaInstallCoordinator.ts +645 -143
  389. package/src/services/OtaService.ts +11 -5
  390. package/src/services/OtaUpdateCheckService.ts +180 -20
  391. package/src/services/PhoneCalendarHelpers.ts +212 -0
  392. package/src/services/PhoneCalendarService.ts +65 -0
  393. package/src/services/PhoneCameraFovCoordinator.ts +210 -0
  394. package/src/services/PhoneNotificationsSync.ts +12 -10
  395. package/src/services/PhonePhotoCoordinator.ts +116 -18
  396. package/src/services/PhoneStreamCoordinator.ts +139 -25
  397. package/src/services/PhoneVideoCoordinator.ts +11 -0
  398. package/src/services/SceneRenderer.ts +16 -0
  399. package/src/services/SentenceTtsPipeline.ts +106 -0
  400. package/src/services/SupportProfileSync.ts +218 -0
  401. package/src/services/TranscriptionRouting.ts +37 -0
  402. package/src/services/TtsTextSanitizer.ts +66 -0
  403. package/src/services/WifiAdbRequest.ts +53 -0
  404. package/src/services/asg/asgCameraApi.ts +615 -33
  405. package/src/services/asg/cameraRollExportCoordinator.ts +627 -0
  406. package/src/services/asg/cameraRollExportLedger.ts +122 -0
  407. package/src/services/asg/galleryNotices.ts +1 -0
  408. package/src/services/asg/galleryPathMigration.ts +22 -0
  409. package/src/services/asg/gallerySettingsService.ts +3 -0
  410. package/src/services/asg/gallerySyncService.ts +184 -73
  411. package/src/services/asg/galleryTransferLedger.ts +312 -0
  412. package/src/services/asg/localNetworkTransport.ts +169 -0
  413. package/src/services/asg/localStorageService.ts +179 -24
  414. package/src/services/asg/mediaProcessingQueue.ts +209 -62
  415. package/src/services/audioPlaybackAssets.ts +8 -0
  416. package/src/services/gallerySyncClock.ts +1 -1
  417. package/src/services/glassesClockSync.ts +2 -2
  418. package/src/services/manifestActions.ts +30 -0
  419. package/src/services/otaDisplayState.ts +57 -16
  420. package/src/services/otaInstallPolicy.ts +29 -3
  421. package/src/services/{asgOtaVersionUrl.ts → otaManifestUrl.ts} +16 -4
  422. package/src/services/slimStreamStatus.ts +16 -5
  423. package/src/stores/apps.ts +12 -0
  424. package/src/stores/bluetoothSettingKeys.ts +2 -0
  425. package/src/stores/gallerySync.ts +4 -0
  426. package/src/stores/glasses.ts +3 -0
  427. package/src/stores/settings.ts +179 -18
  428. package/src/types/applet.ts +2 -0
  429. package/src/types/asg.ts +7 -0
  430. package/src/types/enums.ts +2 -1
  431. package/src/types/hardware.ts +17 -0
  432. package/src/utils/configuredDevHost.ts +17 -0
  433. package/src/utils/devMiniappLaunch.ts +151 -23
  434. package/src/utils/diagnosticContext.ts +17 -17
  435. package/src/utils/hardware/hardware.ts +7 -9
  436. package/src/utils/miniappDiagnostics.ts +226 -0
  437. package/src/utils/permissions/MediaLibraryPermissions.ts +35 -4
  438. package/src/utils/timers/timers.ts +57 -21
  439. package/build/services/asgOtaVersionUrl.d.ts +0 -2
  440. package/build/services/asgOtaVersionUrl.d.ts.map +0 -1
  441. package/build/services/asgOtaVersionUrl.js.map +0 -1
@@ -11,11 +11,39 @@ import {BgTimer} from "../../utils/timers"
11
11
  import {localStorageService} from "./localStorageService"
12
12
  import {validateDownloadedMediaFile} from "./galleryMediaValidation"
13
13
  import {reportInvalidGalleryMedia} from "./GalleryMediaIntegrityReportService"
14
+ import {galleryTransferLedger} from "./galleryTransferLedger"
15
+ import {localNetworkTransport} from "./localNetworkTransport"
16
+
17
+ export interface GalleryCapabilities {
18
+ api_version: number
19
+ range_downloads: boolean
20
+ fixed_length_downloads: boolean
21
+ etag: boolean
22
+ sha256: boolean
23
+ recoverable_trash: boolean
24
+ idempotent_ack: boolean
25
+ selected_capture?: boolean
26
+ recommended_segment_bytes?: number
27
+ max_manifest_page_size?: number
28
+ }
29
+
30
+ type V3ManifestResponse = {
31
+ status: string
32
+ data: {
33
+ api_version: 3
34
+ captures: CaptureGroup[]
35
+ has_more: boolean
36
+ next_cursor?: string | null
37
+ total_count: number
38
+ server_time: number
39
+ }
40
+ }
14
41
 
15
42
  export class AsgCameraApiClient {
16
43
  private baseUrl: string
17
44
  private port: number
18
45
  private lastRequestTime: number = 0
46
+ private galleryCapabilities: GalleryCapabilities | null | undefined
19
47
 
20
48
  constructor(serverUrl?: string, port: number = 8089) {
21
49
  this.port = port
@@ -44,6 +72,7 @@ export class AsgCameraApiClient {
44
72
  if (this.baseUrl !== newUrl) {
45
73
  this.baseUrl = newUrl
46
74
  this.port = newPort
75
+ this.galleryCapabilities = undefined
47
76
  // console.log(`[ASG Camera API] Server changed from ${oldUrl} to ${this.baseUrl}`)
48
77
  } else {
49
78
  // console.log(`[ASG Camera API] Server URL unchanged: ${this.baseUrl}`)
@@ -60,10 +89,9 @@ export class AsgCameraApiClient {
60
89
  /**
61
90
  * Rate limiting helper - ensures minimum delay between requests
62
91
  */
63
- private async rateLimit(): Promise<void> {
92
+ private async rateLimit(minDelay: number = 500): Promise<void> {
64
93
  const now = Date.now()
65
94
  const timeSinceLastRequest = now - this.lastRequestTime
66
- const minDelay = 500 // 500ms minimum delay between requests
67
95
 
68
96
  if (timeSinceLastRequest < minDelay) {
69
97
  const delay = minDelay - timeSinceLastRequest
@@ -77,7 +105,12 @@ export class AsgCameraApiClient {
77
105
  /**
78
106
  * Make a request to the ASG Camera Server with rate limiting and retry logic
79
107
  */
80
- private async makeRequest<T>(endpoint: string, options?: RequestInit, retries: number = 5): Promise<T> {
108
+ private async makeRequest<T>(
109
+ endpoint: string,
110
+ options?: RequestInit,
111
+ retries: number = 5,
112
+ writeIntervalMs: number = 500,
113
+ ): Promise<T> {
81
114
  const url = `${this.baseUrl}${endpoint}`
82
115
  const method = options?.method || "GET"
83
116
 
@@ -96,8 +129,8 @@ export class AsgCameraApiClient {
96
129
 
97
130
  try {
98
131
  // Apply rate limiting only for non-GET requests
99
- if (method !== "GET") {
100
- await this.rateLimit()
132
+ if (method !== "GET" && writeIntervalMs > 0) {
133
+ await this.rateLimit(writeIntervalMs)
101
134
  }
102
135
 
103
136
  // Prepare headers - don't set Content-Type for GET requests
@@ -116,7 +149,7 @@ export class AsgCameraApiClient {
116
149
  console.log(`[ASG Camera API] Headers being sent:`, headers)
117
150
 
118
151
  // N4: Add 30s timeout to all fetch calls in makeRequest
119
- const response = await fetch(url, {
152
+ const response = await localNetworkTransport.fetch(url, {
120
153
  headers,
121
154
  ...options,
122
155
  signal: options?.signal || this.createTimeoutSignal(30000),
@@ -140,7 +173,7 @@ export class AsgCameraApiClient {
140
173
  const retryDelay = Math.min(Math.pow(2, 6 - retries) * 1000, 10000)
141
174
  console.log(`[ASG Camera API] Rate limited, retrying in ${retryDelay}ms (${retries} retries left)`)
142
175
  await new Promise<void>((resolve) => BgTimer.setTimeout(() => resolve(), retryDelay))
143
- return this.makeRequest<T>(endpoint, options, retries - 1)
176
+ return this.makeRequest<T>(endpoint, options, retries - 1, writeIntervalMs)
144
177
  }
145
178
 
146
179
  throw new Error(`HTTP ${response.status}: ${response.statusText}`)
@@ -152,7 +185,12 @@ export class AsgCameraApiClient {
152
185
 
153
186
  if (contentType?.includes("application/json")) {
154
187
  const data = await response.json()
155
- console.log(`[ASG Camera API] JSON Response received:`, data)
188
+ console.log(`[ASG Camera API] JSON response received`, {
189
+ endpoint,
190
+ status: data?.status,
191
+ apiVersion: data?.data?.api_version,
192
+ captureCount: data?.data?.captures?.length,
193
+ })
156
194
  return data
157
195
  } else if (contentType?.includes("image/") || contentType?.includes("application/octet-stream")) {
158
196
  // For image responses and binary data (including AVIF), return the blob
@@ -250,7 +288,7 @@ export class AsgCameraApiClient {
250
288
  // Use browser-like headers since we know the browser works
251
289
  try {
252
290
  console.log(`[ASG Camera API] Making direct fetch to gallery endpoint`)
253
- const response = await fetch(galleryUrl, {
291
+ const response = await localNetworkTransport.fetch(galleryUrl, {
254
292
  method: "GET",
255
293
  headers: {
256
294
  "Accept": "application/json",
@@ -390,7 +428,7 @@ export class AsgCameraApiClient {
390
428
  for (const endpoint of testEndpoints) {
391
429
  try {
392
430
  console.log(`[ASG Camera API] Testing endpoint: ${endpoint}`)
393
- const response = await fetch(`${this.baseUrl}${endpoint}`, {
431
+ const response = await localNetworkTransport.fetch(`${this.baseUrl}${endpoint}`, {
394
432
  method: "HEAD",
395
433
  headers: {
396
434
  "Accept": "*/*",
@@ -411,7 +449,7 @@ export class AsgCameraApiClient {
411
449
  if (endpoint === "/api/gallery") {
412
450
  try {
413
451
  console.log(`[ASG Camera API] Trying GET request for /api/gallery...`)
414
- const getResponse = await fetch(`${this.baseUrl}${endpoint}`, {
452
+ const getResponse = await localNetworkTransport.fetch(`${this.baseUrl}${endpoint}`, {
415
453
  method: "GET",
416
454
  headers: {
417
455
  "Accept": "application/json",
@@ -515,7 +553,7 @@ export class AsgCameraApiClient {
515
553
  const controller = new AbortController()
516
554
  const timeoutId = BgTimer.setTimeout(() => controller.abort(), 3000) // 3 second timeout
517
555
 
518
- const response = await fetch(`${this.baseUrl}/api/health`, {
556
+ const response = await localNetworkTransport.fetch(`${this.baseUrl}/api/health`, {
519
557
  method: "HEAD",
520
558
  signal: controller.signal,
521
559
  })
@@ -602,6 +640,104 @@ export class AsgCameraApiClient {
602
640
  }
603
641
  }
604
642
 
643
+ /** Probe the additive gallery protocol. A 404 cleanly falls back to legacy v2. */
644
+ async getGalleryCapabilities(): Promise<GalleryCapabilities | null> {
645
+ if (this.galleryCapabilities !== undefined) return this.galleryCapabilities
646
+ try {
647
+ const response = await localNetworkTransport.fetch(`${this.baseUrl}/api/v3/capabilities`, {
648
+ headers: {"Accept": "application/json", "User-Agent": "MentraOS-Mobile/1.0"},
649
+ signal: this.createTimeoutSignal(5000),
650
+ })
651
+ if (response.status === 404) {
652
+ this.galleryCapabilities = null
653
+ return null
654
+ }
655
+ if (!response.ok) throw new Error(`HTTP ${response.status}`)
656
+ const payload = await response.json()
657
+ const capabilities = (payload.data || payload) as GalleryCapabilities
658
+ this.galleryCapabilities = capabilities.api_version >= 3 ? capabilities : null
659
+ return this.galleryCapabilities
660
+ } catch (error) {
661
+ // A failed probe must not break old firmware. Do not permanently cache transient failures.
662
+ console.warn("[ASG Camera API] v3 capability probe failed; using legacy sync for this attempt", error)
663
+ return null
664
+ }
665
+ }
666
+
667
+ /** Fetch all lightweight v3 manifest pages, newest first. */
668
+ async getV3Manifest(): Promise<V3ManifestResponse["data"] | null> {
669
+ const capabilities = await this.getGalleryCapabilities()
670
+ if (!capabilities) return null
671
+
672
+ const captures: CaptureGroup[] = []
673
+ let cursor: string | undefined
674
+ let serverTime = Date.now()
675
+ let totalCount = 0
676
+ do {
677
+ const params = new URLSearchParams({limit: String(capabilities.max_manifest_page_size || 100)})
678
+ if (cursor) params.set("cursor", cursor)
679
+ const response = await this.makeRequest<V3ManifestResponse>(`/api/v3/manifest?${params.toString()}`)
680
+ const data = response.data || (response as unknown as V3ManifestResponse["data"])
681
+ captures.push(...(data.captures || []))
682
+ serverTime = data.server_time || serverTime
683
+ totalCount = data.total_count || captures.length
684
+ cursor = data.has_more && data.next_cursor ? data.next_cursor : undefined
685
+ } while (cursor)
686
+
687
+ return {
688
+ api_version: 3,
689
+ captures,
690
+ has_more: false,
691
+ next_cursor: null,
692
+ total_count: totalCount,
693
+ server_time: serverTime,
694
+ }
695
+ }
696
+
697
+ async getV3FileHash(fileName: string): Promise<{sha256: string; etag: string; size: number}> {
698
+ const response = await localNetworkTransport.fetch(
699
+ `${this.baseUrl}/api/v3/hash?file=${encodeURIComponent(fileName)}`,
700
+ {
701
+ headers: {"Accept": "application/json", "User-Agent": "MentraOS-Mobile/1.0"},
702
+ signal: this.createTimeoutSignal(10 * 60 * 1000),
703
+ },
704
+ 10 * 60 * 1000,
705
+ )
706
+ if (!response.ok) throw new Error(`Hash request failed: HTTP ${response.status}`)
707
+ const payload = await response.json()
708
+ return (payload.data || payload) as {sha256: string; etag: string; size: number}
709
+ }
710
+
711
+ async acknowledgeCapture(captureId: string, ackId: string): Promise<void> {
712
+ const response = await this.makeRequest<any>(
713
+ "/api/v3/ack",
714
+ {
715
+ method: "POST",
716
+ body: JSON.stringify({capture_id: captureId, ack_id: ackId}),
717
+ },
718
+ 5,
719
+ 0,
720
+ )
721
+ const data = response.data || response
722
+ if (!data.success && !data.already_trashed) {
723
+ throw new Error(data.message || `Glasses did not acknowledge ${captureId}`)
724
+ }
725
+ }
726
+
727
+ async restoreCapture(captureId: string): Promise<void> {
728
+ const response = await this.makeRequest<any>(
729
+ "/api/v3/restore",
730
+ {
731
+ method: "POST",
732
+ body: JSON.stringify({capture_id: captureId}),
733
+ },
734
+ 5,
735
+ 0,
736
+ )
737
+ const data = response.data || response
738
+ if (!data.success) throw new Error(data.message || `Glasses did not restore ${captureId}`)
739
+ }
740
+
605
741
  /**
606
742
  * Batch sync files from server with controlled concurrency.
607
743
  * Used by the legacy executeDownload path for old asg_client firmware
@@ -637,7 +773,9 @@ export class AsgCameraApiClient {
637
773
  for (let i = 0; i < files.length; i += CONCURRENCY_LIMIT) {
638
774
  const batch = files.slice(i, i + CONCURRENCY_LIMIT)
639
775
  console.log(
640
- `[ASG Camera API] Processing batch ${Math.floor(i / CONCURRENCY_LIMIT) + 1}/${Math.ceil(files.length / CONCURRENCY_LIMIT)}: ${batch.length} files`,
776
+ `[ASG Camera API] Processing batch ${Math.floor(i / CONCURRENCY_LIMIT) + 1}/${Math.ceil(
777
+ files.length / CONCURRENCY_LIMIT,
778
+ )}: ${batch.length} files`,
641
779
  )
642
780
 
643
781
  // Process batch in parallel
@@ -706,12 +844,6 @@ export class AsgCameraApiClient {
706
844
  results.failed.push(result.error)
707
845
  }
708
846
  }
709
-
710
- // Small delay between batches to prevent overwhelming the server
711
- if (i + CONCURRENCY_LIMIT < files.length) {
712
- console.log(`[ASG Camera API] Waiting 300ms between batches`)
713
- await new Promise<void>((resolve) => BgTimer.setTimeout(() => resolve(), 300))
714
- }
715
847
  }
716
848
 
717
849
  console.log(
@@ -724,11 +856,318 @@ export class AsgCameraApiClient {
724
856
  * Download all files in a capture group sequentially.
725
857
  * Reports aggregate byte progress across all files in the group.
726
858
  */
859
+ private async downloadResumableCaptureFile(
860
+ captureId: string,
861
+ file: CaptureGroup["files"][number],
862
+ localFilePath: string,
863
+ onProgress?: (bytesDownloaded: number) => void,
864
+ abortSignal?: AbortSignal,
865
+ ): Promise<void> {
866
+ const segmentBytes = 16 * 1024 * 1024
867
+ const segmentCount = Math.max(1, Math.ceil(file.size / segmentBytes))
868
+ const generation = (file.etag || `${file.size}`).replace(/[^A-Za-z0-9_-]/g, "").slice(0, 48) || "legacy"
869
+ const partPath = (index: number) => `${localFilePath}.partial.${generation}.${index}`
870
+ const assemblingPath = `${localFilePath}.assembling.${generation}`
871
+ const transferStartedAt = Date.now()
872
+ let networkDurationMs = 0
873
+ let networkBytes = 0
874
+
875
+ if (await RNFS.exists(localFilePath)) {
876
+ const existing = await RNFS.stat(localFilePath)
877
+ if (existing.size === file.size) {
878
+ if (file.etag) {
879
+ const remoteHash = file.sha256 ? {sha256: file.sha256, etag: file.etag} : await this.getV3FileHash(file.name)
880
+ if (remoteHash.etag && remoteHash.etag !== file.etag) {
881
+ throw new Error("Source generation changed before existing-file verification")
882
+ }
883
+ const localHash = await RNFS.hash(localFilePath, "sha256")
884
+ if (localHash.toLowerCase() === remoteHash.sha256.toLowerCase()) {
885
+ galleryTransferLedger.markFileHash(captureId, file.name, localHash.toLowerCase())
886
+ onProgress?.(file.size)
887
+ return
888
+ }
889
+ console.warn(`[ASG Camera API] Existing file failed SHA-256 verification; replacing ${file.name}`)
890
+ } else {
891
+ // Legacy servers have no generation/hash endpoint. Preserve old-client
892
+ // compatibility and let the media validator below reject corrupt data.
893
+ onProgress?.(file.size)
894
+ return
895
+ }
896
+ }
897
+ await RNFS.moveFile(localFilePath, `${localFilePath}.recovery.${Date.now()}`).catch(() => {})
898
+ }
899
+
900
+ let completedBytes = 0
901
+ let resumedSegmentCount = 0
902
+ for (let index = 0; index < segmentCount; index++) {
903
+ const start = index * segmentBytes
904
+ const expectedLength = Math.min(segmentBytes, file.size - start)
905
+ if (await RNFS.exists(partPath(index))) {
906
+ const stat = await RNFS.stat(partPath(index))
907
+ if (stat.size === expectedLength) {
908
+ completedBytes += expectedLength
909
+ resumedSegmentCount++
910
+ galleryTransferLedger.markSegmentComplete(captureId, file.name, index)
911
+ continue
912
+ }
913
+ await RNFS.unlink(partPath(index)).catch(() => {})
914
+ }
915
+
916
+ let completed = false
917
+ let lastError: unknown
918
+ for (let attempt = 0; attempt < 5 && !completed; attempt++) {
919
+ if (abortSignal?.aborted) throw new Error("Sync cancelled")
920
+ await RNFS.unlink(partPath(index)).catch(() => {})
921
+ const end = start + expectedLength - 1
922
+ let responseHeaders: Record<string, string> = {}
923
+ const {jobId, promise} = localNetworkTransport.downloadFile({
924
+ fromUrl: `${this.baseUrl}/api/download?file=${encodeURIComponent(file.name)}`,
925
+ toFile: partPath(index),
926
+ headers: {
927
+ "Accept": "*/*",
928
+ "User-Agent": "MentraOS-Mobile/1.0",
929
+ "Range": `bytes=${start}-${end}`,
930
+ ...(file.etag ? {"If-Range": file.etag} : {}),
931
+ },
932
+ connectionTimeout: 300000,
933
+ readTimeout: 300000,
934
+ backgroundTimeout: 24 * 60 * 60 * 1000,
935
+ progressDivider: 2,
936
+ progressInterval: 250,
937
+ begin: (response) => {
938
+ responseHeaders = response.headers || {}
939
+ },
940
+ progress: (progress) => onProgress?.(completedBytes + (progress.bytesWritten || 0)),
941
+ })
942
+
943
+ let abortPollTimer: number | undefined
944
+ if (abortSignal) {
945
+ abortPollTimer = BgTimer.setInterval(() => {
946
+ if (abortSignal.aborted) localNetworkTransport.stopDownload(jobId)
947
+ }, 500)
948
+ }
949
+
950
+ const attemptStartedAt = Date.now()
951
+ try {
952
+ const result = await promise
953
+ const attemptDurationMs = Math.max(1, Date.now() - attemptStartedAt)
954
+ networkDurationMs += attemptDurationMs
955
+ networkBytes += result.bytesWritten || 0
956
+ if (abortSignal?.aborted) throw new Error("Sync cancelled")
957
+ const header = (name: string): string | undefined => {
958
+ const key = Object.keys(responseHeaders).find((candidate) => candidate.toLowerCase() === name.toLowerCase())
959
+ return key ? responseHeaders[key] : undefined
960
+ }
961
+
962
+ if (result.statusCode === 200 && index === 0 && result.bytesWritten === file.size) {
963
+ // Older servers ignore Range. A full fixed/chunked response is still compatible;
964
+ // commit it through the same staging path and skip the remaining segments.
965
+ await RNFS.unlink(assemblingPath).catch(() => {})
966
+ await RNFS.moveFile(partPath(index), assemblingPath)
967
+ completedBytes = file.size
968
+ completed = true
969
+ break
970
+ }
971
+ if (result.statusCode !== 206) throw new Error(`HTTP ${result.statusCode}; expected 206`)
972
+ if (result.bytesWritten !== expectedLength) {
973
+ throw new Error(`Segment ${index} truncated: ${result.bytesWritten}/${expectedLength} bytes`)
974
+ }
975
+ const contentRange = header("content-range")
976
+ if (contentRange !== `bytes ${start}-${end}/${file.size}`) {
977
+ throw new Error(`Invalid Content-Range: ${contentRange || "missing"}`)
978
+ }
979
+ const responseEtag = header("etag")
980
+ if (file.etag && responseEtag && responseEtag !== file.etag) {
981
+ throw new Error("Source generation changed during transfer")
982
+ }
983
+ galleryTransferLedger.markSegmentComplete(captureId, file.name, index)
984
+ completedBytes += expectedLength
985
+ completed = true
986
+ console.log("[ASG Camera API] Gallery segment complete", {
987
+ captureId,
988
+ file: file.name,
989
+ segment: index,
990
+ segmentCount,
991
+ attempt: attempt + 1,
992
+ status: result.statusCode,
993
+ bytes: result.bytesWritten,
994
+ durationMs: attemptDurationMs,
995
+ throughputMbps: (((result.bytesWritten || 0) * 8) / attemptDurationMs / 1000).toFixed(2),
996
+ etag: file.etag,
997
+ })
998
+ } catch (error) {
999
+ lastError = error
1000
+ if (error instanceof Error && error.message === "Sync cancelled") throw error
1001
+ console.warn("[ASG Camera API] Gallery segment attempt failed", {
1002
+ captureId,
1003
+ file: file.name,
1004
+ segment: index,
1005
+ segmentCount,
1006
+ attempt: attempt + 1,
1007
+ rangeStart: start,
1008
+ rangeEnd: end,
1009
+ message: error instanceof Error ? error.message : String(error),
1010
+ })
1011
+ await RNFS.unlink(partPath(index)).catch(() => {})
1012
+ if (attempt < 4) {
1013
+ const delay = Math.min(1000 * 2 ** attempt, 10000) + Math.floor(Math.random() * 500)
1014
+ await new Promise<void>((resolve) => BgTimer.setTimeout(resolve, delay))
1015
+ }
1016
+ } finally {
1017
+ if (abortPollTimer !== undefined) BgTimer.clearInterval(abortPollTimer)
1018
+ }
1019
+ }
1020
+ if (!completed) throw lastError || new Error(`Segment ${index} failed`)
1021
+ if (completedBytes === file.size && (await RNFS.exists(assemblingPath))) break
1022
+ }
1023
+
1024
+ if (!(await RNFS.exists(assemblingPath))) {
1025
+ await RNFS.unlink(assemblingPath).catch(() => {})
1026
+ await RNFS.writeFile(assemblingPath, "", "utf8")
1027
+ for (let index = 0; index < segmentCount; index++) {
1028
+ const data = await RNFS.readFile(partPath(index), "base64")
1029
+ await RNFS.appendFile(assemblingPath, data, "base64")
1030
+ }
1031
+ }
1032
+
1033
+ const assembledStat = await RNFS.stat(assemblingPath)
1034
+ if (assembledStat.size !== file.size) {
1035
+ await RNFS.moveFile(assemblingPath, `${assemblingPath}.invalid.${Date.now()}`).catch(() => {})
1036
+ throw new Error(`Assembled file has ${assembledStat.size}/${file.size} bytes`)
1037
+ }
1038
+
1039
+ const verificationStartedAt = Date.now()
1040
+ const capabilities = await this.getGalleryCapabilities()
1041
+ if (capabilities?.sha256) {
1042
+ const remoteHash = file.sha256 ? {sha256: file.sha256, etag: file.etag} : await this.getV3FileHash(file.name)
1043
+ if (file.etag && remoteHash.etag && file.etag !== remoteHash.etag) {
1044
+ throw new Error("Source generation changed before integrity verification")
1045
+ }
1046
+ const localHash = await RNFS.hash(assemblingPath, "sha256")
1047
+ if (localHash.toLowerCase() !== remoteHash.sha256.toLowerCase()) {
1048
+ await RNFS.moveFile(assemblingPath, `${assemblingPath}.invalid.${Date.now()}`).catch(() => {})
1049
+ throw new Error(`SHA-256 mismatch for ${file.name}`)
1050
+ }
1051
+ galleryTransferLedger.markFileHash(captureId, file.name, localHash.toLowerCase())
1052
+ }
1053
+
1054
+ await RNFS.moveFile(assemblingPath, localFilePath)
1055
+ for (let index = 0; index < segmentCount; index++) await RNFS.unlink(partPath(index)).catch(() => {})
1056
+ console.log("[ASG Camera API] Gallery file committed", {
1057
+ captureId,
1058
+ file: file.name,
1059
+ bytes: file.size,
1060
+ segments: segmentCount,
1061
+ networkDurationMs,
1062
+ networkMbps: networkDurationMs > 0 ? ((networkBytes * 8) / networkDurationMs / 1000).toFixed(2) : "0.00",
1063
+ verificationDurationMs: Date.now() - verificationStartedAt,
1064
+ totalDurationMs: Date.now() - transferStartedAt,
1065
+ resumedSegments: resumedSegmentCount,
1066
+ })
1067
+ onProgress?.(file.size)
1068
+ }
1069
+
727
1070
  async downloadCapture(
728
1071
  capture: CaptureGroup,
729
1072
  onProgress?: (bytesDownloaded: number, totalBytes: number) => void,
730
1073
  abortSignal?: AbortSignal,
731
- ): Promise<{captureDir: string; primaryPath: string; bracketPaths: string[]; sidecarPath?: string}> {
1074
+ ): Promise<{
1075
+ captureDir: string
1076
+ primaryPath: string
1077
+ bracketPaths: string[]
1078
+ sidecarPath?: string
1079
+ thumbnailPath?: string
1080
+ }> {
1081
+ if (capture.files.some((file) => !file.size || file.size <= 0)) {
1082
+ // Preserve support for malformed manifests from pre-v2 development firmware.
1083
+ return this.downloadCaptureLegacyUnsafe(capture, onProgress, abortSignal)
1084
+ }
1085
+ const captureDir = localStorageService.getPhotoFilePath(capture.capture_id)
1086
+ if (!(await RNFS.exists(captureDir))) await RNFS.mkdir(captureDir)
1087
+ const capabilities = await this.getGalleryCapabilities()
1088
+ galleryTransferLedger.ensureCapture(capture, capabilities?.api_version || 2)
1089
+ galleryTransferLedger.transition(capture.capture_id, "TRANSFERRING")
1090
+
1091
+ const sortedFiles = [...capture.files].sort((a, b) => {
1092
+ const order = {bracket: 0, primary: 1, sidecar: 2}
1093
+ return (order[a.role] ?? 1) - (order[b.role] ?? 1)
1094
+ })
1095
+ let completedCaptureBytes = 0
1096
+ let primaryPath = ""
1097
+ const bracketPaths: string[] = []
1098
+ let sidecarPath: string | undefined
1099
+
1100
+ try {
1101
+ for (const file of sortedFiles) {
1102
+ const leafName = file.name.includes("/") ? file.name.split("/").pop()! : file.name
1103
+ const localFilePath = `${captureDir}/${leafName}`
1104
+ await this.downloadResumableCaptureFile(
1105
+ capture.capture_id,
1106
+ file,
1107
+ localFilePath,
1108
+ (fileBytes) => onProgress?.(completedCaptureBytes + fileBytes, capture.total_size),
1109
+ abortSignal,
1110
+ )
1111
+
1112
+ const isVideo = !!file.name.match(/\.(mp4|mov|avi|webm|mkv)$/i)
1113
+ const mediaKind = file.role === "sidecar" ? "unknown" : isVideo ? "video" : "photo"
1114
+ try {
1115
+ await validateDownloadedMediaFile({
1116
+ path: localFilePath,
1117
+ name: file.name,
1118
+ expectedSize: file.size,
1119
+ mediaKind,
1120
+ })
1121
+ } catch (validationError: any) {
1122
+ reportInvalidGalleryMedia({
1123
+ name: file.name,
1124
+ path: localFilePath,
1125
+ mediaKind,
1126
+ stage: "download_capture_validation",
1127
+ reason: validationError?.message || String(validationError),
1128
+ expectedSize: file.size,
1129
+ captureId: capture.capture_id,
1130
+ duration: capture.duration,
1131
+ })
1132
+ await RNFS.moveFile(localFilePath, `${localFilePath}.invalid.${Date.now()}`).catch(() => {})
1133
+ throw validationError
1134
+ }
1135
+
1136
+ completedCaptureBytes += file.size
1137
+ if (file.role === "primary") primaryPath = localFilePath
1138
+ else if (file.role === "bracket") bracketPaths.push(localFilePath)
1139
+ else if (file.role === "sidecar") sidecarPath = localFilePath
1140
+ }
1141
+ if (!primaryPath && sortedFiles.length > 0) {
1142
+ const leafName = sortedFiles[0].name.includes("/") ? sortedFiles[0].name.split("/").pop()! : sortedFiles[0].name
1143
+ primaryPath = `${captureDir}/${leafName}`
1144
+ }
1145
+ const primaryFile = sortedFiles.find((file) => file.role === "primary") || sortedFiles[0]
1146
+ const thumbnailPath =
1147
+ capture.type === "video" && primaryFile
1148
+ ? await this.downloadVideoThumbnail(primaryFile.name, `${captureDir}/.thumb.jpg`, abortSignal)
1149
+ : undefined
1150
+ galleryTransferLedger.transition(capture.capture_id, "VERIFIED")
1151
+ onProgress?.(capture.total_size, capture.total_size)
1152
+ return {captureDir, primaryPath, bracketPaths, sidecarPath, thumbnailPath}
1153
+ } catch (error) {
1154
+ galleryTransferLedger.recordFailure(capture.capture_id, error)
1155
+ throw error
1156
+ }
1157
+ }
1158
+
1159
+ /** Previous direct-to-final implementation kept for binary-source compatibility only. */
1160
+ private async downloadCaptureLegacyUnsafe(
1161
+ capture: CaptureGroup,
1162
+ onProgress?: (bytesDownloaded: number, totalBytes: number) => void,
1163
+ abortSignal?: AbortSignal,
1164
+ ): Promise<{
1165
+ captureDir: string
1166
+ primaryPath: string
1167
+ bracketPaths: string[]
1168
+ sidecarPath?: string
1169
+ thumbnailPath?: string
1170
+ }> {
732
1171
  const captureDir = localStorageService.getPhotoFilePath(capture.capture_id)
733
1172
  console.log(
734
1173
  `[ASG Camera API] downloadCapture: ${capture.capture_id} (${capture.files.length} files) -> ${captureDir}`,
@@ -769,7 +1208,7 @@ export class AsgCameraApiClient {
769
1208
  try {
770
1209
  // Throttle progress: only fire when bytesWritten changes meaningfully
771
1210
  let lastReportedBytes = -1
772
- const {jobId, promise: dlPromise} = RNFS.downloadFile({
1211
+ const {jobId, promise: dlPromise} = localNetworkTransport.downloadFile({
773
1212
  fromUrl: downloadUrl,
774
1213
  toFile: localFilePath,
775
1214
  headers: {
@@ -794,12 +1233,12 @@ export class AsgCameraApiClient {
794
1233
  let abortPollTimer: number | undefined
795
1234
  if (abortSignal) {
796
1235
  if (abortSignal.aborted) {
797
- RNFS.stopDownload(jobId)
1236
+ localNetworkTransport.stopDownload(jobId)
798
1237
  throw new Error("Sync cancelled")
799
1238
  }
800
1239
  abortPollTimer = BgTimer.setInterval(() => {
801
1240
  if (abortSignal.aborted) {
802
- RNFS.stopDownload(jobId)
1241
+ localNetworkTransport.stopDownload(jobId)
803
1242
  }
804
1243
  }, 500)
805
1244
  }
@@ -878,7 +1317,76 @@ export class AsgCameraApiClient {
878
1317
  onProgress(totalBytes, totalBytes)
879
1318
  }
880
1319
 
881
- return {captureDir, primaryPath, bracketPaths, sidecarPath}
1320
+ const primaryFile = sortedFiles.find((file) => file.role === "primary") || sortedFiles[0]
1321
+ const thumbnailPath =
1322
+ capture.type === "video" && primaryFile
1323
+ ? await this.downloadVideoThumbnail(primaryFile.name, `${captureDir}/.thumb.jpg`, abortSignal)
1324
+ : undefined
1325
+
1326
+ return {captureDir, primaryPath, bracketPaths, sidecarPath, thumbnailPath}
1327
+ }
1328
+
1329
+ /** Download an on-demand video thumbnail while the source capture is still on the glasses. */
1330
+ private async downloadVideoThumbnail(
1331
+ sourceFileName: string,
1332
+ thumbnailPath: string,
1333
+ abortSignal?: AbortSignal,
1334
+ ): Promise<string | undefined> {
1335
+ const partialPath = `${thumbnailPath}.partial`
1336
+ const parentDir = thumbnailPath.substring(0, thumbnailPath.lastIndexOf("/"))
1337
+
1338
+ try {
1339
+ if (parentDir && !(await RNFS.exists(parentDir))) {
1340
+ await RNFS.mkdir(parentDir)
1341
+ }
1342
+ await RNFS.unlink(partialPath).catch(() => {})
1343
+ const {jobId, promise} = localNetworkTransport.downloadFile({
1344
+ fromUrl: `${this.baseUrl}/api/photo?file=${encodeURIComponent(sourceFileName)}`,
1345
+ toFile: partialPath,
1346
+ headers: {
1347
+ "Accept": "image/jpeg",
1348
+ "User-Agent": "MentraOS-Mobile/1.0",
1349
+ },
1350
+ connectionTimeout: 60000,
1351
+ readTimeout: 60000,
1352
+ progressDivider: 10,
1353
+ })
1354
+
1355
+ let abortPollTimer: number | undefined
1356
+ if (abortSignal) {
1357
+ if (abortSignal.aborted) {
1358
+ localNetworkTransport.stopDownload(jobId)
1359
+ throw new Error("Sync cancelled")
1360
+ }
1361
+ abortPollTimer = BgTimer.setInterval(() => {
1362
+ if (abortSignal.aborted) localNetworkTransport.stopDownload(jobId)
1363
+ }, 500)
1364
+ }
1365
+
1366
+ let result
1367
+ try {
1368
+ result = await promise
1369
+ } finally {
1370
+ if (abortPollTimer !== undefined) BgTimer.clearInterval(abortPollTimer)
1371
+ }
1372
+
1373
+ if (abortSignal?.aborted) throw new Error("Sync cancelled")
1374
+ if (result.statusCode !== 200 || result.bytesWritten <= 0) {
1375
+ throw new Error(`HTTP ${result.statusCode}, ${result.bytesWritten} bytes`)
1376
+ }
1377
+
1378
+ await RNFS.unlink(thumbnailPath).catch(() => {})
1379
+ await RNFS.moveFile(partialPath, thumbnailPath)
1380
+ console.log(`[ASG Camera API] Downloaded video thumbnail for ${sourceFileName}`)
1381
+ return thumbnailPath
1382
+ } catch (error) {
1383
+ await RNFS.unlink(partialPath).catch(() => {})
1384
+ if (abortSignal?.aborted || (error instanceof Error && error.message === "Sync cancelled")) {
1385
+ throw new Error("Sync cancelled")
1386
+ }
1387
+ console.warn(`[ASG Camera API] Failed to download video thumbnail for ${sourceFileName}:`, error)
1388
+ return undefined
1389
+ }
882
1390
  }
883
1391
 
884
1392
  /**
@@ -893,10 +1401,15 @@ export class AsgCameraApiClient {
893
1401
  }
894
1402
 
895
1403
  try {
896
- const response = await this.makeRequest("/api/delete-files", {
897
- method: "POST",
898
- body: JSON.stringify({files: fileNames}),
899
- })
1404
+ const response = await this.makeRequest(
1405
+ "/api/delete-files",
1406
+ {
1407
+ method: "POST",
1408
+ body: JSON.stringify({files: fileNames}),
1409
+ },
1410
+ 5,
1411
+ 0,
1412
+ )
900
1413
 
901
1414
  // Parse the response format from the ASG server
902
1415
  const responseData = response as any
@@ -954,6 +1467,73 @@ export class AsgCameraApiClient {
954
1467
  onProgress?: (progress: number) => void,
955
1468
  abortSignal?: AbortSignal,
956
1469
  expectedSize?: number,
1470
+ ): Promise<{filePath: string; thumbnailPath?: string; mime_type: string}> {
1471
+ // The legacy public shape did not require a size. Keep that rare call compatible while all
1472
+ // sync manifests use the verified resumable path below.
1473
+ if (!expectedSize || expectedSize <= 0) {
1474
+ return this.downloadFileLegacyUnsafe(filename, includeThumbnail, onProgress, abortSignal, expectedSize)
1475
+ }
1476
+
1477
+ const localFilePath = localStorageService.getPhotoFilePath(filename)
1478
+ const parentDir = localFilePath.substring(0, localFilePath.lastIndexOf("/"))
1479
+ if (!(await RNFS.exists(parentDir))) await RNFS.mkdir(parentDir)
1480
+ const lowerName = filename.toLowerCase()
1481
+ const isVideo = /\.(mp4|mov|avi|webm|mkv)$/.test(lowerName)
1482
+ const captureId = filename.includes("/")
1483
+ ? filename.substring(0, filename.indexOf("/"))
1484
+ : filename.replace(/\.imu\.json$/i, "").replace(/\.[^.]+$/, "")
1485
+ const capture: CaptureGroup = {
1486
+ capture_id: captureId,
1487
+ type: isVideo ? "video" : "photo",
1488
+ timestamp: Date.now(),
1489
+ total_size: expectedSize,
1490
+ files: [{name: filename, size: expectedSize, role: "primary"}],
1491
+ }
1492
+ const capabilities = await this.getGalleryCapabilities()
1493
+ galleryTransferLedger.ensureCapture(capture, capabilities?.api_version || 1)
1494
+ galleryTransferLedger.transition(captureId, "TRANSFERRING")
1495
+ try {
1496
+ await this.downloadResumableCaptureFile(
1497
+ captureId,
1498
+ capture.files[0],
1499
+ localFilePath,
1500
+ (bytes) => onProgress?.(Math.min(100, Math.round((bytes / expectedSize) * 100))),
1501
+ abortSignal,
1502
+ )
1503
+ await validateDownloadedMediaFile({
1504
+ path: localFilePath,
1505
+ name: filename,
1506
+ expectedSize,
1507
+ mediaKind: isVideo ? "video" : "photo",
1508
+ })
1509
+ galleryTransferLedger.transition(captureId, "VERIFIED")
1510
+ const thumbnailPath =
1511
+ includeThumbnail && isVideo
1512
+ ? await this.downloadVideoThumbnail(filename, localStorageService.getThumbnailFilePath(filename), abortSignal)
1513
+ : undefined
1514
+ return {
1515
+ filePath: localFilePath,
1516
+ thumbnailPath,
1517
+ mime_type: isVideo
1518
+ ? "video/mp4"
1519
+ : lowerName.endsWith(".png")
1520
+ ? "image/png"
1521
+ : lowerName.endsWith(".avif")
1522
+ ? "image/avif"
1523
+ : "image/jpeg",
1524
+ }
1525
+ } catch (error) {
1526
+ galleryTransferLedger.recordFailure(captureId, error)
1527
+ throw error
1528
+ }
1529
+ }
1530
+
1531
+ private async downloadFileLegacyUnsafe(
1532
+ filename: string,
1533
+ includeThumbnail: boolean = false,
1534
+ onProgress?: (progress: number) => void,
1535
+ abortSignal?: AbortSignal,
1536
+ expectedSize?: number,
957
1537
  ): Promise<{
958
1538
  filePath: string
959
1539
  thumbnailPath?: string
@@ -991,7 +1571,7 @@ export class AsgCameraApiClient {
991
1571
  // Throttle progress: only call onProgress when percentage actually changes
992
1572
  let lastReportedProgress = -1
993
1573
 
994
- const {jobId, promise: downloadPromise} = RNFS.downloadFile({
1574
+ const {jobId, promise: downloadPromise} = localNetworkTransport.downloadFile({
995
1575
  fromUrl: downloadUrl,
996
1576
  toFile: localFilePath,
997
1577
  headers: {
@@ -1036,12 +1616,12 @@ export class AsgCameraApiClient {
1036
1616
  let abortPollTimer: number | undefined
1037
1617
  if (abortSignal) {
1038
1618
  if (abortSignal.aborted) {
1039
- RNFS.stopDownload(jobId)
1619
+ localNetworkTransport.stopDownload(jobId)
1040
1620
  throw new Error("Sync cancelled")
1041
1621
  }
1042
1622
  abortPollTimer = BgTimer.setInterval(() => {
1043
1623
  if (abortSignal.aborted) {
1044
- RNFS.stopDownload(jobId)
1624
+ localNetworkTransport.stopDownload(jobId)
1045
1625
  }
1046
1626
  }, 500)
1047
1627
  }
@@ -1141,7 +1721,7 @@ export class AsgCameraApiClient {
1141
1721
 
1142
1722
  // The server's /api/photo endpoint serves thumbnails for video files
1143
1723
  // It detects video files and automatically generates/serves thumbnails instead of the full video
1144
- const thumbResult = await RNFS.downloadFile({
1724
+ const thumbResult = await localNetworkTransport.downloadFile({
1145
1725
  fromUrl: `${this.baseUrl}/api/photo?file=${encodeURIComponent(filename)}`,
1146
1726
  toFile: localThumbnailPath as string,
1147
1727
  headers: {
@@ -1181,7 +1761,9 @@ export class AsgCameraApiClient {
1181
1761
  }
1182
1762
  } else {
1183
1763
  console.log(
1184
- `[ASG Camera API] Skipping thumbnail download - includeThumbnail: ${includeThumbnail}, filename: ${filename}, is video extension: ${filename.toLowerCase().match(/\.(mp4|mov|avi|mkv|webm)$/) ? "yes" : "no"}`,
1764
+ `[ASG Camera API] Skipping thumbnail download - includeThumbnail: ${includeThumbnail}, filename: ${filename}, is video extension: ${
1765
+ filename.toLowerCase().match(/\.(mp4|mov|avi|mkv|webm)$/) ? "yes" : "no"
1766
+ }`,
1185
1767
  )
1186
1768
  }
1187
1769