@glassly/engine 0.3.0-dev.462 → 0.3.0-dev.469
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 +108 -50
- 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 +19 -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 +129 -50
- 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 +19 -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.469",
|
|
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.469",
|
|
101
|
+
"@glassly/cloud-client": "0.3.0-dev.469",
|
|
102
|
+
"@glassly/cloud-protocol": "0.3.0-dev.469",
|
|
103
|
+
"@glassly/crust": "0.3.0-dev.469",
|
|
104
|
+
"@glassly/miniapp": "0.3.0-dev.469",
|
|
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.469"
|
|
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.469",
|
|
16
|
+
"releaseSetId": "glassly-0.3.0-dev.469",
|
|
17
|
+
"sourceCommit": "5abdf2c55c0f8f25cf402afca85f7677c753443f",
|
|
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))
|
|
@@ -1551,7 +1564,11 @@ class LocalMiniappRuntime {
|
|
|
1551
1564
|
this.handleOpenUrl(packageName, payload)
|
|
1552
1565
|
break
|
|
1553
1566
|
case MiniappRequestType.DISPLAY_DIAGNOSTICS: {
|
|
1554
|
-
|
|
1567
|
+
// The records are parsed out of the G2PERF lines, which the SDK only logs while g2_perf_logs is on.
|
|
1568
|
+
const settings = useSettingsStore.getState()
|
|
1569
|
+
const enabled =
|
|
1570
|
+
settings.getSetting(SETTINGS.debug_console.key) === true &&
|
|
1571
|
+
settings.getSetting(SETTINGS.g2_perf_logs.key) === true
|
|
1555
1572
|
this.sendResult(packageName, requestId, true, {
|
|
1556
1573
|
enabled,
|
|
1557
1574
|
generatedAt: Date.now(),
|
|
@@ -1748,9 +1765,12 @@ class LocalMiniappRuntime {
|
|
|
1748
1765
|
existing.lastPongAt = Date.now()
|
|
1749
1766
|
existing.unansweredPingRounds = 0
|
|
1750
1767
|
|
|
1751
|
-
//
|
|
1752
|
-
// carry the dashboard view from the start.
|
|
1753
|
-
if (existing.installedManifest?.type === "
|
|
1768
|
+
// The dashboard that holds the role may take the overlay slot: its
|
|
1769
|
+
// capabilities carry the dashboard view from the start.
|
|
1770
|
+
if (existing.installedManifest?.type === "dashboard") {
|
|
1771
|
+
addDashboardApp(packageName)
|
|
1772
|
+
this.syncActiveDashboard()
|
|
1773
|
+
}
|
|
1754
1774
|
const capabilities = this.capabilitiesFor(packageName)
|
|
1755
1775
|
|
|
1756
1776
|
// Build the declared-permission record for the SDK's session.permissions
|
|
@@ -2133,12 +2153,11 @@ class LocalMiniappRuntime {
|
|
|
2133
2153
|
}
|
|
2134
2154
|
}
|
|
2135
2155
|
|
|
2136
|
-
// The settings stream carries the wearer's settings
|
|
2137
|
-
if (
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
})
|
|
2156
|
+
// The settings stream carries the wearer's settings.
|
|
2157
|
+
if (
|
|
2158
|
+
streams.includes(MiniappStreamType.SETTINGS_CHANGED) &&
|
|
2159
|
+
!this.requirePermission(packageName, requestId, "PLATFORM_SETTINGS", "to subscribe to the settings stream")
|
|
2160
|
+
) {
|
|
2142
2161
|
return
|
|
2143
2162
|
}
|
|
2144
2163
|
|
|
@@ -2152,8 +2171,9 @@ class LocalMiniappRuntime {
|
|
|
2152
2171
|
[...cloudTranscriptionStreams].filter((stream) => app.subscriptions.has(stream)),
|
|
2153
2172
|
)
|
|
2154
2173
|
this.replaceStreamSubscribers(packageName, previousSubscriptions, app.subscriptions)
|
|
2155
|
-
//
|
|
2156
|
-
|
|
2174
|
+
// The overlay app presents phone notifications on the glasses while it
|
|
2175
|
+
// listens to them; recorded for every dashboard so the role can move.
|
|
2176
|
+
if (isDashboardApp(packageName)) {
|
|
2157
2177
|
setOverlayViewer(packageName, app.subscriptions.has(MiniappStreamType.PHONE_NOTIFICATION))
|
|
2158
2178
|
}
|
|
2159
2179
|
|
|
@@ -2385,13 +2405,13 @@ class LocalMiniappRuntime {
|
|
|
2385
2405
|
})
|
|
2386
2406
|
return
|
|
2387
2407
|
}
|
|
2388
|
-
// The dashboard view is the overlay
|
|
2408
|
+
// The dashboard view is the overlay app's alone; anyone else's frame
|
|
2389
2409
|
// is dropped, reported rather than silently lost.
|
|
2390
2410
|
if (payload.view === "dashboard" && !isOverlayApp(packageName)) {
|
|
2391
2411
|
this.sendResult(packageName, requestId, true, {
|
|
2392
2412
|
status: "displayed",
|
|
2393
2413
|
presented: false,
|
|
2394
|
-
reason: "not the dashboard
|
|
2414
|
+
reason: "not the active dashboard",
|
|
2395
2415
|
})
|
|
2396
2416
|
return
|
|
2397
2417
|
}
|
|
@@ -2415,10 +2435,11 @@ class LocalMiniappRuntime {
|
|
|
2415
2435
|
}
|
|
2416
2436
|
|
|
2417
2437
|
/**
|
|
2418
|
-
* display.showView: the
|
|
2419
|
-
*
|
|
2420
|
-
*
|
|
2421
|
-
* the
|
|
2438
|
+
* display.showView: the active dashboard takes or releases the overlay
|
|
2439
|
+
* slot. Refused (NOT_PERMITTED) for an app that is not a dashboard;
|
|
2440
|
+
* "unsupported" for a dashboard without the role and while the glasses
|
|
2441
|
+
* cannot show the dashboard view. Idempotent either way; the slot's state
|
|
2442
|
+
* comes back to the app as a display_view change.
|
|
2422
2443
|
*/
|
|
2423
2444
|
private handleShowView(packageName: string, payload: Record<string, unknown>, requestId?: string): void {
|
|
2424
2445
|
if (!this.connectedApps.has(packageName)) {
|
|
@@ -2433,13 +2454,17 @@ class LocalMiniappRuntime {
|
|
|
2433
2454
|
})
|
|
2434
2455
|
return
|
|
2435
2456
|
}
|
|
2436
|
-
if (!
|
|
2457
|
+
if (!isDashboardApp(packageName)) {
|
|
2437
2458
|
this.sendResult(packageName, requestId, false, undefined, {
|
|
2438
2459
|
code: MiniappErrorCode.NOT_PERMITTED,
|
|
2439
|
-
message:
|
|
2460
|
+
message: 'The dashboard view belongs to miniapps of type "dashboard"',
|
|
2440
2461
|
})
|
|
2441
2462
|
return
|
|
2442
2463
|
}
|
|
2464
|
+
if (!isOverlayApp(packageName)) {
|
|
2465
|
+
this.sendResult(packageName, requestId, true, {status: "unsupported", reason: "not the active dashboard"})
|
|
2466
|
+
return
|
|
2467
|
+
}
|
|
2443
2468
|
if (view === "main") {
|
|
2444
2469
|
overlayPages.hide(packageName)
|
|
2445
2470
|
this.sendResult(packageName, requestId, true, {status: "shown"})
|
|
@@ -2463,19 +2488,38 @@ class LocalMiniappRuntime {
|
|
|
2463
2488
|
)
|
|
2464
2489
|
}
|
|
2465
2490
|
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
private
|
|
2471
|
-
|
|
2491
|
+
/**
|
|
2492
|
+
* Reject the request with PERMISSION_NOT_DECLARED unless the caller's
|
|
2493
|
+
* manifest declares `permission`. System apps pass without declaring it.
|
|
2494
|
+
*/
|
|
2495
|
+
private requirePermission(
|
|
2496
|
+
packageName: string,
|
|
2497
|
+
requestId: string | undefined,
|
|
2498
|
+
permission: string,
|
|
2499
|
+
context: string,
|
|
2500
|
+
): boolean {
|
|
2501
|
+
const caller = {
|
|
2502
|
+
system: this.isSystemPackage(packageName),
|
|
2503
|
+
permissions: this.connectedApps.get(packageName)?.installedManifest?.permissions,
|
|
2504
|
+
}
|
|
2505
|
+
if (mayUsePermission(caller, permission)) return true
|
|
2506
|
+
logPermissionNotDeclared(packageName, permission, context, `{"type": "${permission}"}`)
|
|
2472
2507
|
this.sendResult(packageName, requestId, false, undefined, {
|
|
2473
|
-
code: MiniappErrorCode.
|
|
2474
|
-
message:
|
|
2508
|
+
code: MiniappErrorCode.PERMISSION_NOT_DECLARED,
|
|
2509
|
+
message: `${permission} permission not declared in miniapp.json. Add {"type": "${permission}"} to the "permissions" array.`,
|
|
2510
|
+
permission,
|
|
2475
2511
|
})
|
|
2476
2512
|
return false
|
|
2477
2513
|
}
|
|
2478
2514
|
|
|
2515
|
+
// ---------------------------------------------------------------------------
|
|
2516
|
+
// Settings bridge (session.settings) — PLATFORM_SETTINGS permission
|
|
2517
|
+
// ---------------------------------------------------------------------------
|
|
2518
|
+
|
|
2519
|
+
private requireSettingsPermission(packageName: string, requestId: string | undefined): boolean {
|
|
2520
|
+
return this.requirePermission(packageName, requestId, "PLATFORM_SETTINGS", "to use the settings bridge")
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2479
2523
|
/** A bridged key's current value: the store's, or the engine's for a derived key. */
|
|
2480
2524
|
private settingsBridgeValue(key: string): unknown {
|
|
2481
2525
|
if (isDerivedSettingsKey(key)) return this.derivedSettingValue(key)
|
|
@@ -2490,11 +2534,13 @@ class LocalMiniappRuntime {
|
|
|
2490
2534
|
return localDisplayManager.foregroundAppOnScreen()
|
|
2491
2535
|
case "ring_battery":
|
|
2492
2536
|
return ringBatteryFrom(useGlassesStore.getState())
|
|
2537
|
+
case "active_dashboard":
|
|
2538
|
+
return activeDashboard()
|
|
2493
2539
|
}
|
|
2494
2540
|
}
|
|
2495
2541
|
|
|
2496
2542
|
private handleSettingsGet(packageName: string, payload: Record<string, unknown>, requestId?: string): void {
|
|
2497
|
-
if (!this.
|
|
2543
|
+
if (!this.requireSettingsPermission(packageName, requestId)) return
|
|
2498
2544
|
const values: Record<string, unknown> = {}
|
|
2499
2545
|
for (const key of selectSettingsBridgeKeys(payload.keys)) values[key] = this.settingsBridgeValue(key)
|
|
2500
2546
|
this.sendResult(packageName, requestId, true, {values})
|
|
@@ -2505,7 +2551,7 @@ class LocalMiniappRuntime {
|
|
|
2505
2551
|
payload: Record<string, unknown>,
|
|
2506
2552
|
requestId?: string,
|
|
2507
2553
|
): Promise<void> {
|
|
2508
|
-
if (!this.
|
|
2554
|
+
if (!this.requireSettingsPermission(packageName, requestId)) return
|
|
2509
2555
|
const validated = validateSettingsSet(payload)
|
|
2510
2556
|
if (!validated.ok) {
|
|
2511
2557
|
this.sendResult(packageName, requestId, false, undefined, {code: validated.code, message: validated.message})
|
|
@@ -2525,7 +2571,11 @@ class LocalMiniappRuntime {
|
|
|
2525
2571
|
}
|
|
2526
2572
|
|
|
2527
2573
|
private async handleNotificationAppsList(packageName: string, requestId?: string): Promise<void> {
|
|
2528
|
-
if (
|
|
2574
|
+
if (
|
|
2575
|
+
!this.requirePermission(packageName, requestId, "READ_NOTIFICATIONS", "to list the apps that post notifications")
|
|
2576
|
+
) {
|
|
2577
|
+
return
|
|
2578
|
+
}
|
|
2529
2579
|
try {
|
|
2530
2580
|
this.sendResult(packageName, requestId, true, await installedNotificationApps())
|
|
2531
2581
|
} catch (err) {
|
|
@@ -2539,8 +2589,9 @@ class LocalMiniappRuntime {
|
|
|
2539
2589
|
/**
|
|
2540
2590
|
* Push bridged settings as they change: stored keys from the store, the
|
|
2541
2591
|
* 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
|
|
2592
|
+
* the apps store, the ring, the dashboard role). Subscribers declared
|
|
2593
|
+
* PLATFORM_SETTINGS (gated at SUBSCRIBE), so this is a no-op fan-out for
|
|
2594
|
+
* everyone else.
|
|
2544
2595
|
*/
|
|
2545
2596
|
private ensureSettingsBridgeWired(): void {
|
|
2546
2597
|
if (this.settingsBridgeWired) return
|
|
@@ -2576,6 +2627,7 @@ class LocalMiniappRuntime {
|
|
|
2576
2627
|
onAppForegroundChange((foregrounded) => this.broadcastVisibility(foregrounded))
|
|
2577
2628
|
localDisplayManager.onActiveDisplayChange(pushDerived)
|
|
2578
2629
|
useAppStatusStore.subscribe(pushDerived)
|
|
2630
|
+
onOverlayAppsChange(pushDerived)
|
|
2579
2631
|
useGlassesStore.subscribe(
|
|
2580
2632
|
(s) => `${s.ringConnected}:${s.ringBatteryLevel}:${s.controllerConnected}:${s.controllerBatteryLevel}`,
|
|
2581
2633
|
pushDerived,
|
|
@@ -5196,6 +5248,31 @@ class LocalMiniappRuntime {
|
|
|
5196
5248
|
useSettingsStore.subscribe((s) => s.getSetting(SETTINGS.cfw_native_navigation.key), broadcast)
|
|
5197
5249
|
// The dashboard master switch flips the dashboard view.
|
|
5198
5250
|
useSettingsStore.subscribe((s) => s.getSetting(SETTINGS.custom_dashboard_enabled.key), broadcast)
|
|
5251
|
+
// The wearer's choice and the installed dashboards decide who holds the role.
|
|
5252
|
+
const syncRole = () => this.syncActiveDashboard()
|
|
5253
|
+
useSettingsStore.subscribe((s) => s.getSetting(SETTINGS.active_dashboard.key), syncRole)
|
|
5254
|
+
useAppStatusStore.subscribe(syncRole)
|
|
5255
|
+
syncRole()
|
|
5256
|
+
}
|
|
5257
|
+
|
|
5258
|
+
/**
|
|
5259
|
+
* Recompute who holds the dashboard role (ADR-0009). The previous holder
|
|
5260
|
+
* gives up the slot first, so it hears the view change while it is still
|
|
5261
|
+
* the overlay app; then every app sees its views again.
|
|
5262
|
+
*/
|
|
5263
|
+
private syncActiveDashboard(): void {
|
|
5264
|
+
const candidates: Array<{packageName: string; type?: string}> = [...useAppStatusStore.getState().apps]
|
|
5265
|
+
// A dashboard can connect at launch before the apps store has listed it.
|
|
5266
|
+
for (const [packageName, app] of this.connectedApps) {
|
|
5267
|
+
if (candidates.some((candidate) => candidate.packageName === packageName)) continue
|
|
5268
|
+
candidates.push({packageName, type: app.installedManifest?.type})
|
|
5269
|
+
}
|
|
5270
|
+
const next = activeDashboardAmong(candidates)
|
|
5271
|
+
const previous = activeDashboard()
|
|
5272
|
+
if (next === previous) return
|
|
5273
|
+
if (previous !== null) overlayPages.hide(previous, "replaced")
|
|
5274
|
+
setActiveDashboard(next)
|
|
5275
|
+
this.broadcastCapabilities()
|
|
5199
5276
|
}
|
|
5200
5277
|
|
|
5201
5278
|
/** Capabilities as `packageName` sees them: the model profile plus the views this app may draw. */
|
|
@@ -5668,18 +5745,20 @@ class LocalMiniappRuntime {
|
|
|
5668
5745
|
}
|
|
5669
5746
|
}
|
|
5670
5747
|
|
|
5671
|
-
/** Reject the request
|
|
5672
|
-
private
|
|
5748
|
+
/** Reject the request unless the caller may list, start and stop other miniapps (MANAGE_MINIAPPS). */
|
|
5749
|
+
private requireMiniappsPermission(
|
|
5673
5750
|
packageName: string,
|
|
5674
5751
|
requestId: string | undefined,
|
|
5675
5752
|
op: InteropAuditEvent["op"],
|
|
5676
5753
|
target?: string,
|
|
5677
5754
|
): boolean {
|
|
5678
|
-
if (this.
|
|
5679
|
-
this.auditInterop({
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5755
|
+
if (this.requirePermission(packageName, requestId, "MANAGE_MINIAPPS", `to ${op} miniapps`)) return true
|
|
5756
|
+
this.auditInterop({
|
|
5757
|
+
caller: packageName,
|
|
5758
|
+
op,
|
|
5759
|
+
target,
|
|
5760
|
+
ok: false,
|
|
5761
|
+
errorCode: MiniappErrorCode.PERMISSION_NOT_DECLARED,
|
|
5683
5762
|
})
|
|
5684
5763
|
return false
|
|
5685
5764
|
}
|
|
@@ -5704,7 +5783,7 @@ class LocalMiniappRuntime {
|
|
|
5704
5783
|
}
|
|
5705
5784
|
|
|
5706
5785
|
private handleMiniappsList(packageName: string, payload: Record<string, unknown>, requestId?: string): void {
|
|
5707
|
-
if (!this.
|
|
5786
|
+
if (!this.requireMiniappsPermission(packageName, requestId, "list")) return
|
|
5708
5787
|
const includeIncompatible = payload.includeIncompatible === true
|
|
5709
5788
|
const infos = this.interopApps()
|
|
5710
5789
|
.filter(
|
|
@@ -5725,7 +5804,7 @@ class LocalMiniappRuntime {
|
|
|
5725
5804
|
requestId?: string,
|
|
5726
5805
|
): Promise<void> {
|
|
5727
5806
|
const target = payload.packageName as string | undefined
|
|
5728
|
-
if (!this.
|
|
5807
|
+
if (!this.requireMiniappsPermission(packageName, requestId, "start", target)) return
|
|
5729
5808
|
const app = target ? this.interopApps().find((a) => a.packageName === target) : undefined
|
|
5730
5809
|
if (!target || !app) {
|
|
5731
5810
|
this.sendResult(packageName, requestId, false, undefined, {
|
|
@@ -5817,7 +5896,7 @@ class LocalMiniappRuntime {
|
|
|
5817
5896
|
requestId?: string,
|
|
5818
5897
|
): Promise<void> {
|
|
5819
5898
|
const target = payload.packageName as string | undefined
|
|
5820
|
-
if (!this.
|
|
5899
|
+
if (!this.requireMiniappsPermission(packageName, requestId, "stop", target)) return
|
|
5821
5900
|
if (!target) {
|
|
5822
5901
|
this.sendResult(packageName, requestId, false, undefined, {
|
|
5823
5902
|
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
|
+
}
|