@mentra/engine 3.2.0-dev.181 → 3.2.0-dev.190
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 +13 -0
- package/build/engine.d.ts +7 -0
- package/build/engine.d.ts.map +1 -1
- package/build/facades/dev.d.ts.map +1 -1
- package/build/facades/dev.js +30 -0
- package/build/facades/dev.js.map +1 -1
- package/build/facades/phoneNotifications.d.ts +9 -0
- package/build/facades/phoneNotifications.d.ts.map +1 -1
- package/build/facades/phoneNotifications.js +15 -0
- package/build/facades/phoneNotifications.js.map +1 -1
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/runtime/bootstrap.d.ts +9 -0
- package/build/runtime/bootstrap.d.ts.map +1 -1
- package/build/runtime/bootstrap.js.map +1 -1
- package/build/services/CloudClientService.d.ts +1 -1
- package/build/services/CloudClientService.d.ts.map +1 -1
- package/build/services/CloudClientService.js +3 -17
- package/build/services/CloudClientService.js.map +1 -1
- package/build/services/NativeNotificationPolicy.d.ts +13 -0
- package/build/services/NativeNotificationPolicy.d.ts.map +1 -0
- package/build/services/NativeNotificationPolicy.js +12 -0
- package/build/services/NativeNotificationPolicy.js.map +1 -0
- package/build/services/PhoneNotificationsSync.d.ts +13 -0
- package/build/services/PhoneNotificationsSync.d.ts.map +1 -1
- package/build/services/PhoneNotificationsSync.js +102 -26
- package/build/services/PhoneNotificationsSync.js.map +1 -1
- package/build/services/cloudEndpointPolicy.d.ts +19 -0
- package/build/services/cloudEndpointPolicy.d.ts.map +1 -0
- package/build/services/cloudEndpointPolicy.js +20 -0
- package/build/services/cloudEndpointPolicy.js.map +1 -0
- package/build/services/otaManifestPolicy.js +1 -1
- package/build/services/otaManifestPolicy.js.map +1 -1
- package/build/services/otaManifestUrl.d.ts.map +1 -1
- package/build/services/otaManifestUrl.js +2 -1
- package/build/services/otaManifestUrl.js.map +1 -1
- package/build/stores/settings.d.ts.map +1 -1
- package/build/stores/settings.js +46 -5
- package/build/stores/settings.js.map +1 -1
- package/build/types/capabilities/even-realities-g2.d.ts.map +1 -1
- package/build/types/capabilities/even-realities-g2.js +1 -0
- package/build/types/capabilities/even-realities-g2.js.map +1 -1
- package/build/types/hardware.d.ts +2 -0
- package/build/types/hardware.d.ts.map +1 -1
- package/build/types/hardware.js.map +1 -1
- package/package.json +7 -7
- package/src/facades/dev.ts +32 -0
- package/src/facades/phoneNotifications.ts +21 -0
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/runtime/bootstrap.ts +6 -0
- package/src/services/CloudClientService.ts +3 -15
- package/src/services/NativeNotificationPolicy.ts +24 -0
- package/src/services/PhoneNotificationsSync.ts +115 -31
- package/src/services/cloudEndpointPolicy.ts +25 -0
- package/src/services/otaManifestPolicy.ts +1 -1
- package/src/services/otaManifestUrl.ts +2 -1
- package/src/stores/settings.ts +49 -13
- package/src/types/capabilities/even-realities-g2.ts +1 -0
- package/src/types/hardware.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mentra/engine",
|
|
3
|
-
"version": "3.2.0-dev.
|
|
3
|
+
"version": "3.2.0-dev.190",
|
|
4
4
|
"description": "Mentra Engine — miniapp registry, WebView bridge, and miniapp store",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"react-native": "src/index.ts",
|
|
@@ -97,12 +97,12 @@
|
|
|
97
97
|
"registry": "https://registry.npmjs.org/"
|
|
98
98
|
},
|
|
99
99
|
"dependencies": {
|
|
100
|
-
"@mentra/acs-meeting": "3.2.0-dev.
|
|
101
|
-
"@mentra/bluetooth-sdk": "3.2.0-dev.
|
|
102
|
-
"@mentra/cloud-client": "3.2.0-dev.
|
|
103
|
-
"@mentra/cloud-protocol": "3.2.0-dev.
|
|
104
|
-
"@mentra/crust": "3.2.0-dev.
|
|
105
|
-
"@mentra/miniapp": "3.2.0-dev.
|
|
100
|
+
"@mentra/acs-meeting": "3.2.0-dev.190",
|
|
101
|
+
"@mentra/bluetooth-sdk": "3.2.0-dev.190",
|
|
102
|
+
"@mentra/cloud-client": "3.2.0-dev.190",
|
|
103
|
+
"@mentra/cloud-protocol": "3.2.0-dev.190",
|
|
104
|
+
"@mentra/crust": "3.2.0-dev.190",
|
|
105
|
+
"@mentra/miniapp": "3.2.0-dev.190",
|
|
106
106
|
"buffer": "^6.0.3",
|
|
107
107
|
"events": "^3.3.0",
|
|
108
108
|
"react-native-marked": "8.1.1",
|
package/src/facades/dev.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {getConfigValues} from "../runtime/bootstrap"
|
|
|
19
19
|
import {decideDevLaunchRoute} from "../utils/devMiniappLaunch"
|
|
20
20
|
import {registerDevApp, DEV_APP_PACKAGE_NAME, type DevAppRecord} from "../services/AppRegistry"
|
|
21
21
|
import {useAppStatusStore} from "../stores/apps"
|
|
22
|
+
import {scopeCloudUrlOverrides} from "../services/cloudEndpointPolicy"
|
|
22
23
|
|
|
23
24
|
type CloudUrlOverrides = {core?: string; runtime?: string}
|
|
24
25
|
|
|
@@ -28,6 +29,11 @@ type CloudUrlOverrides = {core?: string; runtime?: string}
|
|
|
28
29
|
* host's Metro/env URL resolution); both empty clears the live override.
|
|
29
30
|
*/
|
|
30
31
|
function applyCloudUrlReconnect(urls: CloudUrlOverrides = currentCloudUrlOverrides()): void {
|
|
32
|
+
const hostResolver = getConfigValues().resolveCloudEndpoints
|
|
33
|
+
if (hostResolver) {
|
|
34
|
+
cloudClientService.reconnect(null)
|
|
35
|
+
return
|
|
36
|
+
}
|
|
31
37
|
const resolved = resolveCloudUrlReconnectTarget(urls)
|
|
32
38
|
if (resolved === undefined) {
|
|
33
39
|
cloudClientService.reconnect(null)
|
|
@@ -91,6 +97,32 @@ export const dev = {
|
|
|
91
97
|
},
|
|
92
98
|
/** Override the cloud-v2 URLs and reconnect the live client onto them. */
|
|
93
99
|
setCloudUrls: (urls: {core?: string; runtime?: string}) => {
|
|
100
|
+
const config = getConfigValues()
|
|
101
|
+
if (config.resolveCloudEndpoints) {
|
|
102
|
+
const scope = config.cloudDebugScope ?? ""
|
|
103
|
+
const scoped = scopeCloudUrlOverrides(
|
|
104
|
+
{
|
|
105
|
+
scope: useSettingsStore.getState().getSetting(SETTINGS.cloud_url_deployment.key),
|
|
106
|
+
...currentCloudUrlOverrides(),
|
|
107
|
+
},
|
|
108
|
+
scope,
|
|
109
|
+
urls,
|
|
110
|
+
)
|
|
111
|
+
const values: Record<string, unknown> = {
|
|
112
|
+
[SETTINGS.cloud_url_deployment.key]: scope,
|
|
113
|
+
[SETTINGS.cloud_core_url.key]: scoped.core,
|
|
114
|
+
[SETTINGS.cloud_runtime_url.key]: scoped.runtime,
|
|
115
|
+
[SETTINGS.cached_required_version.key]: "",
|
|
116
|
+
}
|
|
117
|
+
void useSettingsStore
|
|
118
|
+
.getState()
|
|
119
|
+
.setManyLocally(values)
|
|
120
|
+
.then((result) => {
|
|
121
|
+
if (result.is_error()) console.error("engine.dev.setCloudUrls: could not save URLs", result.error)
|
|
122
|
+
else applyCloudUrlReconnect()
|
|
123
|
+
})
|
|
124
|
+
return
|
|
125
|
+
}
|
|
94
126
|
const next = {...currentCloudUrlOverrides(), ...urls}
|
|
95
127
|
const resolved = resolveCloudUrlReconnectTarget(next)
|
|
96
128
|
if (resolved === null) {
|
|
@@ -10,9 +10,30 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import {Platform} from "react-native"
|
|
12
12
|
import CrustModule from "@mentra/crust"
|
|
13
|
+
import BluetoothSdk from "@mentra/bluetooth-sdk/internal"
|
|
14
|
+
import type {NativeNotificationStatus, NativeNotificationDelivery} from "@mentra/bluetooth-sdk"
|
|
15
|
+
import {
|
|
16
|
+
nativeNotificationCapabilities,
|
|
17
|
+
setPhoneNotificationPresentationActive,
|
|
18
|
+
usesNativeNotificationPresentation,
|
|
19
|
+
presentNativePhoneNotification,
|
|
20
|
+
} from "../services/PhoneNotificationsSync"
|
|
13
21
|
import {useSettingsStore, SETTINGS} from "../stores/settings"
|
|
14
22
|
|
|
15
23
|
export const phoneNotifications = {
|
|
24
|
+
nativeCapabilities: nativeNotificationCapabilities,
|
|
25
|
+
setPresentationActive: setPhoneNotificationPresentationActive,
|
|
26
|
+
usesNativePresentation: usesNativeNotificationPresentation,
|
|
27
|
+
presentNative: presentNativePhoneNotification,
|
|
28
|
+
nativeStatus: (): Promise<NativeNotificationStatus> => BluetoothSdk.getNativeNotificationStatus(),
|
|
29
|
+
onNativeStatus: (listener: (status: NativeNotificationStatus) => void) => {
|
|
30
|
+
const subscription = BluetoothSdk.addListener("native_notification_status", listener)
|
|
31
|
+
return () => subscription.remove()
|
|
32
|
+
},
|
|
33
|
+
onNativeDelivery: (listener: (event: NativeNotificationDelivery) => void) => {
|
|
34
|
+
const subscription = BluetoothSdk.addListener("native_notification_delivery", listener)
|
|
35
|
+
return () => subscription.remove()
|
|
36
|
+
},
|
|
16
37
|
/** The phone's installed apps (package, name, icon). (empty on iOS) */
|
|
17
38
|
installedApps: (): Promise<Array<{packageName: string; appName: string; icon: string | null}>> => {
|
|
18
39
|
if (Platform.OS !== "android") return Promise.resolve([])
|
|
@@ -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": "3.2.0",
|
|
15
|
-
"releaseIdentity": "3.2.0-dev.
|
|
16
|
-
"releaseSetId": "mentra-3.2.0-dev.
|
|
17
|
-
"sourceCommit": "
|
|
18
|
-
"otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.
|
|
19
|
-
"otaManifestSha256": "
|
|
15
|
+
"releaseIdentity": "3.2.0-dev.190",
|
|
16
|
+
"releaseSetId": "mentra-3.2.0-dev.190",
|
|
17
|
+
"sourceCommit": "9cec54c1238d6d52a90e2edc84f0b99692b8a81a",
|
|
18
|
+
"otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.190.json",
|
|
19
|
+
"otaManifestSha256": "00504171cce268dae2a22d9c4156cb7bac0b8a4397b35a3890ed561c619e5df0"
|
|
20
20
|
})
|
package/src/runtime/bootstrap.ts
CHANGED
|
@@ -28,6 +28,10 @@ export interface IslandConfigValues {
|
|
|
28
28
|
coreUrl?: string | null
|
|
29
29
|
/** cloud-v2 runtime service base URL. */
|
|
30
30
|
runtimeUrl?: string | null
|
|
31
|
+
/** Host's live manifest/override resolver, also used on automatic reconnects. */
|
|
32
|
+
resolveCloudEndpoints?: () => {core: string; runtime: string}
|
|
33
|
+
/** Scope for debug settings written through engine.dev. */
|
|
34
|
+
cloudDebugScope?: string
|
|
31
35
|
/** Open Runtime's live WebSocket/audio session. Defaults to true. */
|
|
32
36
|
runtimeRealtimeSession?: boolean
|
|
33
37
|
/** Complete allowlist for bundled/local miniapps; null or omitted allows all. */
|
|
@@ -40,6 +44,8 @@ export interface IslandConfigValues {
|
|
|
40
44
|
cloudAuthStorageKey?: string
|
|
41
45
|
/** Deployment-pinned Mentra Live OTA manifest; explicit null disables remote OTA. */
|
|
42
46
|
otaManifestUrl?: string | null
|
|
47
|
+
/** Official deployments retain the legacy-device and embedded release fallback. */
|
|
48
|
+
allowLegacyOtaFallback?: boolean
|
|
43
49
|
/** Deployment capability policy. Omitted entries preserve consumer behavior. */
|
|
44
50
|
features?: Partial<Record<IslandFeatureName, boolean>>
|
|
45
51
|
/** OEM identifier (Mentra is OEM #0); reserved for OEM auth/telemetry. */
|
|
@@ -34,6 +34,7 @@ import {BgTimer} from "../utils/timers"
|
|
|
34
34
|
import {logCloudV2TranscriptMetric} from "./CloudTranscriptE2EMetrics"
|
|
35
35
|
import {LocalMiniappUserIdentity} from "./LocalMiniappUserIdentity"
|
|
36
36
|
import {nativeHttpResponseBody} from "./NativeHttpResponse"
|
|
37
|
+
import {resolveCloudEndpoints} from "./cloudEndpointPolicy"
|
|
37
38
|
|
|
38
39
|
const LOG_TAG = "cloudClient"
|
|
39
40
|
type CloudCore = NonNullable<CloudClient["core"]>
|
|
@@ -56,11 +57,6 @@ function clearPersistentFailureAlarm(): void {
|
|
|
56
57
|
|
|
57
58
|
type Lc3FrameSizeBytes = 20 | 40 | 60
|
|
58
59
|
|
|
59
|
-
// Neutral last-ditch fallbacks (reachable under `adb reverse`) for when the host
|
|
60
|
-
// passes no endpoints. The host normally resolves the real URLs (dev override /
|
|
61
|
-
// Metro host / env) and hands them in via `engine.configure({config})`.
|
|
62
|
-
const FALLBACK_CORE_URL = "http://localhost:3000"
|
|
63
|
-
const FALLBACK_RUNTIME_URL = "http://localhost:3001"
|
|
64
60
|
const LOCAL_AUTH_PORT = 3002
|
|
65
61
|
|
|
66
62
|
let client: CloudClient | null = null
|
|
@@ -100,15 +96,7 @@ const statusListeners = new Set<(snapshot: CloudClientStatusSnapshot) => void>()
|
|
|
100
96
|
const connectionListeners = new Set<(connected: boolean) => void>()
|
|
101
97
|
|
|
102
98
|
function resolveEndpoints(): {core?: string; runtime: string} {
|
|
103
|
-
|
|
104
|
-
const cfg = getConfigValues()
|
|
105
|
-
const runtime = cfg.runtimeUrl === null ? "" : cfg.runtimeUrl?.trim() || FALLBACK_RUNTIME_URL
|
|
106
|
-
if (!runtime) throw new Error("cloudClient: Runtime endpoint is not configured")
|
|
107
|
-
const core = cfg.coreUrl === null ? undefined : cfg.coreUrl?.trim() || FALLBACK_CORE_URL
|
|
108
|
-
return {
|
|
109
|
-
...(core ? {core} : {}),
|
|
110
|
-
runtime,
|
|
111
|
-
}
|
|
99
|
+
return resolveCloudEndpoints(getConfigValues(), endpointsOverride)
|
|
112
100
|
}
|
|
113
101
|
|
|
114
102
|
function getCoreClient(): CloudCore {
|
|
@@ -554,7 +542,7 @@ export const cloudClientService = {
|
|
|
554
542
|
|
|
555
543
|
/**
|
|
556
544
|
* Tear down + rebuild. Pass new endpoints to switch URLs; pass `null` to CLEAR
|
|
557
|
-
* a prior override and
|
|
545
|
+
* a prior override and resume the host resolver or boot config (so cleared/default cloud
|
|
558
546
|
* URLs don't keep reconnecting to a stale override); omit to keep the current.
|
|
559
547
|
*/
|
|
560
548
|
reconnect(endpoints?: {core?: string; runtime: string} | null): void {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type {NativeNotificationConfig} from "@mentra/bluetooth-sdk"
|
|
2
|
+
|
|
3
|
+
export interface NotificationPolicyInput {
|
|
4
|
+
supported: boolean
|
|
5
|
+
selected: boolean
|
|
6
|
+
presentationActive: boolean
|
|
7
|
+
sourceEnabled: boolean
|
|
8
|
+
autoDisplay: boolean
|
|
9
|
+
durationSeconds: number
|
|
10
|
+
doNotDisturb: boolean
|
|
11
|
+
blockedApps: string[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function nativeNotificationConfig(input: NotificationPolicyInput): NativeNotificationConfig {
|
|
15
|
+
return {
|
|
16
|
+
enabled: input.supported && input.selected && input.presentationActive && input.sourceEnabled,
|
|
17
|
+
autoDisplay: input.autoDisplay,
|
|
18
|
+
durationSeconds: Number.isFinite(input.durationSeconds)
|
|
19
|
+
? Math.min(30, Math.max(1, Math.round(input.durationSeconds)))
|
|
20
|
+
: 5,
|
|
21
|
+
doNotDisturb: input.doNotDisturb,
|
|
22
|
+
blockedApps: input.blockedApps,
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -1,46 +1,130 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
* operational kill switch + config (`android_notification_listener_enabled`
|
|
4
|
-
* and `notifications_blocklist`) to the native
|
|
5
|
-
* NotificationListener via `CrustModule.setNotificationConfig`, so
|
|
6
|
-
* the blocklist reaches the listener for ANY host — not just the Mentra App,
|
|
7
|
-
* where this used to live in MantleManager. The Android service is desired by
|
|
8
|
-
* default now that its cold start is isolated from React Native. Native code
|
|
9
|
-
* keeps the component discoverable in Android's notification-access Settings,
|
|
10
|
-
* but only binds the service after access is granted. The debug setting remains
|
|
11
|
-
* a hidden emergency kill switch; Notify's running state gates presentation,
|
|
12
|
-
* not capture. Android-only; a no-op elsewhere. Started by `engine.start()`.
|
|
13
|
-
*/
|
|
1
|
+
/** One engine-owned notification policy: capture remains independent of presentation. */
|
|
2
|
+
import {Platform} from "react-native"
|
|
14
3
|
import {shallow} from "zustand/shallow"
|
|
15
4
|
import CrustModule from "@mentra/crust"
|
|
5
|
+
import BluetoothSdk from "@mentra/bluetooth-sdk/internal"
|
|
6
|
+
import type {PhoneNotificationEvent} from "@mentra/crust"
|
|
16
7
|
|
|
17
8
|
import {useSettingsStore, SETTINGS} from "../stores/settings"
|
|
9
|
+
import {useGlassesStore, isGlassesReady} from "../stores/glasses"
|
|
10
|
+
import {DeviceTypes} from "../types/enums"
|
|
11
|
+
import {getModelCapabilities} from "../types/hardware"
|
|
12
|
+
import {nativeNotificationConfig} from "./NativeNotificationPolicy"
|
|
18
13
|
|
|
19
|
-
let
|
|
14
|
+
let unsubscribers: Array<() => void> = []
|
|
15
|
+
let presentationActive = false
|
|
20
16
|
|
|
21
|
-
function
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
17
|
+
export function nativeNotificationCapabilities() {
|
|
18
|
+
const supported = Boolean(
|
|
19
|
+
getModelCapabilities(useGlassesStore.getState().deviceModel as DeviceTypes)?.hasNativeNotifications,
|
|
20
|
+
)
|
|
21
|
+
return {
|
|
22
|
+
supported,
|
|
23
|
+
source: Platform.OS === "ios" ? ("ancs" as const) : ("phone" as const),
|
|
24
|
+
contentAccess:
|
|
25
|
+
Platform.OS === "android" ? ("full" as const) : supported ? ("app_identity" as const) : ("unsupported" as const),
|
|
26
|
+
perAppFiltering: Platform.OS === "android",
|
|
27
|
+
removal: false, // The verified G2 protocol does not yet include a removal command.
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function desiredConfig() {
|
|
32
|
+
const settings = useSettingsStore.getState()
|
|
33
|
+
return nativeNotificationConfig({
|
|
34
|
+
supported: nativeNotificationCapabilities().supported,
|
|
35
|
+
selected: Boolean(settings.getSetting(SETTINGS.native_notifications_enabled.key)),
|
|
36
|
+
presentationActive,
|
|
37
|
+
sourceEnabled:
|
|
38
|
+
Platform.OS !== "android" || Boolean(settings.getSetting(SETTINGS.android_notification_listener_enabled.key)),
|
|
39
|
+
autoDisplay: Boolean(settings.getSetting(SETTINGS.native_notifications_auto_display.key)),
|
|
40
|
+
durationSeconds: Number(settings.getSetting(SETTINGS.native_notifications_duration.key)),
|
|
41
|
+
doNotDisturb: Boolean(settings.getSetting(SETTINGS.native_notifications_do_not_disturb.key)),
|
|
42
|
+
blockedApps: Platform.OS === "android" ? settings.getSetting(SETTINGS.notifications_blocklist.key) : [],
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function pushNativeConfig(): void {
|
|
47
|
+
if (!nativeNotificationCapabilities().supported) return
|
|
48
|
+
void BluetoothSdk.configureNativeNotifications(desiredConfig()).catch((error: unknown) => {
|
|
49
|
+
if (isGlassesReady(useGlassesStore.getState().connection)) {
|
|
50
|
+
console.warn("PhoneNotificationsSync: native notification configuration failed", error)
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Hosts activate their presentation owner (Notify in the Mentra App) through this seam. */
|
|
56
|
+
export function setPhoneNotificationPresentationActive(active: boolean): void {
|
|
57
|
+
if (active === presentationActive) return
|
|
58
|
+
presentationActive = active
|
|
59
|
+
pushNativeConfig()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function usesNativeNotificationPresentation(): boolean {
|
|
63
|
+
return (
|
|
64
|
+
nativeNotificationCapabilities().supported &&
|
|
65
|
+
presentationActive &&
|
|
66
|
+
Boolean(useSettingsStore.getState().getSetting(SETTINGS.native_notifications_enabled.key))
|
|
27
67
|
)
|
|
28
68
|
}
|
|
29
69
|
|
|
70
|
+
/** Return true when the selected firmware presentation owns this event, including quiet/history mode. */
|
|
71
|
+
export async function presentNativePhoneNotification(event: PhoneNotificationEvent): Promise<boolean> {
|
|
72
|
+
if (!usesNativeNotificationPresentation()) return false
|
|
73
|
+
if (!desiredConfig().enabled) return true
|
|
74
|
+
if (Platform.OS !== "android") return true // G2 receives ANCS directly; never upload metadata back.
|
|
75
|
+
const blocked = useSettingsStore.getState().getSetting(SETTINGS.notifications_blocklist.key)
|
|
76
|
+
if (Array.isArray(blocked) && blocked.includes(event.packageName)) return true
|
|
77
|
+
if (!isGlassesReady(useGlassesStore.getState().connection)) return true
|
|
78
|
+
await BluetoothSdk.sendPhoneNotification({
|
|
79
|
+
notificationId: String(event.notificationId ?? ""),
|
|
80
|
+
packageName: String(event.packageName ?? ""),
|
|
81
|
+
appName: String(event.app ?? ""),
|
|
82
|
+
title: String(event.title ?? ""),
|
|
83
|
+
subtitle: "",
|
|
84
|
+
body: String(event.content ?? ""),
|
|
85
|
+
timestampMs: Number(event.timestamp ?? Date.now()),
|
|
86
|
+
action: 0,
|
|
87
|
+
})
|
|
88
|
+
return true
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function pushConfig(): void {
|
|
92
|
+
const settings = useSettingsStore.getState()
|
|
93
|
+
if (Platform.OS === "android") {
|
|
94
|
+
const enabled = Boolean(settings.getSetting(SETTINGS.android_notification_listener_enabled.key))
|
|
95
|
+
const blocklist = settings.getSetting(SETTINGS.notifications_blocklist.key)
|
|
96
|
+
void CrustModule.setNotificationConfig(enabled, Array.isArray(blocklist) ? blocklist : []).catch((error: unknown) =>
|
|
97
|
+
console.warn("PhoneNotificationsSync: listener configuration failed", error),
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
pushNativeConfig()
|
|
101
|
+
}
|
|
102
|
+
|
|
30
103
|
export function startPhoneNotificationsSync(): void {
|
|
31
|
-
if (
|
|
32
|
-
pushConfig()
|
|
33
|
-
|
|
34
|
-
(
|
|
35
|
-
|
|
36
|
-
|
|
104
|
+
if (unsubscribers.length) return
|
|
105
|
+
pushConfig()
|
|
106
|
+
unsubscribers = [
|
|
107
|
+
useSettingsStore.subscribe(
|
|
108
|
+
(state) =>
|
|
109
|
+
[
|
|
110
|
+
SETTINGS.android_notification_listener_enabled,
|
|
111
|
+
SETTINGS.notifications_blocklist,
|
|
112
|
+
SETTINGS.native_notifications_enabled,
|
|
113
|
+
SETTINGS.native_notifications_auto_display,
|
|
114
|
+
SETTINGS.native_notifications_duration,
|
|
115
|
+
SETTINGS.native_notifications_do_not_disturb,
|
|
116
|
+
].map((setting) => state.getSetting(setting.key)),
|
|
117
|
+
pushConfig,
|
|
118
|
+
{equalityFn: shallow},
|
|
119
|
+
),
|
|
120
|
+
useGlassesStore.subscribe((state) => ({model: state.deviceModel, connection: state.connection}), pushNativeConfig, {
|
|
121
|
+
equalityFn: shallow,
|
|
37
122
|
}),
|
|
38
|
-
|
|
39
|
-
{equalityFn: shallow},
|
|
40
|
-
)
|
|
123
|
+
]
|
|
41
124
|
}
|
|
42
125
|
|
|
43
126
|
export function stopPhoneNotificationsSync(): void {
|
|
44
|
-
|
|
45
|
-
unsubscribe
|
|
127
|
+
setPhoneNotificationPresentationActive(false)
|
|
128
|
+
unsubscribers.forEach((unsubscribe) => unsubscribe())
|
|
129
|
+
unsubscribers = []
|
|
46
130
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type {IslandConfigValues} from "../runtime/bootstrap"
|
|
2
|
+
|
|
3
|
+
export type CloudEndpoints = {core?: string; runtime: string}
|
|
4
|
+
|
|
5
|
+
/** Explicit reconnect pins win; clearing a pin resumes the live host resolver. */
|
|
6
|
+
export function resolveCloudEndpoints(config: IslandConfigValues, override: CloudEndpoints | null): CloudEndpoints {
|
|
7
|
+
if (override) return override
|
|
8
|
+
if (config.resolveCloudEndpoints) return config.resolveCloudEndpoints()
|
|
9
|
+
const runtime = config.runtimeUrl === null ? "" : config.runtimeUrl?.trim() || "http://localhost:3001"
|
|
10
|
+
if (!runtime) throw new Error("cloudClient: Runtime endpoint is not configured")
|
|
11
|
+
const core = config.coreUrl === null ? undefined : config.coreUrl?.trim() || "http://localhost:3000"
|
|
12
|
+
return {...(core ? {core} : {}), runtime}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function scopeCloudUrlOverrides(
|
|
16
|
+
current: {scope: unknown; core?: string; runtime?: string},
|
|
17
|
+
scope: string,
|
|
18
|
+
update: {core?: string; runtime?: string},
|
|
19
|
+
): {core: string; runtime: string} {
|
|
20
|
+
const sameScope = current.scope === scope || (!current.scope && scope === "consumer")
|
|
21
|
+
return {
|
|
22
|
+
core: update.core ?? (sameScope ? (current.core ?? "") : ""),
|
|
23
|
+
runtime: update.runtime ?? (sameScope ? (current.runtime ?? "") : ""),
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -48,6 +48,6 @@ export function resolveDeploymentAwareOtaManifestPolicy({
|
|
|
48
48
|
// A selected deployment is authoritative for every glasses generation. An
|
|
49
49
|
// explicit null is a deliberate OTA-off policy, not permission to inherit a
|
|
50
50
|
// device-reported URL or Mentra's legacy public default.
|
|
51
|
-
if (hostPolicyConfigured) return trimmed(policy.hostReleasePin)
|
|
51
|
+
if (hostPolicyConfigured) return trimmed(policy.developerOverride) || trimmed(policy.hostReleasePin)
|
|
52
52
|
return resolveOtaManifestPolicy(policy)
|
|
53
53
|
}
|
|
@@ -22,7 +22,8 @@ function getHostReleasePin(): string | null {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
function hasDeploymentOtaPolicy(): boolean {
|
|
25
|
-
|
|
25
|
+
const config = getConfigValues()
|
|
26
|
+
return config.otaManifestUrl !== undefined && !config.allowLegacyOtaFallback
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
function getEmbeddedEngineReleasePin(): string | null {
|
package/src/stores/settings.ts
CHANGED
|
@@ -166,6 +166,36 @@ export const SETTINGS: Record<string, Setting> = {
|
|
|
166
166
|
saveOnServer: false,
|
|
167
167
|
persist: true,
|
|
168
168
|
},
|
|
169
|
+
// Select firmware-owned presentation instead of the Mentra card. Notify still
|
|
170
|
+
// controls whether presentation is running on either phone platform.
|
|
171
|
+
native_notifications_enabled: {
|
|
172
|
+
key: "native_notifications_enabled",
|
|
173
|
+
defaultValue: () => false,
|
|
174
|
+
writable: true,
|
|
175
|
+
saveOnServer: false,
|
|
176
|
+
persist: true,
|
|
177
|
+
},
|
|
178
|
+
native_notifications_auto_display: {
|
|
179
|
+
key: "native_notifications_auto_display",
|
|
180
|
+
defaultValue: () => true,
|
|
181
|
+
writable: true,
|
|
182
|
+
saveOnServer: false,
|
|
183
|
+
persist: true,
|
|
184
|
+
},
|
|
185
|
+
native_notifications_duration: {
|
|
186
|
+
key: "native_notifications_duration",
|
|
187
|
+
defaultValue: () => 5,
|
|
188
|
+
writable: true,
|
|
189
|
+
saveOnServer: false,
|
|
190
|
+
persist: true,
|
|
191
|
+
},
|
|
192
|
+
native_notifications_do_not_disturb: {
|
|
193
|
+
key: "native_notifications_do_not_disturb",
|
|
194
|
+
defaultValue: () => false,
|
|
195
|
+
writable: true,
|
|
196
|
+
saveOnServer: false,
|
|
197
|
+
persist: true,
|
|
198
|
+
},
|
|
169
199
|
china_deployment: {
|
|
170
200
|
key: "china_deployment",
|
|
171
201
|
defaultValue: () => (process.env.EXPO_PUBLIC_DEPLOYMENT_REGION === "china" ? true : false),
|
|
@@ -196,6 +226,14 @@ export const SETTINGS: Record<string, Setting> = {
|
|
|
196
226
|
persist: true,
|
|
197
227
|
resetOnBuildEnvChange: true,
|
|
198
228
|
},
|
|
229
|
+
cloud_url_deployment: {
|
|
230
|
+
key: "cloud_url_deployment",
|
|
231
|
+
defaultValue: () => "",
|
|
232
|
+
writable: true,
|
|
233
|
+
saveOnServer: false,
|
|
234
|
+
persist: true,
|
|
235
|
+
resetOnBuildEnvChange: true,
|
|
236
|
+
},
|
|
199
237
|
// Bookmarked Cloud V2 endpoint pairs. Each entry is {label, coreUrl,
|
|
200
238
|
// runtimeUrl} — core + runtime are saved together because they are always
|
|
201
239
|
// applied as a matched set (presets fill both; Save & Test verifies both).
|
|
@@ -793,13 +831,10 @@ export interface SettingsState {
|
|
|
793
831
|
}
|
|
794
832
|
|
|
795
833
|
const getDefaultSettings = () =>
|
|
796
|
-
Object.keys(SETTINGS).reduce(
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
},
|
|
801
|
-
{} as Record<string, any>,
|
|
802
|
-
)
|
|
834
|
+
Object.keys(SETTINGS).reduce((acc, key) => {
|
|
835
|
+
acc[key] = SETTINGS[key].defaultValue()
|
|
836
|
+
return acc
|
|
837
|
+
}, {} as Record<string, any>)
|
|
803
838
|
|
|
804
839
|
// Single-flight for loadAllSettings: the host fires it at module load and
|
|
805
840
|
// engine.start()'s device-store hydration awaits it — without the memo the
|
|
@@ -901,12 +936,13 @@ export const useSettingsStore = create<SettingsState>()(
|
|
|
901
936
|
}
|
|
902
937
|
// console.log("SETTINGS: SET MANY LOCALLY: ", settingsToLoad)
|
|
903
938
|
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
939
|
+
// MMKV writes are synchronous Results, not rejecting Promises. Check
|
|
940
|
+
// each result before publishing the new in-memory settings.
|
|
941
|
+
for (const [key, value] of Object.entries(settingsToLoad)) {
|
|
942
|
+
const saved = storage.save(key, value)
|
|
943
|
+
if (saved.is_error()) throw saved.error
|
|
944
|
+
}
|
|
945
|
+
set((state) => ({settings: {...state.settings, ...settingsToLoad}}))
|
|
910
946
|
})
|
|
911
947
|
},
|
|
912
948
|
// loads any preferences that have been changed from the default and saved to DISK!
|
|
@@ -88,4 +88,5 @@ export const evenRealitiesG2: Capabilities = {
|
|
|
88
88
|
// Dashboard - G2 renders Even Realities' native dashboard in firmware, so
|
|
89
89
|
// MentraOS does not manage the dashboard or expose dashboard settings for it
|
|
90
90
|
hasNativeDashboard: true,
|
|
91
|
+
hasNativeNotifications: true,
|
|
91
92
|
};
|
package/src/types/hardware.ts
CHANGED
|
@@ -122,6 +122,8 @@ export interface PowerCapabilities {
|
|
|
122
122
|
* Complete information about what hardware a device has
|
|
123
123
|
*/
|
|
124
124
|
export interface Capabilities {
|
|
125
|
+
/** Firmware-owned notification history and popups; content relay is separate. */
|
|
126
|
+
hasNativeNotifications?: boolean;
|
|
125
127
|
modelName: string;
|
|
126
128
|
|
|
127
129
|
// Camera capabilities
|