@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
@@ -0,0 +1,627 @@
1
+ import * as RNFS from "@dr.pogodin/react-native-fs"
2
+
3
+ import {MediaLibraryPermissions} from "../../utils/permissions/MediaLibraryPermissions"
4
+ import {BgTimer} from "../../utils/timers"
5
+ import {
6
+ cameraRollEntryId,
7
+ cameraRollExportLedger,
8
+ cameraRollGeneration,
9
+ type CameraRollExportEntry,
10
+ type CameraRollExportPriority,
11
+ } from "./cameraRollExportLedger"
12
+ import {gallerySettingsService} from "./gallerySettingsService"
13
+ import {galleryTransferLedger, type GalleryAssetReceipt} from "./galleryTransferLedger"
14
+ import {localStorageService, type DownloadedFile} from "./localStorageService"
15
+
16
+ const TAG = "[CameraRollExport]"
17
+ const MIN_FREE_SPACE_BYTES = 100 * 1024 * 1024
18
+ const MAX_RETRY_DELAY_MS = 30 * 60 * 1000
19
+
20
+ export interface CameraRollExportSummary {
21
+ enabled: boolean
22
+ total: number
23
+ exported: number
24
+ pending: number
25
+ failed: number
26
+ blockedPermission: number
27
+ fullAccessRequired: number
28
+ missing: number
29
+ bytesPending: number
30
+ isRunning: boolean
31
+ }
32
+
33
+ type SummaryListener = (summary: CameraRollExportSummary) => void
34
+
35
+ class CameraRollExportCoordinator {
36
+ private initialized = false
37
+ private initializing: Promise<void> | null = null
38
+ private lifecycleGeneration = 0
39
+ private enabled = true
40
+ private running = false
41
+ private rerunRequested = false
42
+ private retryTimer: number | null = null
43
+ private activeEntryId: string | null = null
44
+ private clearInProgress = false
45
+ private deletingMediaNames = new Set<string>()
46
+ private listeners = new Set<SummaryListener>()
47
+ private sourceWaiters = new Map<
48
+ string,
49
+ Array<{resolve: (receipt: GalleryAssetReceipt) => void; reject: (error: Error) => void}>
50
+ >()
51
+
52
+ async initialize(): Promise<void> {
53
+ while (!this.initialized) {
54
+ if (!this.initializing) {
55
+ const generation = this.lifecycleGeneration
56
+ this.initializing = this.initializeInternal(generation)
57
+ }
58
+ const initialization = this.initializing
59
+ try {
60
+ await initialization
61
+ } finally {
62
+ if (this.initializing === initialization) this.initializing = null
63
+ }
64
+ }
65
+ }
66
+
67
+ cleanup(): void {
68
+ this.lifecycleGeneration += 1
69
+ if (this.retryTimer != null) BgTimer.clearTimeout(this.retryTimer)
70
+ this.retryTimer = null
71
+ this.initialized = false
72
+ this.rerunRequested = false
73
+ const error = new Error("Camera-roll export coordinator stopped")
74
+ for (const id of [...this.sourceWaiters.keys()]) this.rejectSourceWaiters(id, error)
75
+ if (!this.running) this.activeEntryId = null
76
+ }
77
+
78
+ private async initializeInternal(generation: number): Promise<void> {
79
+ cameraRollExportLedger.setDocumentDirectory(RNFS.DocumentDirectoryPath)
80
+ // A native camera-roll insert is atomic and cannot be cancelled. On a quick engine
81
+ // stop/start, let the old drain commit its receipt before the new lifecycle reconciles.
82
+ while (this.running) {
83
+ await new Promise<void>((resolve) => BgTimer.setTimeout(resolve, 25))
84
+ }
85
+ if (generation !== this.lifecycleGeneration) return
86
+ const enabled = await gallerySettingsService.getAutoSaveToCameraRoll()
87
+ if (generation !== this.lifecycleGeneration) return
88
+ this.enabled = enabled
89
+ await this.reconcileLocalIndex()
90
+ if (generation !== this.lifecycleGeneration) return
91
+ this.initialized = true
92
+ this.emitSummary()
93
+ if (this.enabled) void this.resume("startup")
94
+ }
95
+
96
+ /** Rebuild missing per-generation records after upgrading from legacy clients. */
97
+ private async reconcileLocalIndex(): Promise<void> {
98
+ const files = await localStorageService.getDownloadedFiles()
99
+ const liveIds = new Set<string>()
100
+ const liveGenerationByMediaName = new Map<string, string>()
101
+ for (const file of Object.values(files)) {
102
+ const id = cameraRollEntryId(file.name, file.size, file.modified)
103
+ liveIds.add(id)
104
+ liveGenerationByMediaName.set(file.name, id)
105
+ const current = cameraRollExportLedger.get(id)
106
+ const transfer = file.capture_id
107
+ ? galleryTransferLedger.get(file.capture_id)
108
+ : galleryTransferLedger.get(file.name)
109
+ const receipt = file.assetReceipt || transfer?.assetReceipt || current?.receipt
110
+ const now = Date.now()
111
+ cameraRollExportLedger.put({
112
+ ...(current || {
113
+ id,
114
+ mediaName: file.name,
115
+ generation: cameraRollGeneration(file.name, file.size, file.modified),
116
+ filePath: file.filePath,
117
+ size: file.size,
118
+ modified: file.modified,
119
+ isVideo: file.is_video,
120
+ captureId: file.capture_id || transfer?.captureId,
121
+ priority: "HISTORICAL_BACKFILL" as const,
122
+ attempts: 0,
123
+ createdAt: now,
124
+ updatedAt: now,
125
+ state: "LEGACY_UNKNOWN" as const,
126
+ }),
127
+ filePath: file.filePath,
128
+ localPresent: true,
129
+ receipt,
130
+ requiresFullLibraryReconciliation:
131
+ current?.requiresFullLibraryReconciliation ?? (!current && !receipt && !transfer),
132
+ state: receipt ? "EXPORTED" : this.nextEligibleState(current?.state),
133
+ })
134
+ }
135
+
136
+ // Explicit app deletion removes receipts through deleteLocalMedia/clearLocalMedia. An index
137
+ // miss here is not proof of deletion: recovery may not have rebuilt the row yet, and a load
138
+ // failure is represented as an empty index. Preserve exported receipts as hidden tombstones;
139
+ // they are the authoritative proof that prevents a duplicate native-library insert.
140
+ for (const entry of cameraRollExportLedger.list()) {
141
+ if (liveIds.has(entry.id)) continue
142
+ if (entry.receipt) {
143
+ cameraRollExportLedger.update(entry.id, {state: "EXPORTED", localPresent: false})
144
+ continue
145
+ }
146
+ // A different live generation with the same display name supersedes an unexported stale
147
+ // row. Never let its old path export the replacement's bytes under the wrong fingerprint.
148
+ if (liveGenerationByMediaName.has(entry.mediaName)) {
149
+ cameraRollExportLedger.remove(entry.id)
150
+ continue
151
+ }
152
+ cameraRollExportLedger.update(entry.id, {
153
+ state: "MISSING_LOCAL",
154
+ localPresent: false,
155
+ lastError: "Local media is missing",
156
+ nextRetryAt: undefined,
157
+ })
158
+ }
159
+ }
160
+
161
+ private nextEligibleState(current?: CameraRollExportEntry["state"]): CameraRollExportEntry["state"] {
162
+ if (!this.enabled) return "NOT_REQUESTED"
163
+ // Reconciliation only calls this for files that are present in the durable local index.
164
+ // A previous filesystem miss was transient if that same generation is visible again.
165
+ if (current === "FAILED_PERMANENT") return current
166
+ return "QUEUED"
167
+ }
168
+
169
+ async recordIndexed(
170
+ file: DownloadedFile,
171
+ shouldAutoSave: boolean,
172
+ priority: CameraRollExportPriority = "HISTORICAL_BACKFILL",
173
+ captureId?: string,
174
+ ): Promise<CameraRollExportEntry> {
175
+ await this.initialize()
176
+ const id = cameraRollEntryId(file.name, file.size, file.modified)
177
+ const current = cameraRollExportLedger.get(id)
178
+ const now = Date.now()
179
+ const entry = cameraRollExportLedger.put({
180
+ ...(current || {
181
+ id,
182
+ mediaName: file.name,
183
+ generation: cameraRollGeneration(file.name, file.size, file.modified),
184
+ attempts: 0,
185
+ createdAt: now,
186
+ updatedAt: now,
187
+ }),
188
+ filePath: file.filePath,
189
+ size: file.size,
190
+ modified: file.modified,
191
+ isVideo: file.is_video,
192
+ captureId: captureId || file.capture_id || current?.captureId,
193
+ localPresent: true,
194
+ requiresFullLibraryReconciliation: current?.requiresFullLibraryReconciliation ?? false,
195
+ priority,
196
+ receipt: file.assetReceipt || current?.receipt,
197
+ state: file.assetReceipt || current?.receipt ? "EXPORTED" : shouldAutoSave ? "QUEUED" : "NOT_REQUESTED",
198
+ lastError: undefined,
199
+ nextRetryAt: undefined,
200
+ })
201
+ this.emitSummary()
202
+ return entry
203
+ }
204
+
205
+ async isAutoSaveEnabled(): Promise<boolean> {
206
+ await this.initialize()
207
+ return this.enabled
208
+ }
209
+
210
+ /** Source deletion waits on this barrier while enabled; disabled exports remain safely app-local. */
211
+ async exportForSource(file: DownloadedFile, captureId?: string): Promise<GalleryAssetReceipt | undefined> {
212
+ await this.initialize()
213
+ // A durable receipt proves this generation is already safe regardless of the current
214
+ // preference. Return it before enforcing disabled state so recovery can still ack the
215
+ // glasses after a previously-started export committed.
216
+ const entry = await this.recordIndexed(file, this.enabled, "SOURCE_BARRIER", captureId)
217
+ if (entry.receipt) return entry.receipt
218
+ if (!this.enabled) return undefined
219
+ try {
220
+ return await new Promise<GalleryAssetReceipt>((resolve, reject) => {
221
+ const current = cameraRollExportLedger.get(entry.id)
222
+ if (current?.receipt) {
223
+ resolve(current.receipt)
224
+ return
225
+ }
226
+ if (!current) {
227
+ reject(new Error("Camera-roll export was removed before its source barrier registered"))
228
+ return
229
+ }
230
+ const waiters = this.sourceWaiters.get(entry.id) || []
231
+ waiters.push({resolve, reject})
232
+ this.sourceWaiters.set(entry.id, waiters)
233
+ // Re-read after registration so any future refactor that yields while installing a
234
+ // waiter still cannot miss a receipt committed by the active runner.
235
+ const registered = cameraRollExportLedger.get(entry.id)
236
+ if (registered?.receipt) {
237
+ this.resolveSourceWaiters(entry.id, registered.receipt)
238
+ return
239
+ }
240
+ if (!registered) {
241
+ this.rejectSourceWaiters(
242
+ entry.id,
243
+ new Error("Camera-roll export was removed before its source barrier registered"),
244
+ )
245
+ return
246
+ }
247
+ void this.resume("source barrier")
248
+ })
249
+ } catch (error) {
250
+ // Turning auto-save off makes the durable app-local commit the source-of-truth. A native
251
+ // insert already in progress still finishes and returns its receipt; queued work may stop.
252
+ if (!this.enabled) return undefined
253
+ throw error
254
+ }
255
+ }
256
+
257
+ async setEnabled(enabled: boolean): Promise<void> {
258
+ await this.initialize()
259
+ const previous = this.enabled
260
+ await gallerySettingsService.setAutoSaveToCameraRoll(enabled)
261
+ try {
262
+ this.enabled = enabled
263
+ if (!enabled && this.retryTimer != null) {
264
+ BgTimer.clearTimeout(this.retryTimer)
265
+ this.retryTimer = null
266
+ }
267
+ for (const entry of cameraRollExportLedger.list()) {
268
+ if (entry.receipt || entry.state === "EXPORTED" || entry.state === "MISSING_LOCAL") continue
269
+ if (!enabled && entry.id !== this.activeEntryId) {
270
+ cameraRollExportLedger.update(entry.id, {state: "NOT_REQUESTED", nextRetryAt: undefined})
271
+ this.rejectSourceWaiters(entry.id, new Error("Automatic camera-roll saving was disabled"))
272
+ } else if (enabled) {
273
+ cameraRollExportLedger.update(entry.id, {state: "QUEUED", nextRetryAt: undefined, lastError: undefined})
274
+ }
275
+ }
276
+ } catch (error) {
277
+ this.enabled = previous
278
+ await gallerySettingsService.setAutoSaveToCameraRoll(previous)
279
+ throw error
280
+ }
281
+ this.emitSummary()
282
+ if (enabled) void this.resume("setting enabled")
283
+ }
284
+
285
+ async resume(reason: string): Promise<void> {
286
+ const generation = this.lifecycleGeneration
287
+ await this.initialize()
288
+ if (generation !== this.lifecycleGeneration) return
289
+ console.log(`${TAG} Resume requested: ${reason}`)
290
+ if (cameraRollExportLedger.list().some((entry) => entry.state === "BLOCKED_PERMISSION")) {
291
+ const hasPermission = await MediaLibraryPermissions.checkPermission()
292
+ const hasLimitedAccess = hasPermission && (await MediaLibraryPermissions.hasLimitedAccess())
293
+ if (generation !== this.lifecycleGeneration) return
294
+ if (hasPermission) {
295
+ for (const entry of cameraRollExportLedger.list()) {
296
+ if (entry.state === "BLOCKED_PERMISSION" && !(entry.requiresFullLibraryReconciliation && hasLimitedAccess)) {
297
+ cameraRollExportLedger.update(entry.id, {state: "QUEUED", lastError: undefined})
298
+ }
299
+ }
300
+ }
301
+ }
302
+ if (generation !== this.lifecycleGeneration) return
303
+ if (this.retryTimer != null) {
304
+ BgTimer.clearTimeout(this.retryTimer)
305
+ this.retryTimer = null
306
+ }
307
+ if (this.running) {
308
+ this.rerunRequested = true
309
+ while (this.running) {
310
+ await new Promise<void>((resolve) => BgTimer.setTimeout(resolve, 25))
311
+ }
312
+ if (generation !== this.lifecycleGeneration) return
313
+ // The active drain normally consumes rerunRequested before releasing running. Keep
314
+ // this final durable-work check so a late handoff can never strand an eligible entry.
315
+ if (this.nextEntry()) await this.resume("work remained after concurrent resume")
316
+ return
317
+ }
318
+ this.running = true
319
+ this.emitSummary()
320
+ try {
321
+ do {
322
+ this.rerunRequested = false
323
+ let entry = this.nextEntry()
324
+ while (entry && generation === this.lifecycleGeneration) {
325
+ await this.processEntry(entry, generation)
326
+ if (generation !== this.lifecycleGeneration) break
327
+ entry = this.nextEntry()
328
+ }
329
+ } while (this.rerunRequested && generation === this.lifecycleGeneration)
330
+ } finally {
331
+ this.rerunRequested = false
332
+ this.running = false
333
+ if (generation === this.lifecycleGeneration) {
334
+ this.emitSummary()
335
+ this.scheduleRetry()
336
+ }
337
+ }
338
+ }
339
+
340
+ async retryNow(): Promise<void> {
341
+ const generation = this.lifecycleGeneration
342
+ await this.initialize()
343
+ // A recovered transfer may have rebuilt a missing local-index row since the last lifecycle
344
+ // reconciliation. Refresh before retrying so the same camera-roll generation becomes
345
+ // eligible again; never rewrite entry state underneath an active native insert.
346
+ while (this.running) {
347
+ await new Promise<void>((resolve) => BgTimer.setTimeout(resolve, 25))
348
+ }
349
+ if (generation !== this.lifecycleGeneration) return
350
+ await this.reconcileLocalIndex()
351
+ if (generation !== this.lifecycleGeneration) return
352
+ for (const entry of cameraRollExportLedger.list()) {
353
+ if (
354
+ entry.state === "FAILED_RETRYABLE" ||
355
+ entry.state === "BLOCKED_PERMISSION" ||
356
+ (entry.state === "MISSING_LOCAL" && entry.localPresent !== false)
357
+ ) {
358
+ cameraRollExportLedger.update(entry.id, {state: "QUEUED", nextRetryAt: undefined, lastError: undefined})
359
+ }
360
+ }
361
+ await this.resume("manual retry")
362
+ }
363
+
364
+ subscribe(listener: SummaryListener): () => void {
365
+ this.listeners.add(listener)
366
+ // `enabled` is loaded from durable settings during initialize(). Do not emit the
367
+ // default before that read completes or disabled users briefly see the toggle as on.
368
+ if (this.initialized) listener(this.getSummary())
369
+ return () => this.listeners.delete(listener)
370
+ }
371
+
372
+ getSummary(): CameraRollExportSummary {
373
+ const entries = cameraRollExportLedger
374
+ .list()
375
+ // Exported receipts with no local row are hidden deduplication tombstones. Unexported
376
+ // missing generations remain user-visible so data that can no longer be backfilled is
377
+ // never reported as successfully saved.
378
+ .filter((entry) => entry.localPresent !== false || entry.state === "MISSING_LOCAL")
379
+ const pendingStates = new Set([
380
+ "LEGACY_UNKNOWN",
381
+ "QUEUED",
382
+ "RECONCILING",
383
+ "EXPORTING",
384
+ "FAILED_RETRYABLE",
385
+ "BLOCKED_PERMISSION",
386
+ ])
387
+ return {
388
+ enabled: this.enabled,
389
+ total: entries.length,
390
+ exported: entries.filter((entry) => entry.state === "EXPORTED").length,
391
+ pending: entries.filter((entry) => pendingStates.has(entry.state)).length,
392
+ failed: entries.filter((entry) => entry.state === "FAILED_RETRYABLE" || entry.state === "FAILED_PERMANENT")
393
+ .length,
394
+ blockedPermission: entries.filter((entry) => entry.state === "BLOCKED_PERMISSION").length,
395
+ fullAccessRequired: entries.filter(
396
+ (entry) => entry.state === "BLOCKED_PERMISSION" && entry.requiresFullLibraryReconciliation,
397
+ ).length,
398
+ missing: entries.filter((entry) => entry.state === "MISSING_LOCAL").length,
399
+ bytesPending: entries
400
+ .filter((entry) => pendingStates.has(entry.state))
401
+ .reduce((sum, entry) => sum + entry.size, 0),
402
+ isRunning: this.running,
403
+ }
404
+ }
405
+
406
+ async countNotExported(mediaNames: string[]): Promise<number> {
407
+ await this.initialize()
408
+ const uniqueNames = [...new Set(mediaNames)]
409
+ if (uniqueNames.length === 0) return 0
410
+ const files = await localStorageService.getDownloadedFiles()
411
+ return uniqueNames.filter((name) => {
412
+ const file = files[name]
413
+ if (!file) return true
414
+ const entry = cameraRollExportLedger.get(cameraRollEntryId(file.name, file.size, file.modified))
415
+ return entry?.state !== "EXPORTED"
416
+ }).length
417
+ }
418
+
419
+ /** Serialize local deletion against an in-flight native copy of the same item. */
420
+ async deleteLocalMedia(mediaName: string): Promise<boolean> {
421
+ this.deletingMediaNames.add(mediaName)
422
+ try {
423
+ while (this.activeEntryId && cameraRollExportLedger.get(this.activeEntryId)?.mediaName === mediaName) {
424
+ await new Promise<void>((resolve) => BgTimer.setTimeout(resolve, 25))
425
+ }
426
+ const deleted = await localStorageService.deleteDownloadedFile(mediaName)
427
+ if (deleted) {
428
+ // An already-running export gets to finish and resolve its source barrier. Queued work
429
+ // cannot start while the name is in deletingMediaNames, so reject only after deletion
430
+ // is confirmed. A failed delete leaves the barrier intact and eligible for retry.
431
+ for (const entry of cameraRollExportLedger.list().filter((candidate) => candidate.mediaName === mediaName)) {
432
+ this.rejectSourceWaiters(entry.id, new Error("Local media was deleted before camera-roll export"))
433
+ }
434
+ cameraRollExportLedger.removeForMedia(mediaName)
435
+ }
436
+ return deleted
437
+ } finally {
438
+ this.deletingMediaNames.delete(mediaName)
439
+ this.emitSummary()
440
+ if (this.enabled) void this.resume("local deletion finished")
441
+ }
442
+ }
443
+
444
+ /** Stop scheduling exports, finish the current native copy, then atomically clear app media + receipts. */
445
+ async clearLocalMedia(): Promise<void> {
446
+ this.clearInProgress = true
447
+ try {
448
+ while (this.activeEntryId) {
449
+ await new Promise<void>((resolve) => BgTimer.setTimeout(resolve, 25))
450
+ }
451
+ await localStorageService.clearAllFiles()
452
+ // Let the active native copy deliver its receipt before rejecting queued barriers, and
453
+ // reject them only after the app media clear is confirmed. If clear fails, the durable
454
+ // work and its waiters remain eligible for retry.
455
+ for (const entry of cameraRollExportLedger.list()) {
456
+ this.rejectSourceWaiters(entry.id, new Error("Local media was deleted before camera-roll export"))
457
+ }
458
+ cameraRollExportLedger.clear()
459
+ } finally {
460
+ this.clearInProgress = false
461
+ this.emitSummary()
462
+ if (this.enabled) void this.resume("local clear finished")
463
+ }
464
+ }
465
+
466
+ private nextEntry(): CameraRollExportEntry | undefined {
467
+ const now = Date.now()
468
+ return cameraRollExportLedger
469
+ .list()
470
+ .filter((entry) => {
471
+ if (entry.localPresent === false) return false
472
+ if (this.clearInProgress || this.deletingMediaNames.has(entry.mediaName)) return false
473
+ if (entry.receipt || entry.state === "EXPORTED" || entry.state === "MISSING_LOCAL") return false
474
+ if (!this.enabled) return false
475
+ if (entry.state === "BLOCKED_PERMISSION" || entry.state === "FAILED_PERMANENT") return false
476
+ if (entry.state === "FAILED_RETRYABLE" && (entry.nextRetryAt || 0) > now) return false
477
+ return entry.state !== "NOT_REQUESTED"
478
+ })
479
+ .sort((a, b) => {
480
+ if (a.priority !== b.priority) return a.priority === "SOURCE_BARRIER" ? -1 : 1
481
+ return a.modified - b.modified
482
+ })[0]
483
+ }
484
+
485
+ private async processEntry(original: CameraRollExportEntry, generation: number): Promise<void> {
486
+ let entry = original
487
+ this.activeEntryId = entry.id
488
+ try {
489
+ if (!this.enabled) {
490
+ cameraRollExportLedger.update(entry.id, {state: "NOT_REQUESTED", nextRetryAt: undefined})
491
+ this.rejectSourceWaiters(entry.id, new Error("Automatic camera-roll saving was disabled"))
492
+ return
493
+ }
494
+ const exists = await RNFS.exists(entry.filePath)
495
+ if (generation !== this.lifecycleGeneration) return
496
+ if (!exists) {
497
+ cameraRollExportLedger.update(entry.id, {state: "MISSING_LOCAL", lastError: "Local media is missing"})
498
+ throw new Error("Local media is missing")
499
+ }
500
+ const permission = await MediaLibraryPermissions.checkPermission()
501
+ if (generation !== this.lifecycleGeneration) return
502
+ if (!permission) {
503
+ cameraRollExportLedger.update(entry.id, {
504
+ state: "BLOCKED_PERMISSION",
505
+ lastError: "Camera-roll permission is required",
506
+ })
507
+ throw new Error("Camera-roll permission is required")
508
+ }
509
+ const hasLimitedAccess =
510
+ entry.requiresFullLibraryReconciliation && (await MediaLibraryPermissions.hasLimitedAccess())
511
+ if (generation !== this.lifecycleGeneration) return
512
+ if (hasLimitedAccess) {
513
+ cameraRollExportLedger.update(entry.id, {
514
+ state: "BLOCKED_PERMISSION",
515
+ lastError: "Full photo-library access is required to reconcile this legacy export safely",
516
+ })
517
+ throw new Error("Full photo-library access is required to reconcile this legacy export safely")
518
+ }
519
+ const fsInfo = await RNFS.getFSInfo()
520
+ if (generation !== this.lifecycleGeneration) return
521
+ if (fsInfo.freeSpace < entry.size + MIN_FREE_SPACE_BYTES) {
522
+ throw new Error("Insufficient free space for camera-roll export")
523
+ }
524
+ // A setting change can land while the filesystem and permission checks are in flight.
525
+ // Once the native save starts it must finish atomically, but do not begin a new save
526
+ // after the user has disabled automatic camera-roll exports.
527
+ if (!this.enabled) {
528
+ cameraRollExportLedger.update(entry.id, {state: "NOT_REQUESTED", nextRetryAt: undefined})
529
+ this.rejectSourceWaiters(entry.id, new Error("Automatic camera-roll saving was disabled"))
530
+ return
531
+ }
532
+
533
+ entry = cameraRollExportLedger.update(entry.id, {state: "RECONCILING", lastError: undefined})
534
+ entry = cameraRollExportLedger.update(entry.id, {state: "EXPORTING"})
535
+ const result = await MediaLibraryPermissions.saveToLibraryWithReceipt(entry.filePath, entry.modified)
536
+ if (!result.success) throw new Error(result.error || "Camera-roll export failed")
537
+ const receipt: GalleryAssetReceipt = {
538
+ platform: result.platform,
539
+ uri: result.uri,
540
+ identifier: result.identifier,
541
+ exportedAt: Date.now(),
542
+ }
543
+ entry = cameraRollExportLedger.update(entry.id, {
544
+ state: "EXPORTED",
545
+ receipt,
546
+ requiresFullLibraryReconciliation: false,
547
+ attempts: entry.attempts + 1,
548
+ nextRetryAt: undefined,
549
+ lastError: undefined,
550
+ })
551
+ try {
552
+ await this.mirrorReceipt(entry, receipt)
553
+ } catch (mirrorError) {
554
+ // The per-generation ledger is the authoritative durable receipt. A secondary
555
+ // metadata mirror failure must not cause a duplicate insert or block source ack.
556
+ console.warn(`${TAG} Export receipt mirror failed for ${entry.mediaName}:`, mirrorError)
557
+ }
558
+ this.resolveSourceWaiters(entry.id, receipt)
559
+ } catch (error) {
560
+ const message = error instanceof Error ? error.message : String(error)
561
+ const current = cameraRollExportLedger.get(entry.id)
562
+ if (current && current.state !== "BLOCKED_PERMISSION" && current.state !== "MISSING_LOCAL") {
563
+ const attempts = current.attempts + 1
564
+ cameraRollExportLedger.update(entry.id, {
565
+ state: "FAILED_RETRYABLE",
566
+ attempts,
567
+ lastError: message,
568
+ nextRetryAt: Date.now() + Math.min(2 ** Math.min(attempts, 10) * 1000, MAX_RETRY_DELAY_MS),
569
+ })
570
+ }
571
+ this.rejectSourceWaiters(entry.id, error instanceof Error ? error : new Error(message))
572
+ console.warn(`${TAG} Export remains pending for ${entry.mediaName}: ${message}`)
573
+ } finally {
574
+ if (this.activeEntryId === entry.id) this.activeEntryId = null
575
+ if (generation === this.lifecycleGeneration) this.emitSummary()
576
+ }
577
+ }
578
+
579
+ private async mirrorReceipt(entry: CameraRollExportEntry, receipt: GalleryAssetReceipt): Promise<void> {
580
+ await localStorageService.updateDownloadedFileAssetReceipt(entry.mediaName, receipt)
581
+ const transfer = entry.captureId
582
+ ? galleryTransferLedger.get(entry.captureId)
583
+ : galleryTransferLedger.get(entry.mediaName)
584
+ if (transfer) galleryTransferLedger.update(transfer.captureId, {assetReceipt: receipt})
585
+ }
586
+
587
+ private scheduleRetry(): void {
588
+ if (!this.enabled || this.retryTimer != null) return
589
+ const next = cameraRollExportLedger
590
+ .list()
591
+ .filter((entry) => entry.state === "FAILED_RETRYABLE" && entry.nextRetryAt)
592
+ .sort((a, b) => (a.nextRetryAt || 0) - (b.nextRetryAt || 0))[0]
593
+ if (!next?.nextRetryAt) return
594
+ const generation = this.lifecycleGeneration
595
+ this.retryTimer = BgTimer.setTimeout(
596
+ () => {
597
+ this.retryTimer = null
598
+ if (generation !== this.lifecycleGeneration) return
599
+ void this.resume("retry timer")
600
+ },
601
+ Math.max(0, next.nextRetryAt - Date.now()),
602
+ )
603
+ }
604
+
605
+ private resolveSourceWaiters(id: string, receipt: GalleryAssetReceipt): void {
606
+ for (const waiter of this.sourceWaiters.get(id) || []) waiter.resolve(receipt)
607
+ this.sourceWaiters.delete(id)
608
+ }
609
+
610
+ private rejectSourceWaiters(id: string, error: Error): void {
611
+ for (const waiter of this.sourceWaiters.get(id) || []) waiter.reject(error)
612
+ this.sourceWaiters.delete(id)
613
+ }
614
+
615
+ private emitSummary(): void {
616
+ const summary = this.getSummary()
617
+ for (const listener of this.listeners) {
618
+ try {
619
+ listener(summary)
620
+ } catch (error) {
621
+ console.warn(`${TAG} Summary listener failed`, error)
622
+ }
623
+ }
624
+ }
625
+ }
626
+
627
+ export const cameraRollExportCoordinator = new CameraRollExportCoordinator()