@glassly/engine 0.3.7 → 0.3.8-beta.1013

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 (229) hide show
  1. package/README.md +4 -0
  2. package/build/engine.d.ts +18 -2
  3. package/build/engine.d.ts.map +1 -1
  4. package/build/engine.js +7 -4
  5. package/build/engine.js.map +1 -1
  6. package/build/facades/ai.d.ts +3 -0
  7. package/build/facades/ai.d.ts.map +1 -1
  8. package/build/facades/ai.js +3 -0
  9. package/build/facades/ai.js.map +1 -1
  10. package/build/facades/backup.d.ts +14 -0
  11. package/build/facades/backup.d.ts.map +1 -0
  12. package/build/facades/backup.js +22 -0
  13. package/build/facades/backup.js.map +1 -0
  14. package/build/facades/glasses.d.ts +13 -5
  15. package/build/facades/glasses.d.ts.map +1 -1
  16. package/build/facades/glasses.js +17 -4
  17. package/build/facades/glasses.js.map +1 -1
  18. package/build/generated/releaseMetadata.js +4 -4
  19. package/build/generated/releaseMetadata.js.map +1 -1
  20. package/build/index.d.ts +3 -1
  21. package/build/index.d.ts.map +1 -1
  22. package/build/index.js.map +1 -1
  23. package/build/services/AppRegistry.d.ts +2 -1
  24. package/build/services/AppRegistry.d.ts.map +1 -1
  25. package/build/services/AppRegistry.js +38 -8
  26. package/build/services/AppRegistry.js.map +1 -1
  27. package/build/services/AppStateBackup.d.ts +98 -0
  28. package/build/services/AppStateBackup.d.ts.map +1 -0
  29. package/build/services/AppStateBackup.js +224 -0
  30. package/build/services/AppStateBackup.js.map +1 -0
  31. package/build/services/BlobStore.d.ts.map +1 -1
  32. package/build/services/BlobStore.js +5 -12
  33. package/build/services/BlobStore.js.map +1 -1
  34. package/build/services/CloudClientService.d.ts +3 -1
  35. package/build/services/CloudClientService.d.ts.map +1 -1
  36. package/build/services/CloudClientService.js +42 -2
  37. package/build/services/CloudClientService.js.map +1 -1
  38. package/build/services/DeviceEventRouter.d.ts.map +1 -1
  39. package/build/services/DeviceEventRouter.js +1 -0
  40. package/build/services/DeviceEventRouter.js.map +1 -1
  41. package/build/services/DisplayDiagnostics.d.ts.map +1 -1
  42. package/build/services/DisplayDiagnostics.js +4 -2
  43. package/build/services/DisplayDiagnostics.js.map +1 -1
  44. package/build/services/DisplayProcessor.d.ts +2 -0
  45. package/build/services/DisplayProcessor.d.ts.map +1 -1
  46. package/build/services/DisplayProcessor.js +15 -1
  47. package/build/services/DisplayProcessor.js.map +1 -1
  48. package/build/services/GlassesTouchInput.d.ts.map +1 -1
  49. package/build/services/GlassesTouchInput.js +4 -2
  50. package/build/services/GlassesTouchInput.js.map +1 -1
  51. package/build/services/JSCrashloopReportService.d.ts.map +1 -1
  52. package/build/services/JSCrashloopReportService.js +5 -0
  53. package/build/services/JSCrashloopReportService.js.map +1 -1
  54. package/build/services/JSRouter.d.ts +11 -3
  55. package/build/services/JSRouter.d.ts.map +1 -1
  56. package/build/services/JSRouter.js +36 -6
  57. package/build/services/JSRouter.js.map +1 -1
  58. package/build/services/LocalDisplayManager.d.ts.map +1 -1
  59. package/build/services/LocalDisplayManager.js +5 -2
  60. package/build/services/LocalDisplayManager.js.map +1 -1
  61. package/build/services/LocalMiniappRuntime.d.ts +13 -0
  62. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  63. package/build/services/LocalMiniappRuntime.js +61 -22
  64. package/build/services/LocalMiniappRuntime.js.map +1 -1
  65. package/build/services/LocalSttFallbackCoordinator.d.ts.map +1 -1
  66. package/build/services/LocalSttFallbackCoordinator.js +2 -0
  67. package/build/services/LocalSttFallbackCoordinator.js.map +1 -1
  68. package/build/services/LocalTranslationService.d.ts +2 -0
  69. package/build/services/LocalTranslationService.d.ts.map +1 -1
  70. package/build/services/LocalTranslationService.js +22 -0
  71. package/build/services/LocalTranslationService.js.map +1 -1
  72. package/build/services/MiniappEngine.d.ts +9 -0
  73. package/build/services/MiniappEngine.d.ts.map +1 -1
  74. package/build/services/MiniappEngine.js +60 -27
  75. package/build/services/MiniappEngine.js.map +1 -1
  76. package/build/services/MiniappLauncher.d.ts +2 -0
  77. package/build/services/MiniappLauncher.d.ts.map +1 -1
  78. package/build/services/MiniappLauncher.js +6 -0
  79. package/build/services/MiniappLauncher.js.map +1 -1
  80. package/build/services/NavigationService.d.ts +1 -0
  81. package/build/services/NavigationService.d.ts.map +1 -1
  82. package/build/services/NavigationService.js +20 -6
  83. package/build/services/NavigationService.js.map +1 -1
  84. package/build/services/OfflineActivity.d.ts +54 -0
  85. package/build/services/OfflineActivity.d.ts.map +1 -0
  86. package/build/services/OfflineActivity.js +86 -0
  87. package/build/services/OfflineActivity.js.map +1 -0
  88. package/build/services/PhoneCalendarService.d.ts.map +1 -1
  89. package/build/services/PhoneCalendarService.js +8 -0
  90. package/build/services/PhoneCalendarService.js.map +1 -1
  91. package/build/services/PhoneNotificationsSync.d.ts.map +1 -1
  92. package/build/services/PhoneNotificationsSync.js +7 -3
  93. package/build/services/PhoneNotificationsSync.js.map +1 -1
  94. package/build/services/STTModelManager.d.ts +1 -0
  95. package/build/services/STTModelManager.d.ts.map +1 -1
  96. package/build/services/STTModelManager.js +6 -0
  97. package/build/services/STTModelManager.js.map +1 -1
  98. package/build/services/SceneRenderer.js +1 -1
  99. package/build/services/SceneRenderer.js.map +1 -1
  100. package/build/services/appStateBackupFile.d.ts +32 -0
  101. package/build/services/appStateBackupFile.d.ts.map +1 -0
  102. package/build/services/appStateBackupFile.js +135 -0
  103. package/build/services/appStateBackupFile.js.map +1 -0
  104. package/build/services/controllers.d.ts.map +1 -1
  105. package/build/services/controllers.js +13 -3
  106. package/build/services/controllers.js.map +1 -1
  107. package/build/services/effectiveCapabilities.js +6 -1
  108. package/build/services/effectiveCapabilities.js.map +1 -1
  109. package/build/services/neuralBand/NeuralBandInput.d.ts +12 -1
  110. package/build/services/neuralBand/NeuralBandInput.d.ts.map +1 -1
  111. package/build/services/neuralBand/NeuralBandInput.js +27 -0
  112. package/build/services/neuralBand/NeuralBandInput.js.map +1 -1
  113. package/build/services/neuralBand/NeuralBandSettingsSync.d.ts.map +1 -1
  114. package/build/services/neuralBand/NeuralBandSettingsSync.js +6 -0
  115. package/build/services/neuralBand/NeuralBandSettingsSync.js.map +1 -1
  116. package/build/services/pages/pageEligibility.d.ts.map +1 -1
  117. package/build/services/pages/pageEligibility.js +8 -1
  118. package/build/services/pages/pageEligibility.js.map +1 -1
  119. package/build/services/settingsBridge.d.ts.map +1 -1
  120. package/build/services/settingsBridge.js +1 -0
  121. package/build/services/settingsBridge.js.map +1 -1
  122. package/build/services/{appleWatch/AppleWatchLink.d.ts → watch/WatchLink.d.ts} +8 -7
  123. package/build/services/watch/WatchLink.d.ts.map +1 -0
  124. package/build/services/{appleWatch/AppleWatchLink.js → watch/WatchLink.js} +14 -15
  125. package/build/services/watch/WatchLink.js.map +1 -0
  126. package/build/stores/appVisibility.d.ts.map +1 -1
  127. package/build/stores/appVisibility.js +0 -1
  128. package/build/stores/appVisibility.js.map +1 -1
  129. package/build/stores/apps.d.ts.map +1 -1
  130. package/build/stores/apps.js +22 -10
  131. package/build/stores/apps.js.map +1 -1
  132. package/build/stores/settings.d.ts.map +1 -1
  133. package/build/stores/settings.js +71 -6
  134. package/build/stores/settings.js.map +1 -1
  135. package/build/types/capabilities/even-realities-g1.d.ts.map +1 -1
  136. package/build/types/capabilities/even-realities-g1.js +3 -0
  137. package/build/types/capabilities/even-realities-g1.js.map +1 -1
  138. package/build/types/capabilities/memomind-one.d.ts +3 -0
  139. package/build/types/capabilities/memomind-one.d.ts.map +1 -0
  140. package/build/types/capabilities/memomind-one.js +42 -0
  141. package/build/types/capabilities/memomind-one.js.map +1 -0
  142. package/build/types/capabilities/rayneo-io.d.ts.map +1 -1
  143. package/build/types/capabilities/rayneo-io.js +3 -2
  144. package/build/types/capabilities/rayneo-io.js.map +1 -1
  145. package/build/types/enums.d.ts +8 -2
  146. package/build/types/enums.d.ts.map +1 -1
  147. package/build/types/enums.js +6 -0
  148. package/build/types/enums.js.map +1 -1
  149. package/build/types/hardware.d.ts +3 -1
  150. package/build/types/hardware.d.ts.map +1 -1
  151. package/build/types/hardware.js +3 -1
  152. package/build/types/hardware.js.map +1 -1
  153. package/build/utils/display/index.d.ts +2 -1
  154. package/build/utils/display/index.d.ts.map +1 -1
  155. package/build/utils/display/index.js +3 -2
  156. package/build/utils/display/index.js.map +1 -1
  157. package/build/utils/display/profiles/index.d.ts +2 -1
  158. package/build/utils/display/profiles/index.d.ts.map +1 -1
  159. package/build/utils/display/profiles/index.js +3 -2
  160. package/build/utils/display/profiles/index.js.map +1 -1
  161. package/build/utils/display/profiles/rayneo-io.d.ts +1 -1
  162. package/build/utils/display/profiles/rayneo-io.d.ts.map +1 -1
  163. package/build/utils/display/profiles/rayneo-io.js +1 -1
  164. package/build/utils/display/profiles/rayneo-io.js.map +1 -1
  165. package/build/utils/display/scene/index.d.ts +1 -1
  166. package/build/utils/display/scene/index.d.ts.map +1 -1
  167. package/build/utils/display/scene/index.js +1 -1
  168. package/build/utils/display/scene/index.js.map +1 -1
  169. package/build/utils/display/scene/process.d.ts +6 -0
  170. package/build/utils/display/scene/process.d.ts.map +1 -1
  171. package/build/utils/display/scene/process.js +22 -7
  172. package/build/utils/display/scene/process.js.map +1 -1
  173. package/build/utils/miniappGlobals.d.ts +13 -17
  174. package/build/utils/miniappGlobals.d.ts.map +1 -1
  175. package/build/utils/miniappGlobals.js +24 -56
  176. package/build/utils/miniappGlobals.js.map +1 -1
  177. package/package.json +7 -7
  178. package/src/engine.ts +7 -4
  179. package/src/facades/ai.ts +4 -0
  180. package/src/facades/backup.ts +28 -0
  181. package/src/facades/glasses.ts +22 -5
  182. package/src/generated/releaseMetadata.ts +4 -4
  183. package/src/index.ts +10 -0
  184. package/src/services/AppRegistry.ts +36 -9
  185. package/src/services/AppStateBackup.ts +283 -0
  186. package/src/services/BlobStore.ts +5 -11
  187. package/src/services/CloudClientService.ts +43 -4
  188. package/src/services/DeviceEventRouter.ts +1 -0
  189. package/src/services/DisplayDiagnostics.ts +4 -2
  190. package/src/services/DisplayProcessor.ts +20 -8
  191. package/src/services/GlassesTouchInput.ts +6 -2
  192. package/src/services/JSCrashloopReportService.ts +5 -0
  193. package/src/services/JSRouter.ts +37 -6
  194. package/src/services/LocalDisplayManager.ts +5 -2
  195. package/src/services/LocalMiniappRuntime.ts +61 -22
  196. package/src/services/LocalSttFallbackCoordinator.ts +4 -0
  197. package/src/services/LocalTranslationService.ts +20 -0
  198. package/src/services/MiniappEngine.ts +62 -27
  199. package/src/services/MiniappLauncher.ts +7 -0
  200. package/src/services/NavigationService.ts +28 -8
  201. package/src/services/OfflineActivity.ts +131 -0
  202. package/src/services/PhoneCalendarService.ts +7 -0
  203. package/src/services/PhoneNotificationsSync.ts +8 -3
  204. package/src/services/STTModelManager.ts +6 -0
  205. package/src/services/SceneRenderer.ts +1 -1
  206. package/src/services/appStateBackupFile.ts +171 -0
  207. package/src/services/controllers.ts +14 -3
  208. package/src/services/effectiveCapabilities.ts +6 -1
  209. package/src/services/neuralBand/NeuralBandInput.ts +37 -1
  210. package/src/services/neuralBand/NeuralBandSettingsSync.ts +12 -0
  211. package/src/services/pages/pageEligibility.ts +9 -1
  212. package/src/services/settingsBridge.ts +1 -0
  213. package/src/services/{appleWatch/AppleWatchLink.ts → watch/WatchLink.ts} +20 -20
  214. package/src/stores/appVisibility.ts +0 -1
  215. package/src/stores/apps.ts +22 -11
  216. package/src/stores/settings.ts +78 -10
  217. package/src/types/capabilities/even-realities-g1.ts +4 -0
  218. package/src/types/capabilities/memomind-one.ts +43 -0
  219. package/src/types/capabilities/rayneo-io.ts +3 -2
  220. package/src/types/enums.ts +6 -0
  221. package/src/types/hardware.ts +14 -1
  222. package/src/utils/display/index.ts +4 -2
  223. package/src/utils/display/profiles/index.ts +9 -2
  224. package/src/utils/display/profiles/rayneo-io.ts +6 -1
  225. package/src/utils/display/scene/index.ts +1 -1
  226. package/src/utils/display/scene/process.ts +22 -7
  227. package/src/utils/miniappGlobals.ts +32 -64
  228. package/build/services/appleWatch/AppleWatchLink.d.ts.map +0 -1
  229. package/build/services/appleWatch/AppleWatchLink.js.map +0 -1
@@ -0,0 +1,131 @@
1
+ /**
2
+ * What the phone is running on-device right now (transcription, translation,
3
+ * LLM) and why, for the home offline card. Producers push in; it imports
4
+ * nothing at runtime so their test stubs stay unaffected.
5
+ */
6
+ import type {LocalLlmReason} from "./LlmRouting"
7
+ import type {LocalSttFallbackReason} from "./LocalSttFallbackCoordinator"
8
+
9
+ export type OfflineActivityReason = LocalSttFallbackReason | LocalLlmReason
10
+
11
+ export interface OfflineTranscriptionActivity {
12
+ reason: LocalSttFallbackReason
13
+ model: string
14
+ }
15
+
16
+ export interface OfflineTranslationPair {
17
+ source: string
18
+ target: string
19
+ }
20
+
21
+ export interface OfflineTranslationActivity {
22
+ reason: OfflineActivityReason
23
+ pairs: OfflineTranslationPair[]
24
+ }
25
+
26
+ export interface OfflineLlmActivity {
27
+ reason: LocalLlmReason
28
+ model: string
29
+ requests: number
30
+ }
31
+
32
+ export interface OfflineActivity {
33
+ transcription: OfflineTranscriptionActivity | null
34
+ translation: OfflineTranslationActivity | null
35
+ llm: OfflineLlmActivity | null
36
+ }
37
+
38
+ export interface ServedTranslationPair extends OfflineTranslationPair {
39
+ forceLocal: boolean
40
+ }
41
+
42
+ const EMPTY: OfflineActivity = {transcription: null, translation: null, llm: null}
43
+
44
+ class OfflineActivityTracker {
45
+ private transcription: OfflineTranscriptionActivity | null = null
46
+ private llmRuns: Array<{reason: LocalLlmReason; model: string}> = []
47
+ private translationSource: (() => ServedTranslationPair[]) | null = null
48
+ private snapshot: OfflineActivity = EMPTY
49
+ private snapshotKey = JSON.stringify(EMPTY)
50
+ private listeners = new Set<(activity: OfflineActivity) => void>()
51
+
52
+ current(): OfflineActivity {
53
+ return this.snapshot
54
+ }
55
+
56
+ subscribe(listener: (activity: OfflineActivity) => void): () => void {
57
+ this.listeners.add(listener)
58
+ return () => {
59
+ this.listeners.delete(listener)
60
+ }
61
+ }
62
+
63
+ setTranscription(transcription: OfflineTranscriptionActivity | null): void {
64
+ this.transcription = transcription
65
+ this.refresh()
66
+ }
67
+
68
+ beginLlm(reason: LocalLlmReason, model: string): () => void {
69
+ const run = {reason, model}
70
+ this.llmRuns.push(run)
71
+ this.refresh()
72
+ let ended = false
73
+ return () => {
74
+ if (ended) return
75
+ ended = true
76
+ this.llmRuns = this.llmRuns.filter((r) => r !== run)
77
+ this.refresh()
78
+ }
79
+ }
80
+
81
+ setTranslationSource(source: (() => ServedTranslationPair[]) | null): void {
82
+ this.translationSource = source
83
+ this.refresh()
84
+ }
85
+
86
+ refresh(): void {
87
+ const next: OfflineActivity = {
88
+ transcription: this.transcription,
89
+ translation: this.translation(),
90
+ llm: this.llm(),
91
+ }
92
+ const key = JSON.stringify(next)
93
+ if (key === this.snapshotKey) return
94
+ this.snapshotKey = key
95
+ this.snapshot = next
96
+ for (const listener of this.listeners) {
97
+ try {
98
+ listener(next)
99
+ } catch (error) {
100
+ console.warn("OfflineActivity: listener failed:", error)
101
+ }
102
+ }
103
+ }
104
+
105
+ reset(): void {
106
+ this.transcription = null
107
+ this.llmRuns = []
108
+ this.translationSource = null
109
+ this.refresh()
110
+ }
111
+
112
+ private translation(): OfflineTranslationActivity | null {
113
+ if (!this.transcription || !this.translationSource) return null
114
+ const pairs = this.translationSource()
115
+ if (pairs.length === 0) return null
116
+ return {
117
+ reason: pairs.some((p) => p.forceLocal) ? "force_local" : this.transcription.reason,
118
+ pairs: pairs.map(({source, target}) => ({source, target})),
119
+ }
120
+ }
121
+
122
+ private llm(): OfflineLlmActivity | null {
123
+ const latest = this.llmRuns[this.llmRuns.length - 1]
124
+ if (!latest) return null
125
+ return {reason: latest.reason, model: latest.model, requests: this.llmRuns.length}
126
+ }
127
+ }
128
+
129
+ export {OfflineActivityTracker}
130
+ const offlineActivity = new OfflineActivityTracker()
131
+ export default offlineActivity
@@ -49,9 +49,16 @@ export async function listPhoneCalendarEvents(request: CalendarListRequest): Pro
49
49
  if (calendars.length === 0) return {events: [], truncated: false}
50
50
 
51
51
  const rawEvents = await Calendar.listEvents(calendars, startsAt, endsAt)
52
+ const seen = new Set<string>()
52
53
  const events = rawEvents
53
54
  .map(normalizeCalendarEvent)
54
55
  .filter((event) => calendarEventOverlapsWindow(event, startsAt, endsAt))
56
+ .filter((event) => {
57
+ const key = `${event.title}\n${event.startsAt}\n${event.endsAt}\n${event.allDay}`
58
+ if (seen.has(key)) return false
59
+ seen.add(key)
60
+ return true
61
+ })
55
62
  .sort((a, b) => Date.parse(a.startsAt) - Date.parse(b.startsAt) || Date.parse(a.endsAt) - Date.parse(b.endsAt))
56
63
 
57
64
  return {
@@ -91,6 +91,11 @@ export function nativeNotificationsSupported(): boolean {
91
91
  return getModelCapabilities(model).hasNativeNotifications === true
92
92
  }
93
93
 
94
+ function notificationRelaySupported(): boolean {
95
+ const model = useGlassesStore.getState().deviceModel as DeviceTypes
96
+ return Platform.OS === "ios" && getModelCapabilities(model).hasNotificationRelay === true
97
+ }
98
+
94
99
  /** Popup durations the firmware accepts, or an empty list when it takes any. */
95
100
  export function nativeNotificationPopupSeconds(): number[] {
96
101
  const model = useGlassesStore.getState().deviceModel as DeviceTypes
@@ -141,10 +146,10 @@ export function currentPresentationOwner(hostActive: boolean): PresentationOwner
141
146
  */
142
147
  export function captureAuthorization(): "granted" | "denied" | "unknown" | "not_needed" {
143
148
  if (Platform.OS !== "ios") return "not_needed"
144
- if (!nativeNotificationsSupported()) return "not_needed"
149
+ if (!nativeNotificationsSupported() && !notificationRelaySupported()) return "not_needed"
145
150
  const glasses = useGlassesStore.getState()
146
151
  if (!isGlassesConnected(glasses.connection)) return "unknown"
147
- return glasses.ancsAuthorized ? "granted" : "denied"
152
+ return glasses.ancsAuthorized ? "granted" : notificationRelaySupported() ? "unknown" : "denied"
148
153
  }
149
154
 
150
155
  /**
@@ -178,7 +183,7 @@ function pushListenerConfig(): void {
178
183
  }
179
184
 
180
185
  function pushGlassesConfig(): void {
181
- if (!nativeNotificationsSupported()) return
186
+ if (!nativeNotificationsSupported() && !notificationRelaySupported()) return
182
187
  if (!isGlassesConnected(useGlassesStore.getState().connection)) return
183
188
  void Promise.resolve(BluetoothSdk.setGlassesNotificationConfig(desiredGlassesNotificationConfig())).catch(
184
189
  (err: unknown) =>
@@ -228,6 +228,12 @@ class STTModelManager {
228
228
  return this.currentLanguage
229
229
  }
230
230
 
231
+ currentModelLabel(): string {
232
+ const language = this.config(this.currentLanguage)
233
+ if (!language) return this.currentLanguage
234
+ return language.type === "apple" ? `Apple · ${language.displayName}` : language.displayName
235
+ }
236
+
231
237
  setCurrentLanguage(code: string): void {
232
238
  if (this.config(code)) {
233
239
  this.currentLanguage = code
@@ -250,7 +250,7 @@ class SceneRenderer {
250
250
  return {kind: "legacy", layout, degraded, dropped}
251
251
  }
252
252
 
253
- const processed = processScene(elements, caps, displayProcessor.getProfile())
253
+ const processed = processScene(elements, caps, displayProcessor.getSceneProfile())
254
254
  // A stale baseline means the glasses were cleared since the last commit —
255
255
  // diff from empty so everything repaints (the retained frame stays valid
256
256
  // as the replay source only).
@@ -0,0 +1,171 @@
1
+ /**
2
+ * App state backup files: the share-sheet export and file-picker restore
3
+ * behind Settings > Profile > Backup. What goes in the file is decided in
4
+ * AppStateBackup.ts; this side reads and writes the live stores.
5
+ */
6
+
7
+ import type {MemoryImportResult} from "@glassly/miniapp"
8
+ import {File, Paths} from "expo-file-system"
9
+ import {Platform} from "react-native"
10
+ import Share from "react-native-share"
11
+
12
+ import {SETTINGS, useSettingsStore} from "../stores/settings"
13
+ import {useAppStatusStore} from "../stores/apps"
14
+ import {storage} from "../utils/storage/storage"
15
+ import appRegistry, {saveAlwaysOnOverride, saveWidgetExpandChoice} from "./AppRegistry"
16
+ import {
17
+ AppStateBackupError,
18
+ collectAppStateBackup,
19
+ miniappPrefs,
20
+ miniappsWithStorage,
21
+ parseAppStateBackup,
22
+ restorableSettings,
23
+ restoreMiniappStorage,
24
+ type AppStateBackupBackend,
25
+ } from "./AppStateBackup"
26
+ import {cloudClientService} from "./CloudClientService"
27
+ import {memoryStore} from "./memoryStoreInstance"
28
+ import {respawnMiniapp, runningMiniappPackages, stopMiniapp} from "./MiniappEngine"
29
+
30
+ export interface AppStateExportResult {
31
+ success: boolean
32
+ cancelled?: boolean
33
+ }
34
+
35
+ export interface AppStateRestoreResult {
36
+ /** Settings written. */
37
+ settings: number
38
+ /** Miniapps whose storage was restored. */
39
+ miniapps: number
40
+ memory: MemoryImportResult | null
41
+ /** Installed when the backup was made but not on this phone now. */
42
+ missing: string[]
43
+ }
44
+
45
+ export interface AppStateRestoreOptions {
46
+ /**
47
+ * Install what the backup had installed and this phone lacks. Runs before
48
+ * the restored miniapps start again, so the dashboard comes back with every
49
+ * widget provider its saved layout names: it drops unknown widgets from the
50
+ * layout the next time it saves it.
51
+ */
52
+ reinstall?: (packageNames: string[]) => Promise<void>
53
+ }
54
+
55
+ const PICK_MIME_TYPES =
56
+ Platform.OS === "android" ? ["application/json", "application/octet-stream"] : ["application/json"]
57
+
58
+ const backend: AppStateBackupBackend = {
59
+ get(key) {
60
+ const result = storage.load<unknown>(key)
61
+ return result.is_ok() ? result.value : null
62
+ },
63
+ set(key, value) {
64
+ const result = storage.save(key, value)
65
+ if (result.is_error()) throw result.error
66
+ },
67
+ keys: () => storage.getAllKeys(),
68
+ }
69
+
70
+ export async function exportAppStateFile(): Promise<AppStateExportResult> {
71
+ const userId = await cloudClientService.resolveUserId()
72
+ const installed: Array<{packageName: string; version: string; source?: string}> = []
73
+ for (const app of await appRegistry.getInstalledMiniapps()) {
74
+ const version = await appRegistry.getActiveVersion(app.packageName)
75
+ if (!version) continue
76
+ installed.push({
77
+ packageName: app.packageName,
78
+ version,
79
+ source: appRegistry.getReleaseIdentity(app.packageName, version)?.source,
80
+ })
81
+ }
82
+ const backup = collectAppStateBackup({
83
+ backend,
84
+ userId,
85
+ settings: Object.values(SETTINGS),
86
+ installed,
87
+ memory: await memoryStore.exportBackup(),
88
+ now: Date.now(),
89
+ })
90
+
91
+ const filename = `glassly-backup-${localDate(backup.exportedAt)}.json`
92
+ const file = new File(Paths.cache, filename)
93
+ file.writeSync(JSON.stringify(backup))
94
+ try {
95
+ await Share.open({url: file.uri, type: "application/json", filename, title: "Glassly backup"})
96
+ return {success: true}
97
+ } catch (error: any) {
98
+ // react-native-share rejects when the user dismisses the sheet.
99
+ if (error?.message?.includes("User did not share")) return {success: false, cancelled: true}
100
+ throw error
101
+ }
102
+ }
103
+
104
+ /** Resolves null when the user cancels the picker. */
105
+ export async function importAppStateFile(opts: AppStateRestoreOptions = {}): Promise<AppStateRestoreResult | null> {
106
+ const picked = await File.pickFileAsync({mimeTypes: PICK_MIME_TYPES})
107
+ if (picked.canceled) return null
108
+ let data: unknown
109
+ try {
110
+ data = JSON.parse(await picked.result.text())
111
+ } catch {
112
+ throw new AppStateBackupError("Not a Glassly app state backup")
113
+ }
114
+ const backup = parseAppStateBackup(data)
115
+ const userId = await cloudClientService.resolveUserId()
116
+
117
+ // First, so a memory section this build rejects fails the restore before anything is written.
118
+ const memory = backup.memory ? await memoryStore.importBackup(backup.memory) : null
119
+
120
+ const {plain, indexed} = restorableSettings(backup, Object.values(SETTINGS))
121
+ const written = await useSettingsStore.getState().setManyLocally(plain)
122
+ if (written.is_error()) throw written.error
123
+ // setManyLocally only takes registry keys; `key:index` values go to storage and the store directly.
124
+ for (const [key, value] of Object.entries(indexed)) backend.set(key, value)
125
+ if (Object.keys(indexed).length > 0) {
126
+ useSettingsStore.setState((state) => ({settings: {...state.settings, ...indexed}}))
127
+ }
128
+
129
+ // A running miniapp holds its old values in memory and could write them back over the
130
+ // restore, so the ones getting storage stop now and start again at the end.
131
+ const running = new Set(runningMiniappPackages())
132
+ const restarting = miniappsWithStorage(backup).filter((packageName) => running.has(packageName))
133
+ for (const packageName of restarting) await stopMiniapp(packageName)
134
+
135
+ try {
136
+ const restored = restoreMiniappStorage(backend, userId, backup)
137
+
138
+ const apps = useAppStatusStore.getState()
139
+ for (const [packageName, record] of Object.entries(backup.miniapps)) {
140
+ const prefs = miniappPrefs(record)
141
+ if (prefs.hidden !== undefined) apps.setHiddenStatus(packageName, prefs.hidden)
142
+ if (prefs.alwaysOn !== undefined) saveAlwaysOnOverride(packageName, prefs.alwaysOn)
143
+ for (const [widgetId, expand] of Object.entries(prefs.widgetExpand)) {
144
+ saveWidgetExpandChoice(packageName, widgetId, expand)
145
+ }
146
+ }
147
+ appRegistry.markRefreshNeeded()
148
+ await apps.refresh()
149
+
150
+ const installedNow = new Set((await appRegistry.getInstalledMiniapps()).map((app) => app.packageName))
151
+ const missing = Object.entries(backup.miniapps)
152
+ .filter(([packageName, record]) => record.installed && !installedNow.has(packageName))
153
+ .map(([packageName]) => packageName)
154
+ if (missing.length > 0) await opts.reinstall?.(missing)
155
+
156
+ return {
157
+ settings: Object.keys(plain).length + Object.keys(indexed).length,
158
+ miniapps: restored.packages.length,
159
+ memory,
160
+ missing,
161
+ }
162
+ } finally {
163
+ for (const packageName of restarting) await respawnMiniapp(packageName)
164
+ }
165
+ }
166
+
167
+ function localDate(ms: number): string {
168
+ const d = new Date(ms)
169
+ const pad = (n: number) => String(n).padStart(2, "0")
170
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
171
+ }
@@ -26,8 +26,8 @@ const NEURAL_BAND_SURFACE: InputSurface = {
26
26
  ],
27
27
  }
28
28
 
29
- /** The Apple Watch trackpad's taps and four-way swipes, and the Digital Crown as the dial. */
30
- const APPLE_WATCH_SURFACE: InputSurface = {
29
+ /** A watch trackpad's taps and four-way swipes, and the crown (Apple) or rotary bezel (Wear OS) as the dial. */
30
+ const WATCH_SURFACE: InputSurface = {
31
31
  type: "swipe2d",
32
32
  events: [
33
33
  "TAP",
@@ -42,10 +42,21 @@ const APPLE_WATCH_SURFACE: InputSurface = {
42
42
  ],
43
43
  }
44
44
 
45
+ /** The Gamebrick's D-pad is four absolute swipes; A is the press lifecycle (tap and hold) and B a double tap. */
46
+ const GAMEBRICK_SURFACE: InputSurface = {
47
+ type: "swipe2d",
48
+ events: ["TAP", "DOUBLE_TAP", "PRESS_HOLD", "SWIPE_UP", "SWIPE_DOWN", "SWIPE_LEFT", "SWIPE_RIGHT"],
49
+ }
50
+
45
51
  // The R1 swipes up/down like the temples it stands in for, so it adds nothing.
46
52
  const INPUT_SURFACES: Readonly<Record<string, readonly InputSurface[]>> = {
47
53
  [ControllerTypes.NEURAL_BAND]: [NEURAL_BAND_SURFACE],
48
- [ControllerTypes.APPLE_WATCH]: [APPLE_WATCH_SURFACE],
54
+ [ControllerTypes.APPLE_WATCH]: [WATCH_SURFACE],
55
+ [ControllerTypes.WEAR_OS]: [WATCH_SURFACE],
56
+ [ControllerTypes.GAMEBRICK]: [GAMEBRICK_SURFACE],
57
+ [ControllerTypes.IINE_ANANKE]: [GAMEBRICK_SURFACE],
58
+ [ControllerTypes.ZERO2]: [GAMEBRICK_SURFACE],
59
+ [ControllerTypes.MICRO]: [GAMEBRICK_SURFACE],
49
60
  [ControllerTypes.R1]: [],
50
61
  }
51
62
 
@@ -61,6 +61,8 @@ function glassesCapabilities(model: DeviceTypes): Capabilities {
61
61
  // Glassly CFW supports the complete vector contract, including SVG and rotation.
62
62
  // Upstream g2flash publishes a raster canvas with sceneVector=false.
63
63
  const vector = useGlassesStore.getState().sceneVector === true
64
+ // Depth is a disparity between the lenses; one panel (MemoMind One) draws every element flat.
65
+ const stereo = (base.display.count ?? 1) >= 2
64
66
  return {
65
67
  ...(volumeGate ? withVolumeGate(base) : base),
66
68
  hasNativeNavigation: resolveNativeNavigation(base.hasNativeNavigation, true, forceNativeNavigationOnCfw()),
@@ -68,6 +70,9 @@ function glassesCapabilities(model: DeviceTypes): Capabilities {
68
70
  ...base.display,
69
71
  width: canvas.width,
70
72
  height: canvas.height,
73
+ // A Glassly CFW that publishes a canvas takes positioned elements, whatever
74
+ // the stock profile says (the G1's stock firmware degrades scenes to text walls).
75
+ canPosition: true,
71
76
  maxTextElements: Math.max(
72
77
  base.display.maxTextElements ?? 0,
73
78
  vector ? CFW_VECTOR_MAX_ELEMENTS : CFW_MAX_TEXT_ELEMENTS,
@@ -78,7 +83,7 @@ function glassesCapabilities(model: DeviceTypes): Capabilities {
78
83
  vector ? CFW_VECTOR_MAX_ELEMENTS : CFW_MAX_IMAGE_ELEMENTS,
79
84
  ),
80
85
  ...(vector
81
- ? {shapes: [...ALL_DISPLAY_SHAPES, "svg" as const], animation: true, rotation: true, depth: true}
86
+ ? {shapes: [...ALL_DISPLAY_SHAPES, "svg" as const], animation: true, rotation: true, depth: stereo}
82
87
  : {}),
83
88
  },
84
89
  }
@@ -6,7 +6,12 @@
6
6
  * latest claim is also kept on its own, since the pairing screen runs before
7
7
  * the band's id reaches JS.
8
8
  */
9
- import type {NeuralBandClaimEvent, NeuralBandHandwritingEvent, NeuralBandPointerEvent} from "@glassly/bluetooth-sdk"
9
+ import type {
10
+ NeuralBandClaimEvent,
11
+ NeuralBandFirmwareEvent,
12
+ NeuralBandHandwritingEvent,
13
+ NeuralBandPointerEvent,
14
+ } from "@glassly/bluetooth-sdk"
10
15
  import {MiniappStreamType} from "@glassly/miniapp"
11
16
 
12
17
  import type {KeyboardEventPayload} from "../GlassesKeyboardInput"
@@ -34,6 +39,8 @@ export interface NeuralBandHandwritingUpdate {
34
39
  status: NeuralBandHandwritingStatus
35
40
  }
36
41
 
42
+ export type NeuralBandFirmwareStatus = Omit<NeuralBandFirmwareEvent, "type" | "controllerId"> & {known: boolean}
43
+
37
44
  export interface NeuralBandInputOptions {
38
45
  forward?: (stream: string, payload: unknown) => void
39
46
  now?: () => number
@@ -67,13 +74,25 @@ function codeFor(char: string): string {
67
74
 
68
75
  const IDLE_CLAIM: NeuralBandClaimStatus = {stage: "idle", sessionExpired: false}
69
76
  const IDLE_HANDWRITING: NeuralBandHandwritingStatus = {phase: "idle", message: "", text: ""}
77
+ const IDLE_FIRMWARE: NeuralBandFirmwareStatus = {
78
+ known: false,
79
+ connected: false,
80
+ hasMetaSession: false,
81
+ checked: false,
82
+ updateAvailable: false,
83
+ phase: "idle",
84
+ progress: 0,
85
+ message: "",
86
+ }
70
87
 
71
88
  export class NeuralBandInput {
72
89
  private latestClaim: NeuralBandClaimStatus = IDLE_CLAIM
73
90
  private claims = new Map<string, NeuralBandClaimStatus>()
74
91
  private handwriting = new Map<string, NeuralBandHandwritingStatus>()
92
+ private firmware = new Map<string, NeuralBandFirmwareStatus>()
75
93
  private claimListeners = new Set<(status: NeuralBandClaimStatus) => void>()
76
94
  private handwritingListeners = new Set<(update: NeuralBandHandwritingUpdate) => void>()
95
+ private firmwareListeners = new Set<(update: {id: string; status: NeuralBandFirmwareStatus}) => void>()
77
96
  private readonly forward: (stream: string, payload: unknown) => void
78
97
  private readonly now: () => number
79
98
 
@@ -102,6 +121,15 @@ export class NeuralBandInput {
102
121
  return () => this.handwritingListeners.delete(cb)
103
122
  }
104
123
 
124
+ firmwareStatus(controllerId: string): NeuralBandFirmwareStatus {
125
+ return this.firmware.get(controllerId) ?? IDLE_FIRMWARE
126
+ }
127
+
128
+ onFirmwareStatus(cb: (update: {id: string; status: NeuralBandFirmwareStatus}) => void): () => void {
129
+ this.firmwareListeners.add(cb)
130
+ return () => this.firmwareListeners.delete(cb)
131
+ }
132
+
105
133
  /** A new pairing attempt starts from a clean slate. */
106
134
  resetClaim(): void {
107
135
  this.latestClaim = IDLE_CLAIM
@@ -112,6 +140,14 @@ export class NeuralBandInput {
112
140
  forget(controllerId: string): void {
113
141
  this.claims.delete(controllerId)
114
142
  this.handwriting.delete(controllerId)
143
+ this.firmware.delete(controllerId)
144
+ }
145
+
146
+ handleFirmware(event: NeuralBandFirmwareEvent): void {
147
+ const {type: _type, controllerId, ...rest} = event
148
+ const status: NeuralBandFirmwareStatus = {...rest, known: true}
149
+ this.firmware.set(controllerId, status)
150
+ for (const cb of this.firmwareListeners) cb({id: controllerId, status})
115
151
  }
116
152
 
117
153
  handleClaim(event: NeuralBandClaimEvent): void {
@@ -32,6 +32,13 @@ function pushHand(id: string): void {
32
32
  )
33
33
  }
34
34
 
35
+ function pushVibration(id: string): void {
36
+ const enabled = useSettingsStore.getState().getSetting(SETTINGS.neural_band_vibration.key) !== false
37
+ void BluetoothSdk.neuralBandSetGestureHaptics(id, enabled).catch((err) =>
38
+ console.warn("NeuralBandSettingsSync: vibration push failed:", err),
39
+ )
40
+ }
41
+
35
42
  export function startNeuralBandSettingsSync(): void {
36
43
  if (unsubscribers.length > 0) return
37
44
  let connected = new Set(connectedBandIds())
@@ -44,6 +51,7 @@ export function startNeuralBandSettingsSync(): void {
44
51
  if (connected.has(id)) continue
45
52
  pushAirMouse(id)
46
53
  pushHand(id)
54
+ pushVibration(id)
47
55
  }
48
56
  connected = next
49
57
  },
@@ -56,6 +64,10 @@ export function startNeuralBandSettingsSync(): void {
56
64
  (s) => s.getSetting(SETTINGS.neural_band_hand.key),
57
65
  () => connectedBandIds().forEach(pushHand),
58
66
  ),
67
+ useSettingsStore.subscribe(
68
+ (s) => s.getSetting(SETTINGS.neural_band_vibration.key),
69
+ () => connectedBandIds().forEach(pushVibration),
70
+ ),
59
71
  ]
60
72
  }
61
73
 
@@ -13,7 +13,10 @@ import {isGlassesConnected} from "../GlassesReadiness"
13
13
 
14
14
  /** The SGC appends this marker after the firmware advertises Glassly CFW capabilities (G2, RayNeo iO). */
15
15
  export function isCfwFirmwareVersion(firmwareVersion: string | null | undefined): boolean {
16
- return firmwareVersion?.toLowerCase().includes("glassly cfw") === true
16
+ return (
17
+ firmwareVersion?.toLowerCase().includes("glassly cfw") === true ||
18
+ /^GLASSLYCFW\/[0-9]+$/.test(firmwareVersion ?? "")
19
+ )
17
20
  }
18
21
 
19
22
  /**
@@ -23,6 +26,11 @@ export function isCfwFirmwareVersion(firmwareVersion: string | null | undefined)
23
26
  */
24
27
  export function isCustomDashboardEligible(info: {model?: string | null; firmwareVersion?: string | null}): boolean {
25
28
  if (!info.model) return false
29
+ if (info.model === DeviceTypes.MEMOMIND_ONE) {
30
+ const match = /^GLASSLYCFW\/([0-9]+)$/.exec(info.firmwareVersion ?? "")
31
+ const revision = match ? Number(match[1]) : 0
32
+ return Number.isSafeInteger(revision) && revision >= 10
33
+ }
26
34
  return (
27
35
  getModelCapabilities(info.model as DeviceTypes).hasCustomFirmware === true &&
28
36
  isCfwFirmwareVersion(info.firmwareVersion)
@@ -38,6 +38,7 @@ export const SETTINGS_BRIDGE_KEYS: Readonly<Record<string, SettingsBridgeAccess>
38
38
  notifications_enabled: "write",
39
39
  notifications_popups: "write",
40
40
  notifications_popup_seconds: "write",
41
+ notifications_batch_seconds: "write",
41
42
  notifications_hold_during_apps: "write",
42
43
  notifications_ignore_while_active: "write",
43
44
  notifications_blocklist: "write",