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

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 +492 -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 +516 -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
@@ -2,8 +2,8 @@
2
2
  * LocalDisplayManager
3
3
  *
4
4
  * Phone-side arbiter for display requests originating from LOCAL miniapps
5
- * (the @glassly/miniapp SDK). Mirrors the user-facing behavior of the cloud
6
- * DisplayManager for the features that matter on the phone:
5
+ * (the @glassly/miniapp SDK). Preserves the retired cloud display manager's
6
+ * user-facing behavior for the features that matter on the phone:
7
7
  *
8
8
  * - boot message ("Starting <AppName>…") with a bounded window
9
9
  * - durationMs auto-clear
@@ -115,12 +115,50 @@ class LocalDisplayManager {
115
115
 
116
116
  private coreApp: string | null = null
117
117
  private coreAppDisplay: ActiveDisplay | null = null
118
- private currentDisplay: ActiveDisplay | null = null
118
+ private _currentDisplay: ActiveDisplay | null = null
119
119
  private backgroundLock: BackgroundLock | null = null
120
120
 
121
+ /** Fired when the display transitions between empty and occupied (any package, system screens included). */
122
+ private activeDisplayListeners = new Set<(active: boolean) => void>()
123
+
124
+ private get currentDisplay(): ActiveDisplay | null {
125
+ return this._currentDisplay
126
+ }
127
+
128
+ private set currentDisplay(value: ActiveDisplay | null) {
129
+ const wasActive = this._currentDisplay !== null
130
+ this._currentDisplay = value
131
+ const isActive = value !== null
132
+ if (wasActive === isActive) return
133
+ for (const listener of this.activeDisplayListeners) {
134
+ try {
135
+ listener(isActive)
136
+ } catch (error) {
137
+ console.error(`${LOG_TAG}: onActiveDisplayChange listener failed:`, error)
138
+ }
139
+ }
140
+ }
141
+
142
+ /** True while anything (miniapp or system screen) is on the glasses display. */
143
+ public hasActiveDisplay(): boolean {
144
+ return this._currentDisplay !== null
145
+ }
146
+
147
+ /** Subscribe to empty↔occupied display transitions. Returns an unsubscribe. */
148
+ public onActiveDisplayChange(listener: (active: boolean) => void): () => void {
149
+ this.activeDisplayListeners.add(listener)
150
+ return () => this.activeDisplayListeners.delete(listener)
151
+ }
152
+
121
153
  private bootingApp: BootingApp | null = null
122
154
  private bootQueue: Map<string, {payload: DisplayPayload; resolve?: DisplayRequestResolver}> = new Map()
123
155
 
156
+ /** While true the dashboard owns the glasses: main frames are retained but not sent. */
157
+ private dashboardActive = false
158
+
159
+ /** Miniapp whose visible frame is temporarily covered by an OS exit notice. */
160
+ private exitingApp: {packageName: string; coveredDisplay: ActiveDisplay} | null = null
161
+
124
162
  private expiryTimerId: number | null = null
125
163
 
126
164
  /** Reconnect-replay subscription state (attachToRuntime). */
@@ -200,7 +238,7 @@ class LocalDisplayManager {
200
238
  */
201
239
  public showSystemNotice(text: string, durationMs: number = SYSTEM_NOTICE_DURATION_MS): void {
202
240
  if (!isGlassesConnected(useGlassesStore.getState().connection)) return
203
- if (this.bootingApp) return
241
+ if (this.bootingApp || this.exitingApp) return
204
242
  console.log(`${LOG_TAG}: showSystemNotice("${text}")`)
205
243
  this.sendNow(SYSTEM_NOTICE_PKG, {
206
244
  view: "main",
@@ -228,6 +266,88 @@ class LocalDisplayManager {
228
266
  }
229
267
  }
230
268
 
269
+ /**
270
+ * The package that currently owns the glasses screen, for gesture-claim
271
+ * arbitration (see LocalMiniappRuntime.foregroundClaimsGesture). This is
272
+ * glasses ownership, not the phone's Compositor foreground: a background
273
+ * miniapp rendering to the glasses with the phone pocketed is the owner.
274
+ *
275
+ * The live frame's owner wins; a background app that holds the display lock
276
+ * but has no frame up right now (between renders) still counts. System
277
+ * frames (boot, notices) own nothing claimable. Returns null when the
278
+ * glasses are idle.
279
+ */
280
+ public getGestureOwner(): string | null {
281
+ const current = this.currentDisplay?.packageName ?? null
282
+ if (current && current !== SYSTEM_BOOT_PKG && current !== SYSTEM_NOTICE_PKG) return current
283
+ const lock = this.backgroundLock
284
+ if (lock && this.now() <= lock.expiresAt) return lock.packageName
285
+ return null
286
+ }
287
+
288
+ /**
289
+ * The miniapp whose frame is visibly on the glasses right now.
290
+ *
291
+ * Unlike getGestureOwner(), this deliberately does not fall back to a
292
+ * background display lock: a lock holder with no live frame is not visible
293
+ * and therefore must not be closed by the system double-tap gesture.
294
+ */
295
+ public getVisibleMiniappPackageName(): string | null {
296
+ const current = this.currentDisplay?.packageName ?? null
297
+ if (!current || current === SYSTEM_BOOT_PKG || current === SYSTEM_NOTICE_PKG) return null
298
+ return current
299
+ }
300
+
301
+ /**
302
+ * Cover the currently visible miniapp with a persistent system exit notice.
303
+ * While it is up, miniapp display traffic is held off so a streaming app
304
+ * cannot immediately overwrite the notice. The caller completes the normal
305
+ * stop flow and then calls finishMiniappExit(); onUnmount does the same as a
306
+ * safety net.
307
+ */
308
+ public beginMiniappExit(packageName: string, text: string): boolean {
309
+ if (this.exitingApp !== null) return this.exitingApp.packageName === packageName
310
+ const coveredDisplay = this.currentDisplay
311
+ if (!coveredDisplay || this.getVisibleMiniappPackageName() !== packageName) return false
312
+
313
+ console.log(`${LOG_TAG}: beginMiniappExit(${packageName})`)
314
+ this.exitingApp = {packageName, coveredDisplay}
315
+ this.clearExpiryTimer()
316
+ this.sendNow(SYSTEM_NOTICE_PKG, {
317
+ view: "main",
318
+ layout: {layoutType: "text_wall", text},
319
+ })
320
+ return true
321
+ }
322
+
323
+ /** Remove an exit notice and resume the display state normal app teardown leaves behind. */
324
+ public finishMiniappExit(packageName: string): void {
325
+ if (this.exitingApp?.packageName !== packageName) return
326
+ console.log(`${LOG_TAG}: finishMiniappExit(${packageName})`)
327
+ this.exitingApp = null
328
+
329
+ if (this.currentDisplay?.packageName !== SYSTEM_NOTICE_PKG) return
330
+ this.currentDisplay = null
331
+ this.tryRestoreCoreDisplay()
332
+ if (!this.currentDisplay) this.sendClear()
333
+ }
334
+
335
+ /** Cancel the handoff and put back the frame the exit notice covered. */
336
+ public cancelMiniappExit(packageName: string): void {
337
+ if (this.exitingApp?.packageName !== packageName) return
338
+ const covered = this.exitingApp.coveredDisplay
339
+ this.exitingApp = null
340
+
341
+ if (this.currentDisplay?.packageName !== SYSTEM_NOTICE_PKG) return
342
+ if (covered.expiresAt === null || covered.expiresAt > this.now()) {
343
+ this.restoreDisplay(covered)
344
+ } else {
345
+ this.currentDisplay = null
346
+ this.tryRestoreCoreDisplay()
347
+ if (!this.currentDisplay) this.sendClear()
348
+ }
349
+ }
350
+
231
351
  /** Report-safe ownership snapshot for incident diagnostics. */
232
352
  public getDiagnosticSnapshot(): Record<string, unknown> {
233
353
  return {
@@ -235,6 +355,7 @@ class LocalDisplayManager {
235
355
  currentDisplayPackageName: this.currentDisplay?.packageName ?? null,
236
356
  backgroundLockPackageName: this.backgroundLock?.packageName ?? null,
237
357
  bootingPackageName: this.bootingApp?.packageName ?? null,
358
+ exitingPackageName: this.exitingApp?.packageName ?? null,
238
359
  }
239
360
  }
240
361
 
@@ -297,6 +418,49 @@ class LocalDisplayManager {
297
418
  this.sendClear()
298
419
  }
299
420
  }
421
+
422
+ this.finishMiniappExit(packageName)
423
+ }
424
+
425
+ // ===========================================================================
426
+ // Dashboard switching (called from DashboardCoordinator)
427
+ // ===========================================================================
428
+
429
+ /**
430
+ * Dashboard cover state. On deactivate the retained main frame repaints
431
+ * (scenes replay create-based; legacy re-sends), or the glasses clear.
432
+ */
433
+ public setDashboardActive(active: boolean): void {
434
+ if (this.dashboardActive === active) return
435
+ this.dashboardActive = active
436
+ useDisplayStore.getState().setDashboardActive(active)
437
+ if (active) return
438
+ sceneRenderer.markViewStale("dashboard")
439
+ const current = this.currentDisplay
440
+ if (current && (current.expiresAt === null || this.now() < current.expiresAt)) {
441
+ this.replayCurrent()
442
+ } else {
443
+ this.sendClear()
444
+ }
445
+ }
446
+
447
+ /** Dashboard renderers only; bypasses arbitration and main ownership state. */
448
+ public renderDashboard(packageName: string, payload: DisplayPayload): void {
449
+ if (!this.dashboardActive) return
450
+ if (payload.scene) {
451
+ sceneRenderer.emitScene(packageName, "dashboard", payload.scene)
452
+ return
453
+ }
454
+ if (!payload.layout) return
455
+ const rawEvent = {view: "dashboard", layout: payload.layout}
456
+ let processedEvent: Record<string, unknown>
457
+ try {
458
+ processedEvent = displayProcessor.processDisplayEvent(rawEvent as any)
459
+ } catch (err) {
460
+ console.error(`${LOG_TAG}: DisplayProcessor error, using raw event:`, err)
461
+ processedEvent = rawEvent
462
+ }
463
+ this.pushToNative(processedEvent)
300
464
  }
301
465
 
302
466
  // ===========================================================================
@@ -313,10 +477,18 @@ class LocalDisplayManager {
313
477
  * the boot queue (resolved when drained or superseded).
314
478
  */
315
479
  public request(packageName: string, payload: DisplayPayload, resolve?: DisplayRequestResolver): void {
316
- // Dashboard view: pass straight through (no throttle/arbitration). Local
317
- // dashboard rendering is currently a stub on the phone anyway.
480
+ // Keep the OS exit notice visible for its short handoff window. Without
481
+ // this, streaming captions/terminal frames can replace it immediately.
482
+ if (this.exitingApp !== null) {
483
+ resolve?.({status: "blocked", reason: "a miniapp exit notice is active"})
484
+ return
485
+ }
486
+
487
+ // Dashboard view: only visible while the dashboard covers the glasses.
488
+ // Local miniapp dashboard content remains a stub.
318
489
  if (payload.view === "dashboard") {
319
- this.sendNow(packageName, payload, resolve)
490
+ this.renderDashboard(packageName, payload)
491
+ resolve?.({status: "displayed"})
320
492
  return
321
493
  }
322
494
 
@@ -557,6 +729,23 @@ class LocalDisplayManager {
557
729
  processedEvent = rawEvent
558
730
  }
559
731
 
732
+ // While the dashboard covers the glasses, main frames only update the
733
+ // retained state; setDashboardActive(false) replays the latest.
734
+ if (!this.dashboardActive) this.pushToNative(processedEvent)
735
+
736
+ this.currentDisplay = {packageName, processedEvent, expiresAt}
737
+
738
+ this.clearExpiryTimer()
739
+ if (expiresAt !== null) {
740
+ const delay = Math.max(0, expiresAt - this.now())
741
+ this.expiryTimerId = BgTimer.setTimeout(() => {
742
+ this.expiryTimerId = null
743
+ this.handleExpiry(packageName)
744
+ }, delay)
745
+ }
746
+ }
747
+
748
+ private pushToNative(processedEvent: Record<string, unknown>): void {
560
749
  try {
561
750
  // The display output path is a direct btsdk call now (was a host sendDisplayEvent
562
751
  // hook) so a bare OEM renders to the glasses without wiring it.
@@ -569,17 +758,6 @@ class LocalDisplayManager {
569
758
  } catch (err) {
570
759
  console.error(`${LOG_TAG}: native display failed:`, err)
571
760
  }
572
-
573
- this.currentDisplay = {packageName, processedEvent, expiresAt}
574
-
575
- this.clearExpiryTimer()
576
- if (expiresAt !== null) {
577
- const delay = Math.max(0, expiresAt - this.now())
578
- this.expiryTimerId = BgTimer.setTimeout(() => {
579
- this.expiryTimerId = null
580
- this.handleExpiry(packageName)
581
- }, delay)
582
- }
583
761
  }
584
762
 
585
763
  private handleExpiry(packageName: string): void {
@@ -785,6 +963,8 @@ class LocalDisplayManager {
785
963
  this.coreAppDisplay = null
786
964
  this.currentDisplay = null
787
965
  this.backgroundLock = null
966
+ this.exitingApp = null
967
+ this.dashboardActive = false
788
968
  this.bootQueue.clear()
789
969
  this.now = () => Date.now()
790
970
  this.unsubscribeGlassesStatus?.()
@@ -52,6 +52,7 @@ import {normalizeAiTier, resolveAiTier} from "./AiTierResolution"
52
52
  import type {AiTier, AiTierProvider, SavedLlmKeys} from "./AiTierResolution"
53
53
  import {HttpError} from "@glassly/cloud-client"
54
54
  import localDisplayManager from "./LocalDisplayManager"
55
+ import {claimsGesture} from "./gestureClaims"
55
56
  import nativeNavigationService from "./NativeNavigationService"
56
57
  import type {DisplayPayload} from "./LocalDisplayManager"
57
58
  import headingService from "./HeadingService"
@@ -539,6 +540,8 @@ class LocalMiniappRuntime {
539
540
 
540
541
  /** Connected miniapps keyed by packageName. */
541
542
  private connectedApps: Map<string, ConnectedMiniapp> = new Map()
543
+ /** Per-app claimed gestures (CLAIM_GESTURES) — see foregroundClaimsGesture. */
544
+ private gestureClaims: Map<string, Set<string>> = new Map()
542
545
 
543
546
  /**
544
547
  * Packages that have completed their CONNECT handshake (sent
@@ -1173,6 +1176,7 @@ class LocalMiniappRuntime {
1173
1176
  // core app's saved frame).
1174
1177
  localDisplayManager.onUnmount(packageName)
1175
1178
 
1179
+ this.gestureClaims.delete(packageName)
1176
1180
  this.connectedApps.delete(packageName)
1177
1181
  this.recomputeMicRequirements()
1178
1182
  if (releasedMicGateOverride) {
@@ -1251,6 +1255,9 @@ class LocalMiniappRuntime {
1251
1255
  case MiniappRequestType.SUBSCRIBE:
1252
1256
  this.handleSubscribe(packageName, payload, requestId)
1253
1257
  break
1258
+ case MiniappRequestType.CLAIM_GESTURES:
1259
+ this.handleClaimGestures(packageName, payload, requestId)
1260
+ break
1254
1261
  case MiniappRequestType.DISPLAY:
1255
1262
  this.handleDisplay(packageName, payload, requestId)
1256
1263
  break
@@ -1858,6 +1865,36 @@ class LocalMiniappRuntime {
1858
1865
  this.sendResult(packageName, requestId, true, {applied: true})
1859
1866
  }
1860
1867
 
1868
+ /**
1869
+ * CLAIM_GESTURES (session.input.setClaimedGestures): the app declares which
1870
+ * touch gestures it handles itself, replacing its previous claim set.
1871
+ * Claims suppress the host's default behavior for those gestures (today:
1872
+ * double_tap → dashboard/head-up in DeviceEventRouter) but ONLY while the
1873
+ * claiming app is the one in front of the user — see
1874
+ * {@link foregroundClaimsGesture}. Cleared on app stop.
1875
+ */
1876
+ private handleClaimGestures(packageName: string, payload: Record<string, unknown>, requestId?: string): void {
1877
+ if (!this.connectedApps.has(packageName)) {
1878
+ this.sendResult(packageName, requestId, true)
1879
+ return
1880
+ }
1881
+ const raw = payload.gestures
1882
+ const gestures = Array.isArray(raw) ? raw.filter((g): g is string => typeof g === "string") : []
1883
+ if (gestures.length === 0) this.gestureClaims.delete(packageName)
1884
+ else this.gestureClaims.set(packageName, new Set(gestures))
1885
+ this.sendResult(packageName, requestId, true)
1886
+ }
1887
+
1888
+ /** True iff the miniapp in front of the user has claimed `gesture`. */
1889
+ public foregroundClaimsGesture(gesture: string): boolean {
1890
+ return claimsGesture(
1891
+ this.gestureClaims,
1892
+ gesture,
1893
+ localDisplayManager.getGestureOwner(),
1894
+ useAppStatusStore.getState().foregroundedPackage,
1895
+ )
1896
+ }
1897
+
1861
1898
  private handleSubscribe(packageName: string, payload: Record<string, unknown>, requestId?: string): void {
1862
1899
  const app = this.connectedApps.get(packageName)
1863
1900
  if (!app) return
@@ -0,0 +1,76 @@
1
+ export const MINIAPP_EXIT_NOTICE_MS = 1500
2
+
3
+ interface ExitGestureApp {
4
+ packageName: string
5
+ name: string
6
+ running: boolean
7
+ }
8
+
9
+ interface ExitGestureState {
10
+ apps: ExitGestureApp[]
11
+ foregroundedPackage: string | null
12
+ clearForeground: () => void
13
+ stop: (packageName: string) => Promise<void>
14
+ }
15
+
16
+ export interface ExitGestureDependencies {
17
+ getState: () => ExitGestureState
18
+ getVisiblePackageName: () => string | null
19
+ beginExitNotice: (packageName: string, text: string) => boolean
20
+ finishExitNotice: (packageName: string) => void
21
+ cancelExitNotice: (packageName: string) => void
22
+ setTimeout: (callback: () => void, delay: number) => number
23
+ clearTimeout: (timerId: number) => void
24
+ }
25
+
26
+ /** Owns the short double-tap → exit-notice → normal-stop handoff. */
27
+ export class MiniappExitGesture {
28
+ private pending: {packageName: string; timerId: number} | null = null
29
+
30
+ public constructor(private readonly deps: ExitGestureDependencies) {}
31
+
32
+ /** Returns true when the system gesture was consumed by a visible miniapp exit. */
33
+ public tryExitVisibleMiniapp(): boolean {
34
+ // Swallow repeat taps while the notice is already counting down.
35
+ if (this.pending) return true
36
+
37
+ const packageName = this.deps.getVisiblePackageName()
38
+ if (!packageName) return false
39
+
40
+ const app = this.deps.getState().apps.find((candidate) => candidate.packageName === packageName)
41
+ if (!app?.running) return false
42
+
43
+ const name = app.name.trim() || packageName
44
+ if (!this.deps.beginExitNotice(packageName, `Exiting ${name}…`)) return false
45
+
46
+ const timerId = this.deps.setTimeout(() => {
47
+ const state = this.deps.getState()
48
+ if (state.foregroundedPackage === packageName) state.clearForeground()
49
+
50
+ const stillRunning = state.apps.some((candidate) => candidate.packageName === packageName && candidate.running)
51
+ const stop = stillRunning ? state.stop(packageName) : Promise.resolve()
52
+ void stop
53
+ .catch((error) => {
54
+ console.error(`MiniappExitGesture: failed to stop ${packageName}:`, error)
55
+ })
56
+ .finally(() => {
57
+ if (this.pending?.packageName === packageName) this.pending = null
58
+ // LocalMiniappRuntime.onUnmount normally finishes the notice. This
59
+ // fallback also handles a failed/no-op teardown.
60
+ this.deps.finishExitNotice(packageName)
61
+ })
62
+ }, MINIAPP_EXIT_NOTICE_MS)
63
+
64
+ this.pending = {packageName, timerId}
65
+ return true
66
+ }
67
+
68
+ /** Cancel an in-flight handoff when the device-event router shuts down. */
69
+ public reset(): void {
70
+ if (!this.pending) return
71
+ const {packageName, timerId} = this.pending
72
+ this.pending = null
73
+ this.deps.clearTimeout(timerId)
74
+ this.deps.cancelExitNotice(packageName)
75
+ }
76
+ }
@@ -0,0 +1,130 @@
1
+ import type {OtaCheckCurrentGlassesResult} from "./OtaUpdateCheckService"
2
+
3
+ export const MAX_OTA_AUTO_CHAIN_PASSES = 8
4
+ export const OTA_AUTO_CHAIN_RECONNECT_TIMEOUT_MS = 120_000
5
+
6
+ type OtaAutoChainSession = {
7
+ approvedDowngrade: boolean
8
+ passCount: number
9
+ releaseRange: OtaAutoChainReleaseRange
10
+ reconnectDeadline: number | null
11
+ seenFingerprints: Set<string>
12
+ }
13
+
14
+ export type OtaAutoChainReleaseRange = {
15
+ fromVersion: string | null
16
+ toVersion: string | null
17
+ /** Exact coordinated release identity for the OTA pin, absent for legacy manifests. */
18
+ releaseVersion?: string | null
19
+ }
20
+
21
+ export type OtaAutoChainAdvanceResult =
22
+ | {advance: true; passCount: number}
23
+ | {advance: false; reason: "inactive" | "duplicate" | "max_passes" | "downgrade_not_approved"}
24
+
25
+ let session: OtaAutoChainSession | null = null
26
+
27
+ /**
28
+ * Identify the exact update offer that produced one OTA pass. Including the
29
+ * observed versions and manifest body lets a changed manifest advance while a
30
+ * stale post-install version_info response is rejected as a duplicate.
31
+ */
32
+ export function otaAutoChainFingerprint(result: OtaCheckCurrentGlassesResult): string {
33
+ return JSON.stringify({
34
+ manifestUrl: result.manifestUrl ?? "",
35
+ manifestBody: result.manifestBody ?? "",
36
+ buildNumber: result.buildNumber ?? "",
37
+ mtkFirmwareVersion: result.mtkFirmwareVersion ?? "",
38
+ besFirmwareVersion: result.besFirmwareVersion ?? "",
39
+ updates: result.updates,
40
+ targetBuildNumber: result.updateInfo?.versionCode ?? result.latestVersionInfo?.versionCode ?? 0,
41
+ targetBesVersion: result.besVersion ?? "",
42
+ mtkPatch: result.mtkPatch
43
+ ? {
44
+ startFirmware: result.mtkPatch.start_firmware,
45
+ endFirmware: result.mtkPatch.end_firmware,
46
+ url: result.mtkPatch.url,
47
+ }
48
+ : null,
49
+ })
50
+ }
51
+
52
+ /** Start a chain after the user explicitly approves its first update pass. */
53
+ export function beginOtaAutoChain(
54
+ initialFingerprint: string,
55
+ approvedDowngrade: boolean,
56
+ releaseRange: OtaAutoChainReleaseRange,
57
+ ): void {
58
+ session = {
59
+ approvedDowngrade,
60
+ passCount: 1,
61
+ releaseRange: {...releaseRange},
62
+ reconnectDeadline: null,
63
+ seenFingerprints: new Set([initialFingerprint]),
64
+ }
65
+ }
66
+
67
+ export function isOtaAutoChainActive(): boolean {
68
+ return session !== null
69
+ }
70
+
71
+ export function otaAutoChainReleaseRange(): OtaAutoChainReleaseRange | null {
72
+ return session ? {...session.releaseRange} : null
73
+ }
74
+
75
+ /** End the current chain. Safe to call when no chain is active. */
76
+ export function stopOtaAutoChain(): void {
77
+ session = null
78
+ }
79
+
80
+ /**
81
+ * Start (or continue) the bounded wait for glasses that are rebooting between
82
+ * passes. Re-renders must not extend the original deadline indefinitely.
83
+ */
84
+ export function otaAutoChainReconnectWaitRemaining(now = performance.now()): number | null {
85
+ if (!session) return null
86
+
87
+ session.reconnectDeadline ??= now + OTA_AUTO_CHAIN_RECONNECT_TIMEOUT_MS
88
+ return Math.max(0, session.reconnectDeadline - now)
89
+ }
90
+
91
+ /** A successful reconnect allows a future pass to establish a fresh wait. */
92
+ export function clearOtaAutoChainReconnectWait(): void {
93
+ if (session) session.reconnectDeadline = null
94
+ }
95
+
96
+ /**
97
+ * Admit the next pass of an active chain. Repeated offers, unexpectedly
98
+ * destructive downgrades, and runaway chains fail closed and end automation.
99
+ */
100
+ export function tryAdvanceOtaAutoChain(
101
+ fingerprint: string,
102
+ isDowngrade: boolean,
103
+ targetVersion: string | null,
104
+ releaseVersion: string | null = null,
105
+ ): OtaAutoChainAdvanceResult {
106
+ if (!session) {
107
+ return {advance: false, reason: "inactive"}
108
+ }
109
+
110
+ if (isDowngrade && !session.approvedDowngrade) {
111
+ stopOtaAutoChain()
112
+ return {advance: false, reason: "downgrade_not_approved"}
113
+ }
114
+
115
+ if (session.seenFingerprints.has(fingerprint)) {
116
+ stopOtaAutoChain()
117
+ return {advance: false, reason: "duplicate"}
118
+ }
119
+
120
+ if (session.passCount >= MAX_OTA_AUTO_CHAIN_PASSES) {
121
+ stopOtaAutoChain()
122
+ return {advance: false, reason: "max_passes"}
123
+ }
124
+
125
+ session.seenFingerprints.add(fingerprint)
126
+ session.passCount += 1
127
+ if (targetVersion) session.releaseRange.toVersion = targetVersion
128
+ if (releaseVersion) session.releaseRange.releaseVersion = releaseVersion
129
+ return {advance: true, passCount: session.passCount}
130
+ }
@@ -0,0 +1,80 @@
1
+ import type {OtaProgress, OtaStatus} from "../facades/ota"
2
+
3
+ export const BES_INSTALL_RESTART_MESSAGE =
4
+ "Restart your glasses to safely exit firmware update mode before trying again"
5
+
6
+ function isDownloadPhaseSnapshot(
7
+ otaStatus: OtaStatus | null | undefined,
8
+ otaProgress: OtaProgress | null | undefined,
9
+ ): boolean {
10
+ if (otaStatus?.phase === "download") {
11
+ return true
12
+ }
13
+ if (otaProgress?.stage === "download") {
14
+ return true
15
+ }
16
+ return false
17
+ }
18
+
19
+ /**
20
+ * Offer Change WiFi whenever the OTA flow failed while the glasses were in the
21
+ * download step (any download failure — network, SSL, size cap, verify, etc.).
22
+ *
23
+ * Also covers phone-side watchdog failures that fire while the store still shows
24
+ * an active download phase (stall / global timeout mid-download).
25
+ */
26
+ export function shouldShowChangeWifiForOtaDownloadFailure(
27
+ otaStatus: OtaStatus | null | undefined,
28
+ otaProgress: OtaProgress | null | undefined,
29
+ localErrorMessage: string,
30
+ ): boolean {
31
+ if (otaStatus?.status === "failed" && otaStatus.phase === "download") {
32
+ return true
33
+ }
34
+ if (otaProgress?.status === "FAILED" && otaProgress.stage === "download") {
35
+ return true
36
+ }
37
+ if (localErrorMessage && isDownloadPhaseSnapshot(otaStatus, otaProgress)) {
38
+ return true
39
+ }
40
+ return false
41
+ }
42
+
43
+ export function getOtaErrorMessage(error?: string): string {
44
+ switch (error) {
45
+ case "no_internet":
46
+ return "Glasses WiFi has no internet connection"
47
+ case "clock_skew":
48
+ return "Glasses clock is wrong — syncing time from your phone, then retrying update check"
49
+ case "ssl_error":
50
+ return "Secure connection failed — try a different WiFi network"
51
+ case "download_failed":
52
+ return "Download failed — check glasses WiFi connection"
53
+ case "firmware_too_large":
54
+ return "Firmware file is unexpectedly large — please contact support"
55
+ case "firmware_verify_failed":
56
+ return "Firmware verification failed — please try again or contact support"
57
+ case "apk_verify_failed":
58
+ return "Update verification failed — please try again or contact support"
59
+ case "install_failed":
60
+ return "Install failed — please try again"
61
+ default:
62
+ return error || "Update failed"
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Once a BES install has started, any failure conservatively requires a glasses restart. This
68
+ * avoids adding a BES-specific error code to the wire protocol and, more importantly, never offers
69
+ * an unsafe retry when a generic failure or silence may mean BES entered its raw OTA parser.
70
+ */
71
+ export function shouldRequireGlassesRebootForBesFailure(
72
+ otaStatus: OtaStatus | null | undefined,
73
+ _otaProgress: OtaProgress | null | undefined,
74
+ localErrorMessage: string,
75
+ ): boolean {
76
+ if (otaStatus?.stepType !== "bes" || otaStatus.phase !== "install") {
77
+ return false
78
+ }
79
+ return otaStatus.status === "failed" || Boolean(localErrorMessage)
80
+ }