@mentra/engine 3.2.0-dev.180 → 3.2.0-dev.187

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.
Files changed (39) hide show
  1. package/README.md +13 -0
  2. package/build/engine.d.ts +8 -0
  3. package/build/engine.d.ts.map +1 -1
  4. package/build/facades/pairing.d.ts +3 -1
  5. package/build/facades/pairing.d.ts.map +1 -1
  6. package/build/facades/pairing.js +9 -0
  7. package/build/facades/pairing.js.map +1 -1
  8. package/build/facades/phoneNotifications.d.ts +9 -0
  9. package/build/facades/phoneNotifications.d.ts.map +1 -1
  10. package/build/facades/phoneNotifications.js +15 -0
  11. package/build/facades/phoneNotifications.js.map +1 -1
  12. package/build/generated/releaseMetadata.js +5 -5
  13. package/build/generated/releaseMetadata.js.map +1 -1
  14. package/build/services/NativeNotificationPolicy.d.ts +13 -0
  15. package/build/services/NativeNotificationPolicy.d.ts.map +1 -0
  16. package/build/services/NativeNotificationPolicy.js +12 -0
  17. package/build/services/NativeNotificationPolicy.js.map +1 -0
  18. package/build/services/PhoneNotificationsSync.d.ts +13 -0
  19. package/build/services/PhoneNotificationsSync.d.ts.map +1 -1
  20. package/build/services/PhoneNotificationsSync.js +102 -26
  21. package/build/services/PhoneNotificationsSync.js.map +1 -1
  22. package/build/stores/settings.d.ts.map +1 -1
  23. package/build/stores/settings.js +30 -0
  24. package/build/stores/settings.js.map +1 -1
  25. package/build/types/capabilities/even-realities-g2.d.ts.map +1 -1
  26. package/build/types/capabilities/even-realities-g2.js +1 -0
  27. package/build/types/capabilities/even-realities-g2.js.map +1 -1
  28. package/build/types/hardware.d.ts +2 -0
  29. package/build/types/hardware.d.ts.map +1 -1
  30. package/build/types/hardware.js.map +1 -1
  31. package/package.json +7 -7
  32. package/src/facades/pairing.ts +16 -1
  33. package/src/facades/phoneNotifications.ts +21 -0
  34. package/src/generated/releaseMetadata.ts +5 -5
  35. package/src/services/NativeNotificationPolicy.ts +24 -0
  36. package/src/services/PhoneNotificationsSync.ts +115 -31
  37. package/src/stores/settings.ts +34 -7
  38. package/src/types/capabilities/even-realities-g2.ts +1 -0
  39. package/src/types/hardware.ts +2 -0
@@ -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.180",
16
- "releaseSetId": "mentra-3.2.0-dev.180",
17
- "sourceCommit": "1f24e462561fef37b7082204847a541db4a87c41",
18
- "otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.180.json",
19
- "otaManifestSha256": "cbf2a1b81430bd34942e2a1764138f6d79197767b4434a2d8c44311768f8b873"
15
+ "releaseIdentity": "3.2.0-dev.187",
16
+ "releaseSetId": "mentra-3.2.0-dev.187",
17
+ "sourceCommit": "d8b34892f11593baa3c075b94380253b06eb874d",
18
+ "otaManifestUrl": "https://github.com/Mentra-Community/MentraOS/releases/download/mentra-builds-v3.2.0/mentra-live-ota-3.2.0-dev.187.json",
19
+ "otaManifestSha256": "cf21f4ab6347acb12eefa38d2f0a3b433bc0e05428c527bcda57a8693d7a72f9"
20
20
  })
@@ -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
- * Phone-notifications sync — engine-owned. Pushes the notification-forwarding
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 unsubscribe: (() => void) | null = null
14
+ let unsubscribers: Array<() => void> = []
15
+ let presentationActive = false
20
16
 
21
- function pushConfig(): void {
22
- const s = useSettingsStore.getState()
23
- const listenerEnabled = Boolean(s.getSetting(SETTINGS.android_notification_listener_enabled.key))
24
- const blocklist = s.getSetting(SETTINGS.notifications_blocklist.key)
25
- CrustModule.setNotificationConfig(listenerEnabled, Array.isArray(blocklist) ? blocklist : []).catch((err: unknown) =>
26
- console.warn(`PhoneNotificationsSync: setNotificationConfig failed: ${(err as Error)?.message ?? err}`),
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 (unsubscribe) return
32
- pushConfig() // initial sync so the listener has the current config
33
- unsubscribe = useSettingsStore.subscribe(
34
- (state) => ({
35
- listenerEnabled: state.getSetting(SETTINGS.android_notification_listener_enabled.key),
36
- blocklist: state.getSetting(SETTINGS.notifications_blocklist.key),
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
- () => pushConfig(),
39
- {equalityFn: shallow},
40
- )
123
+ ]
41
124
  }
42
125
 
43
126
  export function stopPhoneNotificationsSync(): void {
44
- unsubscribe?.()
45
- unsubscribe = null
127
+ setPhoneNotificationPresentationActive(false)
128
+ unsubscribers.forEach((unsubscribe) => unsubscribe())
129
+ unsubscribers = []
46
130
  }
@@ -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),
@@ -793,13 +823,10 @@ export interface SettingsState {
793
823
  }
794
824
 
795
825
  const getDefaultSettings = () =>
796
- Object.keys(SETTINGS).reduce(
797
- (acc, key) => {
798
- acc[key] = SETTINGS[key].defaultValue()
799
- return acc
800
- },
801
- {} as Record<string, any>,
802
- )
826
+ Object.keys(SETTINGS).reduce((acc, key) => {
827
+ acc[key] = SETTINGS[key].defaultValue()
828
+ return acc
829
+ }, {} as Record<string, any>)
803
830
 
804
831
  // Single-flight for loadAllSettings: the host fires it at module load and
805
832
  // engine.start()'s device-store hydration awaits it — without the memo the
@@ -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
  };
@@ -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