@glassly/engine 0.3.0-dev.462 → 0.3.0-dev.465
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/build/engine.d.ts +3 -0
- package/build/engine.d.ts.map +1 -1
- package/build/facades/miniapps.d.ts +6 -0
- package/build/facades/miniapps.d.ts.map +1 -1
- package/build/facades/miniapps.js +7 -0
- package/build/facades/miniapps.js.map +1 -1
- package/build/generated/releaseMetadata.js +3 -3
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -1
- package/build/index.js.map +1 -1
- package/build/services/AppRegistry.js +1 -1
- package/build/services/AppRegistry.js.map +1 -1
- package/build/services/LocalMiniappRuntime.d.ts +22 -9
- package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
- package/build/services/LocalMiniappRuntime.js +104 -49
- package/build/services/LocalMiniappRuntime.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 -1
- package/build/services/MiniappLauncher.js.map +1 -1
- package/build/services/miniappPermissions.d.ts +16 -0
- package/build/services/miniappPermissions.d.ts.map +1 -0
- package/build/services/miniappPermissions.js +7 -0
- package/build/services/miniappPermissions.js.map +1 -0
- package/build/services/pages/OverlayPages.d.ts +13 -2
- package/build/services/pages/OverlayPages.d.ts.map +1 -1
- package/build/services/pages/OverlayPages.js +11 -10
- package/build/services/pages/OverlayPages.js.map +1 -1
- package/build/services/pages/PageHost.d.ts +1 -1
- package/build/services/pages/PageHost.d.ts.map +1 -1
- package/build/services/pages/PageHost.js +2 -2
- package/build/services/pages/PageHost.js.map +1 -1
- package/build/services/pages/activeDashboard.d.ts +7 -0
- package/build/services/pages/activeDashboard.d.ts.map +1 -0
- package/build/services/pages/activeDashboard.js +21 -0
- package/build/services/pages/activeDashboard.js.map +1 -0
- package/build/services/pages/overlayApps.d.ts +20 -13
- package/build/services/pages/overlayApps.d.ts.map +1 -1
- package/build/services/pages/overlayApps.js +50 -31
- package/build/services/pages/overlayApps.js.map +1 -1
- package/build/services/settingsBridge.d.ts +15 -14
- package/build/services/settingsBridge.d.ts.map +1 -1
- package/build/services/settingsBridge.js +20 -20
- package/build/services/settingsBridge.js.map +1 -1
- package/build/stores/apps.d.ts +5 -0
- package/build/stores/apps.d.ts.map +1 -1
- package/build/stores/apps.js +40 -2
- package/build/stores/apps.js.map +1 -1
- package/build/stores/settings.d.ts.map +1 -1
- package/build/stores/settings.js +6 -45
- package/build/stores/settings.js.map +1 -1
- package/build/types/applet.d.ts +2 -2
- package/build/types/applet.d.ts.map +1 -1
- package/build/types/applet.js.map +1 -1
- package/package.json +7 -7
- package/src/facades/miniapps.ts +9 -0
- package/src/generated/releaseMetadata.ts +3 -3
- package/src/index.ts +2 -0
- package/src/services/AppRegistry.ts +1 -1
- package/src/services/LocalMiniappRuntime.ts +124 -49
- package/src/services/MiniappLauncher.ts +6 -1
- package/src/services/miniappPermissions.ts +19 -0
- package/src/services/pages/OverlayPages.ts +11 -10
- package/src/services/pages/PageHost.ts +2 -2
- package/src/services/pages/activeDashboard.ts +24 -0
- package/src/services/pages/overlayApps.ts +51 -30
- package/src/services/settingsBridge.ts +20 -20
- package/src/stores/apps.ts +44 -2
- package/src/stores/settings.ts +6 -45
- package/src/types/applet.ts +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glassly/engine",
|
|
3
|
-
"version": "0.3.0-dev.
|
|
3
|
+
"version": "0.3.0-dev.465",
|
|
4
4
|
"description": "Glassly Engine — miniapp registry, WebView bridge, and miniapp store",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"react-native": "src/index.ts",
|
|
@@ -97,17 +97,17 @@
|
|
|
97
97
|
"registry": "https://registry.npmjs.org/"
|
|
98
98
|
},
|
|
99
99
|
"dependencies": {
|
|
100
|
-
"@glassly/bluetooth-sdk": "0.3.0-dev.
|
|
101
|
-
"@glassly/cloud-client": "0.3.0-dev.
|
|
102
|
-
"@glassly/cloud-protocol": "0.3.0-dev.
|
|
103
|
-
"@glassly/crust": "0.3.0-dev.
|
|
104
|
-
"@glassly/miniapp": "0.3.0-dev.
|
|
100
|
+
"@glassly/bluetooth-sdk": "0.3.0-dev.465",
|
|
101
|
+
"@glassly/cloud-client": "0.3.0-dev.465",
|
|
102
|
+
"@glassly/cloud-protocol": "0.3.0-dev.465",
|
|
103
|
+
"@glassly/crust": "0.3.0-dev.465",
|
|
104
|
+
"@glassly/miniapp": "0.3.0-dev.465",
|
|
105
105
|
"buffer": "^6.0.3",
|
|
106
106
|
"events": "^3.3.0",
|
|
107
107
|
"react-native-marked": "8.1.1",
|
|
108
108
|
"semver": "^7.7.4",
|
|
109
109
|
"typesafe-ts": "^1.7.1",
|
|
110
|
-
"@glassly/llm-providers": "0.3.0-dev.
|
|
110
|
+
"@glassly/llm-providers": "0.3.0-dev.465"
|
|
111
111
|
},
|
|
112
112
|
"devDependencies": {
|
|
113
113
|
"@glassly/llm-providers": "workspace:*",
|
package/src/facades/miniapps.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import {useAppStatusStore} from "../stores/apps"
|
|
12
12
|
import localMiniappRuntime from "../services/LocalMiniappRuntime"
|
|
13
|
+
import {activeDashboardAmong} from "../services/pages/activeDashboard"
|
|
13
14
|
import type {ClientApp} from "../types"
|
|
14
15
|
|
|
15
16
|
type AppsState = ReturnType<typeof useAppStatusStore.getState>
|
|
@@ -61,4 +62,12 @@ export const miniapps = {
|
|
|
61
62
|
useAppStatusStore.getState().setHiddenStatus(...args),
|
|
62
63
|
/** Mark a miniapp Always On (starts at launch, stops only from the switcher pill), persisted for the next boot. */
|
|
63
64
|
setAlwaysOn: (...args: Parameters<AppsState["setAlwaysOn"]>) => useAppStatusStore.getState().setAlwaysOn(...args),
|
|
65
|
+
|
|
66
|
+
/** Installed miniapps that can hold the dashboard role (manifest type "dashboard"). */
|
|
67
|
+
dashboards: (): ClientApp[] => useAppStatusStore.getState().apps.filter((app) => app.type === "dashboard"),
|
|
68
|
+
/** The package that holds the dashboard role, or null when no dashboard is installed. */
|
|
69
|
+
activeDashboard: (): string | null => activeDashboardAmong(useAppStatusStore.getState().apps),
|
|
70
|
+
/** Give the dashboard role to an installed dashboard; the previous holder stops and the new one starts. */
|
|
71
|
+
setActiveDashboard: (...args: Parameters<AppsState["setActiveDashboard"]>) =>
|
|
72
|
+
useAppStatusStore.getState().setActiveDashboard(...args),
|
|
64
73
|
}
|
|
@@ -12,9 +12,9 @@ export interface EngineReleaseMetadata {
|
|
|
12
12
|
export const ENGINE_RELEASE_METADATA: Readonly<EngineReleaseMetadata> = Object.freeze({
|
|
13
13
|
"schemaVersion": 1,
|
|
14
14
|
"familyBaseVersion": "0.3.0",
|
|
15
|
-
"releaseIdentity": "0.3.0-dev.
|
|
16
|
-
"releaseSetId": "glassly-0.3.0-dev.
|
|
17
|
-
"sourceCommit": "
|
|
15
|
+
"releaseIdentity": "0.3.0-dev.465",
|
|
16
|
+
"releaseSetId": "glassly-0.3.0-dev.465",
|
|
17
|
+
"sourceCommit": "27c7a1ef04fcc8b4e72275391cf94d756ab17710",
|
|
18
18
|
"otaManifestUrl": null,
|
|
19
19
|
"otaManifestSha256": null
|
|
20
20
|
})
|
package/src/index.ts
CHANGED
|
@@ -76,7 +76,7 @@ export function normalizeManifestPermissions(
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
function normalizeManifestType(raw: unknown): AppletType {
|
|
79
|
-
return raw === "background" || raw === "
|
|
79
|
+
return raw === "background" || raw === "dashboard" || raw === "standard" ? raw : "standard"
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
/**
|
|
@@ -77,7 +77,19 @@ import {
|
|
|
77
77
|
validateSettingsSet,
|
|
78
78
|
type DerivedSettingsKey,
|
|
79
79
|
} from "./settingsBridge"
|
|
80
|
-
import {
|
|
80
|
+
import {mayUsePermission} from "./miniappPermissions"
|
|
81
|
+
import {activeDashboardAmong} from "./pages/activeDashboard"
|
|
82
|
+
import {
|
|
83
|
+
activeDashboard,
|
|
84
|
+
addDashboardApp,
|
|
85
|
+
displayViewsFor,
|
|
86
|
+
isDashboardApp,
|
|
87
|
+
isOverlayApp,
|
|
88
|
+
onOverlayAppsChange,
|
|
89
|
+
removeDashboardApp,
|
|
90
|
+
setActiveDashboard,
|
|
91
|
+
setOverlayViewer,
|
|
92
|
+
} from "./pages/overlayApps"
|
|
81
93
|
import {pagesEligible} from "./pages/pageEligibility"
|
|
82
94
|
import nativeNavigationService from "./NativeNavigationService"
|
|
83
95
|
import type {DisplayPayload} from "./LocalDisplayManager"
|
|
@@ -253,9 +265,6 @@ const SYSTEM_MINIAPP_PACKAGES = new Set([
|
|
|
253
265
|
"com.glassly.ai",
|
|
254
266
|
"com.glassly.feedback",
|
|
255
267
|
"com.glassly.miniappdev",
|
|
256
|
-
// The dashboard: the settings bridge and the miniapp list/start/stop are its side menu and settings
|
|
257
|
-
// page, and the notification app list is its notification settings' per-app filter.
|
|
258
|
-
"com.glassly.dashboard",
|
|
259
268
|
])
|
|
260
269
|
const PING_INTERVAL_MS = 5_000
|
|
261
270
|
// Min gap between "transcription limit reached" notices on the glasses. A
|
|
@@ -342,6 +351,8 @@ const PERMISSION_TYPE_TO_CANONICAL: Record<string, string> = {
|
|
|
342
351
|
MEMORY: "memory",
|
|
343
352
|
AI: "ai",
|
|
344
353
|
SPEAKERS: "speakers",
|
|
354
|
+
MANAGE_MINIAPPS: "manageMiniapps",
|
|
355
|
+
PLATFORM_SETTINGS: "platformSettings",
|
|
345
356
|
}
|
|
346
357
|
|
|
347
358
|
/**
|
|
@@ -456,6 +467,8 @@ const ALL_CANONICAL_PERMISSIONS = [
|
|
|
456
467
|
"ai",
|
|
457
468
|
"speakers",
|
|
458
469
|
"displayMirror",
|
|
470
|
+
"manageMiniapps",
|
|
471
|
+
"platformSettings",
|
|
459
472
|
] as const
|
|
460
473
|
|
|
461
474
|
/**
|
|
@@ -1163,11 +1176,11 @@ class LocalMiniappRuntime {
|
|
|
1163
1176
|
// Drop this app's transcription hints; the cloud subscription rebuild
|
|
1164
1177
|
// below (via replaceStreamSubscribers) re-derives the hint union without it.
|
|
1165
1178
|
this.transcriptionHintsByApp.delete(packageName)
|
|
1166
|
-
//
|
|
1179
|
+
// A dashboard is going: release the slot if it holds it, and drop the
|
|
1167
1180
|
// dashboard view from its capabilities record.
|
|
1168
|
-
if (
|
|
1181
|
+
if (isDashboardApp(packageName)) {
|
|
1169
1182
|
overlayPages.remove(packageName)
|
|
1170
|
-
|
|
1183
|
+
removeDashboardApp(packageName)
|
|
1171
1184
|
}
|
|
1172
1185
|
// Its gesture claims go back to whoever claimed them before.
|
|
1173
1186
|
this.applyGestureClaimChanges(dropGestureClaims(this.gestureClaims, packageName))
|
|
@@ -1748,9 +1761,12 @@ class LocalMiniappRuntime {
|
|
|
1748
1761
|
existing.lastPongAt = Date.now()
|
|
1749
1762
|
existing.unansweredPingRounds = 0
|
|
1750
1763
|
|
|
1751
|
-
//
|
|
1752
|
-
// carry the dashboard view from the start.
|
|
1753
|
-
if (existing.installedManifest?.type === "
|
|
1764
|
+
// The dashboard that holds the role may take the overlay slot: its
|
|
1765
|
+
// capabilities carry the dashboard view from the start.
|
|
1766
|
+
if (existing.installedManifest?.type === "dashboard") {
|
|
1767
|
+
addDashboardApp(packageName)
|
|
1768
|
+
this.syncActiveDashboard()
|
|
1769
|
+
}
|
|
1754
1770
|
const capabilities = this.capabilitiesFor(packageName)
|
|
1755
1771
|
|
|
1756
1772
|
// Build the declared-permission record for the SDK's session.permissions
|
|
@@ -2133,12 +2149,11 @@ class LocalMiniappRuntime {
|
|
|
2133
2149
|
}
|
|
2134
2150
|
}
|
|
2135
2151
|
|
|
2136
|
-
// The settings stream carries the wearer's settings
|
|
2137
|
-
if (
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
})
|
|
2152
|
+
// The settings stream carries the wearer's settings.
|
|
2153
|
+
if (
|
|
2154
|
+
streams.includes(MiniappStreamType.SETTINGS_CHANGED) &&
|
|
2155
|
+
!this.requirePermission(packageName, requestId, "PLATFORM_SETTINGS", "to subscribe to the settings stream")
|
|
2156
|
+
) {
|
|
2142
2157
|
return
|
|
2143
2158
|
}
|
|
2144
2159
|
|
|
@@ -2152,8 +2167,9 @@ class LocalMiniappRuntime {
|
|
|
2152
2167
|
[...cloudTranscriptionStreams].filter((stream) => app.subscriptions.has(stream)),
|
|
2153
2168
|
)
|
|
2154
2169
|
this.replaceStreamSubscribers(packageName, previousSubscriptions, app.subscriptions)
|
|
2155
|
-
//
|
|
2156
|
-
|
|
2170
|
+
// The overlay app presents phone notifications on the glasses while it
|
|
2171
|
+
// listens to them; recorded for every dashboard so the role can move.
|
|
2172
|
+
if (isDashboardApp(packageName)) {
|
|
2157
2173
|
setOverlayViewer(packageName, app.subscriptions.has(MiniappStreamType.PHONE_NOTIFICATION))
|
|
2158
2174
|
}
|
|
2159
2175
|
|
|
@@ -2385,13 +2401,13 @@ class LocalMiniappRuntime {
|
|
|
2385
2401
|
})
|
|
2386
2402
|
return
|
|
2387
2403
|
}
|
|
2388
|
-
// The dashboard view is the overlay
|
|
2404
|
+
// The dashboard view is the overlay app's alone; anyone else's frame
|
|
2389
2405
|
// is dropped, reported rather than silently lost.
|
|
2390
2406
|
if (payload.view === "dashboard" && !isOverlayApp(packageName)) {
|
|
2391
2407
|
this.sendResult(packageName, requestId, true, {
|
|
2392
2408
|
status: "displayed",
|
|
2393
2409
|
presented: false,
|
|
2394
|
-
reason: "not the dashboard
|
|
2410
|
+
reason: "not the active dashboard",
|
|
2395
2411
|
})
|
|
2396
2412
|
return
|
|
2397
2413
|
}
|
|
@@ -2415,10 +2431,11 @@ class LocalMiniappRuntime {
|
|
|
2415
2431
|
}
|
|
2416
2432
|
|
|
2417
2433
|
/**
|
|
2418
|
-
* display.showView: the
|
|
2419
|
-
*
|
|
2420
|
-
*
|
|
2421
|
-
* the
|
|
2434
|
+
* display.showView: the active dashboard takes or releases the overlay
|
|
2435
|
+
* slot. Refused (NOT_PERMITTED) for an app that is not a dashboard;
|
|
2436
|
+
* "unsupported" for a dashboard without the role and while the glasses
|
|
2437
|
+
* cannot show the dashboard view. Idempotent either way; the slot's state
|
|
2438
|
+
* comes back to the app as a display_view change.
|
|
2422
2439
|
*/
|
|
2423
2440
|
private handleShowView(packageName: string, payload: Record<string, unknown>, requestId?: string): void {
|
|
2424
2441
|
if (!this.connectedApps.has(packageName)) {
|
|
@@ -2433,13 +2450,17 @@ class LocalMiniappRuntime {
|
|
|
2433
2450
|
})
|
|
2434
2451
|
return
|
|
2435
2452
|
}
|
|
2436
|
-
if (!
|
|
2453
|
+
if (!isDashboardApp(packageName)) {
|
|
2437
2454
|
this.sendResult(packageName, requestId, false, undefined, {
|
|
2438
2455
|
code: MiniappErrorCode.NOT_PERMITTED,
|
|
2439
|
-
message:
|
|
2456
|
+
message: 'The dashboard view belongs to miniapps of type "dashboard"',
|
|
2440
2457
|
})
|
|
2441
2458
|
return
|
|
2442
2459
|
}
|
|
2460
|
+
if (!isOverlayApp(packageName)) {
|
|
2461
|
+
this.sendResult(packageName, requestId, true, {status: "unsupported", reason: "not the active dashboard"})
|
|
2462
|
+
return
|
|
2463
|
+
}
|
|
2443
2464
|
if (view === "main") {
|
|
2444
2465
|
overlayPages.hide(packageName)
|
|
2445
2466
|
this.sendResult(packageName, requestId, true, {status: "shown"})
|
|
@@ -2463,19 +2484,38 @@ class LocalMiniappRuntime {
|
|
|
2463
2484
|
)
|
|
2464
2485
|
}
|
|
2465
2486
|
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
private
|
|
2471
|
-
|
|
2487
|
+
/**
|
|
2488
|
+
* Reject the request with PERMISSION_NOT_DECLARED unless the caller's
|
|
2489
|
+
* manifest declares `permission`. System apps pass without declaring it.
|
|
2490
|
+
*/
|
|
2491
|
+
private requirePermission(
|
|
2492
|
+
packageName: string,
|
|
2493
|
+
requestId: string | undefined,
|
|
2494
|
+
permission: string,
|
|
2495
|
+
context: string,
|
|
2496
|
+
): boolean {
|
|
2497
|
+
const caller = {
|
|
2498
|
+
system: this.isSystemPackage(packageName),
|
|
2499
|
+
permissions: this.connectedApps.get(packageName)?.installedManifest?.permissions,
|
|
2500
|
+
}
|
|
2501
|
+
if (mayUsePermission(caller, permission)) return true
|
|
2502
|
+
logPermissionNotDeclared(packageName, permission, context, `{"type": "${permission}"}`)
|
|
2472
2503
|
this.sendResult(packageName, requestId, false, undefined, {
|
|
2473
|
-
code: MiniappErrorCode.
|
|
2474
|
-
message:
|
|
2504
|
+
code: MiniappErrorCode.PERMISSION_NOT_DECLARED,
|
|
2505
|
+
message: `${permission} permission not declared in miniapp.json. Add {"type": "${permission}"} to the "permissions" array.`,
|
|
2506
|
+
permission,
|
|
2475
2507
|
})
|
|
2476
2508
|
return false
|
|
2477
2509
|
}
|
|
2478
2510
|
|
|
2511
|
+
// ---------------------------------------------------------------------------
|
|
2512
|
+
// Settings bridge (session.settings) — PLATFORM_SETTINGS permission
|
|
2513
|
+
// ---------------------------------------------------------------------------
|
|
2514
|
+
|
|
2515
|
+
private requireSettingsPermission(packageName: string, requestId: string | undefined): boolean {
|
|
2516
|
+
return this.requirePermission(packageName, requestId, "PLATFORM_SETTINGS", "to use the settings bridge")
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2479
2519
|
/** A bridged key's current value: the store's, or the engine's for a derived key. */
|
|
2480
2520
|
private settingsBridgeValue(key: string): unknown {
|
|
2481
2521
|
if (isDerivedSettingsKey(key)) return this.derivedSettingValue(key)
|
|
@@ -2490,11 +2530,13 @@ class LocalMiniappRuntime {
|
|
|
2490
2530
|
return localDisplayManager.foregroundAppOnScreen()
|
|
2491
2531
|
case "ring_battery":
|
|
2492
2532
|
return ringBatteryFrom(useGlassesStore.getState())
|
|
2533
|
+
case "active_dashboard":
|
|
2534
|
+
return activeDashboard()
|
|
2493
2535
|
}
|
|
2494
2536
|
}
|
|
2495
2537
|
|
|
2496
2538
|
private handleSettingsGet(packageName: string, payload: Record<string, unknown>, requestId?: string): void {
|
|
2497
|
-
if (!this.
|
|
2539
|
+
if (!this.requireSettingsPermission(packageName, requestId)) return
|
|
2498
2540
|
const values: Record<string, unknown> = {}
|
|
2499
2541
|
for (const key of selectSettingsBridgeKeys(payload.keys)) values[key] = this.settingsBridgeValue(key)
|
|
2500
2542
|
this.sendResult(packageName, requestId, true, {values})
|
|
@@ -2505,7 +2547,7 @@ class LocalMiniappRuntime {
|
|
|
2505
2547
|
payload: Record<string, unknown>,
|
|
2506
2548
|
requestId?: string,
|
|
2507
2549
|
): Promise<void> {
|
|
2508
|
-
if (!this.
|
|
2550
|
+
if (!this.requireSettingsPermission(packageName, requestId)) return
|
|
2509
2551
|
const validated = validateSettingsSet(payload)
|
|
2510
2552
|
if (!validated.ok) {
|
|
2511
2553
|
this.sendResult(packageName, requestId, false, undefined, {code: validated.code, message: validated.message})
|
|
@@ -2525,7 +2567,11 @@ class LocalMiniappRuntime {
|
|
|
2525
2567
|
}
|
|
2526
2568
|
|
|
2527
2569
|
private async handleNotificationAppsList(packageName: string, requestId?: string): Promise<void> {
|
|
2528
|
-
if (
|
|
2570
|
+
if (
|
|
2571
|
+
!this.requirePermission(packageName, requestId, "READ_NOTIFICATIONS", "to list the apps that post notifications")
|
|
2572
|
+
) {
|
|
2573
|
+
return
|
|
2574
|
+
}
|
|
2529
2575
|
try {
|
|
2530
2576
|
this.sendResult(packageName, requestId, true, await installedNotificationApps())
|
|
2531
2577
|
} catch (err) {
|
|
@@ -2539,8 +2585,9 @@ class LocalMiniappRuntime {
|
|
|
2539
2585
|
/**
|
|
2540
2586
|
* Push bridged settings as they change: stored keys from the store, the
|
|
2541
2587
|
* derived keys whenever their inputs move (phone activity, the display,
|
|
2542
|
-
* the apps store, the ring). Subscribers
|
|
2543
|
-
* SUBSCRIBE), so this is a no-op fan-out for
|
|
2588
|
+
* the apps store, the ring, the dashboard role). Subscribers declared
|
|
2589
|
+
* PLATFORM_SETTINGS (gated at SUBSCRIBE), so this is a no-op fan-out for
|
|
2590
|
+
* everyone else.
|
|
2544
2591
|
*/
|
|
2545
2592
|
private ensureSettingsBridgeWired(): void {
|
|
2546
2593
|
if (this.settingsBridgeWired) return
|
|
@@ -2576,6 +2623,7 @@ class LocalMiniappRuntime {
|
|
|
2576
2623
|
onAppForegroundChange((foregrounded) => this.broadcastVisibility(foregrounded))
|
|
2577
2624
|
localDisplayManager.onActiveDisplayChange(pushDerived)
|
|
2578
2625
|
useAppStatusStore.subscribe(pushDerived)
|
|
2626
|
+
onOverlayAppsChange(pushDerived)
|
|
2579
2627
|
useGlassesStore.subscribe(
|
|
2580
2628
|
(s) => `${s.ringConnected}:${s.ringBatteryLevel}:${s.controllerConnected}:${s.controllerBatteryLevel}`,
|
|
2581
2629
|
pushDerived,
|
|
@@ -5196,6 +5244,31 @@ class LocalMiniappRuntime {
|
|
|
5196
5244
|
useSettingsStore.subscribe((s) => s.getSetting(SETTINGS.cfw_native_navigation.key), broadcast)
|
|
5197
5245
|
// The dashboard master switch flips the dashboard view.
|
|
5198
5246
|
useSettingsStore.subscribe((s) => s.getSetting(SETTINGS.custom_dashboard_enabled.key), broadcast)
|
|
5247
|
+
// The wearer's choice and the installed dashboards decide who holds the role.
|
|
5248
|
+
const syncRole = () => this.syncActiveDashboard()
|
|
5249
|
+
useSettingsStore.subscribe((s) => s.getSetting(SETTINGS.active_dashboard.key), syncRole)
|
|
5250
|
+
useAppStatusStore.subscribe(syncRole)
|
|
5251
|
+
syncRole()
|
|
5252
|
+
}
|
|
5253
|
+
|
|
5254
|
+
/**
|
|
5255
|
+
* Recompute who holds the dashboard role (ADR-0009). The previous holder
|
|
5256
|
+
* gives up the slot first, so it hears the view change while it is still
|
|
5257
|
+
* the overlay app; then every app sees its views again.
|
|
5258
|
+
*/
|
|
5259
|
+
private syncActiveDashboard(): void {
|
|
5260
|
+
const candidates: Array<{packageName: string; type?: string}> = [...useAppStatusStore.getState().apps]
|
|
5261
|
+
// A dashboard can connect at launch before the apps store has listed it.
|
|
5262
|
+
for (const [packageName, app] of this.connectedApps) {
|
|
5263
|
+
if (candidates.some((candidate) => candidate.packageName === packageName)) continue
|
|
5264
|
+
candidates.push({packageName, type: app.installedManifest?.type})
|
|
5265
|
+
}
|
|
5266
|
+
const next = activeDashboardAmong(candidates)
|
|
5267
|
+
const previous = activeDashboard()
|
|
5268
|
+
if (next === previous) return
|
|
5269
|
+
if (previous !== null) overlayPages.hide(previous, "replaced")
|
|
5270
|
+
setActiveDashboard(next)
|
|
5271
|
+
this.broadcastCapabilities()
|
|
5199
5272
|
}
|
|
5200
5273
|
|
|
5201
5274
|
/** Capabilities as `packageName` sees them: the model profile plus the views this app may draw. */
|
|
@@ -5668,18 +5741,20 @@ class LocalMiniappRuntime {
|
|
|
5668
5741
|
}
|
|
5669
5742
|
}
|
|
5670
5743
|
|
|
5671
|
-
/** Reject the request
|
|
5672
|
-
private
|
|
5744
|
+
/** Reject the request unless the caller may list, start and stop other miniapps (MANAGE_MINIAPPS). */
|
|
5745
|
+
private requireMiniappsPermission(
|
|
5673
5746
|
packageName: string,
|
|
5674
5747
|
requestId: string | undefined,
|
|
5675
5748
|
op: InteropAuditEvent["op"],
|
|
5676
5749
|
target?: string,
|
|
5677
5750
|
): boolean {
|
|
5678
|
-
if (this.
|
|
5679
|
-
this.auditInterop({
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5751
|
+
if (this.requirePermission(packageName, requestId, "MANAGE_MINIAPPS", `to ${op} miniapps`)) return true
|
|
5752
|
+
this.auditInterop({
|
|
5753
|
+
caller: packageName,
|
|
5754
|
+
op,
|
|
5755
|
+
target,
|
|
5756
|
+
ok: false,
|
|
5757
|
+
errorCode: MiniappErrorCode.PERMISSION_NOT_DECLARED,
|
|
5683
5758
|
})
|
|
5684
5759
|
return false
|
|
5685
5760
|
}
|
|
@@ -5704,7 +5779,7 @@ class LocalMiniappRuntime {
|
|
|
5704
5779
|
}
|
|
5705
5780
|
|
|
5706
5781
|
private handleMiniappsList(packageName: string, payload: Record<string, unknown>, requestId?: string): void {
|
|
5707
|
-
if (!this.
|
|
5782
|
+
if (!this.requireMiniappsPermission(packageName, requestId, "list")) return
|
|
5708
5783
|
const includeIncompatible = payload.includeIncompatible === true
|
|
5709
5784
|
const infos = this.interopApps()
|
|
5710
5785
|
.filter(
|
|
@@ -5725,7 +5800,7 @@ class LocalMiniappRuntime {
|
|
|
5725
5800
|
requestId?: string,
|
|
5726
5801
|
): Promise<void> {
|
|
5727
5802
|
const target = payload.packageName as string | undefined
|
|
5728
|
-
if (!this.
|
|
5803
|
+
if (!this.requireMiniappsPermission(packageName, requestId, "start", target)) return
|
|
5729
5804
|
const app = target ? this.interopApps().find((a) => a.packageName === target) : undefined
|
|
5730
5805
|
if (!target || !app) {
|
|
5731
5806
|
this.sendResult(packageName, requestId, false, undefined, {
|
|
@@ -5817,7 +5892,7 @@ class LocalMiniappRuntime {
|
|
|
5817
5892
|
requestId?: string,
|
|
5818
5893
|
): Promise<void> {
|
|
5819
5894
|
const target = payload.packageName as string | undefined
|
|
5820
|
-
if (!this.
|
|
5895
|
+
if (!this.requireMiniappsPermission(packageName, requestId, "stop", target)) return
|
|
5821
5896
|
if (!target) {
|
|
5822
5897
|
this.sendResult(packageName, requestId, false, undefined, {
|
|
5823
5898
|
code: MiniappErrorCode.APP_NOT_FOUND,
|
|
@@ -31,6 +31,7 @@ import {storage} from "../utils/storage/storage"
|
|
|
31
31
|
import appRegistry, {getLocalAppRunningState, saveLocalAppRunningState} from "./AppRegistry"
|
|
32
32
|
import devServerBridge from "./DevServerBridge"
|
|
33
33
|
import localMiniappRuntime, {type InstalledMiniappManifest} from "./LocalMiniappRuntime"
|
|
34
|
+
import {activeDashboardAmong} from "./pages/activeDashboard"
|
|
34
35
|
import type {MiniappLaunchSource} from "@glassly/miniapp"
|
|
35
36
|
import type {JSRouter} from "./JSRouter"
|
|
36
37
|
|
|
@@ -306,17 +307,21 @@ class MiniappLauncher {
|
|
|
306
307
|
*
|
|
307
308
|
* Always On apps (the dashboard) start whether or not they were running
|
|
308
309
|
* when the app was last killed, and get a switcher card like any other app.
|
|
310
|
+
* A dashboard that does not hold the role (ADR-0009) is not started by its
|
|
311
|
+
* Always On flag.
|
|
309
312
|
* Both restore paths skip apps missing required OS permissions without
|
|
310
313
|
* prompting or clearing their saved running/Always On choice.
|
|
311
314
|
*/
|
|
312
315
|
async autostartLocalMiniapps(): Promise<void> {
|
|
313
316
|
const apps = await appRegistry.getInstalledMiniapps()
|
|
317
|
+
const activeDashboard = activeDashboardAmong(apps)
|
|
314
318
|
for (const app of apps) {
|
|
315
319
|
// Only phone-hosted miniapps have a JSContext to re-spawn. Offline
|
|
316
320
|
// built-ins (`local:false, offline:true`) restore their native running
|
|
317
321
|
// state elsewhere and have no bundle to launch.
|
|
318
322
|
if (!app.local) continue
|
|
319
|
-
|
|
323
|
+
const alwaysOn = app.alwaysOn && (app.type !== "dashboard" || app.packageName === activeDashboard)
|
|
324
|
+
if (!alwaysOn && !getLocalAppRunningState(app.packageName)) continue
|
|
320
325
|
if (this.isRunning(app.packageName)) continue
|
|
321
326
|
try {
|
|
322
327
|
const missing = await permissions.missingRequired(app.permissions ?? [])
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a miniapp may use an API gated on a manifest permission
|
|
3
|
+
* (miniapp.json -> permissions): `session.miniapps` needs MANAGE_MINIAPPS,
|
|
4
|
+
* `session.settings` PLATFORM_SETTINGS, the notification app list
|
|
5
|
+
* READ_NOTIFICATIONS. System apps pass without declaring it. Pure, so the
|
|
6
|
+
* runtime only does the reply.
|
|
7
|
+
*/
|
|
8
|
+
export interface PermissionCaller {
|
|
9
|
+
system: boolean
|
|
10
|
+
permissions?: ReadonlyArray<{type?: string}>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function declaresPermission(permissions: PermissionCaller["permissions"], permission: string): boolean {
|
|
14
|
+
return (permissions ?? []).some((declared) => declared.type?.toUpperCase() === permission)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function mayUsePermission(caller: PermissionCaller, permission: string): boolean {
|
|
18
|
+
return caller.system || declaresPermission(caller.permissions, permission)
|
|
19
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The overlay
|
|
3
|
-
* slot, the only Page implementation the page host shows. The
|
|
4
|
-
* the slot and the eligibility watch; a page only relays:
|
|
5
|
-
* is reported to its app as a view change, and the app's
|
|
6
|
-
* slot through `renderOverlay` under its package name.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
2
|
+
* The overlay app's page (ADR-0006, ADR-0009): one Page per dashboard that
|
|
3
|
+
* has taken the slot, the only Page implementation the page host shows. The
|
|
4
|
+
* engine keeps the slot and the eligibility watch; a page only relays:
|
|
5
|
+
* holding the slot is reported to its app as a view change, and the app's
|
|
6
|
+
* frames reach the slot through `renderOverlay` under its package name. Only
|
|
7
|
+
* the dashboard that holds the role may show; when the role moves, the
|
|
8
|
+
* previous holder hears it was replaced. The runtime supplies the sink that
|
|
9
|
+
* delivers to apps, so this module never imports it.
|
|
9
10
|
*/
|
|
10
11
|
import pageHost, {type Page, type PageHiddenReason} from "./PageHost"
|
|
11
12
|
import {isOverlayApp} from "./overlayApps"
|
|
@@ -44,10 +45,10 @@ class OverlayPages {
|
|
|
44
45
|
return pageHost.showing === page
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
/** Release the slot if the app holds it. */
|
|
48
|
-
hide(packageName: string): void {
|
|
48
|
+
/** Release the slot if the app holds it; "replaced" when the dashboard role moved on. */
|
|
49
|
+
hide(packageName: string, reason: PageHiddenReason = "hidden"): void {
|
|
49
50
|
const page = this.pages.get(packageName)
|
|
50
|
-
if (page) pageHost.hide(page)
|
|
51
|
+
if (page) pageHost.hide(page, reason)
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
/** The app stopped: release the slot without telling it, and forget its page. */
|
|
@@ -72,10 +72,10 @@ class PageHost {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
/** Release the slot if `page` holds it. */
|
|
75
|
-
hide(page: Page): void {
|
|
75
|
+
hide(page: Page, reason: PageHiddenReason = "hidden"): void {
|
|
76
76
|
this.enqueue(() => {
|
|
77
77
|
if (this.showingPage !== page) return
|
|
78
|
-
this.release(
|
|
78
|
+
this.release(reason)
|
|
79
79
|
})
|
|
80
80
|
}
|
|
81
81
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who holds the dashboard role (ADR-0009): the wearer's choice when that
|
|
3
|
+
* miniapp is installed, else the default (the bundled dashboard), else the
|
|
4
|
+
* first installed dashboard by package name, else nobody. Read at decision
|
|
5
|
+
* time, never cached, so an uninstall hands the role back without anyone
|
|
6
|
+
* rewriting the setting.
|
|
7
|
+
*/
|
|
8
|
+
import {SETTINGS, useSettingsStore} from "../../stores/settings"
|
|
9
|
+
|
|
10
|
+
export function resolveActiveDashboard(installed: readonly string[], chosen: unknown, fallback: string): string | null {
|
|
11
|
+
if (typeof chosen === "string" && installed.includes(chosen)) return chosen
|
|
12
|
+
if (installed.includes(fallback)) return fallback
|
|
13
|
+
return [...installed].sort()[0] ?? null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** The active dashboard among `apps`; only miniapps whose manifest type is "dashboard" can hold the role. */
|
|
17
|
+
export function activeDashboardAmong(apps: ReadonlyArray<{packageName: string; type?: string}>): string | null {
|
|
18
|
+
const installed = apps.filter((app) => app.type === "dashboard").map((app) => app.packageName)
|
|
19
|
+
return resolveActiveDashboard(
|
|
20
|
+
installed,
|
|
21
|
+
useSettingsStore.getState().getSetting(SETTINGS.active_dashboard.key),
|
|
22
|
+
SETTINGS.active_dashboard.defaultValue(),
|
|
23
|
+
)
|
|
24
|
+
}
|