@glassly/engine 0.1.0-dev.62 → 0.1.0-dev.89
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 +1 -1
- package/build/engine.d.ts +7 -2
- package/build/engine.d.ts.map +1 -1
- package/build/engine.js +3 -0
- package/build/engine.js.map +1 -1
- package/build/facades/display.d.ts +10 -3
- package/build/facades/display.d.ts.map +1 -1
- package/build/facades/display.js +11 -1
- package/build/facades/display.js.map +1 -1
- package/build/facades/displayMirror.d.ts +2 -6
- package/build/facades/displayMirror.d.ts.map +1 -1
- package/build/facades/displayMirror.js +7 -14
- package/build/facades/displayMirror.js.map +1 -1
- package/build/facades/glasses.d.ts +1 -0
- package/build/facades/glasses.d.ts.map +1 -1
- package/build/facades/glasses.js +2 -0
- package/build/facades/glasses.js.map +1 -1
- package/build/facades/phoneNotifications.d.ts +12 -2
- package/build/facades/phoneNotifications.d.ts.map +1 -1
- package/build/facades/phoneNotifications.js +30 -10
- package/build/facades/phoneNotifications.js.map +1 -1
- package/build/generated/releaseMetadata.js +3 -3
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/services/CustomDashboardService.d.ts +11 -10
- package/build/services/CustomDashboardService.d.ts.map +1 -1
- package/build/services/CustomDashboardService.js +70 -69
- package/build/services/CustomDashboardService.js.map +1 -1
- package/build/services/DashboardCoordinator.d.ts +26 -0
- package/build/services/DashboardCoordinator.d.ts.map +1 -0
- package/build/services/DashboardCoordinator.js +125 -0
- package/build/services/DashboardCoordinator.js.map +1 -0
- package/build/services/DeviceEventRouter.d.ts.map +1 -1
- package/build/services/DeviceEventRouter.js +5 -7
- package/build/services/DeviceEventRouter.js.map +1 -1
- package/build/services/LocalDisplayManager.d.ts +10 -0
- package/build/services/LocalDisplayManager.d.ts.map +1 -1
- package/build/services/LocalDisplayManager.js +66 -12
- package/build/services/LocalDisplayManager.js.map +1 -1
- package/build/services/PhoneNotificationsSync.d.ts +13 -0
- package/build/services/PhoneNotificationsSync.d.ts.map +1 -1
- package/build/services/PhoneNotificationsSync.js +101 -9
- package/build/services/PhoneNotificationsSync.js.map +1 -1
- package/build/services/PhoneStreamCoordinator.d.ts.map +1 -1
- package/build/services/PhoneStreamCoordinator.js +9 -6
- package/build/services/PhoneStreamCoordinator.js.map +1 -1
- package/build/services/SceneRenderer.d.ts.map +1 -1
- package/build/services/SceneRenderer.js +5 -0
- package/build/services/SceneRenderer.js.map +1 -1
- package/build/services/effectiveCapabilities.d.ts +1 -1
- package/build/services/effectiveCapabilities.d.ts.map +1 -1
- package/build/services/effectiveCapabilities.js +9 -4
- package/build/services/effectiveCapabilities.js.map +1 -1
- package/build/services/iosNotificationAppCatalog.d.ts +13 -0
- package/build/services/iosNotificationAppCatalog.d.ts.map +1 -0
- package/build/services/iosNotificationAppCatalog.js +28 -0
- package/build/services/iosNotificationAppCatalog.js.map +1 -0
- package/build/stores/display.d.ts +4 -4
- package/build/stores/display.d.ts.map +1 -1
- package/build/stores/display.js +12 -36
- package/build/stores/display.js.map +1 -1
- package/build/stores/glasses.d.ts.map +1 -1
- package/build/stores/glasses.js +2 -0
- package/build/stores/glasses.js.map +1 -1
- package/build/stores/settings.d.ts.map +1 -1
- package/build/stores/settings.js +53 -2
- package/build/stores/settings.js.map +1 -1
- package/package.json +7 -7
- package/src/engine.ts +3 -0
- package/src/facades/display.ts +11 -1
- package/src/facades/displayMirror.ts +7 -15
- package/src/facades/glasses.ts +2 -0
- package/src/facades/phoneNotifications.ts +32 -10
- package/src/generated/releaseMetadata.ts +3 -3
- package/src/services/CustomDashboardService.ts +67 -66
- package/src/services/DashboardCoordinator.ts +121 -0
- package/src/services/DeviceEventRouter.ts +5 -7
- package/src/services/LocalDisplayManager.ts +66 -14
- package/src/services/PhoneNotificationsSync.ts +125 -9
- package/src/services/PhoneStreamCoordinator.ts +10 -6
- package/src/services/SceneRenderer.ts +4 -0
- package/src/services/effectiveCapabilities.ts +9 -4
- package/src/services/iosNotificationAppCatalog.ts +39 -0
- package/src/stores/display.ts +16 -48
- package/src/stores/glasses.ts +2 -0
- package/src/stores/settings.ts +53 -2
|
@@ -1,25 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* phoneNotifications facade — `engine.phoneNotifications`: forward phone
|
|
3
|
-
* notifications to the glasses, with a per-app blocklist.
|
|
4
|
-
* NotificationListenerService); the getters return sensible defaults on iOS.
|
|
3
|
+
* notifications to the glasses, with a per-app blocklist.
|
|
5
4
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* Android: apps come from the NotificationListenerService's installed-apps
|
|
6
|
+
* enumeration and the blocklist filters capture phone-side.
|
|
7
|
+
* iOS: apps are the seeded catalog plus what the glasses' ANCS client has
|
|
8
|
+
* reported posting notifications (`ios_notification_seen_apps`); the blocklist
|
|
9
|
+
* is mapped onto the G2's on-glass allowlist by PhoneNotificationsSync.
|
|
10
|
+
* Capture itself is controlled by Android notification access / iOS ANCS
|
|
11
|
+
* authorization (`glasses.ancsAuthorized`), not by this facade.
|
|
10
12
|
*/
|
|
11
13
|
import {Platform} from "react-native"
|
|
12
14
|
import CrustModule from "@glassly/crust"
|
|
13
15
|
import {useSettingsStore, SETTINGS} from "../stores/settings"
|
|
16
|
+
import {useGlassesStore} from "../stores/glasses"
|
|
17
|
+
import {getKnownNotificationApps} from "../services/PhoneNotificationsSync"
|
|
14
18
|
|
|
15
19
|
export const phoneNotifications = {
|
|
16
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Apps to show in the notification filter UI (package/bundle id, name, icon).
|
|
22
|
+
* Android: installed apps. iOS: seeded catalog ∪ glasses-reported apps
|
|
23
|
+
* (icons unavailable — iOS offers no cross-app icon lookup).
|
|
24
|
+
*/
|
|
17
25
|
installedApps: (): Promise<Array<{packageName: string; appName: string; icon: string | null}>> => {
|
|
18
|
-
if (Platform.OS
|
|
19
|
-
return
|
|
26
|
+
if (Platform.OS === "android") return CrustModule.getInstalledApps()
|
|
27
|
+
if (Platform.OS !== "ios") return Promise.resolve([])
|
|
28
|
+
return Promise.resolve(
|
|
29
|
+
getKnownNotificationApps().map((app) => ({
|
|
30
|
+
packageName: app.appId,
|
|
31
|
+
appName: app.displayName,
|
|
32
|
+
icon: null,
|
|
33
|
+
})),
|
|
34
|
+
)
|
|
20
35
|
},
|
|
21
36
|
|
|
22
|
-
/** The per-app blocklist (package
|
|
37
|
+
/** The per-app blocklist (package/bundle ids whose notifications are NOT forwarded). */
|
|
23
38
|
blocklist: (): string[] => {
|
|
24
39
|
const b = useSettingsStore.getState().getSetting(SETTINGS.notifications_blocklist.key)
|
|
25
40
|
return Array.isArray(b) ? b : []
|
|
@@ -38,4 +53,11 @@ export const phoneNotifications = {
|
|
|
38
53
|
if (Platform.OS !== "android") return
|
|
39
54
|
return CrustModule.openNotificationListenerSettings()
|
|
40
55
|
},
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* iOS: whether the connected glasses may read the phone's ANCS notification
|
|
59
|
+
* stream (the "Share System Notifications" Bluetooth consent). Always false
|
|
60
|
+
* on Android — Android capture is the notification listener instead.
|
|
61
|
+
*/
|
|
62
|
+
ancsAuthorized: (): boolean => Boolean(useGlassesStore.getState().ancsAuthorized),
|
|
41
63
|
}
|
|
@@ -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.1.0",
|
|
15
|
-
"releaseIdentity": "0.1.0-dev.
|
|
16
|
-
"releaseSetId": "glassly-0.1.0-dev.
|
|
17
|
-
"sourceCommit": "
|
|
15
|
+
"releaseIdentity": "0.1.0-dev.89",
|
|
16
|
+
"releaseSetId": "glassly-0.1.0-dev.89",
|
|
17
|
+
"sourceCommit": "c66a621d66d70251e257f73c9a79ee1f8e079be9",
|
|
18
18
|
"otaManifestUrl": null,
|
|
19
19
|
"otaManifestSha256": null
|
|
20
20
|
})
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
* Scene API dashboard for G2 custom firmware.
|
|
3
3
|
*
|
|
4
4
|
* G2.swift marks the published firmware version after it detects the CFW
|
|
5
|
-
* capability advertisement.
|
|
6
|
-
*
|
|
7
|
-
* on its native behavior.
|
|
5
|
+
* capability advertisement. DashboardCoordinator owns open/close and display
|
|
6
|
+
* switching; this service renders the widgets and handles gestures while open.
|
|
8
7
|
*
|
|
9
8
|
* The dashboard is composed of user-orderable widgets (clock, calendar, apps
|
|
10
9
|
* button, notifications button) stacked vertically on the CFW scene canvas.
|
|
@@ -19,6 +18,7 @@ import {useAppStatusStore} from "../stores/apps"
|
|
|
19
18
|
import {useGlassesStore} from "../stores/glasses"
|
|
20
19
|
import {SETTINGS, useSettingsStore} from "../stores/settings"
|
|
21
20
|
import type {ClientApp} from "../types/applet"
|
|
21
|
+
import {TextMeasurer} from "../utils/display/measurer"
|
|
22
22
|
import type {SceneElementInput} from "../utils/display/scene"
|
|
23
23
|
import {BgTimer} from "../utils/timers"
|
|
24
24
|
import {
|
|
@@ -32,13 +32,17 @@ import {
|
|
|
32
32
|
type DashboardWidgetSetting,
|
|
33
33
|
type MenuAppSetting,
|
|
34
34
|
} from "./CustomDashboardModel"
|
|
35
|
+
import displayProcessor from "./DisplayProcessor"
|
|
35
36
|
import {isGlassesConnected} from "./GlassesReadiness"
|
|
37
|
+
import localDisplayManager from "./LocalDisplayManager"
|
|
36
38
|
import {listPhoneCalendarEvents, PhoneCalendarError} from "./PhoneCalendarService"
|
|
37
39
|
import sceneRenderer from "./SceneRenderer"
|
|
38
40
|
|
|
39
41
|
const DASHBOARD_APP_ID = "system.custom-dashboard"
|
|
40
42
|
const CLOCK_REFRESH_MS = 30_000
|
|
41
43
|
const CALENDAR_REFRESH_MS = 5 * 60_000
|
|
44
|
+
/** Matches the boot message's window; stops have no unmount-driven end signal. */
|
|
45
|
+
const STOP_NOTICE_MS = 1500
|
|
42
46
|
|
|
43
47
|
// CFW blits the stock compositor's 576×288 canvas; used when the firmware has
|
|
44
48
|
// not reported scene capabilities yet.
|
|
@@ -49,6 +53,15 @@ const WIDGET_GAP = 10
|
|
|
49
53
|
/** Row height for the glasses' builtin 20px font engine. */
|
|
50
54
|
const LINE_H = 24
|
|
51
55
|
const BUTTON_H = 34
|
|
56
|
+
/**
|
|
57
|
+
* Selection marker: a text-hugging bar under the focused label instead of a
|
|
58
|
+
* boxed border. Drawn as a rect (h 3, border 2 — the strokes overlap into a
|
|
59
|
+
* solid bar) because mode 15 renders only glyphs; the bar must be in the
|
|
60
|
+
* raster or the firmware never shows it.
|
|
61
|
+
*/
|
|
62
|
+
const UNDERLINE_H = 3
|
|
63
|
+
/** Row-top → underline-top: just under the builtin font's 27px line. */
|
|
64
|
+
const UNDERLINE_OFFSET_Y = BUTTON_H - 4
|
|
52
65
|
/**
|
|
53
66
|
* App-menu icon grid: 2 rows × 4 columns per page. CFW rasterizes scenes
|
|
54
67
|
* phone-side, so the stock firmware's 4-image container pool does not bound
|
|
@@ -73,6 +86,8 @@ function eventLine(event: CalendarEvent): string {
|
|
|
73
86
|
class CustomDashboardService {
|
|
74
87
|
private started = false
|
|
75
88
|
private open = false
|
|
89
|
+
/** Set by DashboardCoordinator; asks it to close (and restore main content). */
|
|
90
|
+
public onRequestClose: (() => void) | null = null
|
|
76
91
|
private view: "main" | "apps" = "main"
|
|
77
92
|
private mainIndex = 0
|
|
78
93
|
private appsIndex = 0
|
|
@@ -129,30 +144,22 @@ class CustomDashboardService {
|
|
|
129
144
|
if (this.isEligible()) void this.refreshCalendarAndRender()
|
|
130
145
|
}, CALENDAR_REFRESH_MS)
|
|
131
146
|
this.eligible = this.isEligible()
|
|
132
|
-
if (this.eligible) {
|
|
133
|
-
this.ensureContextualDashboard()
|
|
134
|
-
void this.refreshCalendarAndRender()
|
|
135
|
-
}
|
|
136
147
|
}
|
|
137
148
|
|
|
138
149
|
/**
|
|
139
150
|
* Track eligible⇄ineligible transitions from any trigger (setting toggle,
|
|
140
|
-
* connect/disconnect, firmware change).
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* the dashboard, blanks the glasses view, and drops the retained scene so a
|
|
144
|
-
* stale frame can't be replayed after the feature was turned off.
|
|
151
|
+
* connect/disconnect, firmware change). Leaving closes the dashboard via the
|
|
152
|
+
* coordinator (which restores main content) and drops the retained scene so
|
|
153
|
+
* a stale frame can't be replayed after the feature was turned off.
|
|
145
154
|
*/
|
|
146
155
|
private syncEligibility(): boolean {
|
|
147
156
|
const eligible = this.isEligible()
|
|
148
157
|
if (eligible === this.eligible) return eligible
|
|
149
158
|
this.eligible = eligible
|
|
150
|
-
if (eligible) {
|
|
151
|
-
this.
|
|
152
|
-
} else {
|
|
159
|
+
if (!eligible) {
|
|
160
|
+
if (this.open) this.onRequestClose?.()
|
|
153
161
|
this.open = false
|
|
154
162
|
this.resetNavigation()
|
|
155
|
-
sceneRenderer.emitScene(DASHBOARD_APP_ID, "dashboard", [])
|
|
156
163
|
sceneRenderer.clearApp(DASHBOARD_APP_ID)
|
|
157
164
|
}
|
|
158
165
|
return eligible
|
|
@@ -165,6 +172,7 @@ class CustomDashboardService {
|
|
|
165
172
|
if (this.calendarTimer !== null) BgTimer.clearInterval(this.calendarTimer)
|
|
166
173
|
this.clockTimer = null
|
|
167
174
|
this.calendarTimer = null
|
|
175
|
+
if (this.open) this.onRequestClose?.()
|
|
168
176
|
this.open = false
|
|
169
177
|
this.eligible = false
|
|
170
178
|
this.resetNavigation()
|
|
@@ -188,29 +196,17 @@ class CustomDashboardService {
|
|
|
188
196
|
return this.open
|
|
189
197
|
}
|
|
190
198
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if (up && !this.open) this.resetNavigation()
|
|
197
|
-
this.open = up
|
|
198
|
-
if (up) void this.refreshCalendarAndRender()
|
|
199
|
+
/** Coordinator opened the dashboard: render fresh from the top. */
|
|
200
|
+
show(): void {
|
|
201
|
+
this.open = true
|
|
202
|
+
this.resetNavigation()
|
|
203
|
+
void this.refreshCalendarAndRender()
|
|
199
204
|
}
|
|
200
205
|
|
|
201
|
-
/**
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
this.
|
|
205
|
-
if (this.open) {
|
|
206
|
-
this.resetNavigation()
|
|
207
|
-
void this.refreshCalendarAndRender()
|
|
208
|
-
}
|
|
209
|
-
void Promise.resolve(BluetoothSdk.toggleHeadUp()).catch((error) => {
|
|
210
|
-
this.open = !this.open
|
|
211
|
-
console.error("CustomDashboard: toggleHeadUp failed", error)
|
|
212
|
-
})
|
|
213
|
-
return true
|
|
206
|
+
/** Coordinator closed the dashboard. */
|
|
207
|
+
hide(): void {
|
|
208
|
+
this.open = false
|
|
209
|
+
this.resetNavigation()
|
|
214
210
|
}
|
|
215
211
|
|
|
216
212
|
/** Returns true when an open custom dashboard consumed the gesture. */
|
|
@@ -224,7 +220,8 @@ class CustomDashboardService {
|
|
|
224
220
|
this.render()
|
|
225
221
|
return true
|
|
226
222
|
}
|
|
227
|
-
|
|
223
|
+
this.onRequestClose?.()
|
|
224
|
+
return true
|
|
228
225
|
}
|
|
229
226
|
if (this.view === "apps") return this.handleAppMenuGesture(gestureName)
|
|
230
227
|
return this.handleMainGesture(gestureName)
|
|
@@ -288,26 +285,21 @@ class CustomDashboardService {
|
|
|
288
285
|
return normalizeDashboardWidgets(useSettingsStore.getState().getSetting(SETTINGS.custom_dashboard_widgets.key))
|
|
289
286
|
}
|
|
290
287
|
|
|
291
|
-
private async close(): Promise<void> {
|
|
292
|
-
if (!this.open) return
|
|
293
|
-
this.open = false
|
|
294
|
-
await Promise.resolve(BluetoothSdk.toggleHeadUp()).catch((error) => {
|
|
295
|
-
console.error("CustomDashboard: close failed", error)
|
|
296
|
-
})
|
|
297
|
-
}
|
|
298
|
-
|
|
299
288
|
private async openNotificationsPanel(): Promise<void> {
|
|
300
|
-
|
|
289
|
+
this.onRequestClose?.()
|
|
301
290
|
await BluetoothSdk.showNativeNotificationPanel().catch((error) => {
|
|
302
291
|
console.error("CustomDashboard: notification panel failed", error)
|
|
303
292
|
})
|
|
304
293
|
}
|
|
305
294
|
|
|
306
295
|
private async toggleApp(packageName: string): Promise<void> {
|
|
307
|
-
|
|
296
|
+
this.onRequestClose?.()
|
|
308
297
|
const app = useAppStatusStore.getState().apps.find((candidate) => candidate.packageName === packageName)
|
|
309
298
|
if (!app) return
|
|
310
299
|
if (app.running) {
|
|
300
|
+
// Mirror the "Starting X…" boot message: without this the dashboard just
|
|
301
|
+
// closes and the stop is invisible on the glasses.
|
|
302
|
+
localDisplayManager.showSystemNotice(`Stopping ${app.name}…`, STOP_NOTICE_MS)
|
|
311
303
|
await useAppStatusStore.getState().stop(app.packageName)
|
|
312
304
|
} else {
|
|
313
305
|
await useAppStatusStore.getState().start(app, {skipNavigation: true})
|
|
@@ -318,16 +310,8 @@ class CustomDashboardService {
|
|
|
318
310
|
return this.widgets().some((widget) => widget.id === "calendar" && widget.enabled)
|
|
319
311
|
}
|
|
320
312
|
|
|
321
|
-
private ensureContextualDashboard(): void {
|
|
322
|
-
const settings = useSettingsStore.getState()
|
|
323
|
-
if (settings.getSetting(SETTINGS.contextual_dashboard.key) !== true) {
|
|
324
|
-
// DeviceManager selects its retained dashboard Scene only while this is
|
|
325
|
-
// enabled. Custom firmware eligibility makes that dashboard automatic.
|
|
326
|
-
void settings.setSetting(SETTINGS.contextual_dashboard.key, true, false)
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
313
|
private async refreshCalendarAndRender(): Promise<void> {
|
|
314
|
+
if (!this.open) return
|
|
331
315
|
if (!this.calendarWidgetEnabled()) {
|
|
332
316
|
this.calendarStatus = "loading"
|
|
333
317
|
this.calendarEvents = []
|
|
@@ -360,6 +344,19 @@ class CustomDashboardService {
|
|
|
360
344
|
this.render()
|
|
361
345
|
}
|
|
362
346
|
|
|
347
|
+
/** Underline hugging a builtin-font label. x/rowY are the label box's origin. */
|
|
348
|
+
private labelUnderline(id: string, x: number, rowY: number, label: string): SceneElementInput {
|
|
349
|
+
// Same metrics processScene uses to ellipsize builtin text; the raster
|
|
350
|
+
// insets text by 2px, so the bar starts under the first glyph.
|
|
351
|
+
const textW = new TextMeasurer(displayProcessor.getProfile()).measureText(label)
|
|
352
|
+
return {
|
|
353
|
+
type: "rect",
|
|
354
|
+
id,
|
|
355
|
+
box: {x: x + 2, y: rowY + UNDERLINE_OFFSET_Y, w: textW + 4, h: UNDERLINE_H},
|
|
356
|
+
style: {border: 2},
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
363
360
|
private canvas(): {width: number; height: number} {
|
|
364
361
|
const {sceneCanvasWidth, sceneCanvasHeight} = useGlassesStore.getState()
|
|
365
362
|
if (sceneCanvasWidth && sceneCanvasWidth > 0 && sceneCanvasHeight && sceneCanvasHeight > 0) {
|
|
@@ -369,7 +366,9 @@ class CustomDashboardService {
|
|
|
369
366
|
}
|
|
370
367
|
|
|
371
368
|
private render(): void {
|
|
372
|
-
|
|
369
|
+
// Only while open: emitting into the single native view while closed
|
|
370
|
+
// would clobber the main content.
|
|
371
|
+
if (!this.open || !this.isEligible()) return
|
|
373
372
|
const elements = this.view === "apps" ? this.appMenuElements() : this.mainElements()
|
|
374
373
|
sceneRenderer.emitScene(DASHBOARD_APP_ID, "dashboard", elements)
|
|
375
374
|
}
|
|
@@ -444,24 +443,26 @@ class CustomDashboardService {
|
|
|
444
443
|
style: {font: "builtin", breakMode: "strict-word", overflow: "ellipsis"},
|
|
445
444
|
})
|
|
446
445
|
} else if (widget.id === "apps") {
|
|
447
|
-
const isSelected = selected?.kind === "apps-menu"
|
|
448
446
|
elements.push({
|
|
449
447
|
type: "text",
|
|
450
448
|
id: "apps-button",
|
|
451
449
|
box: {x: MARGIN_X, y, w: width, h: BUTTON_H},
|
|
452
450
|
text: "Apps",
|
|
453
|
-
style: {font: "builtin"
|
|
451
|
+
style: {font: "builtin"},
|
|
454
452
|
})
|
|
453
|
+
if (selected?.kind === "apps-menu") elements.push(this.labelUnderline("apps-underline", MARGIN_X, y, "Apps"))
|
|
455
454
|
y += BUTTON_H + WIDGET_GAP
|
|
456
455
|
} else if (widget.id === "notifications") {
|
|
457
|
-
const isSelected = selected?.kind === "notifications"
|
|
458
456
|
elements.push({
|
|
459
457
|
type: "text",
|
|
460
458
|
id: "notifications",
|
|
461
459
|
box: {x: MARGIN_X, y, w: width, h: BUTTON_H},
|
|
462
460
|
text: "Notifications",
|
|
463
|
-
style: {font: "builtin"
|
|
461
|
+
style: {font: "builtin"},
|
|
464
462
|
})
|
|
463
|
+
if (selected?.kind === "notifications") {
|
|
464
|
+
elements.push(this.labelUnderline("notifications-underline", MARGIN_X, y, "Notifications"))
|
|
465
|
+
}
|
|
465
466
|
y += BUTTON_H + WIDGET_GAP
|
|
466
467
|
}
|
|
467
468
|
}
|
|
@@ -535,7 +536,7 @@ class CustomDashboardService {
|
|
|
535
536
|
return elements
|
|
536
537
|
}
|
|
537
538
|
|
|
538
|
-
// Icon grid, paged, row-major. Selection is the
|
|
539
|
+
// Icon grid, paged, row-major. Selection is the underlined cell.
|
|
539
540
|
const perPage = APPS_PER_PAGE
|
|
540
541
|
const pageStart = Math.floor(this.appsIndex / perPage) * perPage
|
|
541
542
|
const page = apps.slice(pageStart, pageStart + perPage)
|
|
@@ -569,8 +570,8 @@ class CustomDashboardService {
|
|
|
569
570
|
elements.push({
|
|
570
571
|
type: "rect",
|
|
571
572
|
id: "apps-selection",
|
|
572
|
-
box: {x, y, w: APP_CELL, h:
|
|
573
|
-
style: {border: 2
|
|
573
|
+
box: {x, y: y + APP_CELL + 2, w: APP_CELL, h: UNDERLINE_H},
|
|
574
|
+
style: {border: 2},
|
|
574
575
|
})
|
|
575
576
|
}
|
|
576
577
|
})
|
|
@@ -584,7 +585,7 @@ class CustomDashboardService {
|
|
|
584
585
|
type: "text",
|
|
585
586
|
id: "apps-selected-name",
|
|
586
587
|
box: {x: MARGIN_X, y: listTop + gridH + 12, w: width, h: LINE_H},
|
|
587
|
-
text: `${selectedApp.name}${selectedApp.running ? "
|
|
588
|
+
text: `${selectedApp.name}${selectedApp.running ? " (running)" : ""}`,
|
|
588
589
|
style: {font: "builtin", breakMode: "strict-word", overflow: "ellipsis"},
|
|
589
590
|
})
|
|
590
591
|
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phone-side dashboard switching. Native retains a single display state; this
|
|
3
|
+
* service decides when the dashboard covers the main content (head-up,
|
|
4
|
+
* double-tap) and which renderer draws it: the CFW custom dashboard, or the
|
|
5
|
+
* legacy time/battery wall for stock devices.
|
|
6
|
+
*/
|
|
7
|
+
import {useGlassesStore} from "../stores/glasses"
|
|
8
|
+
import {SETTINGS, useSettingsStore} from "../stores/settings"
|
|
9
|
+
import {BgTimer} from "../utils/timers"
|
|
10
|
+
import customDashboardService from "./CustomDashboardService"
|
|
11
|
+
import {isGlassesConnected} from "./GlassesReadiness"
|
|
12
|
+
import localDisplayManager from "./LocalDisplayManager"
|
|
13
|
+
|
|
14
|
+
const LEGACY_CLOCK_REFRESH_MS = 30_000
|
|
15
|
+
const LEGACY_DASHBOARD_PKG = "system.dashboard"
|
|
16
|
+
|
|
17
|
+
class DashboardCoordinator {
|
|
18
|
+
private opened = false
|
|
19
|
+
private renderer: "custom" | "legacy" | null = null
|
|
20
|
+
private legacyTimer: number | null = null
|
|
21
|
+
|
|
22
|
+
start(): void {
|
|
23
|
+
customDashboardService.onRequestClose = () => this.close()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
stop(): void {
|
|
27
|
+
this.close()
|
|
28
|
+
customDashboardService.onRequestClose = null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get isOpen(): boolean {
|
|
32
|
+
return this.opened
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Head position (real or simulated). Opening respects contextual_dashboard. */
|
|
36
|
+
setHeadUp(up: boolean): void {
|
|
37
|
+
if (up === this.opened) return
|
|
38
|
+
if (up) {
|
|
39
|
+
if (useSettingsStore.getState().getSetting(SETTINGS.contextual_dashboard.key) === false) return
|
|
40
|
+
this.open()
|
|
41
|
+
} else {
|
|
42
|
+
this.close()
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Double-tap on a CFW-eligible device. False when not eligible. */
|
|
47
|
+
toggleCustom(): boolean {
|
|
48
|
+
if (!customDashboardService.isEligible()) return false
|
|
49
|
+
if (this.opened) this.close()
|
|
50
|
+
else this.open()
|
|
51
|
+
return true
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Double-tap fallback for devices without a firmware dashboard. */
|
|
55
|
+
toggleLegacy(): void {
|
|
56
|
+
if (this.opened) this.close()
|
|
57
|
+
else this.open()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** True when an open dashboard consumed the gesture. */
|
|
61
|
+
handleGesture(gestureName: string): boolean {
|
|
62
|
+
if (!this.opened) return false
|
|
63
|
+
if (this.renderer === "custom") return customDashboardService.handleGesture(gestureName)
|
|
64
|
+
if (gestureName === "double_tap") {
|
|
65
|
+
this.close()
|
|
66
|
+
return true
|
|
67
|
+
}
|
|
68
|
+
return false
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Show the dashboard over the main content (ignores contextual_dashboard). */
|
|
72
|
+
open(): void {
|
|
73
|
+
if (this.opened) return
|
|
74
|
+
if (!isGlassesConnected(useGlassesStore.getState().connection)) return
|
|
75
|
+
this.opened = true
|
|
76
|
+
localDisplayManager.setDashboardActive(true)
|
|
77
|
+
if (customDashboardService.isEligible()) {
|
|
78
|
+
this.renderer = "custom"
|
|
79
|
+
customDashboardService.show()
|
|
80
|
+
} else {
|
|
81
|
+
this.renderer = "legacy"
|
|
82
|
+
this.renderLegacy()
|
|
83
|
+
this.legacyTimer = BgTimer.setInterval(() => this.renderLegacy(), LEGACY_CLOCK_REFRESH_MS)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Close and restore the retained main content. */
|
|
88
|
+
close(): void {
|
|
89
|
+
if (!this.opened) return
|
|
90
|
+
this.opened = false
|
|
91
|
+
if (this.legacyTimer !== null) {
|
|
92
|
+
BgTimer.clearInterval(this.legacyTimer)
|
|
93
|
+
this.legacyTimer = null
|
|
94
|
+
}
|
|
95
|
+
if (this.renderer === "custom") customDashboardService.hide()
|
|
96
|
+
this.renderer = null
|
|
97
|
+
localDisplayManager.setDashboardActive(false)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Parity with the retired native head-up wall: time, date, battery. */
|
|
101
|
+
private renderLegacy(): void {
|
|
102
|
+
if (!this.opened) return
|
|
103
|
+
const now = new Date()
|
|
104
|
+
const time = new Intl.DateTimeFormat(undefined, {
|
|
105
|
+
hour: "2-digit",
|
|
106
|
+
minute: "2-digit",
|
|
107
|
+
hour12: useSettingsStore.getState().getSetting(SETTINGS.twelve_hour_time.key) === true,
|
|
108
|
+
}).format(now)
|
|
109
|
+
const date = `${String(now.getMonth() + 1).padStart(2, "0")}/${String(now.getDate()).padStart(2, "0")}`
|
|
110
|
+
const level = useGlassesStore.getState().batteryLevel
|
|
111
|
+
const batt = typeof level === "number" && level >= 0 ? ` ${level}%` : ""
|
|
112
|
+
localDisplayManager.renderDashboard(LEGACY_DASHBOARD_PKG, {
|
|
113
|
+
view: "dashboard",
|
|
114
|
+
layout: {layoutType: "text_wall", text: `${time} ${date}${batt} Connected`},
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const dashboardCoordinator = new DashboardCoordinator()
|
|
120
|
+
export default dashboardCoordinator
|
|
121
|
+
export {DashboardCoordinator}
|
|
@@ -35,7 +35,7 @@ import {retirePendingSelectionOnPromotion} from "./PairingIdentity"
|
|
|
35
35
|
import GlobalEventEmitter from "../utils/GlobalEventEmitter"
|
|
36
36
|
import {BgTimer} from "../utils/timers"
|
|
37
37
|
import {asgCameraApi} from "./asg/asgCameraApi"
|
|
38
|
-
import
|
|
38
|
+
import dashboardCoordinator from "./DashboardCoordinator"
|
|
39
39
|
import gestureDotsService from "./GestureDotsService"
|
|
40
40
|
|
|
41
41
|
let subs: Array<{remove: () => void}> = []
|
|
@@ -200,7 +200,7 @@ export function startDeviceEventRouter(): void {
|
|
|
200
200
|
BluetoothSdk.addListener("touch_event", (event) => {
|
|
201
201
|
// While the custom dashboard is visible it owns its focus/navigation
|
|
202
202
|
// gestures; do not leak those into a miniapp running underneath it.
|
|
203
|
-
if (
|
|
203
|
+
if (dashboardCoordinator.handleGesture(event.gestureName)) return
|
|
204
204
|
|
|
205
205
|
// Gesture-dots debug indicator (any device, gated by its setting). It
|
|
206
206
|
// only paints while the screen is otherwise empty; the gesture still
|
|
@@ -227,7 +227,7 @@ export function startDeviceEventRouter(): void {
|
|
|
227
227
|
if (miniappExitGesture.tryExitVisibleMiniapp()) return
|
|
228
228
|
|
|
229
229
|
// Eligible G2 custom firmware replaces the native dashboard.
|
|
230
|
-
if (
|
|
230
|
+
if (dashboardCoordinator.toggleCustom()) return
|
|
231
231
|
|
|
232
232
|
const useNativeDashboard = useSettingsStore.getState().getSetting(SETTINGS.use_native_dashboard.key) !== false
|
|
233
233
|
if (useNativeDashboard) {
|
|
@@ -238,9 +238,7 @@ export function startDeviceEventRouter(): void {
|
|
|
238
238
|
})
|
|
239
239
|
}
|
|
240
240
|
} else {
|
|
241
|
-
|
|
242
|
-
console.error("DeviceEventRouter: toggleHeadUp failed:", err)
|
|
243
|
-
})
|
|
241
|
+
dashboardCoordinator.toggleLegacy()
|
|
244
242
|
}
|
|
245
243
|
}
|
|
246
244
|
}),
|
|
@@ -259,7 +257,7 @@ export function startDeviceEventRouter(): void {
|
|
|
259
257
|
// Head position — translate native {up:boolean} → SDK {position:"up"|"down"}.
|
|
260
258
|
subs.push(
|
|
261
259
|
BluetoothSdk.addListener("head_up", (event) => {
|
|
262
|
-
|
|
260
|
+
dashboardCoordinator.setHeadUp(event.up)
|
|
263
261
|
localMiniappRuntime.forwardEvent("head_up", {position: event.up ? "up" : "down", timestamp: Date.now()})
|
|
264
262
|
}),
|
|
265
263
|
)
|
|
@@ -153,6 +153,9 @@ class LocalDisplayManager {
|
|
|
153
153
|
private bootingApp: BootingApp | null = null
|
|
154
154
|
private bootQueue: Map<string, {payload: DisplayPayload; resolve?: DisplayRequestResolver}> = new Map()
|
|
155
155
|
|
|
156
|
+
/** While true the dashboard owns the glasses: main frames are retained but not sent. */
|
|
157
|
+
private dashboardActive = false
|
|
158
|
+
|
|
156
159
|
/** Miniapp whose visible frame is temporarily covered by an OS exit notice. */
|
|
157
160
|
private exitingApp: {packageName: string; coveredDisplay: ActiveDisplay} | null = null
|
|
158
161
|
|
|
@@ -419,6 +422,47 @@ class LocalDisplayManager {
|
|
|
419
422
|
this.finishMiniappExit(packageName)
|
|
420
423
|
}
|
|
421
424
|
|
|
425
|
+
// ===========================================================================
|
|
426
|
+
// Dashboard switching (called from DashboardCoordinator)
|
|
427
|
+
// ===========================================================================
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Dashboard cover state. On deactivate the retained main frame repaints
|
|
431
|
+
* (scenes replay create-based; legacy re-sends), or the glasses clear.
|
|
432
|
+
*/
|
|
433
|
+
public setDashboardActive(active: boolean): void {
|
|
434
|
+
if (this.dashboardActive === active) return
|
|
435
|
+
this.dashboardActive = active
|
|
436
|
+
useDisplayStore.getState().setDashboardActive(active)
|
|
437
|
+
if (active) return
|
|
438
|
+
sceneRenderer.markViewStale("dashboard")
|
|
439
|
+
const current = this.currentDisplay
|
|
440
|
+
if (current && (current.expiresAt === null || this.now() < current.expiresAt)) {
|
|
441
|
+
this.replayCurrent()
|
|
442
|
+
} else {
|
|
443
|
+
this.sendClear()
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** Dashboard renderers only; bypasses arbitration and main ownership state. */
|
|
448
|
+
public renderDashboard(packageName: string, payload: DisplayPayload): void {
|
|
449
|
+
if (!this.dashboardActive) return
|
|
450
|
+
if (payload.scene) {
|
|
451
|
+
sceneRenderer.emitScene(packageName, "dashboard", payload.scene)
|
|
452
|
+
return
|
|
453
|
+
}
|
|
454
|
+
if (!payload.layout) return
|
|
455
|
+
const rawEvent = {view: "dashboard", layout: payload.layout}
|
|
456
|
+
let processedEvent: Record<string, unknown>
|
|
457
|
+
try {
|
|
458
|
+
processedEvent = displayProcessor.processDisplayEvent(rawEvent as any)
|
|
459
|
+
} catch (err) {
|
|
460
|
+
console.error(`${LOG_TAG}: DisplayProcessor error, using raw event:`, err)
|
|
461
|
+
processedEvent = rawEvent
|
|
462
|
+
}
|
|
463
|
+
this.pushToNative(processedEvent)
|
|
464
|
+
}
|
|
465
|
+
|
|
422
466
|
// ===========================================================================
|
|
423
467
|
// Display request API (called from LocalMiniappRuntime.handleDisplay)
|
|
424
468
|
// ===========================================================================
|
|
@@ -440,10 +484,11 @@ class LocalDisplayManager {
|
|
|
440
484
|
return
|
|
441
485
|
}
|
|
442
486
|
|
|
443
|
-
// Dashboard view:
|
|
444
|
-
// dashboard
|
|
487
|
+
// Dashboard view: only visible while the dashboard covers the glasses.
|
|
488
|
+
// Local miniapp dashboard content remains a stub.
|
|
445
489
|
if (payload.view === "dashboard") {
|
|
446
|
-
this.
|
|
490
|
+
this.renderDashboard(packageName, payload)
|
|
491
|
+
resolve?.({status: "displayed"})
|
|
447
492
|
return
|
|
448
493
|
}
|
|
449
494
|
|
|
@@ -684,6 +729,23 @@ class LocalDisplayManager {
|
|
|
684
729
|
processedEvent = rawEvent
|
|
685
730
|
}
|
|
686
731
|
|
|
732
|
+
// While the dashboard covers the glasses, main frames only update the
|
|
733
|
+
// retained state; setDashboardActive(false) replays the latest.
|
|
734
|
+
if (!this.dashboardActive) this.pushToNative(processedEvent)
|
|
735
|
+
|
|
736
|
+
this.currentDisplay = {packageName, processedEvent, expiresAt}
|
|
737
|
+
|
|
738
|
+
this.clearExpiryTimer()
|
|
739
|
+
if (expiresAt !== null) {
|
|
740
|
+
const delay = Math.max(0, expiresAt - this.now())
|
|
741
|
+
this.expiryTimerId = BgTimer.setTimeout(() => {
|
|
742
|
+
this.expiryTimerId = null
|
|
743
|
+
this.handleExpiry(packageName)
|
|
744
|
+
}, delay)
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
private pushToNative(processedEvent: Record<string, unknown>): void {
|
|
687
749
|
try {
|
|
688
750
|
// The display output path is a direct btsdk call now (was a host sendDisplayEvent
|
|
689
751
|
// hook) so a bare OEM renders to the glasses without wiring it.
|
|
@@ -696,17 +758,6 @@ class LocalDisplayManager {
|
|
|
696
758
|
} catch (err) {
|
|
697
759
|
console.error(`${LOG_TAG}: native display failed:`, err)
|
|
698
760
|
}
|
|
699
|
-
|
|
700
|
-
this.currentDisplay = {packageName, processedEvent, expiresAt}
|
|
701
|
-
|
|
702
|
-
this.clearExpiryTimer()
|
|
703
|
-
if (expiresAt !== null) {
|
|
704
|
-
const delay = Math.max(0, expiresAt - this.now())
|
|
705
|
-
this.expiryTimerId = BgTimer.setTimeout(() => {
|
|
706
|
-
this.expiryTimerId = null
|
|
707
|
-
this.handleExpiry(packageName)
|
|
708
|
-
}, delay)
|
|
709
|
-
}
|
|
710
761
|
}
|
|
711
762
|
|
|
712
763
|
private handleExpiry(packageName: string): void {
|
|
@@ -913,6 +964,7 @@ class LocalDisplayManager {
|
|
|
913
964
|
this.currentDisplay = null
|
|
914
965
|
this.backgroundLock = null
|
|
915
966
|
this.exitingApp = null
|
|
967
|
+
this.dashboardActive = false
|
|
916
968
|
this.bootQueue.clear()
|
|
917
969
|
this.now = () => Date.now()
|
|
918
970
|
this.unsubscribeGlassesStatus?.()
|