@glassly/engine 0.1.0-dev.1 → 0.1.0-dev.101

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 (343) hide show
  1. package/README.md +57 -19
  2. package/build/bluetooth-sdk/debug.d.ts +2 -0
  3. package/build/bluetooth-sdk/debug.d.ts.map +1 -0
  4. package/build/bluetooth-sdk/debug.js +2 -0
  5. package/build/bluetooth-sdk/debug.js.map +1 -0
  6. package/build/bluetooth-sdk/index.d.ts +3 -0
  7. package/build/bluetooth-sdk/index.d.ts.map +1 -0
  8. package/build/bluetooth-sdk/index.js +3 -0
  9. package/build/bluetooth-sdk/index.js.map +1 -0
  10. package/build/bluetooth-sdk/ota-transport.d.ts +2 -0
  11. package/build/bluetooth-sdk/ota-transport.d.ts.map +1 -0
  12. package/build/bluetooth-sdk/ota-transport.js +2 -0
  13. package/build/bluetooth-sdk/ota-transport.js.map +1 -0
  14. package/build/bluetooth-sdk/photo-receiver.d.ts +3 -0
  15. package/build/bluetooth-sdk/photo-receiver.d.ts.map +1 -0
  16. package/build/bluetooth-sdk/photo-receiver.js +3 -0
  17. package/build/bluetooth-sdk/photo-receiver.js.map +1 -0
  18. package/build/bluetooth-sdk/react.d.ts +2 -0
  19. package/build/bluetooth-sdk/react.d.ts.map +1 -0
  20. package/build/bluetooth-sdk/react.js +2 -0
  21. package/build/bluetooth-sdk/react.js.map +1 -0
  22. package/build/bluetooth-sdk/types.d.ts +2 -0
  23. package/build/bluetooth-sdk/types.d.ts.map +1 -0
  24. package/build/bluetooth-sdk/types.js +2 -0
  25. package/build/bluetooth-sdk/types.js.map +1 -0
  26. package/build/engine.d.ts +30 -18
  27. package/build/engine.d.ts.map +1 -1
  28. package/build/engine.js +12 -3
  29. package/build/engine.js.map +1 -1
  30. package/build/facades/dev.d.ts +1 -1
  31. package/build/facades/display.d.ts +11 -4
  32. package/build/facades/display.d.ts.map +1 -1
  33. package/build/facades/display.js +12 -2
  34. package/build/facades/display.js.map +1 -1
  35. package/build/facades/displayMirror.d.ts +2 -6
  36. package/build/facades/displayMirror.d.ts.map +1 -1
  37. package/build/facades/displayMirror.js +7 -14
  38. package/build/facades/displayMirror.js.map +1 -1
  39. package/build/facades/glasses.d.ts +6 -13
  40. package/build/facades/glasses.d.ts.map +1 -1
  41. package/build/facades/glasses.js +7 -3
  42. package/build/facades/glasses.js.map +1 -1
  43. package/build/facades/ota.d.ts +21 -21
  44. package/build/facades/ota.d.ts.map +1 -1
  45. package/build/facades/ota.js +26 -10
  46. package/build/facades/ota.js.map +1 -1
  47. package/build/facades/pairing.d.ts +11 -5
  48. package/build/facades/pairing.d.ts.map +1 -1
  49. package/build/facades/pairing.js +93 -17
  50. package/build/facades/pairing.js.map +1 -1
  51. package/build/facades/phoneNotifications.d.ts +12 -2
  52. package/build/facades/phoneNotifications.d.ts.map +1 -1
  53. package/build/facades/phoneNotifications.js +30 -10
  54. package/build/facades/phoneNotifications.js.map +1 -1
  55. package/build/facades/speakers.d.ts +1 -1
  56. package/build/facades/speakers.d.ts.map +1 -1
  57. package/build/facades/speakers.js.map +1 -1
  58. package/build/facades/speech.d.ts +7 -6
  59. package/build/facades/speech.d.ts.map +1 -1
  60. package/build/facades/speech.js +1 -1
  61. package/build/facades/speech.js.map +1 -1
  62. package/build/generated/releaseMetadata.d.ts +12 -0
  63. package/build/generated/releaseMetadata.d.ts.map +1 -0
  64. package/build/generated/releaseMetadata.js +10 -0
  65. package/build/generated/releaseMetadata.js.map +1 -0
  66. package/build/index.d.ts +9 -6
  67. package/build/index.d.ts.map +1 -1
  68. package/build/index.js +7 -5
  69. package/build/index.js.map +1 -1
  70. package/build/react/MentraLiveOtaFlow.d.ts +44 -0
  71. package/build/react/MentraLiveOtaFlow.d.ts.map +1 -0
  72. package/build/react/MentraLiveOtaFlow.js +322 -0
  73. package/build/react/MentraLiveOtaFlow.js.map +1 -0
  74. package/build/react/index.d.ts +3 -0
  75. package/build/react/index.d.ts.map +1 -0
  76. package/build/react/index.js +3 -0
  77. package/build/react/index.js.map +1 -0
  78. package/build/react/useEngineSnapshot.d.ts +2 -0
  79. package/build/react/useEngineSnapshot.d.ts.map +1 -0
  80. package/build/react/useEngineSnapshot.js +14 -0
  81. package/build/react/useEngineSnapshot.js.map +1 -0
  82. package/build/react/useMentraLiveOta.d.ts +84 -0
  83. package/build/react/useMentraLiveOta.d.ts.map +1 -0
  84. package/build/react/useMentraLiveOta.js +627 -0
  85. package/build/react/useMentraLiveOta.js.map +1 -0
  86. package/build/services/AppRegistry.d.ts +8 -0
  87. package/build/services/AppRegistry.d.ts.map +1 -1
  88. package/build/services/AppRegistry.js +15 -0
  89. package/build/services/AppRegistry.js.map +1 -1
  90. package/build/services/BlobStore.d.ts +2 -0
  91. package/build/services/BlobStore.d.ts.map +1 -1
  92. package/build/services/BlobStore.js +63 -10
  93. package/build/services/BlobStore.js.map +1 -1
  94. package/build/services/CustomDashboardModel.d.ts +48 -0
  95. package/build/services/CustomDashboardModel.d.ts.map +1 -0
  96. package/build/services/CustomDashboardModel.js +73 -0
  97. package/build/services/CustomDashboardModel.js.map +1 -0
  98. package/build/services/CustomDashboardService.d.ts +55 -0
  99. package/build/services/CustomDashboardService.d.ts.map +1 -0
  100. package/build/services/CustomDashboardService.js +520 -0
  101. package/build/services/CustomDashboardService.js.map +1 -0
  102. package/build/services/DashboardCoordinator.d.ts +26 -0
  103. package/build/services/DashboardCoordinator.d.ts.map +1 -0
  104. package/build/services/DashboardCoordinator.js +125 -0
  105. package/build/services/DashboardCoordinator.js.map +1 -0
  106. package/build/services/DeviceEventRouter.d.ts.map +1 -1
  107. package/build/services/DeviceEventRouter.js +45 -6
  108. package/build/services/DeviceEventRouter.js.map +1 -1
  109. package/build/services/DisplayProcessor.d.ts +3 -3
  110. package/build/services/DisplayProcessor.js +3 -3
  111. package/build/services/DisplayProcessor.js.map +1 -1
  112. package/build/services/GestureDotsService.d.ts +40 -0
  113. package/build/services/GestureDotsService.d.ts.map +1 -0
  114. package/build/services/GestureDotsService.js +167 -0
  115. package/build/services/GestureDotsService.js.map +1 -0
  116. package/build/services/GlassesStatusProjection.d.ts +15 -1
  117. package/build/services/GlassesStatusProjection.d.ts.map +1 -1
  118. package/build/services/GlassesStatusProjection.js +18 -11
  119. package/build/services/GlassesStatusProjection.js.map +1 -1
  120. package/build/services/HotspotOtaTransport.d.ts.map +1 -1
  121. package/build/services/HotspotOtaTransport.js +8 -7
  122. package/build/services/HotspotOtaTransport.js.map +1 -1
  123. package/build/services/LocalDisplayManager.d.ts +55 -3
  124. package/build/services/LocalDisplayManager.d.ts.map +1 -1
  125. package/build/services/LocalDisplayManager.js +196 -16
  126. package/build/services/LocalDisplayManager.js.map +1 -1
  127. package/build/services/LocalMiniappRuntime.d.ts +13 -0
  128. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  129. package/build/services/LocalMiniappRuntime.js +32 -0
  130. package/build/services/LocalMiniappRuntime.js.map +1 -1
  131. package/build/services/MiniappExitGesture.d.ts +33 -0
  132. package/build/services/MiniappExitGesture.d.ts.map +1 -0
  133. package/build/services/MiniappExitGesture.js +54 -0
  134. package/build/services/MiniappExitGesture.js.map +1 -0
  135. package/build/services/OtaAutoChain.d.ts +41 -0
  136. package/build/services/OtaAutoChain.d.ts.map +1 -0
  137. package/build/services/OtaAutoChain.js +91 -0
  138. package/build/services/OtaAutoChain.js.map +1 -0
  139. package/build/services/OtaErrorMapping.d.ts +18 -0
  140. package/build/services/OtaErrorMapping.d.ts.map +1 -0
  141. package/build/services/OtaErrorMapping.js +63 -0
  142. package/build/services/OtaErrorMapping.js.map +1 -0
  143. package/build/services/OtaInstallCoordinator.d.ts +26 -3
  144. package/build/services/OtaInstallCoordinator.d.ts.map +1 -1
  145. package/build/services/OtaInstallCoordinator.js +38 -13
  146. package/build/services/OtaInstallCoordinator.js.map +1 -1
  147. package/build/services/OtaService.d.ts.map +1 -1
  148. package/build/services/OtaService.js +1 -1
  149. package/build/services/OtaService.js.map +1 -1
  150. package/build/services/OtaUpdateCheckService.d.ts +4 -1
  151. package/build/services/OtaUpdateCheckService.d.ts.map +1 -1
  152. package/build/services/OtaUpdateCheckService.js +28 -16
  153. package/build/services/OtaUpdateCheckService.js.map +1 -1
  154. package/build/services/PhoneNotificationsSync.d.ts +13 -0
  155. package/build/services/PhoneNotificationsSync.d.ts.map +1 -1
  156. package/build/services/PhoneNotificationsSync.js +101 -9
  157. package/build/services/PhoneNotificationsSync.js.map +1 -1
  158. package/build/services/PhonePhotoCoordinator.js +1 -1
  159. package/build/services/PhonePhotoCoordinator.js.map +1 -1
  160. package/build/services/PhoneStreamCoordinator.d.ts.map +1 -1
  161. package/build/services/PhoneStreamCoordinator.js +9 -6
  162. package/build/services/PhoneStreamCoordinator.js.map +1 -1
  163. package/build/services/SceneRenderer.d.ts.map +1 -1
  164. package/build/services/SceneRenderer.js +5 -0
  165. package/build/services/SceneRenderer.js.map +1 -1
  166. package/build/services/StreamLifecycleController.d.ts +4 -0
  167. package/build/services/StreamLifecycleController.d.ts.map +1 -1
  168. package/build/services/StreamLifecycleController.js +4 -0
  169. package/build/services/StreamLifecycleController.js.map +1 -1
  170. package/build/services/asg/localNetworkTransport.d.ts.map +1 -1
  171. package/build/services/asg/localNetworkTransport.js +13 -12
  172. package/build/services/asg/localNetworkTransport.js.map +1 -1
  173. package/build/services/effectiveCapabilities.d.ts +1 -1
  174. package/build/services/effectiveCapabilities.d.ts.map +1 -1
  175. package/build/services/effectiveCapabilities.js +26 -7
  176. package/build/services/effectiveCapabilities.js.map +1 -1
  177. package/build/services/gestureClaims.d.ts +18 -0
  178. package/build/services/gestureClaims.d.ts.map +1 -0
  179. package/build/services/gestureClaims.js +24 -0
  180. package/build/services/gestureClaims.js.map +1 -0
  181. package/build/services/gestureDotGlyphs.d.ts +8 -0
  182. package/build/services/gestureDotGlyphs.d.ts.map +1 -0
  183. package/build/services/gestureDotGlyphs.js +13 -0
  184. package/build/services/gestureDotGlyphs.js.map +1 -0
  185. package/build/services/glassesClockSync.d.ts.map +1 -1
  186. package/build/services/glassesClockSync.js +5 -1
  187. package/build/services/glassesClockSync.js.map +1 -1
  188. package/build/services/iosNotificationAppCatalog.d.ts +13 -0
  189. package/build/services/iosNotificationAppCatalog.d.ts.map +1 -0
  190. package/build/services/iosNotificationAppCatalog.js +28 -0
  191. package/build/services/iosNotificationAppCatalog.js.map +1 -0
  192. package/build/services/otaDisplayState.d.ts +1 -1
  193. package/build/services/otaDisplayState.d.ts.map +1 -1
  194. package/build/services/otaDisplayState.js.map +1 -1
  195. package/build/services/otaInstallPolicy.d.ts +5 -1
  196. package/build/services/otaInstallPolicy.d.ts.map +1 -1
  197. package/build/services/otaInstallPolicy.js +4 -0
  198. package/build/services/otaInstallPolicy.js.map +1 -1
  199. package/build/services/otaLegacyMapping.d.ts +1 -1
  200. package/build/services/otaLegacyMapping.d.ts.map +1 -1
  201. package/build/services/otaLegacyMapping.js.map +1 -1
  202. package/build/services/otaManifestPolicy.d.ts +14 -0
  203. package/build/services/otaManifestPolicy.d.ts.map +1 -0
  204. package/build/services/otaManifestPolicy.js +20 -0
  205. package/build/services/otaManifestPolicy.js.map +1 -0
  206. package/build/services/otaManifestUrl.d.ts +2 -1
  207. package/build/services/otaManifestUrl.d.ts.map +1 -1
  208. package/build/services/otaManifestUrl.js +24 -32
  209. package/build/services/otaManifestUrl.js.map +1 -1
  210. package/build/services/otaReleaseVersion.d.ts +10 -0
  211. package/build/services/otaReleaseVersion.d.ts.map +1 -0
  212. package/build/services/otaReleaseVersion.js +14 -0
  213. package/build/services/otaReleaseVersion.js.map +1 -0
  214. package/build/stores/appVisibility.d.ts +3 -0
  215. package/build/stores/appVisibility.d.ts.map +1 -0
  216. package/build/stores/appVisibility.js +3 -0
  217. package/build/stores/appVisibility.js.map +1 -0
  218. package/build/stores/apps.d.ts.map +1 -1
  219. package/build/stores/apps.js +7 -1
  220. package/build/stores/apps.js.map +1 -1
  221. package/build/stores/core.d.ts +3 -3
  222. package/build/stores/core.d.ts.map +1 -1
  223. package/build/stores/core.js.map +1 -1
  224. package/build/stores/display.d.ts +4 -4
  225. package/build/stores/display.d.ts.map +1 -1
  226. package/build/stores/display.js +12 -36
  227. package/build/stores/display.js.map +1 -1
  228. package/build/stores/glasses.d.ts +10 -4
  229. package/build/stores/glasses.d.ts.map +1 -1
  230. package/build/stores/glasses.js +2 -0
  231. package/build/stores/glasses.js.map +1 -1
  232. package/build/stores/settings.d.ts.map +1 -1
  233. package/build/stores/settings.js +140 -5
  234. package/build/stores/settings.js.map +1 -1
  235. package/build/types/applet.d.ts +3 -1
  236. package/build/types/applet.d.ts.map +1 -1
  237. package/build/types/applet.js +0 -3
  238. package/build/types/applet.js.map +1 -1
  239. package/build/types/capabilities/even-realities-g2.d.ts.map +1 -1
  240. package/build/types/capabilities/even-realities-g2.js +2 -2
  241. package/build/types/capabilities/even-realities-g2.js.map +1 -1
  242. package/build/types/enums.d.ts +1 -5
  243. package/build/types/enums.d.ts.map +1 -1
  244. package/build/types/enums.js +1 -8
  245. package/build/types/enums.js.map +1 -1
  246. package/build/types/hardware.d.ts +1 -1
  247. package/build/types/hardware.d.ts.map +1 -1
  248. package/build/types/hardware.js +1 -0
  249. package/build/types/hardware.js.map +1 -1
  250. package/build/types/index.d.ts +3 -7
  251. package/build/types/index.d.ts.map +1 -1
  252. package/build/types/index.js +3 -7
  253. package/build/types/index.js.map +1 -1
  254. package/build/utils/display/composer/ColumnComposer.d.ts +1 -1
  255. package/build/utils/display/composer/ColumnComposer.js +1 -1
  256. package/build/utils/display/composer/ColumnComposer.js.map +1 -1
  257. package/build/utils/display/scene/process.d.ts.map +1 -1
  258. package/build/utils/display/scene/process.js +4 -3
  259. package/build/utils/display/scene/process.js.map +1 -1
  260. package/build/utils/display/scene/types.d.ts +6 -2
  261. package/build/utils/display/scene/types.d.ts.map +1 -1
  262. package/build/utils/display/scene/types.js.map +1 -1
  263. package/package.json +69 -35
  264. package/src/bluetooth-sdk/debug.ts +1 -0
  265. package/src/bluetooth-sdk/index.ts +2 -0
  266. package/src/bluetooth-sdk/ota-transport.ts +1 -0
  267. package/src/bluetooth-sdk/photo-receiver.ts +2 -0
  268. package/src/bluetooth-sdk/react.ts +1 -0
  269. package/src/bluetooth-sdk/types.ts +1 -0
  270. package/src/engine.ts +12 -3
  271. package/src/facades/display.ts +12 -2
  272. package/src/facades/displayMirror.ts +7 -15
  273. package/src/facades/glasses.ts +8 -4
  274. package/src/facades/ota.ts +36 -11
  275. package/src/facades/pairing.ts +103 -23
  276. package/src/facades/phoneNotifications.ts +32 -10
  277. package/src/facades/speakers.ts +1 -1
  278. package/src/facades/speech.ts +21 -7
  279. package/src/generated/releaseMetadata.ts +20 -0
  280. package/src/index.ts +31 -5
  281. package/src/react/MentraLiveOtaFlow.tsx +754 -0
  282. package/src/react/index.ts +21 -0
  283. package/src/react/useEngineSnapshot.ts +16 -0
  284. package/src/react/useMentraLiveOta.ts +793 -0
  285. package/src/services/AppRegistry.ts +16 -0
  286. package/src/services/BlobStore.ts +57 -10
  287. package/src/services/CustomDashboardModel.ts +83 -0
  288. package/src/services/CustomDashboardService.ts +544 -0
  289. package/src/services/DashboardCoordinator.ts +121 -0
  290. package/src/services/DeviceEventRouter.ts +48 -8
  291. package/src/services/DisplayProcessor.ts +3 -3
  292. package/src/services/GestureDotsService.ts +164 -0
  293. package/src/services/GlassesStatusProjection.ts +20 -11
  294. package/src/services/HotspotOtaTransport.ts +8 -7
  295. package/src/services/LocalDisplayManager.ts +198 -18
  296. package/src/services/LocalMiniappRuntime.ts +37 -0
  297. package/src/services/MiniappExitGesture.ts +76 -0
  298. package/src/services/OtaAutoChain.ts +130 -0
  299. package/src/services/OtaErrorMapping.ts +80 -0
  300. package/src/services/OtaInstallCoordinator.ts +40 -13
  301. package/src/services/OtaService.ts +1 -2
  302. package/src/services/OtaUpdateCheckService.ts +33 -19
  303. package/src/services/PhoneNotificationsSync.ts +125 -9
  304. package/src/services/PhonePhotoCoordinator.ts +1 -1
  305. package/src/services/PhoneStreamCoordinator.ts +10 -6
  306. package/src/services/SceneRenderer.ts +4 -0
  307. package/src/services/StreamLifecycleController.ts +4 -0
  308. package/src/services/asg/localNetworkTransport.ts +20 -19
  309. package/src/services/effectiveCapabilities.ts +27 -7
  310. package/src/services/gestureClaims.ts +28 -0
  311. package/src/services/gestureDotGlyphs.ts +15 -0
  312. package/src/services/glassesClockSync.ts +5 -1
  313. package/src/services/iosNotificationAppCatalog.ts +39 -0
  314. package/src/services/otaDisplayState.ts +1 -1
  315. package/src/services/otaInstallPolicy.ts +5 -1
  316. package/src/services/otaLegacyMapping.ts +1 -1
  317. package/src/services/otaManifestPolicy.ts +43 -0
  318. package/src/services/otaManifestUrl.ts +27 -36
  319. package/src/services/otaReleaseVersion.ts +22 -0
  320. package/src/stores/appVisibility.ts +3 -0
  321. package/src/stores/apps.ts +7 -1
  322. package/src/stores/core.ts +4 -4
  323. package/src/stores/display.ts +16 -48
  324. package/src/stores/glasses.ts +15 -6
  325. package/src/stores/settings.ts +140 -5
  326. package/src/types/applet.ts +3 -1
  327. package/src/types/capabilities/even-realities-g2.ts +3 -2
  328. package/src/types/enums.ts +1 -9
  329. package/src/types/hardware.ts +2 -1
  330. package/src/types/index.ts +3 -7
  331. package/src/utils/display/composer/ColumnComposer.ts +1 -1
  332. package/src/utils/display/scene/process.ts +4 -3
  333. package/src/utils/display/scene/types.ts +0 -0
  334. package/build/devtools.d.ts +0 -11
  335. package/build/devtools.d.ts.map +0 -1
  336. package/build/devtools.js +0 -13
  337. package/build/devtools.js.map +0 -1
  338. package/build/internal.d.ts +0 -67
  339. package/build/internal.d.ts.map +0 -1
  340. package/build/internal.js +0 -90
  341. package/build/internal.js.map +0 -1
  342. package/src/devtools.ts +0 -13
  343. package/src/internal.ts +0 -155
@@ -17,8 +17,7 @@
17
17
  * state written mid-pass queues a follow-up pass — the same way a setState
18
18
  * during React effects scheduled a re-render after the current effect batch.
19
19
  */
20
- import BluetoothSdk from "@glassly/bluetooth-sdk/internal"
21
- import type {OtaProgress, OtaStatus} from "@glassly/bluetooth-sdk/internal"
20
+ import BluetoothSdk, {type OtaProgress, type OtaStatus} from "@glassly/bluetooth-sdk"
22
21
  import GlobalEventEmitter from "../utils/GlobalEventEmitter"
23
22
  import {isGlassesConnected, useGlassesStore} from "../stores/glasses"
24
23
  import {resolveOtaManifestUrl} from "./otaManifestUrl"
@@ -27,10 +26,12 @@ import type {OtaCheckCurrentGlassesResult} from "./OtaUpdateCheckService"
27
26
  import {deriveDisplayState, type DisplayState} from "./otaDisplayState"
28
27
  import {
29
28
  BES_CONTINUE_LOCKOUT_MS,
29
+ BES_RESTART_TIMEOUT_MS,
30
30
  DOWNLOAD_STUCK_TIMEOUT_MS,
31
31
  GLOBAL_OTA_TIMEOUT_MS,
32
32
  LEGACY_APK_COMPLETION_SETTLE_MS,
33
33
  LEGACY_BES_CONTINUE_LOCKOUT_MS,
34
+ LEGACY_BES_RESTART_TIMEOUT_MS,
34
35
  LEGACY_DOWNLOAD_STUCK_TIMEOUT_MS,
35
36
  LEGACY_GLOBAL_OTA_TIMEOUT_MS,
36
37
  LEGACY_MTK_INSTALL_TIMEOUT_MS,
@@ -170,6 +171,8 @@ export interface OtaInstallSnapshot {
170
171
  /** Pre-ota_start phone staging/join state for a hotspot attempt. */
171
172
  hotspotPhase: HotspotOtaPhase
172
173
  hotspotArtifactPercent: number | null
174
+ /** Transport selected from the checked glasses capabilities and Wi-Fi state. */
175
+ transport: "wifi" | "hotspot"
173
176
  }
174
177
 
175
178
  type OtaStartOutcome = "pending" | "acknowledged" | "rejected"
@@ -244,6 +247,7 @@ class OtaInstallCoordinator {
244
247
  private pingInterval: ReturnType<typeof setInterval> | null = null
245
248
  private queryReplyTimeout: ReturnType<typeof setTimeout> | null = null
246
249
  private continueLockoutTimer: ReturnType<typeof setTimeout> | null = null
250
+ private restartRecoveryTimeout: ReturnType<typeof setTimeout> | null = null
247
251
  private legacyApkSettleTimer: ReturnType<typeof setTimeout> | null = null
248
252
  private mtkStallDetectTimer: ReturnType<typeof setTimeout> | null = null
249
253
  private mtkSimTickTimer: ReturnType<typeof setInterval> | null = null
@@ -407,7 +411,7 @@ class OtaInstallCoordinator {
407
411
  if (this.otaStartOwnership?.outcome === "pending") {
408
412
  void this.sendOtaStartWithWatchdogs()
409
413
  } else {
410
- void BluetoothSdk.sendOtaQueryStatus().catch(() => {})
414
+ void BluetoothSdk.queryOtaStatus().catch(() => {})
411
415
  this.armQueryReplyFallback("retry")
412
416
  }
413
417
  }
@@ -449,16 +453,17 @@ class OtaInstallCoordinator {
449
453
  * stays in the screen): clear the selected update, and after an APK step
450
454
  * clear the stale build number so the next check re-reads version_info.
451
455
  */
452
- finish(): void {
456
+ async finish(): Promise<void> {
453
457
  if (this.otaStartOwnership?.outcome !== "pending") {
454
458
  this.otaStartOwnership = null
455
459
  }
456
460
  useGlassesStore.getState().setOtaUpdateAvailable(null)
457
461
  if (this.apkStepSeen) {
458
- BluetoothSdk.updateGlasses({buildNumber: ""})
459
462
  useGlassesStore.getState().setGlassesInfo({buildNumber: ""})
460
463
  }
461
- void this.teardownHotspotTransport()
464
+ // The post-install check needs internet again. Do not let callers leave the
465
+ // terminal screen while Android still owns the no-internet hotspot route.
466
+ await this.teardownHotspotTransport()
462
467
  }
463
468
 
464
469
  /**
@@ -467,8 +472,8 @@ class OtaInstallCoordinator {
467
472
  * deriveDisplayState doesn't resurrect the stale install when the host
468
473
  * navigates away and back through /ota routes.
469
474
  */
470
- discard(): void {
471
- this.finish()
475
+ async discard(): Promise<void> {
476
+ await this.finish()
472
477
  const store = useGlassesStore.getState()
473
478
  store.setOtaStatus(null)
474
479
  store.setOtaProgress(null)
@@ -505,6 +510,7 @@ class OtaInstallCoordinator {
505
510
  versionChangePhase: this.deriveVersionChangePhase(connected),
506
511
  hotspotPhase: this.hotspotPhase,
507
512
  hotspotArtifactPercent: this.hotspotArtifactPercent,
513
+ transport: this.selectedTransport,
508
514
  }
509
515
  }
510
516
 
@@ -997,12 +1003,22 @@ class OtaInstallCoordinator {
997
1003
  // (15s unified, 35s for legacy-shaped sessions — WP 8C-f).
998
1004
  if (displayStateChanged && displayState === "restarting") {
999
1005
  this.clearContinueLockoutTimer()
1006
+ this.clearRestartRecoveryTimeout()
1000
1007
  const lockoutMs = legacySession ? LEGACY_BES_CONTINUE_LOCKOUT_MS : BES_CONTINUE_LOCKOUT_MS
1008
+ const restartTimeoutMs = legacySession ? LEGACY_BES_RESTART_TIMEOUT_MS : BES_RESTART_TIMEOUT_MS
1001
1009
  this.setContinueButtonDisabled(true)
1002
1010
  this.continueLockoutTimer = setTimeout(() => {
1003
1011
  this.continueLockoutTimer = null
1004
1012
  this.setContinueButtonDisabled(false)
1005
1013
  }, lockoutMs)
1014
+ this.restartRecoveryTimeout = setTimeout(() => {
1015
+ this.restartRecoveryTimeout = null
1016
+ if (this.computeDisplayStateNow() !== "restarting") return
1017
+ console.log(`[OTA_PROGRESS] watchdog: glasses did not finish restarting in ${restartTimeoutMs}ms`)
1018
+ this.setErrorMsg(OtaProgressMessages.restartTimeout)
1019
+ }, restartTimeoutMs)
1020
+ } else if (displayStateChanged) {
1021
+ this.clearRestartRecoveryTimeout()
1006
1022
  }
1007
1023
 
1008
1024
  // Legacy MTK install stall simulation (WP 8C-e): display-only. The simulation
@@ -1065,7 +1081,7 @@ class OtaInstallCoordinator {
1065
1081
  if (this.otaStartOwnership?.outcome === "pending") {
1066
1082
  void this.sendOtaStartWithWatchdogs()
1067
1083
  }
1068
- void BluetoothSdk.sendOtaQueryStatus()
1084
+ void BluetoothSdk.queryOtaStatus().catch(() => {})
1069
1085
  this.armQueryReplyFallback("reconnect")
1070
1086
  return true
1071
1087
  }
@@ -1109,7 +1125,7 @@ class OtaInstallCoordinator {
1109
1125
  if (noSessionYet) {
1110
1126
  if (!isIdleStatus && this.otaStartOwnership?.outcome === "acknowledged") {
1111
1127
  console.log("[OTA_PROGRESS] initial mount, acknowledged session has no cached status — reconciling")
1112
- void BluetoothSdk.sendOtaQueryStatus().catch(() => {})
1128
+ void BluetoothSdk.queryOtaStatus().catch(() => {})
1113
1129
  this.armQueryReplyFallback("initial-mount")
1114
1130
  return
1115
1131
  }
@@ -1125,7 +1141,7 @@ class OtaInstallCoordinator {
1125
1141
  void this.sendOtaStartWithWatchdogs()
1126
1142
  } else {
1127
1143
  console.log("[OTA_PROGRESS] initial mount, session exists, sending ota_query_status")
1128
- void BluetoothSdk.sendOtaQueryStatus()
1144
+ void BluetoothSdk.queryOtaStatus().catch(() => {})
1129
1145
  this.armQueryReplyFallback("initial-mount")
1130
1146
  }
1131
1147
  }
@@ -1154,7 +1170,7 @@ class OtaInstallCoordinator {
1154
1170
  this.clearProgressTimeout()
1155
1171
  this.onFirstActivity()
1156
1172
  this.onFirstNonZeroProgress()
1157
- void BluetoothSdk.sendOtaQueryStatus()
1173
+ void BluetoothSdk.queryOtaStatus().catch(() => {})
1158
1174
  useGlassesStore.getState().setMtkUpdatedThisSession(true)
1159
1175
  }
1160
1176
 
@@ -1345,7 +1361,7 @@ class OtaInstallCoordinator {
1345
1361
  const s = useGlassesStore.getState().otaStatus
1346
1362
  if (s?.stepType === "apk" && s.phase === "install" && s.status === "in_progress") {
1347
1363
  this.apkInstallPollInFlight = true
1348
- void BluetoothSdk.sendOtaQueryStatus()
1364
+ void BluetoothSdk.queryOtaStatus()
1349
1365
  .catch(() => {})
1350
1366
  .finally(() => {
1351
1367
  this.apkInstallPollInFlight = false
@@ -1492,6 +1508,9 @@ class OtaInstallCoordinator {
1492
1508
  otaVersionUrl = this.hotspotManifestUrl
1493
1509
  this.maybeArmStuckWatchdog()
1494
1510
  }
1511
+ if (!otaVersionUrl) {
1512
+ throw new Error("OTA is disabled because this build has no immutable manifest pin")
1513
+ }
1495
1514
  console.log(`[OTA_PROGRESS] sending ota_start with ${this.selectedTransport} manifest URL: ${otaVersionUrl}`)
1496
1515
  nativeStartAttempted = true
1497
1516
  await BluetoothSdk.startOtaUpdate(otaVersionUrl)
@@ -1662,6 +1681,13 @@ class OtaInstallCoordinator {
1662
1681
  }
1663
1682
  }
1664
1683
 
1684
+ private clearRestartRecoveryTimeout(): void {
1685
+ if (this.restartRecoveryTimeout) {
1686
+ clearTimeout(this.restartRecoveryTimeout)
1687
+ this.restartRecoveryTimeout = null
1688
+ }
1689
+ }
1690
+
1665
1691
  private clearLegacyApkSettleTimer(): void {
1666
1692
  if (this.legacyApkSettleTimer) {
1667
1693
  clearTimeout(this.legacyApkSettleTimer)
@@ -1700,6 +1726,7 @@ class OtaInstallCoordinator {
1700
1726
  this.clearPingInterval()
1701
1727
  this.clearLegacyApkSettleTimer()
1702
1728
  this.clearMtkSimulationTimers()
1729
+ this.clearRestartRecoveryTimeout()
1703
1730
  }
1704
1731
  }
1705
1732
 
@@ -10,8 +10,7 @@
10
10
  *
11
11
  * Started by `engine.start()`. Idempotent.
12
12
  */
13
- import BluetoothSdk from "@glassly/bluetooth-sdk/internal"
14
- import type {OtaStatus} from "@glassly/bluetooth-sdk/internal"
13
+ import BluetoothSdk, {type OtaStatus} from "@glassly/bluetooth-sdk"
15
14
  import GlobalEventEmitter from "../utils/GlobalEventEmitter"
16
15
  import {useGlassesStore} from "../stores/glasses"
17
16
  import {handleOtaClockSkewFromGlasses} from "./glassesClockSync"
@@ -1,9 +1,9 @@
1
- import BluetoothSdk from "@glassly/bluetooth-sdk/internal"
2
- import type {OtaUpdateInfo} from "@glassly/bluetooth-sdk/internal"
1
+ import BluetoothSdk, {type OtaUpdateInfo} from "@glassly/bluetooth-sdk"
2
+ import {ENGINE_RELEASE_METADATA} from "../generated/releaseMetadata"
3
3
  import {getGlassesSystemTimeMs, isGlassesConnected, useGlassesStore, waitForGlassesState} from "../stores/glasses"
4
- import {SETTINGS, useSettingsStore} from "../stores/settings"
5
4
  import {maybeFixGlassesClockFromVersionInfo} from "./glassesClockSync"
6
- import {resolveOtaManifestUrl} from "./otaManifestUrl"
5
+ import {hasConfiguredModernOtaManifestPin, resolveOtaManifestUrl} from "./otaManifestUrl"
6
+ import {resolveOtaReleaseVersion} from "./otaReleaseVersion"
7
7
 
8
8
  /**
9
9
  * Phone-side mirror of ASG's `DOWNGRADE_FLOOR_VERSION_CODE`. Set to the versionCode of the first
@@ -39,6 +39,7 @@ export interface BesFirmware {
39
39
  }
40
40
 
41
41
  export interface VersionJson {
42
+ releaseVersion?: string
42
43
  apps?: {
43
44
  [packageName: string]: VersionInfo
44
45
  }
@@ -67,6 +68,8 @@ export interface OtaCheckResult {
67
68
  * instead of racing it with a second fetch of the same URL.
68
69
  */
69
70
  manifestBody: string | null
71
+ /** Coordinated release identity for the selected OTA pin. */
72
+ releaseVersion: string | null
70
73
  /**
71
74
  * Why the check failed, when it did. "network" is retryable (connection/server
72
75
  * trouble); "pin_unavailable" is permanent for this app build (manifest 404/gone,
@@ -107,6 +110,7 @@ function emptyCheckResult(skippedReason?: OtaCheckSkippedReason): OtaCheckCurren
107
110
  besVersion: null,
108
111
  isApkDowngrade: false,
109
112
  manifestBody: null,
113
+ releaseVersion: null,
110
114
  updateInfo: null,
111
115
  isRequired: true,
112
116
  skippedReason,
@@ -123,7 +127,7 @@ async function fetchVersionInfoDetailed(url: string): Promise<ManifestFetchResul
123
127
  try {
124
128
  const response = await fetch(url)
125
129
  if (!response.ok) {
126
- console.error("Failed to fetch version info:", response.status)
130
+ console.warn("Failed to fetch version info:", response.status)
127
131
  // 4xx: the manifest does not exist (or is gone) — permanent for this app
128
132
  // build, retrying cannot help. Everything else is transient server/network
129
133
  // trouble and retryable.
@@ -135,11 +139,11 @@ async function fetchVersionInfoDetailed(url: string): Promise<ManifestFetchResul
135
139
  try {
136
140
  return {json: await response.json()}
137
141
  } catch (parseError) {
138
- console.error("OTA: manifest is not valid JSON:", parseError)
142
+ console.warn("OTA: manifest is not valid JSON:", parseError)
139
143
  return {json: null, failureReason: "pin_unavailable"}
140
144
  }
141
145
  } catch (error) {
142
- console.error("OTA: Error fetching version info:", error)
146
+ console.warn("OTA: Error fetching version info:", error)
143
147
  return {json: null, failureReason: "network"}
144
148
  }
145
149
  }
@@ -299,6 +303,7 @@ export async function checkForOtaUpdate(
299
303
  besVersion: null,
300
304
  isApkDowngrade: false,
301
305
  manifestBody: null,
306
+ releaseVersion: null,
302
307
  checkFailureReason: fetched.failureReason ?? "network",
303
308
  }
304
309
  }
@@ -330,6 +335,7 @@ export async function checkForOtaUpdate(
330
335
  besVersion: null,
331
336
  isApkDowngrade: false,
332
337
  manifestBody: null,
338
+ releaseVersion: null,
333
339
  checkFailureReason: "pin_unavailable",
334
340
  }
335
341
  }
@@ -368,6 +374,12 @@ export async function checkForOtaUpdate(
368
374
  besVersion: versionJson?.bes_firmware?.version || null,
369
375
  isApkDowngrade: apkDirection === "downgrade",
370
376
  manifestBody: versionJson ? JSON.stringify(versionJson) : null,
377
+ releaseVersion: resolveOtaReleaseVersion({
378
+ manifestReleaseVersion: versionJson.releaseVersion,
379
+ manifestUrl: otaVersionUrl,
380
+ packagedManifestUrl: ENGINE_RELEASE_METADATA.otaManifestUrl,
381
+ packagedReleaseIdentity: ENGINE_RELEASE_METADATA.releaseIdentity,
382
+ }),
371
383
  }
372
384
  } catch (error) {
373
385
  console.error("Error checking for OTA update:", error)
@@ -380,6 +392,7 @@ export async function checkForOtaUpdate(
380
392
  besVersion: null,
381
393
  isApkDowngrade: false,
382
394
  manifestBody: null,
395
+ releaseVersion: null,
383
396
  checkFailureReason: "network",
384
397
  }
385
398
  }
@@ -401,22 +414,19 @@ export async function checkCurrentGlassesForUpdate(
401
414
  return emptyCheckResult("disconnected")
402
415
  }
403
416
 
404
- const embeddedManifestUrl = process.env.EXPO_PUBLIC_ASG_OTA_VERSION_URL?.trim()
405
- const superMode = useSettingsStore.getState().getSetting(SETTINGS.super_mode.key)
406
- const overrideUrl = useSettingsStore.getState().getSetting(SETTINGS.ota_version_url.key)
407
- const overrideActive = superMode && typeof overrideUrl === "string" && overrideUrl.trim() !== ""
408
- if (!embeddedManifestUrl && !overrideActive) {
409
- console.log("OTA: check skipped - mobile app has no embedded OTA manifest pin")
417
+ if (!hasConfiguredModernOtaManifestPin()) {
418
+ console.log("OTA: check skipped - this app or Engine package has no embedded OTA manifest pin")
410
419
  return emptyCheckResult("dev_build")
411
420
  }
412
- if (!embeddedManifestUrl) {
413
- console.log("OTA: no embedded manifest pin, but super-mode manifest override is active - checking anyway")
414
- }
415
421
 
416
422
  if (refreshVersionInfo) {
417
- void BluetoothSdk.requestVersionInfo().catch((error) => {
418
- console.warn("OTA: Failed to request version_info from glasses:", error)
419
- })
423
+ void BluetoothSdk.requestVersionInfo()
424
+ .then((versionInfo) => {
425
+ useGlassesStore.getState().setGlassesInfo(versionInfo)
426
+ })
427
+ .catch((error) => {
428
+ console.warn("OTA: Failed to request version_info from glasses:", error)
429
+ })
420
430
  }
421
431
 
422
432
  let buildNumber = useGlassesStore.getState().buildNumber
@@ -470,6 +480,10 @@ export async function checkCurrentGlassesForUpdate(
470
480
  }
471
481
 
472
482
  const manifestUrl = resolveOtaManifestUrl(useGlassesStore.getState().otaVersionUrl, buildNumber)
483
+ if (!manifestUrl) {
484
+ console.log("OTA: check skipped - no OTA manifest pin resolved")
485
+ return emptyCheckResult("dev_build")
486
+ }
473
487
  const result = await checkForOtaUpdate(
474
488
  manifestUrl,
475
489
  buildNumber,
@@ -1,24 +1,50 @@
1
1
  /**
2
- * Phone-notifications sync — engine-owned. Pushes the notification-forwarding
3
- * operational kill switch + config (`android_notification_listener_enabled`
4
- * and `notifications_blocklist`) to the native
5
- * NotificationListener via `CrustModule.setNotificationConfig`, so
2
+ * Phone-notifications sync — engine-owned. Started by `engine.start()`.
3
+ *
4
+ * Android: pushes the notification-forwarding operational kill switch + config
5
+ * (`android_notification_listener_enabled` and `notifications_blocklist`) to
6
+ * the native NotificationListener via `CrustModule.setNotificationConfig`, so
6
7
  * the blocklist reaches the listener for ANY host — not just the Glassly App,
7
8
  * where this used to live in MantleManager. The Android service is desired by
8
9
  * default now that its cold start is isolated from React Native. Native code
9
10
  * keeps the component discoverable in Android's notification-access Settings,
10
11
  * but only binds the service after access is granted. The debug setting remains
11
12
  * a hidden emergency kill switch; Notify's running state gates presentation,
12
- * not capture. Android-only; a no-op elsewhere. Started by `engine.start()`.
13
+ * not capture.
14
+ *
15
+ * iOS: the G2 consumes notifications on-firmware via ANCS, so the phone's job
16
+ * is config, not capture. This sync (a) records the apps the glasses report
17
+ * posting notifications (`notification_app_seen` → `ios_notification_seen_apps`)
18
+ * and (b) maps the shared `notifications_blocklist` + the G2 display settings
19
+ * onto the native `notification_filter` / `notification_ctrl` DeviceStore keys,
20
+ * which the bluetooth-sdk pushes to the glasses (and replays on reconnect).
21
+ * The firmware filter is allow-based and fail-open: an empty blocklist disables
22
+ * whitelisting entirely; any block enables it with allowlist = (catalog ∪ seen)
23
+ * minus blocked.
13
24
  */
25
+ import {Platform} from "react-native"
14
26
  import {shallow} from "zustand/shallow"
15
27
  import CrustModule from "@glassly/crust"
28
+ import BluetoothSdk from "@glassly/bluetooth-sdk/internal"
29
+ import type {
30
+ G2NotificationCtrlSetting,
31
+ G2NotificationFilterSetting,
32
+ NotificationAppSeenEvent,
33
+ } from "@glassly/bluetooth-sdk/internal"
16
34
 
17
35
  import {useSettingsStore, SETTINGS} from "../stores/settings"
36
+ import {IOS_NOTIFICATION_APP_CATALOG} from "./iosNotificationAppCatalog"
37
+
38
+ export interface SeenNotificationApp {
39
+ appId: string
40
+ displayName: string
41
+ lastSeen: number
42
+ }
18
43
 
19
44
  let unsubscribe: (() => void) | null = null
45
+ let seenAppsSubscription: {remove(): void} | null = null
20
46
 
21
- function pushConfig(): void {
47
+ function pushAndroidConfig(): void {
22
48
  const s = useSettingsStore.getState()
23
49
  const listenerEnabled = Boolean(s.getSetting(SETTINGS.android_notification_listener_enabled.key))
24
50
  const blocklist = s.getSetting(SETTINGS.notifications_blocklist.key)
@@ -27,20 +53,110 @@ function pushConfig(): void {
27
53
  )
28
54
  }
29
55
 
56
+ export function getSeenNotificationApps(): SeenNotificationApp[] {
57
+ const seen = useSettingsStore.getState().getSetting(SETTINGS.ios_notification_seen_apps.key)
58
+ return Array.isArray(seen) ? (seen as SeenNotificationApp[]) : []
59
+ }
60
+
61
+ /** Catalog ∪ seen apps, seen display names winning. Sorted by name. */
62
+ export function getKnownNotificationApps(): Array<{appId: string; displayName: string}> {
63
+ const merged = new Map<string, string>()
64
+ for (const app of IOS_NOTIFICATION_APP_CATALOG) {
65
+ merged.set(app.appId, app.displayName)
66
+ }
67
+ for (const app of getSeenNotificationApps()) {
68
+ if (app.appId) merged.set(app.appId, app.displayName || app.appId)
69
+ }
70
+ return [...merged.entries()]
71
+ .map(([appId, displayName]) => ({appId, displayName}))
72
+ .sort((a, b) => a.displayName.localeCompare(b.displayName))
73
+ }
74
+
75
+ export function computeG2NotificationFilter(): G2NotificationFilterSetting {
76
+ const blocklistValue = useSettingsStore.getState().getSetting(SETTINGS.notifications_blocklist.key)
77
+ const blocklist = Array.isArray(blocklistValue) ? (blocklistValue as string[]) : []
78
+ const blocked = new Set(blocklist)
79
+ // The list is included even while filtering is off, so the on-glass file
80
+ // already holds the user's current list when a first block enables it.
81
+ const apps = getKnownNotificationApps()
82
+ .filter((app) => !blocked.has(app.appId))
83
+ .map((app) => ({id: app.appId, name: app.displayName}))
84
+ return {enabled: blocked.size > 0, apps}
85
+ }
86
+
87
+ function computeG2NotificationCtrl(): G2NotificationCtrlSetting {
88
+ const s = useSettingsStore.getState()
89
+ return {
90
+ enabled: Boolean(s.getSetting(SETTINGS.g2_notifications_enabled.key)),
91
+ auto_display: Boolean(s.getSetting(SETTINGS.g2_notification_auto_display.key)),
92
+ disp_time: Number(s.getSetting(SETTINGS.g2_notification_disp_time.key)) || 0,
93
+ avoid_disturb: Boolean(s.getSetting(SETTINGS.g2_notification_avoid_disturb.key)),
94
+ }
95
+ }
96
+
97
+ function pushIosConfig(): void {
98
+ // The native DeviceStore dedupes unchanged values and replays these to the
99
+ // glasses on every reconnect, so pushing eagerly is cheap.
100
+ void Promise.resolve(
101
+ BluetoothSdk.updateBluetoothSettings({
102
+ notification_ctrl: computeG2NotificationCtrl(),
103
+ notification_filter: computeG2NotificationFilter(),
104
+ }),
105
+ ).catch((err: unknown) =>
106
+ console.warn(`PhoneNotificationsSync: notification config push failed: ${(err as Error)?.message ?? err}`),
107
+ )
108
+ }
109
+
110
+ /** Record a glasses-reported source app. Writes only on new app / renamed app. */
111
+ function recordSeenApp(event: NotificationAppSeenEvent): void {
112
+ if (!event.appId) return
113
+ const seen = getSeenNotificationApps()
114
+ const existing = seen.find((app) => app.appId === event.appId)
115
+ const displayName = event.displayName || existing?.displayName || event.appId
116
+ if (existing && existing.displayName === displayName) return
117
+ const next = seen
118
+ .filter((app) => app.appId !== event.appId)
119
+ .concat([{appId: event.appId, displayName, lastSeen: event.timestamp || Date.now()}])
120
+ useSettingsStore.getState().setSetting(SETTINGS.ios_notification_seen_apps.key, next)
121
+ }
122
+
30
123
  export function startPhoneNotificationsSync(): void {
31
124
  if (unsubscribe) return
32
- pushConfig() // initial sync so the listener has the current config
125
+
126
+ if (Platform.OS === "android") {
127
+ pushAndroidConfig() // initial sync so the listener has the current config
128
+ unsubscribe = useSettingsStore.subscribe(
129
+ (state) => ({
130
+ listenerEnabled: state.getSetting(SETTINGS.android_notification_listener_enabled.key),
131
+ blocklist: state.getSetting(SETTINGS.notifications_blocklist.key),
132
+ }),
133
+ () => pushAndroidConfig(),
134
+ {equalityFn: shallow},
135
+ )
136
+ return
137
+ }
138
+
139
+ if (Platform.OS !== "ios") return
140
+
141
+ pushIosConfig()
33
142
  unsubscribe = useSettingsStore.subscribe(
34
143
  (state) => ({
35
- listenerEnabled: state.getSetting(SETTINGS.android_notification_listener_enabled.key),
36
144
  blocklist: state.getSetting(SETTINGS.notifications_blocklist.key),
145
+ seenApps: state.getSetting(SETTINGS.ios_notification_seen_apps.key),
146
+ enabled: state.getSetting(SETTINGS.g2_notifications_enabled.key),
147
+ autoDisplay: state.getSetting(SETTINGS.g2_notification_auto_display.key),
148
+ dispTime: state.getSetting(SETTINGS.g2_notification_disp_time.key),
149
+ avoidDisturb: state.getSetting(SETTINGS.g2_notification_avoid_disturb.key),
37
150
  }),
38
- () => pushConfig(),
151
+ () => pushIosConfig(),
39
152
  {equalityFn: shallow},
40
153
  )
154
+ seenAppsSubscription = BluetoothSdk.addListener("notification_app_seen", recordSeenApp)
41
155
  }
42
156
 
43
157
  export function stopPhoneNotificationsSync(): void {
44
158
  unsubscribe?.()
45
159
  unsubscribe = null
160
+ seenAppsSubscription?.remove()
161
+ seenAppsSubscription = null
46
162
  }
@@ -166,7 +166,7 @@ export class PhonePhotoCoordinator {
166
166
  // to time out. Fail fast with a typed error.
167
167
  //
168
168
  // We DON'T pre-check `hasCamera` here — the canonical capability data
169
- // lives in `getModelCapabilities(deviceModel)` in the engine types and
169
+ // lives in `getModelCapabilities(deviceModel)` from @glassly/engine and
170
170
  // pulling that into this file would add a cross-package import. If a
171
171
  // cameraless device receives the BLE photo command, the glasses-side
172
172
  // handler will return a photo_response error within ~1s and the gated
@@ -796,12 +796,6 @@ export class PhoneStreamCoordinator {
796
796
  for (const reject of entry.hlsReadyRejecters) reject(pendingErr)
797
797
  entry.hlsReadyResolvers = []
798
798
  entry.hlsReadyRejecters = []
799
- // Cloudflare DELETE is fire-and-forget — failing it doesn't undo the
800
- // BLE stop we're about to do, and Cloudflare's idle-input scavenger
801
- // will eventually clean up leaked inputs.
802
- teardownManagedStream(entry.liveInputId).catch((err) => {
803
- console.warn("[STREAM] teardownManagedStream failed:", err)
804
- })
805
799
  }
806
800
 
807
801
  try {
@@ -816,6 +810,16 @@ export class PhoneStreamCoordinator {
816
810
  // Only clear if we're still the active entry (defensive — runExclusive
817
811
  // serializes us, so this should always be true).
818
812
  if (this.current === entry) this.current = null
813
+
814
+ if (entry.kind === "managed") {
815
+ // Start remote cleanup only after the publisher has stopped, but do not
816
+ // hold the local transition lock on an unbounded network request. The
817
+ // runtime's durable cleanup queue owns retries if this request fails or
818
+ // the app exits before Cloudflare finishes finalizing the recording.
819
+ void teardownManagedStream(entry.liveInputId).catch((err) => {
820
+ console.warn("[STREAM] teardownManagedStream failed:", err)
821
+ })
822
+ }
819
823
  }
820
824
  }
821
825
  }
@@ -273,6 +273,10 @@ class SceneRenderer {
273
273
  }
274
274
 
275
275
  private sendFrame(frame: SceneFrame): void {
276
+ // Single native view: only the visible surface's frames go out. Suppressed
277
+ // main frames stay committed; setDashboardActive(false) replays them.
278
+ const dashboardActive = useDisplayStore.getState().dashboardActive
279
+ if (dashboardActive !== (frame.view === "dashboard")) return
276
280
  try {
277
281
  // Frames carry the canvas they were laid out against; only a frame from
278
282
  // a path that predates the tag falls back to the current capabilities.
@@ -2,6 +2,10 @@
2
2
  * Drives RTMP stream liveliness checks via a keep-alive heartbeat:
3
3
  * `setActive(true)` starts the timer; each tick sends a keep-alive with a
4
4
  * fresh ackId; `maxMissedAcks` consecutive timeouts fire `onTimeout`.
5
+ *
6
+ * Phone-owned successor to the retired cloud stream lifecycle controller.
7
+ * It uses the local `LifecycleLogger` interface because pino is not a phone
8
+ * dependency.
5
9
  */
6
10
 
7
11
  export interface LifecycleLogger {
@@ -1,5 +1,5 @@
1
1
  import * as RNFS from "@dr.pogodin/react-native-fs"
2
- import {LocalNetwork, type LocalNetworkDownloadProgress} from "@glassly/bluetooth-sdk/internal"
2
+ import {otaLocalNetwork, type OtaLocalNetworkDownloadProgress} from "@glassly/bluetooth-sdk/ota-transport"
3
3
  import {Buffer} from "buffer"
4
4
  import {Platform} from "react-native"
5
5
  import WifiManager from "react-native-wifi-reborn"
@@ -17,7 +17,7 @@ export function shouldUseScopedLocalNetwork(os: string, moduleAvailable: boolean
17
17
  }
18
18
 
19
19
  function supportsScopedConnection(): boolean {
20
- return shouldUseScopedLocalNetwork(Platform.OS, LocalNetwork != null)
20
+ return shouldUseScopedLocalNetwork(Platform.OS, otaLocalNetwork.isAvailable())
21
21
  }
22
22
 
23
23
  function normalizeHeaders(headers?: HeadersInit): Record<string, string> {
@@ -48,25 +48,25 @@ export const localNetworkTransport = {
48
48
  await WifiManager.connectToProtectedSSID(ssid, password, false, false)
49
49
  return undefined
50
50
  }
51
- const result = await LocalNetwork!.connect(ssid, password)
51
+ const result = await otaLocalNetwork.connect(ssid, password)
52
52
  scopedConnectionActive = true
53
53
  return result.localAddress
54
54
  },
55
55
 
56
56
  async disconnect(): Promise<void> {
57
57
  if (supportsScopedConnection()) {
58
- await LocalNetwork!.disconnect().catch(() => {})
58
+ await otaLocalNetwork.disconnect().catch(() => {})
59
59
  }
60
60
  scopedConnectionActive = false
61
61
  nativeJobs.clear()
62
62
  },
63
63
 
64
64
  async fetch(url: string | URL | Request, init?: RequestInit, timeoutMs = 30_000): Promise<Response> {
65
- if (!scopedConnectionActive || !LocalNetwork || typeof url !== "string") {
65
+ if (!scopedConnectionActive || !otaLocalNetwork.isAvailable() || typeof url !== "string") {
66
66
  return globalThis.fetch(url, init)
67
67
  }
68
68
 
69
- const nativeModule = LocalNetwork
69
+ const nativeModule = otaLocalNetwork
70
70
  const id = requestId("request")
71
71
  const signal = init?.signal
72
72
  const cancel = () => void nativeModule.cancel(id)
@@ -92,13 +92,13 @@ export const localNetworkTransport = {
92
92
  },
93
93
 
94
94
  downloadFile(options: DownloadOptions): DownloadHandle {
95
- if (!scopedConnectionActive || !LocalNetwork) return RNFS.downloadFile(options)
95
+ if (!scopedConnectionActive || !otaLocalNetwork.isAvailable()) return RNFS.downloadFile(options)
96
96
 
97
97
  const jobId = nextJobId++
98
98
  const id = requestId("download")
99
99
  let began = false
100
100
  nativeJobs.set(jobId, id)
101
- const subscription = LocalNetwork.addListener("downloadProgress", (event: LocalNetworkDownloadProgress) => {
101
+ const subscription = otaLocalNetwork.onDownloadProgress((event: OtaLocalNetworkDownloadProgress) => {
102
102
  if (event.requestId !== id) return
103
103
  if (!began) {
104
104
  began = true
@@ -116,14 +116,15 @@ export const localNetworkTransport = {
116
116
  })
117
117
  })
118
118
 
119
- const promise = LocalNetwork.download(
120
- id,
121
- options.fromUrl,
122
- options.toFile,
123
- options.headers || {},
124
- options.connectionTimeout || 30_000,
125
- options.readTimeout || 30_000,
126
- )
119
+ const promise = otaLocalNetwork
120
+ .download(
121
+ id,
122
+ options.fromUrl,
123
+ options.toFile,
124
+ options.headers || {},
125
+ options.connectionTimeout || 30_000,
126
+ options.readTimeout || 30_000,
127
+ )
127
128
  .then((result) => {
128
129
  if (!began) {
129
130
  options.begin?.({
@@ -154,16 +155,16 @@ export const localNetworkTransport = {
154
155
 
155
156
  stopDownload(jobId: number): void {
156
157
  const nativeId = nativeJobs.get(jobId)
157
- if (nativeId && LocalNetwork) {
158
+ if (nativeId && otaLocalNetwork.isAvailable()) {
158
159
  cancelledNativeJobs.add(jobId)
159
- void LocalNetwork.cancel(nativeId)
160
+ void otaLocalNetwork.cancel(nativeId)
160
161
  return
161
162
  }
162
163
  RNFS.stopDownload(jobId)
163
164
  },
164
165
  }
165
166
 
166
- LocalNetwork?.addListener("networkLost", () => {
167
+ otaLocalNetwork.onNetworkLost(() => {
167
168
  scopedConnectionActive = false
168
169
  nativeJobs.clear()
169
170
  })