@glassly/engine 0.1.0-dev.0 → 0.1.0-dev.26
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 +53 -19
- package/build/bluetooth-sdk/debug.d.ts +2 -0
- package/build/bluetooth-sdk/debug.d.ts.map +1 -0
- package/build/bluetooth-sdk/debug.js +2 -0
- package/build/bluetooth-sdk/debug.js.map +1 -0
- package/build/bluetooth-sdk/index.d.ts +3 -0
- package/build/bluetooth-sdk/index.d.ts.map +1 -0
- package/build/bluetooth-sdk/index.js +3 -0
- package/build/bluetooth-sdk/index.js.map +1 -0
- package/build/bluetooth-sdk/ota-transport.d.ts +2 -0
- package/build/bluetooth-sdk/ota-transport.d.ts.map +1 -0
- package/build/bluetooth-sdk/ota-transport.js +2 -0
- package/build/bluetooth-sdk/ota-transport.js.map +1 -0
- package/build/bluetooth-sdk/photo-receiver.d.ts +3 -0
- package/build/bluetooth-sdk/photo-receiver.d.ts.map +1 -0
- package/build/bluetooth-sdk/photo-receiver.js +3 -0
- package/build/bluetooth-sdk/photo-receiver.js.map +1 -0
- package/build/bluetooth-sdk/react.d.ts +2 -0
- package/build/bluetooth-sdk/react.d.ts.map +1 -0
- package/build/bluetooth-sdk/react.js +2 -0
- package/build/bluetooth-sdk/react.js.map +1 -0
- package/build/bluetooth-sdk/types.d.ts +2 -0
- package/build/bluetooth-sdk/types.d.ts.map +1 -0
- package/build/bluetooth-sdk/types.js +2 -0
- package/build/bluetooth-sdk/types.js.map +1 -0
- package/build/engine.d.ts +23 -16
- package/build/engine.d.ts.map +1 -1
- package/build/engine.js +9 -3
- package/build/engine.js.map +1 -1
- package/build/facades/dev.d.ts +1 -1
- package/build/facades/display.d.ts +1 -1
- package/build/facades/display.d.ts.map +1 -1
- package/build/facades/display.js +1 -1
- package/build/facades/display.js.map +1 -1
- package/build/facades/glasses.d.ts +5 -13
- package/build/facades/glasses.d.ts.map +1 -1
- package/build/facades/glasses.js +5 -3
- package/build/facades/glasses.js.map +1 -1
- package/build/facades/ota.d.ts +19 -21
- package/build/facades/ota.d.ts.map +1 -1
- package/build/facades/ota.js +23 -9
- package/build/facades/ota.js.map +1 -1
- package/build/facades/pairing.d.ts +11 -5
- package/build/facades/pairing.d.ts.map +1 -1
- package/build/facades/pairing.js +93 -17
- package/build/facades/pairing.js.map +1 -1
- package/build/facades/speakers.d.ts +1 -1
- package/build/facades/speakers.d.ts.map +1 -1
- package/build/facades/speakers.js.map +1 -1
- package/build/facades/speech.d.ts +7 -6
- package/build/facades/speech.d.ts.map +1 -1
- package/build/facades/speech.js +1 -1
- package/build/facades/speech.js.map +1 -1
- package/build/generated/releaseMetadata.d.ts +12 -0
- package/build/generated/releaseMetadata.d.ts.map +1 -0
- package/build/generated/releaseMetadata.js +10 -0
- package/build/generated/releaseMetadata.js.map +1 -0
- package/build/index.d.ts +7 -4
- package/build/index.d.ts.map +1 -1
- package/build/index.js +5 -3
- package/build/index.js.map +1 -1
- package/build/react/MentraLiveOtaFlow.d.ts +39 -0
- package/build/react/MentraLiveOtaFlow.d.ts.map +1 -0
- package/build/react/MentraLiveOtaFlow.js +200 -0
- package/build/react/MentraLiveOtaFlow.js.map +1 -0
- package/build/react/index.d.ts +3 -0
- package/build/react/index.d.ts.map +1 -0
- package/build/react/index.js +3 -0
- package/build/react/index.js.map +1 -0
- package/build/react/useEngineSnapshot.d.ts +2 -0
- package/build/react/useEngineSnapshot.d.ts.map +1 -0
- package/build/react/useEngineSnapshot.js +14 -0
- package/build/react/useEngineSnapshot.js.map +1 -0
- package/build/react/useMentraLiveOta.d.ts +80 -0
- package/build/react/useMentraLiveOta.d.ts.map +1 -0
- package/build/react/useMentraLiveOta.js +550 -0
- package/build/react/useMentraLiveOta.js.map +1 -0
- package/build/services/AppRegistry.d.ts +8 -0
- package/build/services/AppRegistry.d.ts.map +1 -1
- package/build/services/AppRegistry.js +15 -0
- package/build/services/AppRegistry.js.map +1 -1
- package/build/services/BlobStore.d.ts +2 -0
- package/build/services/BlobStore.d.ts.map +1 -1
- package/build/services/BlobStore.js +63 -10
- package/build/services/BlobStore.js.map +1 -1
- package/build/services/CustomDashboardModel.d.ts +48 -0
- package/build/services/CustomDashboardModel.d.ts.map +1 -0
- package/build/services/CustomDashboardModel.js +73 -0
- package/build/services/CustomDashboardModel.js.map +1 -0
- package/build/services/CustomDashboardService.d.ts +61 -0
- package/build/services/CustomDashboardService.d.ts.map +1 -0
- package/build/services/CustomDashboardService.js +552 -0
- package/build/services/CustomDashboardService.js.map +1 -0
- package/build/services/DeviceEventRouter.d.ts.map +1 -1
- package/build/services/DeviceEventRouter.js +44 -3
- package/build/services/DeviceEventRouter.js.map +1 -1
- package/build/services/GestureDotsService.d.ts +35 -0
- package/build/services/GestureDotsService.d.ts.map +1 -0
- package/build/services/GestureDotsService.js +149 -0
- package/build/services/GestureDotsService.js.map +1 -0
- package/build/services/GlassesStatusProjection.d.ts +15 -1
- package/build/services/GlassesStatusProjection.d.ts.map +1 -1
- package/build/services/GlassesStatusProjection.js +18 -11
- package/build/services/GlassesStatusProjection.js.map +1 -1
- package/build/services/HotspotOtaTransport.d.ts.map +1 -1
- package/build/services/HotspotOtaTransport.js +8 -7
- package/build/services/HotspotOtaTransport.js.map +1 -1
- package/build/services/LocalDisplayManager.d.ts +43 -1
- package/build/services/LocalDisplayManager.d.ts.map +1 -1
- package/build/services/LocalDisplayManager.js +128 -2
- package/build/services/LocalDisplayManager.js.map +1 -1
- package/build/services/LocalMiniappRuntime.d.ts +13 -0
- package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
- package/build/services/LocalMiniappRuntime.js +32 -0
- package/build/services/LocalMiniappRuntime.js.map +1 -1
- package/build/services/MiniappExitGesture.d.ts +33 -0
- package/build/services/MiniappExitGesture.d.ts.map +1 -0
- package/build/services/MiniappExitGesture.js +54 -0
- package/build/services/MiniappExitGesture.js.map +1 -0
- package/build/services/OtaAutoChain.d.ts +41 -0
- package/build/services/OtaAutoChain.d.ts.map +1 -0
- package/build/services/OtaAutoChain.js +91 -0
- package/build/services/OtaAutoChain.js.map +1 -0
- package/build/services/OtaErrorMapping.d.ts +18 -0
- package/build/services/OtaErrorMapping.d.ts.map +1 -0
- package/build/services/OtaErrorMapping.js +63 -0
- package/build/services/OtaErrorMapping.js.map +1 -0
- package/build/services/OtaInstallCoordinator.d.ts +24 -3
- package/build/services/OtaInstallCoordinator.d.ts.map +1 -1
- package/build/services/OtaInstallCoordinator.js +17 -12
- package/build/services/OtaInstallCoordinator.js.map +1 -1
- package/build/services/OtaService.d.ts.map +1 -1
- package/build/services/OtaService.js +1 -1
- package/build/services/OtaService.js.map +1 -1
- package/build/services/OtaUpdateCheckService.d.ts +4 -1
- package/build/services/OtaUpdateCheckService.d.ts.map +1 -1
- package/build/services/OtaUpdateCheckService.js +28 -16
- package/build/services/OtaUpdateCheckService.js.map +1 -1
- package/build/services/asg/localNetworkTransport.d.ts.map +1 -1
- package/build/services/asg/localNetworkTransport.js +13 -12
- package/build/services/asg/localNetworkTransport.js.map +1 -1
- package/build/services/gestureClaims.d.ts +18 -0
- package/build/services/gestureClaims.d.ts.map +1 -0
- package/build/services/gestureClaims.js +24 -0
- package/build/services/gestureClaims.js.map +1 -0
- package/build/services/gestureDotGlyphs.d.ts +8 -0
- package/build/services/gestureDotGlyphs.d.ts.map +1 -0
- package/build/services/gestureDotGlyphs.js +12 -0
- package/build/services/gestureDotGlyphs.js.map +1 -0
- package/build/services/glassesClockSync.d.ts.map +1 -1
- package/build/services/glassesClockSync.js +5 -1
- package/build/services/glassesClockSync.js.map +1 -1
- package/build/services/otaDisplayState.d.ts +1 -1
- package/build/services/otaDisplayState.d.ts.map +1 -1
- package/build/services/otaDisplayState.js.map +1 -1
- package/build/services/otaInstallPolicy.d.ts +1 -1
- package/build/services/otaInstallPolicy.d.ts.map +1 -1
- package/build/services/otaInstallPolicy.js.map +1 -1
- package/build/services/otaLegacyMapping.d.ts +1 -1
- package/build/services/otaLegacyMapping.d.ts.map +1 -1
- package/build/services/otaLegacyMapping.js.map +1 -1
- package/build/services/otaManifestPolicy.d.ts +14 -0
- package/build/services/otaManifestPolicy.d.ts.map +1 -0
- package/build/services/otaManifestPolicy.js +20 -0
- package/build/services/otaManifestPolicy.js.map +1 -0
- package/build/services/otaManifestUrl.d.ts +2 -1
- package/build/services/otaManifestUrl.d.ts.map +1 -1
- package/build/services/otaManifestUrl.js +24 -32
- package/build/services/otaManifestUrl.js.map +1 -1
- package/build/services/otaReleaseVersion.d.ts +10 -0
- package/build/services/otaReleaseVersion.d.ts.map +1 -0
- package/build/services/otaReleaseVersion.js +14 -0
- package/build/services/otaReleaseVersion.js.map +1 -0
- package/build/stores/appVisibility.d.ts +3 -0
- package/build/stores/appVisibility.d.ts.map +1 -0
- package/build/stores/appVisibility.js +3 -0
- package/build/stores/appVisibility.js.map +1 -0
- package/build/stores/apps.d.ts.map +1 -1
- package/build/stores/apps.js +7 -1
- package/build/stores/apps.js.map +1 -1
- package/build/stores/core.d.ts +3 -3
- package/build/stores/core.d.ts.map +1 -1
- package/build/stores/core.js.map +1 -1
- package/build/stores/glasses.d.ts +10 -4
- package/build/stores/glasses.d.ts.map +1 -1
- package/build/stores/glasses.js.map +1 -1
- package/build/stores/settings.d.ts.map +1 -1
- package/build/stores/settings.js +87 -3
- package/build/stores/settings.js.map +1 -1
- package/build/types/applet.d.ts +2 -0
- package/build/types/applet.d.ts.map +1 -1
- package/build/types/applet.js +0 -3
- package/build/types/applet.js.map +1 -1
- package/build/types/enums.d.ts +1 -5
- package/build/types/enums.d.ts.map +1 -1
- package/build/types/enums.js +1 -8
- package/build/types/enums.js.map +1 -1
- package/build/types/hardware.d.ts +1 -1
- package/build/types/hardware.d.ts.map +1 -1
- package/build/types/hardware.js.map +1 -1
- package/build/types/index.d.ts +2 -6
- package/build/types/index.d.ts.map +1 -1
- package/build/types/index.js +2 -6
- package/build/types/index.js.map +1 -1
- package/build/utils/display/scene/process.d.ts.map +1 -1
- package/build/utils/display/scene/process.js +4 -3
- package/build/utils/display/scene/process.js.map +1 -1
- package/build/utils/display/scene/types.d.ts +6 -2
- package/build/utils/display/scene/types.d.ts.map +1 -1
- package/build/utils/display/scene/types.js.map +1 -1
- package/package.json +65 -33
- package/src/bluetooth-sdk/debug.ts +1 -0
- package/src/bluetooth-sdk/index.ts +2 -0
- package/src/bluetooth-sdk/ota-transport.ts +1 -0
- package/src/bluetooth-sdk/photo-receiver.ts +2 -0
- package/src/bluetooth-sdk/react.ts +1 -0
- package/src/bluetooth-sdk/types.ts +1 -0
- package/src/engine.ts +9 -3
- package/src/facades/display.ts +1 -1
- package/src/facades/glasses.ts +6 -4
- package/src/facades/ota.ts +33 -10
- package/src/facades/pairing.ts +103 -23
- package/src/facades/speakers.ts +1 -1
- package/src/facades/speech.ts +21 -7
- package/src/generated/releaseMetadata.ts +20 -0
- package/src/index.ts +29 -4
- package/src/react/MentraLiveOtaFlow.tsx +569 -0
- package/src/react/index.ts +21 -0
- package/src/react/useEngineSnapshot.ts +16 -0
- package/src/react/useMentraLiveOta.ts +707 -0
- package/src/services/AppRegistry.ts +16 -0
- package/src/services/BlobStore.ts +57 -10
- package/src/services/CustomDashboardModel.ts +83 -0
- package/src/services/CustomDashboardService.ts +575 -0
- package/src/services/DeviceEventRouter.ts +47 -5
- package/src/services/GestureDotsService.ts +147 -0
- package/src/services/GlassesStatusProjection.ts +20 -11
- package/src/services/HotspotOtaTransport.ts +8 -7
- package/src/services/LocalDisplayManager.ts +130 -2
- package/src/services/LocalMiniappRuntime.ts +37 -0
- package/src/services/MiniappExitGesture.ts +76 -0
- package/src/services/OtaAutoChain.ts +130 -0
- package/src/services/OtaErrorMapping.ts +80 -0
- package/src/services/OtaInstallCoordinator.ts +19 -13
- package/src/services/OtaService.ts +1 -2
- package/src/services/OtaUpdateCheckService.ts +33 -19
- package/src/services/asg/localNetworkTransport.ts +20 -19
- package/src/services/gestureClaims.ts +28 -0
- package/src/services/gestureDotGlyphs.ts +14 -0
- package/src/services/glassesClockSync.ts +5 -1
- package/src/services/otaDisplayState.ts +1 -1
- package/src/services/otaInstallPolicy.ts +1 -1
- package/src/services/otaLegacyMapping.ts +1 -1
- package/src/services/otaManifestPolicy.ts +43 -0
- package/src/services/otaManifestUrl.ts +27 -36
- package/src/services/otaReleaseVersion.ts +22 -0
- package/src/stores/appVisibility.ts +3 -0
- package/src/stores/apps.ts +7 -1
- package/src/stores/core.ts +4 -4
- package/src/stores/glasses.ts +13 -6
- package/src/stores/settings.ts +87 -3
- package/src/types/applet.ts +2 -0
- package/src/types/enums.ts +1 -9
- package/src/types/hardware.ts +1 -1
- package/src/types/index.ts +2 -6
- package/src/utils/display/scene/process.ts +4 -3
- package/src/utils/display/scene/types.ts +0 -0
- package/build/devtools.d.ts +0 -11
- package/build/devtools.d.ts.map +0 -1
- package/build/devtools.js +0 -13
- package/build/devtools.js.map +0 -1
- package/build/internal.d.ts +0 -67
- package/build/internal.d.ts.map +0 -1
- package/build/internal.js +0 -90
- package/build/internal.js.map +0 -1
- package/src/devtools.ts +0 -13
- package/src/internal.ts +0 -155
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gesture dots — a debug indicator that flashes a small glyph top-right on the
|
|
3
|
+
* glasses when a touch gesture arrives, while nothing else is on the display:
|
|
4
|
+
* a filled circle for a single tap, an arrow for a swipe, cleared after a
|
|
5
|
+
* short pulse. Extracted from the CFW dashboard service; it renders through
|
|
6
|
+
* the ordinary scene pipeline, so it works on any positioning device (stock
|
|
7
|
+
* G2 included), and is gated by the gesture_dots debug setting.
|
|
8
|
+
*
|
|
9
|
+
* The glyphs are pre-baked images (not text): the stock firmware's builtin
|
|
10
|
+
* font has no ●/↑/↓, and a single image element with a stable id and box maps
|
|
11
|
+
* to ONE re-used firmware image container — flash and idle swap its bytes
|
|
12
|
+
* (data-only update, no page rebuild) instead of creating/removing containers.
|
|
13
|
+
*/
|
|
14
|
+
import {SETTINGS, useSettingsStore} from "../stores/settings"
|
|
15
|
+
import type {SceneElementInput} from "../utils/display/scene"
|
|
16
|
+
import {BgTimer} from "../utils/timers"
|
|
17
|
+
import {GESTURE_DOT_GLYPHS, GESTURE_DOT_GLYPH_SIZE, type GestureDotGlyph} from "./gestureDotGlyphs"
|
|
18
|
+
import localDisplayManager from "./LocalDisplayManager"
|
|
19
|
+
import sceneRenderer from "./SceneRenderer"
|
|
20
|
+
|
|
21
|
+
const DOTS_APP_ID = "system.gesture-dots"
|
|
22
|
+
/** How long a gesture stays on the indicator. */
|
|
23
|
+
const GESTURE_FLASH_MS = 700
|
|
24
|
+
const MARGIN_X = 16
|
|
25
|
+
const MARGIN_TOP = 10
|
|
26
|
+
|
|
27
|
+
class GestureDotsService {
|
|
28
|
+
private started = false
|
|
29
|
+
private flash: GestureDotGlyph | null = null
|
|
30
|
+
private flashTimer: number | null = null
|
|
31
|
+
/** Finger currently on the touch bar (tap_start..tap_end; the CFW forwards holds as these edges). */
|
|
32
|
+
private held = false
|
|
33
|
+
/** Whether the indicator scene currently has anything on it (needs a clear when the flash ends). */
|
|
34
|
+
private visible = false
|
|
35
|
+
private unsubs: Array<() => void> = []
|
|
36
|
+
|
|
37
|
+
start(): void {
|
|
38
|
+
if (this.started) return
|
|
39
|
+
this.started = true
|
|
40
|
+
this.unsubs.push(
|
|
41
|
+
// The indicator owns the screen only while it is empty: drop our
|
|
42
|
+
// retained state when something else takes over, and clear any
|
|
43
|
+
// leftover flash when the display empties again.
|
|
44
|
+
localDisplayManager.onActiveDisplayChange((active) => {
|
|
45
|
+
if (active) this.visible = false
|
|
46
|
+
else this.render()
|
|
47
|
+
}),
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
stop(): void {
|
|
52
|
+
for (const unsub of this.unsubs) unsub()
|
|
53
|
+
this.unsubs = []
|
|
54
|
+
if (this.flashTimer !== null) BgTimer.clearTimeout(this.flashTimer)
|
|
55
|
+
this.flashTimer = null
|
|
56
|
+
this.flash = null
|
|
57
|
+
this.held = false
|
|
58
|
+
this.visible = false
|
|
59
|
+
this.started = false
|
|
60
|
+
sceneRenderer.clearApp(DOTS_APP_ID)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private enabled(): boolean {
|
|
64
|
+
return useSettingsStore.getState().getSetting(SETTINGS.gesture_dots.key) === true
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Flash a gesture on the indicator. Other gesture names are ignored. The
|
|
69
|
+
* device-event router calls this after the CFW dashboard had its chance to
|
|
70
|
+
* consume the gesture, so dashboard navigation does not flash.
|
|
71
|
+
*/
|
|
72
|
+
noteGesture(gestureName: string): void {
|
|
73
|
+
if (!this.started || !this.enabled()) return
|
|
74
|
+
// Tap edges show a steady circle for as long as the finger is down (on
|
|
75
|
+
// CFW these arrive for holds: ET_LONG/ET_REL forwarded as
|
|
76
|
+
// tap_start/tap_end), with no timer — tap_end clears it.
|
|
77
|
+
if (gestureName === "tap_start" || gestureName === "tap_end") {
|
|
78
|
+
const held = gestureName === "tap_start"
|
|
79
|
+
if (this.held === held) return
|
|
80
|
+
this.held = held
|
|
81
|
+
this.render()
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
const flash: GestureDotGlyph | null =
|
|
85
|
+
gestureName === "single_tap"
|
|
86
|
+
? "dot"
|
|
87
|
+
: gestureName === "swipe_up"
|
|
88
|
+
? "up"
|
|
89
|
+
: gestureName === "swipe_down"
|
|
90
|
+
? "down"
|
|
91
|
+
: null
|
|
92
|
+
if (!flash) return
|
|
93
|
+
this.flash = flash
|
|
94
|
+
if (this.flashTimer !== null) BgTimer.clearTimeout(this.flashTimer)
|
|
95
|
+
this.flashTimer = BgTimer.setTimeout(() => {
|
|
96
|
+
this.flashTimer = null
|
|
97
|
+
this.flash = null
|
|
98
|
+
this.render()
|
|
99
|
+
}, GESTURE_FLASH_MS)
|
|
100
|
+
this.render()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Paint or clear the indicator. Nothing is drawn while anything else is on
|
|
105
|
+
* the glasses (miniapp frame or system screen) or when no gesture was just
|
|
106
|
+
* received. The top-right anchor resolves against the device's real canvas,
|
|
107
|
+
* so no size math here.
|
|
108
|
+
*
|
|
109
|
+
* Between flashes the element STAYS in the scene with the blank glyph:
|
|
110
|
+
* removing it would tear down the firmware image container and rebuild the
|
|
111
|
+
* page on every flash (a rebuild storm on stock firmware). Only disabling
|
|
112
|
+
* the setting removes the element outright.
|
|
113
|
+
*/
|
|
114
|
+
private render(): void {
|
|
115
|
+
if (!this.started) return
|
|
116
|
+
if (localDisplayManager.hasActiveDisplay()) return
|
|
117
|
+
const shown = this.flash ?? (this.held ? "dot" : null)
|
|
118
|
+
if (!this.enabled()) {
|
|
119
|
+
if (this.visible) {
|
|
120
|
+
this.visible = false
|
|
121
|
+
sceneRenderer.emitScene(DOTS_APP_ID, "main", [])
|
|
122
|
+
}
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
if (!shown && !this.visible) return
|
|
126
|
+
const elements: SceneElementInput[] = [
|
|
127
|
+
{
|
|
128
|
+
type: "image",
|
|
129
|
+
id: "gesture-dot",
|
|
130
|
+
box: {
|
|
131
|
+
x: MARGIN_X,
|
|
132
|
+
y: MARGIN_TOP,
|
|
133
|
+
w: GESTURE_DOT_GLYPH_SIZE,
|
|
134
|
+
h: GESTURE_DOT_GLYPH_SIZE,
|
|
135
|
+
anchor: "top-right",
|
|
136
|
+
},
|
|
137
|
+
data: GESTURE_DOT_GLYPHS[shown ?? "blank"],
|
|
138
|
+
},
|
|
139
|
+
]
|
|
140
|
+
this.visible = true
|
|
141
|
+
sceneRenderer.emitScene(DOTS_APP_ID, "main", elements)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const gestureDotsService = new GestureDotsService()
|
|
146
|
+
export default gestureDotsService
|
|
147
|
+
export {GestureDotsService}
|
|
@@ -11,22 +11,26 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Started by `engine.start()`. Idempotent.
|
|
13
13
|
*/
|
|
14
|
-
import BluetoothSdk from "@glassly/bluetooth-sdk
|
|
14
|
+
import BluetoothSdk, {type PublicGlassesStatus} from "@glassly/bluetooth-sdk"
|
|
15
15
|
import {useCoreStore} from "../stores/core"
|
|
16
16
|
import {useGlassesStore} from "../stores/glasses"
|
|
17
|
-
import localMiniappRuntime from "./LocalMiniappRuntime"
|
|
18
17
|
|
|
19
18
|
let unsubs: Array<() => void> = []
|
|
20
19
|
let projectionRunId = 0
|
|
20
|
+
let glassesStatusForwarder: ((status: Partial<PublicGlassesStatus>) => void) | null = null
|
|
21
|
+
let hydrationPromise: Promise<void> | null = null
|
|
21
22
|
|
|
22
|
-
export function startGlassesStatusProjection(
|
|
23
|
-
|
|
23
|
+
export function startGlassesStatusProjection(
|
|
24
|
+
forwarder?: (status: Partial<PublicGlassesStatus>) => void,
|
|
25
|
+
): Promise<void> {
|
|
26
|
+
if (forwarder) glassesStatusForwarder = forwarder
|
|
27
|
+
if (unsubs.length) return hydrationPromise ?? Promise.resolve()
|
|
24
28
|
|
|
25
29
|
const runId = ++projectionRunId
|
|
26
30
|
let bluetoothEventSeen = false
|
|
27
31
|
let glassesEventSeen = false
|
|
28
32
|
|
|
29
|
-
|
|
33
|
+
const bluetoothHydration = BluetoothSdk.getBluetoothStatus()
|
|
30
34
|
.then((status) => {
|
|
31
35
|
if (runId !== projectionRunId || bluetoothEventSeen) return
|
|
32
36
|
useCoreStore.getState().setCoreInfo(status)
|
|
@@ -35,7 +39,7 @@ export function startGlassesStatusProjection(): void {
|
|
|
35
39
|
console.warn("GlassesStatusProjection: getBluetoothStatus failed", error)
|
|
36
40
|
})
|
|
37
41
|
|
|
38
|
-
|
|
42
|
+
const glassesHydration = BluetoothSdk.getGlassesStatus()
|
|
39
43
|
.then((status) => {
|
|
40
44
|
if (runId !== projectionRunId || glassesEventSeen) return
|
|
41
45
|
useGlassesStore.getState().setGlassesInfo(status)
|
|
@@ -46,28 +50,33 @@ export function startGlassesStatusProjection(): void {
|
|
|
46
50
|
|
|
47
51
|
// Bluetooth-adapter status -> core store.
|
|
48
52
|
unsubs.push(
|
|
49
|
-
BluetoothSdk.
|
|
53
|
+
BluetoothSdk.subscribeBluetoothStatus((changed) => {
|
|
50
54
|
bluetoothEventSeen = true
|
|
51
55
|
useCoreStore.getState().setCoreInfo(changed)
|
|
52
56
|
}),
|
|
53
57
|
)
|
|
54
58
|
|
|
55
|
-
// Glasses status -> glasses store (+
|
|
56
|
-
// OTA-available flag on disconnect).
|
|
59
|
+
// Glasses status -> glasses store (+ optional full-runtime forwarding to local
|
|
60
|
+
// miniapps; clear any stale OTA-available flag on disconnect).
|
|
57
61
|
unsubs.push(
|
|
58
|
-
BluetoothSdk.
|
|
62
|
+
BluetoothSdk.subscribeGlassesStatus((changed) => {
|
|
59
63
|
glassesEventSeen = true
|
|
60
64
|
useGlassesStore.getState().setGlassesInfo(changed)
|
|
61
|
-
|
|
65
|
+
glassesStatusForwarder?.(changed)
|
|
62
66
|
if (changed.connection?.state === "disconnected") {
|
|
63
67
|
useGlassesStore.getState().setOtaUpdateAvailable(null)
|
|
64
68
|
}
|
|
65
69
|
}),
|
|
66
70
|
)
|
|
71
|
+
|
|
72
|
+
hydrationPromise = Promise.allSettled([bluetoothHydration, glassesHydration]).then(() => undefined)
|
|
73
|
+
return hydrationPromise
|
|
67
74
|
}
|
|
68
75
|
|
|
69
76
|
export function stopGlassesStatusProjection(): void {
|
|
70
77
|
projectionRunId++
|
|
71
78
|
unsubs.forEach((unsub) => unsub())
|
|
72
79
|
unsubs = []
|
|
80
|
+
glassesStatusForwarder = null
|
|
81
|
+
hydrationPromise = null
|
|
73
82
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import BluetoothSdk, {isEnabledHotspotStatus
|
|
1
|
+
import BluetoothSdk, {isEnabledHotspotStatus} from "@glassly/bluetooth-sdk"
|
|
2
|
+
import {otaServer} from "@glassly/bluetooth-sdk/ota-transport"
|
|
2
3
|
// One coordinator selects the native Android or iOS staging implementation at runtime.
|
|
3
4
|
// eslint-disable-next-line react-native/split-platform-components
|
|
4
5
|
import {PermissionsAndroid, Platform} from "react-native"
|
|
@@ -83,7 +84,7 @@ class HotspotOtaTransport {
|
|
|
83
84
|
let localAddress = scopedAddress
|
|
84
85
|
if (Platform.OS === "ios") {
|
|
85
86
|
try {
|
|
86
|
-
localAddress = await
|
|
87
|
+
localAddress = await otaServer.waitForWifiAddress(hotspot.localIp, 15_000)
|
|
87
88
|
} catch (error) {
|
|
88
89
|
throw new HotspotOtaTransportError(
|
|
89
90
|
"hotspot_join_failed",
|
|
@@ -94,10 +95,10 @@ class HotspotOtaTransport {
|
|
|
94
95
|
const artifactPaths = Object.fromEntries(this.prepared.map((artifact) => [artifact.sha256, artifact.filePath]))
|
|
95
96
|
// Start exactly one native listener to learn its selected port, then atomically replace
|
|
96
97
|
// the placeholder with the immutable rewritten manifest before ota_start is sent.
|
|
97
|
-
const server = await
|
|
98
|
+
const server = await otaServer.start("{}", artifactPaths, localAddress)
|
|
98
99
|
this.serverStarted = true
|
|
99
100
|
const manifest = rewriteManifestForLocalServer(checkResult.manifestBody, this.prepared, server.baseUrl)
|
|
100
|
-
const published = await
|
|
101
|
+
const published = await otaServer.start(manifest, artifactPaths, server.host)
|
|
101
102
|
if (published.manifestUrl !== server.manifestUrl) {
|
|
102
103
|
throw new Error("Local OTA server endpoint changed while publishing the manifest")
|
|
103
104
|
}
|
|
@@ -141,11 +142,11 @@ class HotspotOtaTransport {
|
|
|
141
142
|
destination: string,
|
|
142
143
|
onProgress?: (bytesWritten: number, contentLength: number) => void,
|
|
143
144
|
): Promise<{statusCode: number}> => {
|
|
144
|
-
const subscription =
|
|
145
|
+
const subscription = otaServer.onArtifactDownloadProgress((event) => {
|
|
145
146
|
if (event.destination === destination) onProgress?.(event.bytesWritten, event.contentLength)
|
|
146
147
|
})
|
|
147
148
|
try {
|
|
148
|
-
return await
|
|
149
|
+
return await otaServer.downloadArtifact(entry.url, destination)
|
|
149
150
|
} finally {
|
|
150
151
|
subscription.remove()
|
|
151
152
|
}
|
|
@@ -164,7 +165,7 @@ class HotspotOtaTransport {
|
|
|
164
165
|
console.warn("[OTA_PROGRESS] glasses hotspot shutdown was not confirmed after bounded retries")
|
|
165
166
|
}
|
|
166
167
|
}
|
|
167
|
-
if (this.serverStarted) await
|
|
168
|
+
if (this.serverStarted) await otaServer.stop().catch(() => {})
|
|
168
169
|
if (this.localNetworkConnected) await localNetworkTransport.disconnect().catch(() => {})
|
|
169
170
|
await cleanupArtifacts().catch(() => {})
|
|
170
171
|
this.prepared = []
|
|
@@ -115,12 +115,47 @@ class LocalDisplayManager {
|
|
|
115
115
|
|
|
116
116
|
private coreApp: string | null = null
|
|
117
117
|
private coreAppDisplay: ActiveDisplay | null = null
|
|
118
|
-
private
|
|
118
|
+
private _currentDisplay: ActiveDisplay | null = null
|
|
119
119
|
private backgroundLock: BackgroundLock | null = null
|
|
120
120
|
|
|
121
|
+
/** Fired when the display transitions between empty and occupied (any package, system screens included). */
|
|
122
|
+
private activeDisplayListeners = new Set<(active: boolean) => void>()
|
|
123
|
+
|
|
124
|
+
private get currentDisplay(): ActiveDisplay | null {
|
|
125
|
+
return this._currentDisplay
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private set currentDisplay(value: ActiveDisplay | null) {
|
|
129
|
+
const wasActive = this._currentDisplay !== null
|
|
130
|
+
this._currentDisplay = value
|
|
131
|
+
const isActive = value !== null
|
|
132
|
+
if (wasActive === isActive) return
|
|
133
|
+
for (const listener of this.activeDisplayListeners) {
|
|
134
|
+
try {
|
|
135
|
+
listener(isActive)
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error(`${LOG_TAG}: onActiveDisplayChange listener failed:`, error)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** True while anything (miniapp or system screen) is on the glasses display. */
|
|
143
|
+
public hasActiveDisplay(): boolean {
|
|
144
|
+
return this._currentDisplay !== null
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Subscribe to empty↔occupied display transitions. Returns an unsubscribe. */
|
|
148
|
+
public onActiveDisplayChange(listener: (active: boolean) => void): () => void {
|
|
149
|
+
this.activeDisplayListeners.add(listener)
|
|
150
|
+
return () => this.activeDisplayListeners.delete(listener)
|
|
151
|
+
}
|
|
152
|
+
|
|
121
153
|
private bootingApp: BootingApp | null = null
|
|
122
154
|
private bootQueue: Map<string, {payload: DisplayPayload; resolve?: DisplayRequestResolver}> = new Map()
|
|
123
155
|
|
|
156
|
+
/** Miniapp whose visible frame is temporarily covered by an OS exit notice. */
|
|
157
|
+
private exitingApp: {packageName: string; coveredDisplay: ActiveDisplay} | null = null
|
|
158
|
+
|
|
124
159
|
private expiryTimerId: number | null = null
|
|
125
160
|
|
|
126
161
|
/** Reconnect-replay subscription state (attachToRuntime). */
|
|
@@ -200,7 +235,7 @@ class LocalDisplayManager {
|
|
|
200
235
|
*/
|
|
201
236
|
public showSystemNotice(text: string, durationMs: number = SYSTEM_NOTICE_DURATION_MS): void {
|
|
202
237
|
if (!isGlassesConnected(useGlassesStore.getState().connection)) return
|
|
203
|
-
if (this.bootingApp) return
|
|
238
|
+
if (this.bootingApp || this.exitingApp) return
|
|
204
239
|
console.log(`${LOG_TAG}: showSystemNotice("${text}")`)
|
|
205
240
|
this.sendNow(SYSTEM_NOTICE_PKG, {
|
|
206
241
|
view: "main",
|
|
@@ -228,6 +263,88 @@ class LocalDisplayManager {
|
|
|
228
263
|
}
|
|
229
264
|
}
|
|
230
265
|
|
|
266
|
+
/**
|
|
267
|
+
* The package that currently owns the glasses screen, for gesture-claim
|
|
268
|
+
* arbitration (see LocalMiniappRuntime.foregroundClaimsGesture). This is
|
|
269
|
+
* glasses ownership, not the phone's Compositor foreground: a background
|
|
270
|
+
* miniapp rendering to the glasses with the phone pocketed is the owner.
|
|
271
|
+
*
|
|
272
|
+
* The live frame's owner wins; a background app that holds the display lock
|
|
273
|
+
* but has no frame up right now (between renders) still counts. System
|
|
274
|
+
* frames (boot, notices) own nothing claimable. Returns null when the
|
|
275
|
+
* glasses are idle.
|
|
276
|
+
*/
|
|
277
|
+
public getGestureOwner(): string | null {
|
|
278
|
+
const current = this.currentDisplay?.packageName ?? null
|
|
279
|
+
if (current && current !== SYSTEM_BOOT_PKG && current !== SYSTEM_NOTICE_PKG) return current
|
|
280
|
+
const lock = this.backgroundLock
|
|
281
|
+
if (lock && this.now() <= lock.expiresAt) return lock.packageName
|
|
282
|
+
return null
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* The miniapp whose frame is visibly on the glasses right now.
|
|
287
|
+
*
|
|
288
|
+
* Unlike getGestureOwner(), this deliberately does not fall back to a
|
|
289
|
+
* background display lock: a lock holder with no live frame is not visible
|
|
290
|
+
* and therefore must not be closed by the system double-tap gesture.
|
|
291
|
+
*/
|
|
292
|
+
public getVisibleMiniappPackageName(): string | null {
|
|
293
|
+
const current = this.currentDisplay?.packageName ?? null
|
|
294
|
+
if (!current || current === SYSTEM_BOOT_PKG || current === SYSTEM_NOTICE_PKG) return null
|
|
295
|
+
return current
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Cover the currently visible miniapp with a persistent system exit notice.
|
|
300
|
+
* While it is up, miniapp display traffic is held off so a streaming app
|
|
301
|
+
* cannot immediately overwrite the notice. The caller completes the normal
|
|
302
|
+
* stop flow and then calls finishMiniappExit(); onUnmount does the same as a
|
|
303
|
+
* safety net.
|
|
304
|
+
*/
|
|
305
|
+
public beginMiniappExit(packageName: string, text: string): boolean {
|
|
306
|
+
if (this.exitingApp !== null) return this.exitingApp.packageName === packageName
|
|
307
|
+
const coveredDisplay = this.currentDisplay
|
|
308
|
+
if (!coveredDisplay || this.getVisibleMiniappPackageName() !== packageName) return false
|
|
309
|
+
|
|
310
|
+
console.log(`${LOG_TAG}: beginMiniappExit(${packageName})`)
|
|
311
|
+
this.exitingApp = {packageName, coveredDisplay}
|
|
312
|
+
this.clearExpiryTimer()
|
|
313
|
+
this.sendNow(SYSTEM_NOTICE_PKG, {
|
|
314
|
+
view: "main",
|
|
315
|
+
layout: {layoutType: "text_wall", text},
|
|
316
|
+
})
|
|
317
|
+
return true
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Remove an exit notice and resume the display state normal app teardown leaves behind. */
|
|
321
|
+
public finishMiniappExit(packageName: string): void {
|
|
322
|
+
if (this.exitingApp?.packageName !== packageName) return
|
|
323
|
+
console.log(`${LOG_TAG}: finishMiniappExit(${packageName})`)
|
|
324
|
+
this.exitingApp = null
|
|
325
|
+
|
|
326
|
+
if (this.currentDisplay?.packageName !== SYSTEM_NOTICE_PKG) return
|
|
327
|
+
this.currentDisplay = null
|
|
328
|
+
this.tryRestoreCoreDisplay()
|
|
329
|
+
if (!this.currentDisplay) this.sendClear()
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** Cancel the handoff and put back the frame the exit notice covered. */
|
|
333
|
+
public cancelMiniappExit(packageName: string): void {
|
|
334
|
+
if (this.exitingApp?.packageName !== packageName) return
|
|
335
|
+
const covered = this.exitingApp.coveredDisplay
|
|
336
|
+
this.exitingApp = null
|
|
337
|
+
|
|
338
|
+
if (this.currentDisplay?.packageName !== SYSTEM_NOTICE_PKG) return
|
|
339
|
+
if (covered.expiresAt === null || covered.expiresAt > this.now()) {
|
|
340
|
+
this.restoreDisplay(covered)
|
|
341
|
+
} else {
|
|
342
|
+
this.currentDisplay = null
|
|
343
|
+
this.tryRestoreCoreDisplay()
|
|
344
|
+
if (!this.currentDisplay) this.sendClear()
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
231
348
|
/** Report-safe ownership snapshot for incident diagnostics. */
|
|
232
349
|
public getDiagnosticSnapshot(): Record<string, unknown> {
|
|
233
350
|
return {
|
|
@@ -235,6 +352,7 @@ class LocalDisplayManager {
|
|
|
235
352
|
currentDisplayPackageName: this.currentDisplay?.packageName ?? null,
|
|
236
353
|
backgroundLockPackageName: this.backgroundLock?.packageName ?? null,
|
|
237
354
|
bootingPackageName: this.bootingApp?.packageName ?? null,
|
|
355
|
+
exitingPackageName: this.exitingApp?.packageName ?? null,
|
|
238
356
|
}
|
|
239
357
|
}
|
|
240
358
|
|
|
@@ -297,6 +415,8 @@ class LocalDisplayManager {
|
|
|
297
415
|
this.sendClear()
|
|
298
416
|
}
|
|
299
417
|
}
|
|
418
|
+
|
|
419
|
+
this.finishMiniappExit(packageName)
|
|
300
420
|
}
|
|
301
421
|
|
|
302
422
|
// ===========================================================================
|
|
@@ -313,6 +433,13 @@ class LocalDisplayManager {
|
|
|
313
433
|
* the boot queue (resolved when drained or superseded).
|
|
314
434
|
*/
|
|
315
435
|
public request(packageName: string, payload: DisplayPayload, resolve?: DisplayRequestResolver): void {
|
|
436
|
+
// Keep the OS exit notice visible for its short handoff window. Without
|
|
437
|
+
// this, streaming captions/terminal frames can replace it immediately.
|
|
438
|
+
if (this.exitingApp !== null) {
|
|
439
|
+
resolve?.({status: "blocked", reason: "a miniapp exit notice is active"})
|
|
440
|
+
return
|
|
441
|
+
}
|
|
442
|
+
|
|
316
443
|
// Dashboard view: pass straight through (no throttle/arbitration). Local
|
|
317
444
|
// dashboard rendering is currently a stub on the phone anyway.
|
|
318
445
|
if (payload.view === "dashboard") {
|
|
@@ -785,6 +912,7 @@ class LocalDisplayManager {
|
|
|
785
912
|
this.coreAppDisplay = null
|
|
786
913
|
this.currentDisplay = null
|
|
787
914
|
this.backgroundLock = null
|
|
915
|
+
this.exitingApp = null
|
|
788
916
|
this.bootQueue.clear()
|
|
789
917
|
this.now = () => Date.now()
|
|
790
918
|
this.unsubscribeGlassesStatus?.()
|
|
@@ -52,6 +52,7 @@ import {normalizeAiTier, resolveAiTier} from "./AiTierResolution"
|
|
|
52
52
|
import type {AiTier, AiTierProvider, SavedLlmKeys} from "./AiTierResolution"
|
|
53
53
|
import {HttpError} from "@glassly/cloud-client"
|
|
54
54
|
import localDisplayManager from "./LocalDisplayManager"
|
|
55
|
+
import {claimsGesture} from "./gestureClaims"
|
|
55
56
|
import nativeNavigationService from "./NativeNavigationService"
|
|
56
57
|
import type {DisplayPayload} from "./LocalDisplayManager"
|
|
57
58
|
import headingService from "./HeadingService"
|
|
@@ -539,6 +540,8 @@ class LocalMiniappRuntime {
|
|
|
539
540
|
|
|
540
541
|
/** Connected miniapps keyed by packageName. */
|
|
541
542
|
private connectedApps: Map<string, ConnectedMiniapp> = new Map()
|
|
543
|
+
/** Per-app claimed gestures (CLAIM_GESTURES) — see foregroundClaimsGesture. */
|
|
544
|
+
private gestureClaims: Map<string, Set<string>> = new Map()
|
|
542
545
|
|
|
543
546
|
/**
|
|
544
547
|
* Packages that have completed their CONNECT handshake (sent
|
|
@@ -1173,6 +1176,7 @@ class LocalMiniappRuntime {
|
|
|
1173
1176
|
// core app's saved frame).
|
|
1174
1177
|
localDisplayManager.onUnmount(packageName)
|
|
1175
1178
|
|
|
1179
|
+
this.gestureClaims.delete(packageName)
|
|
1176
1180
|
this.connectedApps.delete(packageName)
|
|
1177
1181
|
this.recomputeMicRequirements()
|
|
1178
1182
|
if (releasedMicGateOverride) {
|
|
@@ -1251,6 +1255,9 @@ class LocalMiniappRuntime {
|
|
|
1251
1255
|
case MiniappRequestType.SUBSCRIBE:
|
|
1252
1256
|
this.handleSubscribe(packageName, payload, requestId)
|
|
1253
1257
|
break
|
|
1258
|
+
case MiniappRequestType.CLAIM_GESTURES:
|
|
1259
|
+
this.handleClaimGestures(packageName, payload, requestId)
|
|
1260
|
+
break
|
|
1254
1261
|
case MiniappRequestType.DISPLAY:
|
|
1255
1262
|
this.handleDisplay(packageName, payload, requestId)
|
|
1256
1263
|
break
|
|
@@ -1858,6 +1865,36 @@ class LocalMiniappRuntime {
|
|
|
1858
1865
|
this.sendResult(packageName, requestId, true, {applied: true})
|
|
1859
1866
|
}
|
|
1860
1867
|
|
|
1868
|
+
/**
|
|
1869
|
+
* CLAIM_GESTURES (session.input.setClaimedGestures): the app declares which
|
|
1870
|
+
* touch gestures it handles itself, replacing its previous claim set.
|
|
1871
|
+
* Claims suppress the host's default behavior for those gestures (today:
|
|
1872
|
+
* double_tap → dashboard/head-up in DeviceEventRouter) but ONLY while the
|
|
1873
|
+
* claiming app is the one in front of the user — see
|
|
1874
|
+
* {@link foregroundClaimsGesture}. Cleared on app stop.
|
|
1875
|
+
*/
|
|
1876
|
+
private handleClaimGestures(packageName: string, payload: Record<string, unknown>, requestId?: string): void {
|
|
1877
|
+
if (!this.connectedApps.has(packageName)) {
|
|
1878
|
+
this.sendResult(packageName, requestId, true)
|
|
1879
|
+
return
|
|
1880
|
+
}
|
|
1881
|
+
const raw = payload.gestures
|
|
1882
|
+
const gestures = Array.isArray(raw) ? raw.filter((g): g is string => typeof g === "string") : []
|
|
1883
|
+
if (gestures.length === 0) this.gestureClaims.delete(packageName)
|
|
1884
|
+
else this.gestureClaims.set(packageName, new Set(gestures))
|
|
1885
|
+
this.sendResult(packageName, requestId, true)
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
/** True iff the miniapp in front of the user has claimed `gesture`. */
|
|
1889
|
+
public foregroundClaimsGesture(gesture: string): boolean {
|
|
1890
|
+
return claimsGesture(
|
|
1891
|
+
this.gestureClaims,
|
|
1892
|
+
gesture,
|
|
1893
|
+
localDisplayManager.getGestureOwner(),
|
|
1894
|
+
useAppStatusStore.getState().foregroundedPackage,
|
|
1895
|
+
)
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1861
1898
|
private handleSubscribe(packageName: string, payload: Record<string, unknown>, requestId?: string): void {
|
|
1862
1899
|
const app = this.connectedApps.get(packageName)
|
|
1863
1900
|
if (!app) return
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export const MINIAPP_EXIT_NOTICE_MS = 1500
|
|
2
|
+
|
|
3
|
+
interface ExitGestureApp {
|
|
4
|
+
packageName: string
|
|
5
|
+
name: string
|
|
6
|
+
running: boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface ExitGestureState {
|
|
10
|
+
apps: ExitGestureApp[]
|
|
11
|
+
foregroundedPackage: string | null
|
|
12
|
+
clearForeground: () => void
|
|
13
|
+
stop: (packageName: string) => Promise<void>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ExitGestureDependencies {
|
|
17
|
+
getState: () => ExitGestureState
|
|
18
|
+
getVisiblePackageName: () => string | null
|
|
19
|
+
beginExitNotice: (packageName: string, text: string) => boolean
|
|
20
|
+
finishExitNotice: (packageName: string) => void
|
|
21
|
+
cancelExitNotice: (packageName: string) => void
|
|
22
|
+
setTimeout: (callback: () => void, delay: number) => number
|
|
23
|
+
clearTimeout: (timerId: number) => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Owns the short double-tap → exit-notice → normal-stop handoff. */
|
|
27
|
+
export class MiniappExitGesture {
|
|
28
|
+
private pending: {packageName: string; timerId: number} | null = null
|
|
29
|
+
|
|
30
|
+
public constructor(private readonly deps: ExitGestureDependencies) {}
|
|
31
|
+
|
|
32
|
+
/** Returns true when the system gesture was consumed by a visible miniapp exit. */
|
|
33
|
+
public tryExitVisibleMiniapp(): boolean {
|
|
34
|
+
// Swallow repeat taps while the notice is already counting down.
|
|
35
|
+
if (this.pending) return true
|
|
36
|
+
|
|
37
|
+
const packageName = this.deps.getVisiblePackageName()
|
|
38
|
+
if (!packageName) return false
|
|
39
|
+
|
|
40
|
+
const app = this.deps.getState().apps.find((candidate) => candidate.packageName === packageName)
|
|
41
|
+
if (!app?.running) return false
|
|
42
|
+
|
|
43
|
+
const name = app.name.trim() || packageName
|
|
44
|
+
if (!this.deps.beginExitNotice(packageName, `Exiting ${name}…`)) return false
|
|
45
|
+
|
|
46
|
+
const timerId = this.deps.setTimeout(() => {
|
|
47
|
+
const state = this.deps.getState()
|
|
48
|
+
if (state.foregroundedPackage === packageName) state.clearForeground()
|
|
49
|
+
|
|
50
|
+
const stillRunning = state.apps.some((candidate) => candidate.packageName === packageName && candidate.running)
|
|
51
|
+
const stop = stillRunning ? state.stop(packageName) : Promise.resolve()
|
|
52
|
+
void stop
|
|
53
|
+
.catch((error) => {
|
|
54
|
+
console.error(`MiniappExitGesture: failed to stop ${packageName}:`, error)
|
|
55
|
+
})
|
|
56
|
+
.finally(() => {
|
|
57
|
+
if (this.pending?.packageName === packageName) this.pending = null
|
|
58
|
+
// LocalMiniappRuntime.onUnmount normally finishes the notice. This
|
|
59
|
+
// fallback also handles a failed/no-op teardown.
|
|
60
|
+
this.deps.finishExitNotice(packageName)
|
|
61
|
+
})
|
|
62
|
+
}, MINIAPP_EXIT_NOTICE_MS)
|
|
63
|
+
|
|
64
|
+
this.pending = {packageName, timerId}
|
|
65
|
+
return true
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Cancel an in-flight handoff when the device-event router shuts down. */
|
|
69
|
+
public reset(): void {
|
|
70
|
+
if (!this.pending) return
|
|
71
|
+
const {packageName, timerId} = this.pending
|
|
72
|
+
this.pending = null
|
|
73
|
+
this.deps.clearTimeout(timerId)
|
|
74
|
+
this.deps.cancelExitNotice(packageName)
|
|
75
|
+
}
|
|
76
|
+
}
|