@glassly/engine 0.3.7 → 0.3.8-dev.976
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.
- package/README.md +4 -0
- package/build/engine.d.ts +8 -2
- package/build/engine.d.ts.map +1 -1
- package/build/engine.js +7 -4
- package/build/engine.js.map +1 -1
- package/build/facades/backup.d.ts +14 -0
- package/build/facades/backup.d.ts.map +1 -0
- package/build/facades/backup.js +22 -0
- package/build/facades/backup.js.map +1 -0
- package/build/facades/glasses.d.ts +4 -4
- package/build/facades/glasses.d.ts.map +1 -1
- package/build/facades/glasses.js +4 -4
- package/build/facades/glasses.js.map +1 -1
- package/build/generated/releaseMetadata.js +4 -4
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts +1 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js.map +1 -1
- package/build/services/AppRegistry.d.ts +2 -1
- package/build/services/AppRegistry.d.ts.map +1 -1
- package/build/services/AppRegistry.js +38 -8
- package/build/services/AppRegistry.js.map +1 -1
- package/build/services/AppStateBackup.d.ts +98 -0
- package/build/services/AppStateBackup.d.ts.map +1 -0
- package/build/services/AppStateBackup.js +224 -0
- package/build/services/AppStateBackup.js.map +1 -0
- package/build/services/BlobStore.d.ts.map +1 -1
- package/build/services/BlobStore.js +5 -12
- package/build/services/BlobStore.js.map +1 -1
- package/build/services/CloudClientService.d.ts +1 -0
- package/build/services/CloudClientService.d.ts.map +1 -1
- package/build/services/CloudClientService.js +34 -2
- package/build/services/CloudClientService.js.map +1 -1
- package/build/services/DisplayProcessor.d.ts +2 -0
- package/build/services/DisplayProcessor.d.ts.map +1 -1
- package/build/services/DisplayProcessor.js +15 -1
- package/build/services/DisplayProcessor.js.map +1 -1
- package/build/services/GlassesTouchInput.d.ts.map +1 -1
- package/build/services/GlassesTouchInput.js +4 -2
- package/build/services/GlassesTouchInput.js.map +1 -1
- package/build/services/JSCrashloopReportService.d.ts.map +1 -1
- package/build/services/JSCrashloopReportService.js +5 -0
- package/build/services/JSCrashloopReportService.js.map +1 -1
- package/build/services/JSRouter.d.ts +11 -3
- package/build/services/JSRouter.d.ts.map +1 -1
- package/build/services/JSRouter.js +36 -6
- package/build/services/JSRouter.js.map +1 -1
- package/build/services/LocalMiniappRuntime.d.ts +13 -0
- package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
- package/build/services/LocalMiniappRuntime.js +54 -20
- package/build/services/LocalMiniappRuntime.js.map +1 -1
- package/build/services/MiniappEngine.d.ts +9 -0
- package/build/services/MiniappEngine.d.ts.map +1 -1
- package/build/services/MiniappEngine.js +60 -27
- package/build/services/MiniappEngine.js.map +1 -1
- package/build/services/MiniappLauncher.d.ts +2 -0
- package/build/services/MiniappLauncher.d.ts.map +1 -1
- package/build/services/MiniappLauncher.js +6 -0
- package/build/services/MiniappLauncher.js.map +1 -1
- package/build/services/NavigationService.d.ts +1 -0
- package/build/services/NavigationService.d.ts.map +1 -1
- package/build/services/NavigationService.js +20 -6
- package/build/services/NavigationService.js.map +1 -1
- package/build/services/PhoneCalendarService.d.ts.map +1 -1
- package/build/services/PhoneCalendarService.js +8 -0
- package/build/services/PhoneCalendarService.js.map +1 -1
- package/build/services/PhoneNotificationsSync.d.ts.map +1 -1
- package/build/services/PhoneNotificationsSync.js +7 -3
- package/build/services/PhoneNotificationsSync.js.map +1 -1
- package/build/services/SceneRenderer.js +1 -1
- package/build/services/SceneRenderer.js.map +1 -1
- package/build/services/appStateBackupFile.d.ts +32 -0
- package/build/services/appStateBackupFile.d.ts.map +1 -0
- package/build/services/appStateBackupFile.js +135 -0
- package/build/services/appStateBackupFile.js.map +1 -0
- package/build/services/controllers.d.ts.map +1 -1
- package/build/services/controllers.js +11 -3
- package/build/services/controllers.js.map +1 -1
- package/build/services/effectiveCapabilities.js +3 -0
- package/build/services/effectiveCapabilities.js.map +1 -1
- package/build/services/pages/pageEligibility.d.ts.map +1 -1
- package/build/services/pages/pageEligibility.js +8 -1
- package/build/services/pages/pageEligibility.js.map +1 -1
- package/build/services/settingsBridge.d.ts.map +1 -1
- package/build/services/settingsBridge.js +1 -0
- package/build/services/settingsBridge.js.map +1 -1
- package/build/services/{appleWatch/AppleWatchLink.d.ts → watch/WatchLink.d.ts} +8 -7
- package/build/services/watch/WatchLink.d.ts.map +1 -0
- package/build/services/{appleWatch/AppleWatchLink.js → watch/WatchLink.js} +14 -15
- package/build/services/watch/WatchLink.js.map +1 -0
- package/build/stores/apps.d.ts.map +1 -1
- package/build/stores/apps.js +14 -2
- package/build/stores/apps.js.map +1 -1
- package/build/stores/settings.d.ts.map +1 -1
- package/build/stores/settings.js +45 -3
- package/build/stores/settings.js.map +1 -1
- package/build/types/capabilities/even-realities-g1.d.ts.map +1 -1
- package/build/types/capabilities/even-realities-g1.js +3 -0
- package/build/types/capabilities/even-realities-g1.js.map +1 -1
- package/build/types/capabilities/memomind-one.d.ts +3 -0
- package/build/types/capabilities/memomind-one.d.ts.map +1 -0
- package/build/types/capabilities/memomind-one.js +41 -0
- package/build/types/capabilities/memomind-one.js.map +1 -0
- package/build/types/enums.d.ts +6 -2
- package/build/types/enums.d.ts.map +1 -1
- package/build/types/enums.js +4 -0
- package/build/types/enums.js.map +1 -1
- package/build/types/hardware.d.ts +3 -1
- package/build/types/hardware.d.ts.map +1 -1
- package/build/types/hardware.js +3 -1
- package/build/types/hardware.js.map +1 -1
- package/build/utils/display/index.d.ts +2 -1
- package/build/utils/display/index.d.ts.map +1 -1
- package/build/utils/display/index.js +3 -2
- package/build/utils/display/index.js.map +1 -1
- package/build/utils/display/profiles/index.d.ts +2 -1
- package/build/utils/display/profiles/index.d.ts.map +1 -1
- package/build/utils/display/profiles/index.js +3 -2
- package/build/utils/display/profiles/index.js.map +1 -1
- package/build/utils/display/profiles/rayneo-io.d.ts +1 -1
- package/build/utils/display/profiles/rayneo-io.d.ts.map +1 -1
- package/build/utils/display/profiles/rayneo-io.js +1 -1
- package/build/utils/display/profiles/rayneo-io.js.map +1 -1
- package/build/utils/display/scene/index.d.ts +1 -1
- package/build/utils/display/scene/index.d.ts.map +1 -1
- package/build/utils/display/scene/index.js +1 -1
- package/build/utils/display/scene/index.js.map +1 -1
- package/build/utils/display/scene/process.d.ts +6 -0
- package/build/utils/display/scene/process.d.ts.map +1 -1
- package/build/utils/display/scene/process.js +22 -7
- package/build/utils/display/scene/process.js.map +1 -1
- package/build/utils/miniappGlobals.d.ts +13 -17
- package/build/utils/miniappGlobals.d.ts.map +1 -1
- package/build/utils/miniappGlobals.js +24 -56
- package/build/utils/miniappGlobals.js.map +1 -1
- package/package.json +7 -7
- package/src/engine.ts +7 -4
- package/src/facades/backup.ts +28 -0
- package/src/facades/glasses.ts +5 -5
- package/src/generated/releaseMetadata.ts +4 -4
- package/src/index.ts +1 -0
- package/src/services/AppRegistry.ts +36 -9
- package/src/services/AppStateBackup.ts +283 -0
- package/src/services/BlobStore.ts +5 -11
- package/src/services/CloudClientService.ts +35 -4
- package/src/services/DisplayProcessor.ts +20 -8
- package/src/services/GlassesTouchInput.ts +6 -2
- package/src/services/JSCrashloopReportService.ts +5 -0
- package/src/services/JSRouter.ts +37 -6
- package/src/services/LocalMiniappRuntime.ts +55 -20
- package/src/services/MiniappEngine.ts +62 -27
- package/src/services/MiniappLauncher.ts +7 -0
- package/src/services/NavigationService.ts +28 -8
- package/src/services/PhoneCalendarService.ts +7 -0
- package/src/services/PhoneNotificationsSync.ts +8 -3
- package/src/services/SceneRenderer.ts +1 -1
- package/src/services/appStateBackupFile.ts +171 -0
- package/src/services/controllers.ts +12 -3
- package/src/services/effectiveCapabilities.ts +3 -0
- package/src/services/pages/pageEligibility.ts +9 -1
- package/src/services/settingsBridge.ts +1 -0
- package/src/services/{appleWatch/AppleWatchLink.ts → watch/WatchLink.ts} +20 -20
- package/src/stores/apps.ts +13 -2
- package/src/stores/settings.ts +52 -7
- package/src/types/capabilities/even-realities-g1.ts +4 -0
- package/src/types/capabilities/memomind-one.ts +42 -0
- package/src/types/enums.ts +4 -0
- package/src/types/hardware.ts +14 -1
- package/src/utils/display/index.ts +4 -2
- package/src/utils/display/profiles/index.ts +9 -2
- package/src/utils/display/profiles/rayneo-io.ts +6 -1
- package/src/utils/display/scene/index.ts +1 -1
- package/src/utils/display/scene/process.ts +22 -7
- package/src/utils/miniappGlobals.ts +32 -64
- package/build/services/appleWatch/AppleWatchLink.d.ts.map +0 -1
- package/build/services/appleWatch/AppleWatchLink.js.map +0 -1
|
@@ -658,7 +658,8 @@ class AppRegistry {
|
|
|
658
658
|
}
|
|
659
659
|
}
|
|
660
660
|
|
|
661
|
-
|
|
661
|
+
/** Undefined when no bundle is on disk (offline built-ins, HTTP-direct dev apps). */
|
|
662
|
+
public async getActiveVersion(packageName: string): Promise<string | undefined> {
|
|
662
663
|
let versions = this.getInstalledVersions(packageName)
|
|
663
664
|
// Treat MMKV as a hint, not authority. A stored version may have been
|
|
664
665
|
// GC'd off disk without this pointer being updated.
|
|
@@ -677,8 +678,10 @@ class AppRegistry {
|
|
|
677
678
|
}
|
|
678
679
|
versions = versions.filter((v) => semver.valid(v))
|
|
679
680
|
versions.sort((a, b) => semver.rcompare(a, b))
|
|
680
|
-
|
|
681
|
-
|
|
681
|
+
const latest = versions[0] as string | undefined
|
|
682
|
+
// Never persist undefined: MMKV.set throws on it.
|
|
683
|
+
if (latest) this.setActiveVersion(packageName, latest)
|
|
684
|
+
return latest
|
|
682
685
|
}
|
|
683
686
|
|
|
684
687
|
public setActiveVersion(packageName: string, version: string): Result<void, Error> {
|
|
@@ -727,7 +730,13 @@ class AppRegistry {
|
|
|
727
730
|
const installed = diskApps.filter(
|
|
728
731
|
(app) => !offlinePackages.has(app.packageName) && !devPackages.has(app.packageName),
|
|
729
732
|
)
|
|
730
|
-
|
|
733
|
+
// The last switcher-card capture comes back from disk, so a relaunched
|
|
734
|
+
// app's card is not blank until it is minimized again.
|
|
735
|
+
const withScreenshot = (app: ClientApp): ClientApp => ({
|
|
736
|
+
...app,
|
|
737
|
+
screenshot: app.screenshot ?? getLocalAppScreenshot(app.packageName),
|
|
738
|
+
})
|
|
739
|
+
return [...installed.map(withScreenshot), ...dev.map(withScreenshot), ...offline]
|
|
731
740
|
}
|
|
732
741
|
|
|
733
742
|
public async getInstalledMiniapps(): Promise<ClientApp[]> {
|
|
@@ -747,6 +756,8 @@ class AppRegistry {
|
|
|
747
756
|
const out: ClientApp[] = []
|
|
748
757
|
for (const lmaInfo of installedInfo) {
|
|
749
758
|
const versionString = await this.getActiveVersion(lmaInfo.packageName)
|
|
759
|
+
// A package dir with no usable bundle (e.g. only a half-written version) has nothing to run.
|
|
760
|
+
if (!versionString) continue
|
|
750
761
|
const versionInfo = lmaInfo.versions[versionString]
|
|
751
762
|
|
|
752
763
|
const manifest = this.getMiniappManifest(lmaInfo.packageName, versionString) as {
|
|
@@ -963,6 +974,19 @@ export function saveWidgetExpandChoice(packageName: string, widgetId: string, ex
|
|
|
963
974
|
storage.save(`${packageName}_widget_expand_${widgetId}`, expandOnTap)
|
|
964
975
|
}
|
|
965
976
|
|
|
977
|
+
/**
|
|
978
|
+
* Where a screenshot captured under an earlier install now lives: iOS gives
|
|
979
|
+
* every install a new container, so a stored absolute file URI goes stale
|
|
980
|
+
* while the file itself is still in the documents directory.
|
|
981
|
+
*/
|
|
982
|
+
function rerootDocumentUri(uri: string): string | null {
|
|
983
|
+
const marker = "/Documents/"
|
|
984
|
+
const at = uri.indexOf(marker)
|
|
985
|
+
if (at < 0) return null
|
|
986
|
+
const rerooted = new File(Paths.document, uri.slice(at + marker.length)).uri
|
|
987
|
+
return rerooted === uri ? null : rerooted
|
|
988
|
+
}
|
|
989
|
+
|
|
966
990
|
export function getLocalAppScreenshot(packageName: string): string | undefined {
|
|
967
991
|
const key = `${packageName}_screenshot`
|
|
968
992
|
const res = storage.load<string>(key)
|
|
@@ -971,14 +995,17 @@ export function getLocalAppScreenshot(packageName: string): string | undefined {
|
|
|
971
995
|
const uri = res.value
|
|
972
996
|
if (uri.startsWith("file://")) {
|
|
973
997
|
try {
|
|
974
|
-
if (
|
|
975
|
-
|
|
976
|
-
|
|
998
|
+
if (new File(uri).exists) return uri
|
|
999
|
+
const rerooted = rerootDocumentUri(uri)
|
|
1000
|
+
if (rerooted && new File(rerooted).exists) {
|
|
1001
|
+
storage.save(key, rerooted)
|
|
1002
|
+
return rerooted
|
|
977
1003
|
}
|
|
978
1004
|
} catch {
|
|
979
|
-
|
|
980
|
-
return undefined
|
|
1005
|
+
// fall through: the stored uri is unusable
|
|
981
1006
|
}
|
|
1007
|
+
storage.remove(key)
|
|
1008
|
+
return undefined
|
|
982
1009
|
}
|
|
983
1010
|
|
|
984
1011
|
return uri
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App state backups (Settings > Profile > Backup): one JSON file carrying the
|
|
3
|
+
* system settings, every miniapp's SimpleStorage, the installed miniapp list
|
|
4
|
+
* with each app's home flags, and the memory timeline.
|
|
5
|
+
*
|
|
6
|
+
* It exists so a logout or reinstall doesn't mean signing back into every
|
|
7
|
+
* miniapp, which is why the file holds live credentials (miniapp tokens,
|
|
8
|
+
* provider API keys) as plain JSON. Pairing identity, cloud endpoints, and
|
|
9
|
+
* consent/login state stay out: they belong to the install and the account,
|
|
10
|
+
* not to the user's preferences.
|
|
11
|
+
*
|
|
12
|
+
* Storage keys are written without their user segment, so a backup restores
|
|
13
|
+
* into whatever namespace the phone has now (account or `local_`). Restore
|
|
14
|
+
* overwrites key by key and leaves keys the backup doesn't carry alone.
|
|
15
|
+
*
|
|
16
|
+
* Pure: the MMKV view, settings registry, and memory snapshot are passed in by
|
|
17
|
+
* appStateBackupFile.ts.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type {MemoryBackup} from "@glassly/miniapp"
|
|
21
|
+
|
|
22
|
+
/** Key-value surface (MMKV in production, values already JSON-decoded). */
|
|
23
|
+
export interface AppStateBackupBackend {
|
|
24
|
+
get(key: string): unknown | null
|
|
25
|
+
set(key: string, value: unknown): void
|
|
26
|
+
keys(): string[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The fields of a SETTINGS descriptor that decide whether it is backed up. */
|
|
30
|
+
export interface BackupSettingDescriptor {
|
|
31
|
+
key: string
|
|
32
|
+
persist: boolean
|
|
33
|
+
writable: boolean
|
|
34
|
+
nativeAuthoritative?: true
|
|
35
|
+
resetOnBuildEnvChange?: true
|
|
36
|
+
indexer?: (key: string) => string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface AppStateBackupMiniapp {
|
|
40
|
+
/** Set when the miniapp was installed at export. */
|
|
41
|
+
installed?: {version: string; source?: string}
|
|
42
|
+
/** SimpleStorage key → stored value. */
|
|
43
|
+
storage: Record<string, unknown>
|
|
44
|
+
/** Home flags by suffix: `hidden`, `always_on`, `widget_expand_<widgetId>`. */
|
|
45
|
+
prefs: Record<string, unknown>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface AppStateBackup {
|
|
49
|
+
format: "glassly-app-state"
|
|
50
|
+
version: 1
|
|
51
|
+
/** Epoch ms the backup was written. */
|
|
52
|
+
exportedAt: number
|
|
53
|
+
/** Stored setting values; indexed settings keep their `key:index` storage key. */
|
|
54
|
+
settings: Record<string, unknown>
|
|
55
|
+
miniapps: Record<string, AppStateBackupMiniapp>
|
|
56
|
+
memory: MemoryBackup | null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Thrown for a file that isn't an app state backup this build can read. */
|
|
60
|
+
export class AppStateBackupError extends Error {
|
|
61
|
+
constructor(message: string) {
|
|
62
|
+
super(message)
|
|
63
|
+
this.name = "AppStateBackupError"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const STORAGE_ROOT = "localstorage_"
|
|
68
|
+
const PREF_SUFFIXES = ["hidden", "always_on"] as const
|
|
69
|
+
const WIDGET_EXPAND_PREFIX = "widget_expand_"
|
|
70
|
+
// Same rule as the miniapp.json schema; keeps a hand-edited file from writing keys no app reads.
|
|
71
|
+
const PACKAGE_NAME_RE = /^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)+$/
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Persisted, writable settings that are still not the user's to carry between
|
|
75
|
+
* installs: account and consent state, the paired devices, and caches.
|
|
76
|
+
*/
|
|
77
|
+
const EXCLUDED_SETTINGS = new Set([
|
|
78
|
+
"auth_email",
|
|
79
|
+
"disable_cloud",
|
|
80
|
+
"ai_data_consent_version",
|
|
81
|
+
"ai_data_consent_agreed",
|
|
82
|
+
"login_skipped",
|
|
83
|
+
"has_paired_glasses",
|
|
84
|
+
"keyboard_name",
|
|
85
|
+
"cached_required_version",
|
|
86
|
+
"offline_camera_running",
|
|
87
|
+
"offline_translation_running",
|
|
88
|
+
])
|
|
89
|
+
|
|
90
|
+
export function isBackupSetting(setting: BackupSettingDescriptor): boolean {
|
|
91
|
+
return (
|
|
92
|
+
setting.persist &&
|
|
93
|
+
setting.writable &&
|
|
94
|
+
!setting.nativeAuthoritative &&
|
|
95
|
+
!setting.resetOnBuildEnvChange &&
|
|
96
|
+
!EXCLUDED_SETTINGS.has(setting.key)
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface CollectAppStateInput {
|
|
101
|
+
backend: AppStateBackupBackend
|
|
102
|
+
userId: string
|
|
103
|
+
settings: BackupSettingDescriptor[]
|
|
104
|
+
installed: Array<{packageName: string; version: string; source?: string}>
|
|
105
|
+
memory: MemoryBackup | null
|
|
106
|
+
now: number
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function collectAppStateBackup(input: CollectAppStateInput): AppStateBackup {
|
|
110
|
+
const {backend, settings} = input
|
|
111
|
+
const keys = backend.keys()
|
|
112
|
+
|
|
113
|
+
const settingValues: Record<string, unknown> = {}
|
|
114
|
+
for (const setting of settings) {
|
|
115
|
+
if (!isBackupSetting(setting)) continue
|
|
116
|
+
if (setting.indexer) {
|
|
117
|
+
for (const key of keys) {
|
|
118
|
+
if (!key.startsWith(`${setting.key}:`)) continue
|
|
119
|
+
const value = backend.get(key)
|
|
120
|
+
if (value != null) settingValues[key] = value
|
|
121
|
+
}
|
|
122
|
+
continue
|
|
123
|
+
}
|
|
124
|
+
const value = backend.get(setting.key)
|
|
125
|
+
if (value != null) settingValues[setting.key] = value
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const miniapps: Record<string, AppStateBackupMiniapp> = {}
|
|
129
|
+
const entry = (packageName: string) => (miniapps[packageName] ??= {storage: {}, prefs: {}})
|
|
130
|
+
|
|
131
|
+
for (const app of input.installed) {
|
|
132
|
+
const record = entry(app.packageName)
|
|
133
|
+
record.installed = app.source ? {version: app.version, source: app.source} : {version: app.version}
|
|
134
|
+
for (const suffix of PREF_SUFFIXES) {
|
|
135
|
+
const value = backend.get(`${app.packageName}_${suffix}`)
|
|
136
|
+
if (value != null) record.prefs[suffix] = value
|
|
137
|
+
}
|
|
138
|
+
const expandPrefix = `${app.packageName}_${WIDGET_EXPAND_PREFIX}`
|
|
139
|
+
for (const key of keys) {
|
|
140
|
+
if (!key.startsWith(expandPrefix)) continue
|
|
141
|
+
const value = backend.get(key)
|
|
142
|
+
if (value != null) record.prefs[key.slice(app.packageName.length + 1)] = value
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Longest first, so a package whose name extends another's still claims its own keys.
|
|
147
|
+
const known = input.installed.map((app) => app.packageName).sort((a, b) => b.length - a.length)
|
|
148
|
+
const prefix = `${STORAGE_ROOT}${input.userId}_`
|
|
149
|
+
for (const key of keys) {
|
|
150
|
+
if (!key.startsWith(prefix)) continue
|
|
151
|
+
const split = splitStorageKey(key.slice(prefix.length), known)
|
|
152
|
+
if (!split) continue
|
|
153
|
+
const value = backend.get(key)
|
|
154
|
+
if (value != null) entry(split.packageName).storage[split.key] = value
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
format: "glassly-app-state",
|
|
159
|
+
version: 1,
|
|
160
|
+
exportedAt: input.now,
|
|
161
|
+
settings: settingValues,
|
|
162
|
+
miniapps,
|
|
163
|
+
memory: input.memory,
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** `{packageName}_{key}` → its parts. Package names may contain `_`, so known names win over the first `_`. */
|
|
168
|
+
function splitStorageKey(rest: string, known: string[]): {packageName: string; key: string} | null {
|
|
169
|
+
const packageName = known.find((name) => rest.startsWith(`${name}_`)) ?? rest.slice(0, rest.indexOf("_"))
|
|
170
|
+
const key = rest.slice(packageName.length + 1)
|
|
171
|
+
if (!packageName || !key) return null
|
|
172
|
+
return {packageName, key}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function parseAppStateBackup(data: unknown): AppStateBackup {
|
|
176
|
+
if (!isRecord(data) || data.format !== "glassly-app-state") {
|
|
177
|
+
throw new AppStateBackupError("Not a Glassly app state backup")
|
|
178
|
+
}
|
|
179
|
+
if (typeof data.version !== "number" || data.version > 1) {
|
|
180
|
+
throw new AppStateBackupError(`Unsupported app state backup version ${String(data.version)}`)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const settings = isRecord(data.settings) ? data.settings : {}
|
|
184
|
+
const miniapps: Record<string, AppStateBackupMiniapp> = {}
|
|
185
|
+
if (isRecord(data.miniapps)) {
|
|
186
|
+
for (const [packageName, raw] of Object.entries(data.miniapps)) {
|
|
187
|
+
if (!PACKAGE_NAME_RE.test(packageName) || !isRecord(raw)) continue
|
|
188
|
+
const record: AppStateBackupMiniapp = {
|
|
189
|
+
storage: isRecord(raw.storage) ? raw.storage : {},
|
|
190
|
+
prefs: isRecord(raw.prefs) ? raw.prefs : {},
|
|
191
|
+
}
|
|
192
|
+
if (isRecord(raw.installed) && typeof raw.installed.version === "string") {
|
|
193
|
+
record.installed = {version: raw.installed.version}
|
|
194
|
+
if (typeof raw.installed.source === "string") record.installed.source = raw.installed.source
|
|
195
|
+
}
|
|
196
|
+
miniapps[packageName] = record
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let memory: MemoryBackup | null = null
|
|
201
|
+
if (data.memory != null) {
|
|
202
|
+
if (!isRecord(data.memory) || data.memory.format !== "glassly-memory") {
|
|
203
|
+
throw new AppStateBackupError("The backup's memory section is not a memory backup")
|
|
204
|
+
}
|
|
205
|
+
memory = data.memory as unknown as MemoryBackup
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return {format: "glassly-app-state", version: 1, exportedAt: Number(data.exportedAt) || 0, settings, miniapps, memory}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* The backup's settings this build will restore, split by how they're written:
|
|
213
|
+
* `plain` through the settings store, `indexed` (`key:index`) straight to storage.
|
|
214
|
+
* Keys this build doesn't know, or doesn't back up, are dropped.
|
|
215
|
+
*/
|
|
216
|
+
export function restorableSettings(
|
|
217
|
+
backup: AppStateBackup,
|
|
218
|
+
settings: BackupSettingDescriptor[],
|
|
219
|
+
): {plain: Record<string, unknown>; indexed: Record<string, unknown>} {
|
|
220
|
+
const byKey = new Map(settings.map((setting) => [setting.key, setting]))
|
|
221
|
+
const plain: Record<string, unknown> = {}
|
|
222
|
+
const indexed: Record<string, unknown> = {}
|
|
223
|
+
for (const [key, value] of Object.entries(backup.settings)) {
|
|
224
|
+
if (value == null) continue
|
|
225
|
+
const colon = key.indexOf(":")
|
|
226
|
+
const setting = byKey.get(colon < 0 ? key : key.slice(0, colon))
|
|
227
|
+
if (!setting || !isBackupSetting(setting)) continue
|
|
228
|
+
if (colon < 0 && !setting.indexer) plain[key] = value
|
|
229
|
+
else if (colon > 0 && setting.indexer) indexed[key] = value
|
|
230
|
+
}
|
|
231
|
+
return {plain, indexed}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Miniapps the backup has storage for. */
|
|
235
|
+
export function miniappsWithStorage(backup: AppStateBackup): string[] {
|
|
236
|
+
return Object.entries(backup.miniapps)
|
|
237
|
+
.filter(([, record]) => Object.entries(record.storage).some(([key, value]) => key && value != null))
|
|
238
|
+
.map(([packageName]) => packageName)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Write every miniapp's storage into `userId`'s namespace. Returns the packages that got keys. */
|
|
242
|
+
export function restoreMiniappStorage(
|
|
243
|
+
backend: AppStateBackupBackend,
|
|
244
|
+
userId: string,
|
|
245
|
+
backup: AppStateBackup,
|
|
246
|
+
): {packages: string[]; keys: number} {
|
|
247
|
+
const packages: string[] = []
|
|
248
|
+
let keys = 0
|
|
249
|
+
for (const [packageName, record] of Object.entries(backup.miniapps)) {
|
|
250
|
+
let wrote = 0
|
|
251
|
+
for (const [key, value] of Object.entries(record.storage)) {
|
|
252
|
+
if (!key || value == null) continue
|
|
253
|
+
backend.set(`${STORAGE_ROOT}${userId}_${packageName}_${key}`, value)
|
|
254
|
+
wrote++
|
|
255
|
+
}
|
|
256
|
+
if (wrote > 0) packages.push(packageName)
|
|
257
|
+
keys += wrote
|
|
258
|
+
}
|
|
259
|
+
return {packages, keys}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export interface MiniappPrefs {
|
|
263
|
+
hidden?: boolean
|
|
264
|
+
alwaysOn?: boolean
|
|
265
|
+
widgetExpand: Record<string, boolean>
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** A miniapp's home flags from the backup, with anything that isn't a boolean dropped. */
|
|
269
|
+
export function miniappPrefs(record: AppStateBackupMiniapp): MiniappPrefs {
|
|
270
|
+
const prefs: MiniappPrefs = {widgetExpand: {}}
|
|
271
|
+
if (typeof record.prefs.hidden === "boolean") prefs.hidden = record.prefs.hidden
|
|
272
|
+
if (typeof record.prefs.always_on === "boolean") prefs.alwaysOn = record.prefs.always_on
|
|
273
|
+
for (const [key, value] of Object.entries(record.prefs)) {
|
|
274
|
+
if (!key.startsWith(WIDGET_EXPAND_PREFIX) || typeof value !== "boolean") continue
|
|
275
|
+
const widgetId = key.slice(WIDGET_EXPAND_PREFIX.length)
|
|
276
|
+
if (widgetId) prefs.widgetExpand[widgetId] = value
|
|
277
|
+
}
|
|
278
|
+
return prefs
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
282
|
+
return value != null && typeof value === "object" && !Array.isArray(value)
|
|
283
|
+
}
|
|
@@ -504,20 +504,14 @@ export class BlobStore {
|
|
|
504
504
|
const key = (typeof payload.key === "string" && payload.key) || this.makeId()
|
|
505
505
|
const mimeFilter = payload.mimeType as string | undefined
|
|
506
506
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
// Picker dismissed / cancelled (or unavailable) → resolve to null, not an error.
|
|
512
|
-
console.log(`${LOG_TAG}: blob import picker dismissed`, err)
|
|
513
|
-
this.hooks.sendResult(packageName, requestId, true, null)
|
|
514
|
-
return
|
|
515
|
-
}
|
|
516
|
-
const file = Array.isArray(picked) ? picked[0] : picked
|
|
517
|
-
if (!file) {
|
|
507
|
+
// The options form resolves {canceled, result} and never throws; the deprecated
|
|
508
|
+
// (initialUri, mimeType) form resolves a bare File only when a mime type is passed.
|
|
509
|
+
const picked = await File.pickFileAsync(mimeFilter ? {mimeTypes: mimeFilter} : {})
|
|
510
|
+
if (picked.canceled) {
|
|
518
511
|
this.hooks.sendResult(packageName, requestId, true, null)
|
|
519
512
|
return
|
|
520
513
|
}
|
|
514
|
+
const file = picked.result
|
|
521
515
|
try {
|
|
522
516
|
const size = file.size ?? 0
|
|
523
517
|
if (this.overQuota(packageName, key, size)) {
|
|
@@ -21,7 +21,7 @@ import type {
|
|
|
21
21
|
StoreListing,
|
|
22
22
|
} from "@glassly/cloud-client/react-native"
|
|
23
23
|
import type {SubjectTokenType} from "@glassly/cloud-client"
|
|
24
|
-
import {Platform} from "react-native"
|
|
24
|
+
import {AppState, Platform} from "react-native"
|
|
25
25
|
import type {AudioSubscription, QuotaStatusPayload, TranscriptionData, TranslationData} from "@glassly/cloud-protocol"
|
|
26
26
|
|
|
27
27
|
import BluetoothSdk from "@glassly/bluetooth-sdk/internal"
|
|
@@ -70,6 +70,8 @@ const LOCAL_AUTH_PORT = 3002
|
|
|
70
70
|
|
|
71
71
|
let client: CloudClient | null = null
|
|
72
72
|
let connected = false
|
|
73
|
+
/** The cloud closed us for a newer socket; the client stays down until reconnect(). */
|
|
74
|
+
let superseded = false
|
|
73
75
|
let persistentFailureTimer: ReturnType<typeof BgTimer.setTimeout> | null = null
|
|
74
76
|
let persistentFailureNotified = false
|
|
75
77
|
let audioSubscriptions: AudioSubscription[] = []
|
|
@@ -84,6 +86,7 @@ let authStateUnsubscribe: (() => void) | null = null
|
|
|
84
86
|
let localDevRuntimeToken: {runtimeUrl: string; token: string; expiresAtMs: number} | null = null
|
|
85
87
|
let lc3FrameSizeUnsubscribe: (() => void) | null = null
|
|
86
88
|
let disableCloudUnsubscribe: (() => void) | null = null
|
|
89
|
+
let foregroundUnsubscribe: (() => void) | null = null
|
|
87
90
|
let coreTokenSyncPromise: Promise<string> | null = null
|
|
88
91
|
/**
|
|
89
92
|
* A device-local namespace (`local_…`) whose data still needs to be adopted
|
|
@@ -511,6 +514,26 @@ function stopLc3FrameSizeWatcher(): void {
|
|
|
511
514
|
lc3FrameSizeUnsubscribe = null
|
|
512
515
|
}
|
|
513
516
|
|
|
517
|
+
/**
|
|
518
|
+
* Reclaim a superseded session only on foreground: the user picked this phone
|
|
519
|
+
* up, and a reclaim on any other signal would fight the newer socket.
|
|
520
|
+
*/
|
|
521
|
+
function startForegroundWatcher(): void {
|
|
522
|
+
if (foregroundUnsubscribe) return
|
|
523
|
+
|
|
524
|
+
const subscription = AppState.addEventListener("change", (state) => {
|
|
525
|
+
if (state !== "active" || !superseded || !client) return
|
|
526
|
+
console.log(`${LOG_TAG}: foregrounded after supersede; reconnecting runtime`)
|
|
527
|
+
cloudClientService.reconnect()
|
|
528
|
+
})
|
|
529
|
+
foregroundUnsubscribe = () => subscription.remove()
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function stopForegroundWatcher(): void {
|
|
533
|
+
foregroundUnsubscribe?.()
|
|
534
|
+
foregroundUnsubscribe = null
|
|
535
|
+
}
|
|
536
|
+
|
|
514
537
|
/**
|
|
515
538
|
* Apply a mid-session flip of the Privacy toggle without a restart: turning it
|
|
516
539
|
* on tears the live client down, turning it back off reconnects. Installed by
|
|
@@ -596,6 +619,7 @@ function construct(): void {
|
|
|
596
619
|
c.runtime.onConnected(() => {
|
|
597
620
|
if (c !== client) return
|
|
598
621
|
connected = true
|
|
622
|
+
superseded = false
|
|
599
623
|
console.log(`${LOG_TAG}: runtime connected`)
|
|
600
624
|
console.log(`${LOG_TAG}: debug: ws-session-debug connected`)
|
|
601
625
|
// Cloud is up — cancel any pending persistent-failure alarm and re-arm for the
|
|
@@ -613,10 +637,9 @@ function construct(): void {
|
|
|
613
637
|
c.runtime.onDisconnected((info) => {
|
|
614
638
|
if (c !== client) return
|
|
615
639
|
connected = false
|
|
640
|
+
superseded = /superseded by newer session/i.test(info.reason)
|
|
616
641
|
console.log(`${LOG_TAG}: runtime disconnected (${info.reason})`)
|
|
617
|
-
console.log(
|
|
618
|
-
`${LOG_TAG}: debug: ws-session-debug disconnected reason=${info.reason} willStayDown=${/superseded by newer session/i.test(info.reason)}`,
|
|
619
|
-
)
|
|
642
|
+
console.log(`${LOG_TAG}: debug: ws-session-debug disconnected reason=${info.reason} willStayDown=${superseded}`)
|
|
620
643
|
// Arm the persistent-failure alarm once; if we're still down when it fires, raise
|
|
621
644
|
// the notification. A reconnect within the window cancels it (onConnected above).
|
|
622
645
|
if (!persistentFailureTimer && !persistentFailureNotified) {
|
|
@@ -659,6 +682,7 @@ function construct(): void {
|
|
|
659
682
|
)
|
|
660
683
|
|
|
661
684
|
startLc3FrameSizeWatcher()
|
|
685
|
+
startForegroundWatcher()
|
|
662
686
|
}
|
|
663
687
|
|
|
664
688
|
/**
|
|
@@ -720,6 +744,7 @@ export const cloudClientService = {
|
|
|
720
744
|
client = null
|
|
721
745
|
localDevRuntimeToken = null
|
|
722
746
|
connected = false
|
|
747
|
+
superseded = false
|
|
723
748
|
resetRuntimeStatus()
|
|
724
749
|
if (wasConnected) notifyConnectionListeners(false)
|
|
725
750
|
|
|
@@ -835,11 +860,13 @@ export const cloudClientService = {
|
|
|
835
860
|
console.warn(`${LOG_TAG}: stop close() failed: ${(err as Error)?.message ?? err}`)
|
|
836
861
|
}
|
|
837
862
|
stopLc3FrameSizeWatcher()
|
|
863
|
+
stopForegroundWatcher()
|
|
838
864
|
stopAuthWatch()
|
|
839
865
|
clearRuntimeEventSubscriptions()
|
|
840
866
|
clearPersistentFailureAlarm()
|
|
841
867
|
const wasConnected = connected
|
|
842
868
|
client = null
|
|
869
|
+
superseded = false
|
|
843
870
|
// stop() is part of the logout lifecycle and runs before the host emits
|
|
844
871
|
// SIGNED_OUT. Remove the persisted namespace owner here so a later account
|
|
845
872
|
// can never inherit it. A force-stop/process kill does not call stop(), so
|
|
@@ -855,6 +882,10 @@ export const cloudClientService = {
|
|
|
855
882
|
shutdown(): void {
|
|
856
883
|
this.stop()
|
|
857
884
|
stopDisableCloudWatcher()
|
|
885
|
+
// The next engine.start() is configured afresh; an override left by a
|
|
886
|
+
// reconnect() in this process would otherwise outrank the new config
|
|
887
|
+
// and keep the client on the previous backend.
|
|
888
|
+
endpointsOverride = null
|
|
858
889
|
},
|
|
859
890
|
|
|
860
891
|
/** Device-side managed photo: presign now, deliver bytes, await ready. */
|
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
Z100_PROFILE,
|
|
21
21
|
NEX_PROFILE,
|
|
22
22
|
RAYNEO_IO_PROFILE,
|
|
23
|
+
MEMOMIND_ONE_PROFILE,
|
|
24
|
+
RAYNEO_IO_CFW_PROFILE,
|
|
23
25
|
TextMeasurer,
|
|
24
26
|
TextWrapper,
|
|
25
27
|
ColumnComposer,
|
|
@@ -42,12 +44,7 @@ import {isGlassesConnected} from "./GlassesReadiness"
|
|
|
42
44
|
* Display event types that we process
|
|
43
45
|
*/
|
|
44
46
|
export type DisplayLayoutType =
|
|
45
|
-
| "
|
|
46
|
-
| "text_line"
|
|
47
|
-
| "text_rows"
|
|
48
|
-
| "reference_card"
|
|
49
|
-
| "double_text_wall"
|
|
50
|
-
| "bitmap_view"
|
|
47
|
+
"text_wall" | "text_line" | "text_rows" | "reference_card" | "double_text_wall" | "bitmap_view"
|
|
51
48
|
|
|
52
49
|
/**
|
|
53
50
|
* Raw display event from the cloud/WebSocket
|
|
@@ -208,6 +205,16 @@ const DEVICE_PROFILES: Partial<Record<DeviceTypes, DisplayProfile>> = {
|
|
|
208
205
|
[DeviceTypes.NEX]: NEX_PROFILE,
|
|
209
206
|
[DeviceTypes.MACH1]: Z100_PROFILE, // Mach1 uses same hardware as Vuzix Z100
|
|
210
207
|
[DeviceTypes.RAYNEO_IO]: RAYNEO_IO_PROFILE,
|
|
208
|
+
[DeviceTypes.MEMOMIND_ONE]: MEMOMIND_ONE_PROFILE,
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Scene text profiles for models whose Glassly CFW draws text with a font of
|
|
213
|
+
* its own, used while the CFW publishes a scene canvas.
|
|
214
|
+
*/
|
|
215
|
+
const CFW_SCENE_PROFILES: Partial<Record<DeviceTypes, DisplayProfile>> = {
|
|
216
|
+
[DeviceTypes.RAYNEO_IO]: RAYNEO_IO_CFW_PROFILE,
|
|
217
|
+
[DeviceTypes.MEMOMIND_ONE]: MEMOMIND_ONE_PROFILE,
|
|
211
218
|
}
|
|
212
219
|
|
|
213
220
|
// =============================================================================
|
|
@@ -275,8 +282,7 @@ export class DisplayProcessor {
|
|
|
275
282
|
// Seed the device profile from the saved default wearable, read off the engine
|
|
276
283
|
// settings store directly (was a host settings hook).
|
|
277
284
|
const defaultWearable = useSettingsStore.getState().getSetting(ISLAND_SETTINGS_KEYS.defaultWearable) as
|
|
278
|
-
|
|
|
279
|
-
| undefined
|
|
285
|
+
string | undefined
|
|
280
286
|
if (defaultWearable) {
|
|
281
287
|
this.setDeviceModel(defaultWearable)
|
|
282
288
|
console.log(`DISPLAY_PROCESSOR: Initialized DisplayProcessor with default wearable: ${defaultWearable}`)
|
|
@@ -380,6 +386,12 @@ export class DisplayProcessor {
|
|
|
380
386
|
return this.profile
|
|
381
387
|
}
|
|
382
388
|
|
|
389
|
+
/** Profile scene text is wrapped with: the CFW's own while its scene page is up, else the model's. */
|
|
390
|
+
public getSceneProfile(): DisplayProfile {
|
|
391
|
+
const cfw = CFW_SCENE_PROFILES[this.deviceModel]
|
|
392
|
+
return cfw && (useGlassesStore.getState().sceneCanvasWidth ?? 0) > 0 ? cfw : this.profile
|
|
393
|
+
}
|
|
394
|
+
|
|
383
395
|
// ===========================================================================
|
|
384
396
|
// Display Event Processing
|
|
385
397
|
// ===========================================================================
|
|
@@ -110,7 +110,7 @@ function routeTouchEvent(event: TouchEvent): void {
|
|
|
110
110
|
const doubleTap = gesture === "double_tap"
|
|
111
111
|
// The Neural Band's middle finger (index 1) shares the tap names. Either
|
|
112
112
|
// finger leaves the app on screen; the firmware dashboard is the index finger's.
|
|
113
|
-
const osDoubleTap = doubleTap && event.index
|
|
113
|
+
const osDoubleTap = doubleTap && !(event.source === 3 && event.index === 1)
|
|
114
114
|
const osHeadUp = gesture === "head_up"
|
|
115
115
|
|
|
116
116
|
if (doubleTap) {
|
|
@@ -156,7 +156,11 @@ function routeTouchEvent(event: TouchEvent): void {
|
|
|
156
156
|
// pagesEligible: connected Glassly CFW glasses, whose firmware leaves the
|
|
157
157
|
// dashboard to the phone; other firmware opens its own on a lift.
|
|
158
158
|
const opensNativeDashboard = osDoubleTap || (osHeadUp && pagesEligible())
|
|
159
|
-
if (
|
|
159
|
+
if (
|
|
160
|
+
opensNativeDashboard &&
|
|
161
|
+
!nativeNavigationService.isActive &&
|
|
162
|
+
isGlassesConnected(useGlassesStore.getState().connection)
|
|
163
|
+
) {
|
|
160
164
|
useDisplayStore.getState().setDisplayEvent({layout: {layoutType: "unavailable", reason: "firmware-dashboard"}})
|
|
161
165
|
void Promise.resolve(BluetoothSdk.showDashboard()).catch((err) => {
|
|
162
166
|
console.error("DeviceEventRouter: showDashboard failed:", err)
|
|
@@ -19,6 +19,11 @@ export async function submitJSCrashloopReport(params: {
|
|
|
19
19
|
}): Promise<AutomaticReportSubmissionStatus> {
|
|
20
20
|
const {packageName, reason, lastLogLines = []} = params
|
|
21
21
|
const app = useAppStatusStore.getState().apps.find((a) => a.packageName === packageName)
|
|
22
|
+
// A dev miniapp's crash is its developer's to fix; JSRouter forwards it to their dev server.
|
|
23
|
+
if (app?.isMiniappDev) {
|
|
24
|
+
console.log(`[${LOG_TAG}] Not reporting a dev miniapp crashloop:`, packageName)
|
|
25
|
+
return {status: "skipped", reason: "dev miniapp"}
|
|
26
|
+
}
|
|
22
27
|
const appName = app?.name ?? packageName
|
|
23
28
|
const result = await submitAutomaticReport({
|
|
24
29
|
kind: "automatic",
|