@mentra/crust 0.1.0-dev.0 → 3.1.0-dev.1
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 +28 -22
- package/android/src/main/AndroidManifest.xml +14 -0
- package/android/src/main/java/com/mentra/crust/CrustModule.kt +237 -33
- package/android/src/main/java/com/mentra/crust/jsc/JSCPolyfillBridge.kt +42 -1
- package/android/src/main/java/com/mentra/crust/navigation/NavigationManager.kt +2 -0
- package/android/src/main/java/com/mentra/crust/services/NotificationListener.kt +273 -90
- package/android/src/main/java/com/mentra/crust/services/NotificationProcessBridge.kt +222 -0
- package/build/Crust.types.d.ts +6 -1
- package/build/Crust.types.d.ts.map +1 -1
- package/build/Crust.types.js.map +1 -1
- package/build/CrustModule.d.ts +9 -1
- package/build/CrustModule.d.ts.map +1 -1
- package/build/CrustModule.js.map +1 -1
- package/build/CrustModule.web.d.ts +10 -1
- package/build/CrustModule.web.d.ts.map +1 -1
- package/build/CrustModule.web.js +13 -1
- package/build/CrustModule.web.js.map +1 -1
- package/ios/CrustModule.swift +288 -47
- package/package.json +7 -3
- package/plugin/build/index.d.ts +2 -0
- package/plugin/build/index.js +2 -0
- package/plugin/build/withAndroid.js +56 -1
- package/src/Crust.types.ts +6 -1
- package/src/CrustModule.ts +10 -4
- package/src/CrustModule.web.ts +13 -1
package/build/Crust.types.d.ts
CHANGED
|
@@ -119,7 +119,12 @@ export type PhoneNotificationEvent = {
|
|
|
119
119
|
app: string;
|
|
120
120
|
title: string;
|
|
121
121
|
content: string;
|
|
122
|
-
|
|
122
|
+
/**
|
|
123
|
+
* Android notification priority: PRIORITY_MIN (-2) through PRIORITY_MAX (2).
|
|
124
|
+
* Derived from the channel importance when the ranking is available, since
|
|
125
|
+
* Notification.priority reads 0 for channel-based notifications.
|
|
126
|
+
*/
|
|
127
|
+
priority: number;
|
|
123
128
|
timestamp: number;
|
|
124
129
|
packageName: string;
|
|
125
130
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Crust.types.d.ts","sourceRoot":"","sources":["../src/Crust.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,SAAS,EAAE,SAAS,EAAC,MAAM,cAAc,CAAA;AAEtD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,GAAG,EAAE,MAAM,CAAA;CACZ,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAA;IAC9C,aAAa,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAA;IACnD,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,KAAK,IAAI,CAAA;IACvD,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,KAAK,IAAI,CAAA;IACrD,UAAU,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,IAAI,CAAA;IAC7C,aAAa,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAA;IACnD,UAAU,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,IAAI,CAAA;IAC7C,aAAa,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,CAAA;IACnD,SAAS,EAAE,CAAC,MAAM,EAAE,cAAc,KAAK,IAAI,CAAA;CAC5C,CAAA;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B,mEAAmE;IACnE,sBAAsB,EAAE,MAAM,CAAA;CAC/B,CAAA;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,wDAAwD;IACxD,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,KAAK,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAC,CAAC,CAAA;IACzC;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,mBAAmB,EAAE,CAAA;CAC9B,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,uEAAuE;IACvE,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAA;IAClB,qFAAqF;IACrF,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB,qCAAqC;IACrC,cAAc,EAAE,MAAM,CAAA;CACvB,CAAA;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,kEAAkE;IAClE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,oCAAoC;IACpC,SAAS,EAAE,MAAM,CAAA;IACjB,kBAAkB,EAAE,CAAC,KAAK,EAAE,sBAAsB,KAAK,IAAI,CAAA;IAC3D,4BAA4B,EAAE,CAAC,KAAK,EAAE,+BAA+B,KAAK,IAAI,CAAA;IAC9E,wBAAwB,EAAE,CAAC,KAAK,EAAE,2BAA2B,KAAK,IAAI,CAAA;CACvE,CAAA;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B;;;;OAIG;IACH,YAAY,EAAE,MAAM,CAAA;IACpB,2FAA2F;IAC3F,cAAc,EAAE,MAAM,CAAA;IACtB,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,4EAA4E;IAC5E,2BAA2B,CAAC,EAAE,MAAM,CAAA;IACpC,2DAA2D;IAC3D,wBAAwB,CAAC,EAAE,MAAM,CAAA;IACjC,iDAAiD;IACjD,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,uFAAuF;IACvF,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,sFAAsF;IACtF,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAChC,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,OAAO,CAAA;IAClB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,sBAAsB,GAAG;IACnC,cAAc,EAAE,MAAM,CAAA;IACtB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,+BAA+B,GAAG;IAC5C,eAAe,EAAE,MAAM,CAAA;IACvB,WAAW,EAAE,MAAM,CAAA;IACnB,cAAc,EAAE,MAAM,CAAA;CACvB,CAAA;AAED,MAAM,MAAM,2BAA2B,GAAG;IACxC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB,CAAA;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,CAAC,KAAK,EAAE;QAAC,WAAW,EAAE,kBAAkB,CAAA;KAAC,KAAK,IAAI,CAAA;IAC1D,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAA;CAC7B,CAAA"}
|
|
1
|
+
{"version":3,"file":"Crust.types.d.ts","sourceRoot":"","sources":["../src/Crust.types.ts"],"names":[],"mappings":"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"}
|
package/build/Crust.types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Crust.types.js","sourceRoot":"","sources":["../src/Crust.types.ts"],"names":[],"mappings":"","sourcesContent":["import type {StyleProp, ViewStyle} from \"react-native\"\n\nexport type OnLoadEventPayload = {\n url: string\n}\n\nexport type CrustModuleEvents = {\n onChange: (params: ChangeEventPayload) => void\n onNavManeuver: (params: NavManeuverPayload) => void\n onNavRerouting: (params: Record<string, never>) => void\n onNavArrived: (params: Record<string, never>) => void\n onNavError: (params: NavErrorPayload) => void\n onNavLocation: (params: NavLocationPayload) => void\n onNavRoute: (params: NavRoutePayload) => void\n onNavOffRoute: (params: NavOffRoutePayload) => void\n onHeading: (params: HeadingPayload) => void\n}\n\nexport type NavOffRoutePayload = {\n /** Approximate perpendicular distance in meters from the route. */\n offRouteDistanceMeters: number\n}\n\nexport type HeadingPayload = {\n /** Compass heading in degrees, 0 = north, 90 = east. */\n degrees: number\n}\n\nexport type NavRoutePayload = {\n points: Array<{lat: number; lng: number}>\n /**\n * Ordered step list along the route, when supplied by the host. Each\n * step is one navigable segment — typically one straight stretch of\n * road ending in a maneuver. Optional for back-compat with older\n * hosts that only ship the polyline. The SDK's pivot module consumes\n * this to enrich pivots with `fromRoad` / `toRoad` metadata.\n */\n steps?: NavRouteStepPayload[]\n}\n\nexport type NavRouteStepPayload = {\n /** Coordinate where this step begins (== end of the previous step). */\n lat: number\n lng: number\n /**\n * Index into `NavRoutePayload.points[]` where this step starts.\n * Lets consumers correlate step boundaries with polyline geometry.\n */\n routeIndex: number\n /** Name of the road traversed during this step. Null when the engine has no name. */\n road?: string | null\n /**\n * Categorical maneuver performed at the END of this step (i.e. how\n * the user exits this step). Same vocabulary as `NavManeuverPayload.maneuverType`.\n */\n maneuver: string\n /** Length of this step in meters. */\n distanceMeters: number\n}\n\nexport type NavLocationPayload = {\n lat: number\n lng: number\n /** Horizontal accuracy in meters, if reported by the platform. */\n accuracy: number | null\n /** Unix ms timestamp of the fix. */\n timestamp: number\n phone_notification: (event: PhoneNotificationEvent) => void\n phone_notification_dismissed: (event: PhoneNotificationDismissedEvent) => void\n captions_tester_incident: (event: CaptionsTesterIncidentEvent) => void\n}\n\nexport type ChangeEventPayload = {\n value: string\n}\n\nexport type NavManeuverPayload = {\n /**\n * Categorical type of the upcoming maneuver. One of: STRAIGHT,\n * SLIGHT_LEFT, SLIGHT_RIGHT, TURN_LEFT, TURN_RIGHT, SHARP_LEFT,\n * SHARP_RIGHT, U_TURN, ARRIVE.\n */\n maneuverType: string\n /** Distance in meters from the user's current position to that maneuver. -1 if unknown. */\n distanceMeters: number\n /** Road the user is currently on, per the Nav SDK. Null if unavailable. */\n fromRoad?: string | null\n /**\n * Legacy \"next road\" field — historically populated from the same\n * source as `fromRoad` (the current step's road, NOT the road\n * after the upcoming turn). Kept for back-compat. New consumers\n * should read `nextStepRoad` instead.\n */\n toRoad?: string | null\n /**\n * Road the user will be on AFTER the upcoming maneuver, sourced\n * from the Nav SDK's `remainingSteps[0]`. This is the value to\n * show as the \"next street\" headline on the maneuver card. Null\n * when the SDK hasn't surfaced a remaining step yet (final leg,\n * pre-first-NavInfo, etc.).\n */\n nextStepRoad?: string | null\n /** Total remaining distance to final destination, meters. -1 if unknown. */\n distanceToDestinationMeters?: number\n /** Total remaining travel time, seconds. -1 if unknown. */\n timeToDestinationSeconds?: number\n /** Current speed in m/s. Null if unavailable. */\n currentSpeedMps?: number | null\n /** Speed limit on the current road segment in m/s. Null if unknown / not regulated. */\n speedLimitMps?: number | null\n /** Bearing along the route at the user's current position, 0–360. Null if unknown. */\n routeHeadingDeg?: number | null\n}\n\nexport type NavErrorPayload = {\n message: string\n}\n\nexport type InstalledApp = {\n packageName: string\n appName: string\n isBlocked: boolean\n icon: string | null\n}\n\nexport type PhoneNotificationEvent = {\n notificationId: string\n app: string\n title: string\n content: string\n priority:
|
|
1
|
+
{"version":3,"file":"Crust.types.js","sourceRoot":"","sources":["../src/Crust.types.ts"],"names":[],"mappings":"","sourcesContent":["import type {StyleProp, ViewStyle} from \"react-native\"\n\nexport type OnLoadEventPayload = {\n url: string\n}\n\nexport type CrustModuleEvents = {\n onChange: (params: ChangeEventPayload) => void\n onNavManeuver: (params: NavManeuverPayload) => void\n onNavRerouting: (params: Record<string, never>) => void\n onNavArrived: (params: Record<string, never>) => void\n onNavError: (params: NavErrorPayload) => void\n onNavLocation: (params: NavLocationPayload) => void\n onNavRoute: (params: NavRoutePayload) => void\n onNavOffRoute: (params: NavOffRoutePayload) => void\n onHeading: (params: HeadingPayload) => void\n}\n\nexport type NavOffRoutePayload = {\n /** Approximate perpendicular distance in meters from the route. */\n offRouteDistanceMeters: number\n}\n\nexport type HeadingPayload = {\n /** Compass heading in degrees, 0 = north, 90 = east. */\n degrees: number\n}\n\nexport type NavRoutePayload = {\n points: Array<{lat: number; lng: number}>\n /**\n * Ordered step list along the route, when supplied by the host. Each\n * step is one navigable segment — typically one straight stretch of\n * road ending in a maneuver. Optional for back-compat with older\n * hosts that only ship the polyline. The SDK's pivot module consumes\n * this to enrich pivots with `fromRoad` / `toRoad` metadata.\n */\n steps?: NavRouteStepPayload[]\n}\n\nexport type NavRouteStepPayload = {\n /** Coordinate where this step begins (== end of the previous step). */\n lat: number\n lng: number\n /**\n * Index into `NavRoutePayload.points[]` where this step starts.\n * Lets consumers correlate step boundaries with polyline geometry.\n */\n routeIndex: number\n /** Name of the road traversed during this step. Null when the engine has no name. */\n road?: string | null\n /**\n * Categorical maneuver performed at the END of this step (i.e. how\n * the user exits this step). Same vocabulary as `NavManeuverPayload.maneuverType`.\n */\n maneuver: string\n /** Length of this step in meters. */\n distanceMeters: number\n}\n\nexport type NavLocationPayload = {\n lat: number\n lng: number\n /** Horizontal accuracy in meters, if reported by the platform. */\n accuracy: number | null\n /** Unix ms timestamp of the fix. */\n timestamp: number\n phone_notification: (event: PhoneNotificationEvent) => void\n phone_notification_dismissed: (event: PhoneNotificationDismissedEvent) => void\n captions_tester_incident: (event: CaptionsTesterIncidentEvent) => void\n}\n\nexport type ChangeEventPayload = {\n value: string\n}\n\nexport type NavManeuverPayload = {\n /**\n * Categorical type of the upcoming maneuver. One of: STRAIGHT,\n * SLIGHT_LEFT, SLIGHT_RIGHT, TURN_LEFT, TURN_RIGHT, SHARP_LEFT,\n * SHARP_RIGHT, U_TURN, ARRIVE.\n */\n maneuverType: string\n /** Distance in meters from the user's current position to that maneuver. -1 if unknown. */\n distanceMeters: number\n /** Road the user is currently on, per the Nav SDK. Null if unavailable. */\n fromRoad?: string | null\n /**\n * Legacy \"next road\" field — historically populated from the same\n * source as `fromRoad` (the current step's road, NOT the road\n * after the upcoming turn). Kept for back-compat. New consumers\n * should read `nextStepRoad` instead.\n */\n toRoad?: string | null\n /**\n * Road the user will be on AFTER the upcoming maneuver, sourced\n * from the Nav SDK's `remainingSteps[0]`. This is the value to\n * show as the \"next street\" headline on the maneuver card. Null\n * when the SDK hasn't surfaced a remaining step yet (final leg,\n * pre-first-NavInfo, etc.).\n */\n nextStepRoad?: string | null\n /** Total remaining distance to final destination, meters. -1 if unknown. */\n distanceToDestinationMeters?: number\n /** Total remaining travel time, seconds. -1 if unknown. */\n timeToDestinationSeconds?: number\n /** Current speed in m/s. Null if unavailable. */\n currentSpeedMps?: number | null\n /** Speed limit on the current road segment in m/s. Null if unknown / not regulated. */\n speedLimitMps?: number | null\n /** Bearing along the route at the user's current position, 0–360. Null if unknown. */\n routeHeadingDeg?: number | null\n}\n\nexport type NavErrorPayload = {\n message: string\n}\n\nexport type InstalledApp = {\n packageName: string\n appName: string\n isBlocked: boolean\n icon: string | null\n}\n\nexport type PhoneNotificationEvent = {\n notificationId: string\n app: string\n title: string\n content: string\n /**\n * Android notification priority: PRIORITY_MIN (-2) through PRIORITY_MAX (2).\n * Derived from the channel importance when the ranking is available, since\n * Notification.priority reads 0 for channel-based notifications.\n */\n priority: number\n timestamp: number\n packageName: string\n}\n\nexport type PhoneNotificationDismissedEvent = {\n notificationKey: string\n packageName: string\n notificationId: string\n}\n\nexport type CaptionsTesterIncidentEvent = {\n action?: string\n timestamp?: number\n failure_code?: string\n failure_message?: string\n test_run_id?: string\n scenario_name?: string\n source?: string\n [key: string]: unknown\n}\n\nexport type CrustViewProps = {\n url: string\n onLoad: (event: {nativeEvent: OnLoadEventPayload}) => void\n style?: StyleProp<ViewStyle>\n}\n"]}
|
package/build/CrustModule.d.ts
CHANGED
|
@@ -4,6 +4,12 @@ declare class CrustModule extends NativeModule<CrustModuleEvents> {
|
|
|
4
4
|
PI: number;
|
|
5
5
|
hello(): string;
|
|
6
6
|
setValueAsync(value: string): Promise<void>;
|
|
7
|
+
nativeHttpRequest(method: string, url: string, headers: Record<string, string>, body?: string | null): Promise<{
|
|
8
|
+
status: number;
|
|
9
|
+
statusText: string;
|
|
10
|
+
headers: Record<string, string>;
|
|
11
|
+
body: string;
|
|
12
|
+
}>;
|
|
7
13
|
showAVRoutePicker(tintColor?: string | null): void;
|
|
8
14
|
/**
|
|
9
15
|
* iOS: configure `preferredScreenEdgesDeferringSystemGestures`. When an
|
|
@@ -15,10 +21,11 @@ declare class CrustModule extends NativeModule<CrustModuleEvents> {
|
|
|
15
21
|
* per-app equivalent; system gestures are configured at the OS level).
|
|
16
22
|
*/
|
|
17
23
|
setDeferredSystemGestures(edges: Array<"top" | "bottom" | "left" | "right" | "all">): Promise<void>;
|
|
18
|
-
setNotificationConfig(
|
|
24
|
+
setNotificationConfig(listenerEnabled: boolean, blocklist: string[]): Promise<void>;
|
|
19
25
|
getInstalledApps(): Promise<InstalledApp[]>;
|
|
20
26
|
getInstalledAppsForNotifications(): Promise<InstalledApp[]>;
|
|
21
27
|
hasNotificationListenerPermission(): Promise<boolean>;
|
|
28
|
+
refreshNotificationListener(): Promise<boolean>;
|
|
22
29
|
openNotificationListenerSettings(): Promise<boolean>;
|
|
23
30
|
isBetaBuild(): Promise<boolean>;
|
|
24
31
|
showLocationServicesDialog(): Promise<boolean>;
|
|
@@ -51,6 +58,7 @@ declare class CrustModule extends NativeModule<CrustModuleEvents> {
|
|
|
51
58
|
success: boolean;
|
|
52
59
|
uri?: string;
|
|
53
60
|
identifier?: string;
|
|
61
|
+
existing?: boolean;
|
|
54
62
|
error?: string;
|
|
55
63
|
}>;
|
|
56
64
|
startNavigation(lat: number, lng: number, options?: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CrustModule.d.ts","sourceRoot":"","sources":["../src/CrustModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAsB,MAAM,MAAM,CAAA;AAEtD,OAAO,EAAC,iBAAiB,EAAE,YAAY,EAAC,MAAM,eAAe,CAAA;AAE7D,OAAO,OAAO,WAAY,SAAQ,YAAY,CAAC,iBAAiB,CAAC;IAC/D,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,IAAI,MAAM;IACf,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAC3C,iBAAiB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAElD;;;;;;;;OAQG;IACH,yBAAyB,
|
|
1
|
+
{"version":3,"file":"CrustModule.d.ts","sourceRoot":"","sources":["../src/CrustModule.ts"],"names":[],"mappings":"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"}
|
package/build/CrustModule.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CrustModule.js","sourceRoot":"","sources":["../src/CrustModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,mBAAmB,EAAC,MAAM,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"CrustModule.js","sourceRoot":"","sources":["../src/CrustModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,mBAAmB,EAAC,MAAM,MAAM,CAAA;AA8LtD,yDAAyD;AACzD,eAAe,mBAAmB,CAAc,OAAO,CAAC,CAAA","sourcesContent":["import {NativeModule, requireNativeModule} from \"expo\"\n\nimport {CrustModuleEvents, InstalledApp} from \"./Crust.types\"\n\ndeclare class CrustModule extends NativeModule<CrustModuleEvents> {\n PI: number\n hello(): string\n setValueAsync(value: string): Promise<void>\n nativeHttpRequest(\n method: string,\n url: string,\n headers: Record<string, string>,\n body?: string | null,\n ): Promise<{status: number; statusText: string; headers: Record<string, string>; body: string}>\n showAVRoutePicker(tintColor?: string | null): void\n\n /**\n * iOS: configure `preferredScreenEdgesDeferringSystemGestures`. When an\n * edge is deferred, the first swipe across that edge is consumed by the\n * app and the system gesture (Control Center, Notification Center, Home\n * indicator) only fires on a second swipe — i.e. a two-swipe-to-exit UX.\n *\n * Pass `[]` to restore default behavior. Android: no-op (Android has no\n * per-app equivalent; system gestures are configured at the OS level).\n */\n setDeferredSystemGestures(edges: Array<\"top\" | \"bottom\" | \"left\" | \"right\" | \"all\">): Promise<void>\n\n // MentraOS Notification Commands\n setNotificationConfig(listenerEnabled: boolean, blocklist: string[]): Promise<void>\n getInstalledApps(): Promise<InstalledApp[]>\n getInstalledAppsForNotifications(): Promise<InstalledApp[]>\n hasNotificationListenerPermission(): Promise<boolean>\n refreshNotificationListener(): Promise<boolean>\n openNotificationListenerSettings(): Promise<boolean>\n isBetaBuild(): Promise<boolean>\n // location services commands\n showLocationServicesDialog(): Promise<boolean>\n isLocationServicesEnabled(): Promise<boolean>\n openLocationSettings(): Promise<boolean>\n openAppSettings(): Promise<boolean>\n openBluetoothSettings(): Promise<boolean>\n\n // Image Processing Commands\n processGalleryImage(\n inputPath: string,\n outputPath: string,\n options: {\n lensCorrection?: boolean\n colorCorrection?: boolean\n },\n ): Promise<{\n success: boolean\n outputPath?: string\n processingTimeMs?: number\n error?: string\n }>\n\n mergeHdrBrackets(\n underPath: string,\n normalPath: string,\n overPath: string,\n outputPath: string,\n ): Promise<{\n success: boolean\n outputPath?: string\n processingTimeMs?: number\n error?: string\n }>\n\n stabilizeVideo(\n inputPath: string,\n imuPath: string,\n outputPath: string,\n ): Promise<{\n success: boolean\n outputPath?: string\n processingTimeMs?: number\n error?: string\n }>\n\n // Media Library Commands\n saveToGalleryWithDate(\n filePath: string,\n captureTimeMillis?: number,\n displayName?: string,\n ): Promise<{\n success: boolean\n uri?: string\n identifier?: string\n existing?: boolean\n error?: string\n }>\n\n // Navigation (Android only — iOS stubs return error)\n startNavigation(\n lat: number,\n lng: number,\n options?: {\n simulate?: boolean\n speedMultiplier?: number\n /** Optional multi-stop list. When present takes precedence over lat/lng. Last entry is the final destination. */\n stops?: Array<{lat: number; lng: number}>\n /** \"walking\" | \"driving\" | \"cycling\" | \"two_wheeler\". Defaults driving. */\n mode?: string\n avoid?: {highways?: boolean; tolls?: boolean; ferries?: boolean}\n /** Force a reroute when the user is more than N meters past a pivot they didn't take. */\n missedTurnRerouteMeters?: number\n },\n ): Promise<{ok: boolean; error?: string}>\n stopNavigation(): Promise<{ok: boolean; error?: string}>\n\n /**\n * Show the Google Nav SDK Terms & Conditions dialog if not already\n * accepted. Idempotent — resolves immediately with `{accepted: true}`\n * when the user has already accepted (cached in-process / on-disk /\n * inside the SDK).\n */\n requestNavigationPermission(): Promise<{ok: boolean; accepted: boolean; error?: string}>\n\n /**\n * Dev-only: clear cached T&C acceptance (SDK flag + on-disk pref +\n * in-process flag) so the next requestNavigationPermission() re-shows\n * the dialog. Android only; iOS returns {ok: false, error: \"not\n * supported on iOS\"}.\n */\n resetNavigationPermission(): Promise<{ok: boolean; error?: string}>\n\n /**\n * Dev-only: nudge the simulated user position ~offsetMeters perpendicular\n * to the route so the Nav SDK reroutes. Default 20m. Android only.\n */\n simulateDeviation(offsetMeters?: number): Promise<{ok: boolean; error?: string}>\n\n /**\n * Dev toggle. When enabled, the native NavigationManager shifts every\n * reported location ~8m perpendicular to the route bearing, simulating\n * a pedestrian walking on the wrong sidewalk. Only meaningful in\n * simulate mode; lets us verify the SDK's along-path pivot trigger\n * fires even when the user never crosses the 7m pivot point radius.\n * Android-only today; iOS is a no-op stub.\n */\n setWrongSidewalkOffset(enabled: boolean): Promise<{ok: boolean; error?: string}>\n\n /**\n * Dev toggle. When enabled, the native NavigationManager takes over\n * from the Google simulator and walks the user along a modified\n * polyline that omits crossing micro-steps — reproducing the\n * wrong-sidewalk-then-missed-the-turn scenario. Android-only today;\n * iOS is a no-op stub.\n */\n setSkipCrossings(enabled: boolean): Promise<{ok: boolean; error?: string}>\n\n // Heading / compass (Android only)\n startHeading(): Promise<{ok: boolean; error?: string}>\n stopHeading(): Promise<{ok: boolean; error?: string}>\n\n // MentraJS Runtime — per-miniapp JSContext lifecycle.\n /**\n * Spawn a per-miniapp JS context. Re-spawn is allowed: a live context\n * with the same packageName is killed first. Returns true if the\n * polyfill bundle + miniapp source evaluated without throwing.\n *\n * The polyfill bundle is `mobile/modules/mentrajs-runtime/dist/startup.js`\n * (shipped inside the host binary). It installs window-style globals\n * (console, timers, fetch, localStorage, crypto) atop the JSC runtime.\n */\n mentraJsSpawn(packageName: string, polyfillBundle: string, miniappJs: string): Promise<boolean>\n mentraJsEvaluate(packageName: string, source: string): Promise<unknown>\n mentraJsKill(packageName: string): Promise<void>\n /**\n * Push a `{kind: \"event\"|\"response\", …}` envelope into the named\n * context's globalThis.__deliver. Returns when the underlying\n * evaluateScript completes (the JS handler runs synchronously on the\n * context's queue).\n */\n mentraJsDispatchToJs(packageName: string, envelope: Record<string, unknown>): Promise<void>\n mentraJsSetManifest(packageName: string, permissions: string[]): Promise<void>\n /** Diagnostic — returns the packageNames of every live JSContext. */\n mentraJsAlivePackages(): string[]\n /** Diagnostic — force a GC cycle on the named context. */\n mentraJsDebugForceGC(packageName: string): Promise<boolean>\n /**\n * Read the bundled MentraJS polyfill (startup.js) shipped inside the\n * host binary. Synchronous — host RN code calls this once on app boot\n * and caches the string, then passes it to every mentraJsSpawn so\n * every JSContext starts with the same polyfill ABI.\n */\n mentraJsLoadPolyfillBundle(): string\n}\n\n// This call loads the native module object from the JSI.\nexport default requireNativeModule<CrustModule>(\"Crust\")\n"]}
|
|
@@ -3,13 +3,22 @@ import { CrustModuleEvents } from "./Crust.types";
|
|
|
3
3
|
declare class CrustModule extends NativeModule<CrustModuleEvents> {
|
|
4
4
|
PI: number;
|
|
5
5
|
setValueAsync(value: string): Promise<void>;
|
|
6
|
+
nativeHttpRequest(method: string, url: string, headers: Record<string, string>, body?: string | null): Promise<{
|
|
7
|
+
status: number;
|
|
8
|
+
statusText: string;
|
|
9
|
+
headers: {
|
|
10
|
+
[k: string]: string;
|
|
11
|
+
};
|
|
12
|
+
body: string;
|
|
13
|
+
}>;
|
|
6
14
|
hello(): string;
|
|
7
15
|
showAVRoutePicker(_tintColor?: string | null): void;
|
|
8
16
|
setDeferredSystemGestures(_edges: string[]): Promise<void>;
|
|
9
|
-
setNotificationConfig(
|
|
17
|
+
setNotificationConfig(_listenerEnabled: boolean, _blocklist: string[]): Promise<void>;
|
|
10
18
|
getInstalledApps(): Promise<never[]>;
|
|
11
19
|
getInstalledAppsForNotifications(): Promise<never[]>;
|
|
12
20
|
hasNotificationListenerPermission(): Promise<boolean>;
|
|
21
|
+
refreshNotificationListener(): Promise<boolean>;
|
|
13
22
|
openNotificationListenerSettings(): Promise<boolean>;
|
|
14
23
|
isBetaBuild(): Promise<boolean>;
|
|
15
24
|
mentraJsSpawn(_pkg: string, _polyfill: string, _miniappJs: string): Promise<boolean>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CrustModule.web.d.ts","sourceRoot":"","sources":["../src/CrustModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,YAAY,EAAC,MAAM,MAAM,CAAA;AAEpD,OAAO,EAAC,iBAAiB,EAAC,MAAM,eAAe,CAAA;AAE/C,cAAM,WAAY,SAAQ,YAAY,CAAC,iBAAiB,CAAC;IACvD,EAAE,SAAU;IACN,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"CrustModule.web.d.ts","sourceRoot":"","sources":["../src/CrustModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,YAAY,EAAC,MAAM,MAAM,CAAA;AAEpD,OAAO,EAAC,iBAAiB,EAAC,MAAM,eAAe,CAAA;AAE/C,cAAM,WAAY,SAAQ,YAAY,CAAC,iBAAiB,CAAC;IACvD,EAAE,SAAU;IACN,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAG3C,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI;;;;;;;;IAS1G,KAAK;IAGL,iBAAiB,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI;IACtC,yBAAyB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAC1D,qBAAqB,CAAC,gBAAgB,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IACrF,gBAAgB;IAGhB,gCAAgC;IAGhC,iCAAiC;IAGjC,2BAA2B;IAG3B,gCAAgC;IAGhC,WAAW;IAGX,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM;IAGjE,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;IAG3C,YAAY,CAAC,IAAI,EAAE,MAAM;IAGzB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAGhE,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE;IAGxD,qBAAqB;IAGf,oBAAoB,CAAC,IAAI,EAAE,MAAM;IAGvC,0BAA0B;CAG3B;;AAED,wBAA4D"}
|
package/build/CrustModule.web.js
CHANGED
|
@@ -4,12 +4,21 @@ class CrustModule extends NativeModule {
|
|
|
4
4
|
async setValueAsync(value) {
|
|
5
5
|
this.emit("onChange", { value });
|
|
6
6
|
}
|
|
7
|
+
async nativeHttpRequest(method, url, headers, body) {
|
|
8
|
+
const response = await fetch(url, { method, headers, body });
|
|
9
|
+
return {
|
|
10
|
+
status: response.status,
|
|
11
|
+
statusText: response.statusText,
|
|
12
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
13
|
+
body: await response.text(),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
7
16
|
hello() {
|
|
8
17
|
return "Hello world! 👋";
|
|
9
18
|
}
|
|
10
19
|
showAVRoutePicker(_tintColor) { }
|
|
11
20
|
async setDeferredSystemGestures(_edges) { }
|
|
12
|
-
async setNotificationConfig(
|
|
21
|
+
async setNotificationConfig(_listenerEnabled, _blocklist) { }
|
|
13
22
|
async getInstalledApps() {
|
|
14
23
|
return [];
|
|
15
24
|
}
|
|
@@ -19,6 +28,9 @@ class CrustModule extends NativeModule {
|
|
|
19
28
|
async hasNotificationListenerPermission() {
|
|
20
29
|
return false;
|
|
21
30
|
}
|
|
31
|
+
async refreshNotificationListener() {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
22
34
|
async openNotificationListenerSettings() {
|
|
23
35
|
return false;
|
|
24
36
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CrustModule.web.js","sourceRoot":"","sources":["../src/CrustModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,iBAAiB,EAAE,YAAY,EAAC,MAAM,MAAM,CAAA;AAIpD,MAAM,WAAY,SAAQ,YAA+B;IACvD,EAAE,GAAG,IAAI,CAAC,EAAE,CAAA;IACZ,KAAK,CAAC,aAAa,CAAC,KAAa;QAC/B,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,EAAC,KAAK,EAAC,CAAC,CAAA;IAChC,CAAC;IACD,KAAK;QACH,OAAO,iBAAiB,CAAA;IAC1B,CAAC;IACD,iBAAiB,CAAC,UAA0B,IAAG,CAAC;IAChD,KAAK,CAAC,yBAAyB,CAAC,MAAgB,IAAkB,CAAC;IACnE,KAAK,CAAC,qBAAqB,CAAC,
|
|
1
|
+
{"version":3,"file":"CrustModule.web.js","sourceRoot":"","sources":["../src/CrustModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,iBAAiB,EAAE,YAAY,EAAC,MAAM,MAAM,CAAA;AAIpD,MAAM,WAAY,SAAQ,YAA+B;IACvD,EAAE,GAAG,IAAI,CAAC,EAAE,CAAA;IACZ,KAAK,CAAC,aAAa,CAAC,KAAa;QAC/B,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,EAAC,KAAK,EAAC,CAAC,CAAA;IAChC,CAAC;IACD,KAAK,CAAC,iBAAiB,CAAC,MAAc,EAAE,GAAW,EAAE,OAA+B,EAAE,IAAoB;QACxG,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,EAAC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAC,CAAC,CAAA;QAC1D,OAAO;YACL,MAAM,EAAE,QAAQ,CAAC,MAAM;YACvB,UAAU,EAAE,QAAQ,CAAC,UAAU;YAC/B,OAAO,EAAE,MAAM,CAAC,WAAW,CAAC,QAAQ,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;YACvD,IAAI,EAAE,MAAM,QAAQ,CAAC,IAAI,EAAE;SAC5B,CAAA;IACH,CAAC;IACD,KAAK;QACH,OAAO,iBAAiB,CAAA;IAC1B,CAAC;IACD,iBAAiB,CAAC,UAA0B,IAAG,CAAC;IAChD,KAAK,CAAC,yBAAyB,CAAC,MAAgB,IAAkB,CAAC;IACnE,KAAK,CAAC,qBAAqB,CAAC,gBAAyB,EAAE,UAAoB,IAAkB,CAAC;IAC9F,KAAK,CAAC,gBAAgB;QACpB,OAAO,EAAE,CAAA;IACX,CAAC;IACD,KAAK,CAAC,gCAAgC;QACpC,OAAO,EAAE,CAAA;IACX,CAAC;IACD,KAAK,CAAC,iCAAiC;QACrC,OAAO,KAAK,CAAA;IACd,CAAC;IACD,KAAK,CAAC,2BAA2B;QAC/B,OAAO,KAAK,CAAA;IACd,CAAC;IACD,KAAK,CAAC,gCAAgC;QACpC,OAAO,KAAK,CAAA;IACd,CAAC;IACD,KAAK,CAAC,WAAW;QACf,OAAO,KAAK,CAAA;IACd,CAAC;IACD,KAAK,CAAC,aAAa,CAAC,IAAY,EAAE,SAAiB,EAAE,UAAkB;QACrE,OAAO,KAAK,CAAA;IACd,CAAC;IACD,KAAK,CAAC,gBAAgB,CAAC,IAAY,EAAE,IAAY;QAC/C,OAAO,IAAI,CAAA;IACb,CAAC;IACD,KAAK,CAAC,YAAY,CAAC,IAAY;QAC7B,OAAM;IACR,CAAC;IACD,KAAK,CAAC,oBAAoB,CAAC,IAAY,EAAE,IAA6B;QACpE,OAAM;IACR,CAAC;IACD,KAAK,CAAC,mBAAmB,CAAC,IAAY,EAAE,MAAgB;QACtD,OAAM;IACR,CAAC;IACD,qBAAqB;QACnB,OAAO,EAAE,CAAA;IACX,CAAC;IACD,KAAK,CAAC,oBAAoB,CAAC,IAAY;QACrC,OAAO,KAAK,CAAA;IACd,CAAC;IACD,0BAA0B;QACxB,OAAO,EAAE,CAAA;IACX,CAAC;CACF;AAED,eAAe,iBAAiB,CAAC,WAAW,EAAE,aAAa,CAAC,CAAA","sourcesContent":["import {registerWebModule, NativeModule} from \"expo\"\n\nimport {CrustModuleEvents} from \"./Crust.types\"\n\nclass CrustModule extends NativeModule<CrustModuleEvents> {\n PI = Math.PI\n async setValueAsync(value: string): Promise<void> {\n this.emit(\"onChange\", {value})\n }\n async nativeHttpRequest(method: string, url: string, headers: Record<string, string>, body?: string | null) {\n const response = await fetch(url, {method, headers, body})\n return {\n status: response.status,\n statusText: response.statusText,\n headers: Object.fromEntries(response.headers.entries()),\n body: await response.text(),\n }\n }\n hello() {\n return \"Hello world! 👋\"\n }\n showAVRoutePicker(_tintColor?: string | null) {}\n async setDeferredSystemGestures(_edges: string[]): Promise<void> {}\n async setNotificationConfig(_listenerEnabled: boolean, _blocklist: string[]): Promise<void> {}\n async getInstalledApps() {\n return []\n }\n async getInstalledAppsForNotifications() {\n return []\n }\n async hasNotificationListenerPermission() {\n return false\n }\n async refreshNotificationListener() {\n return false\n }\n async openNotificationListenerSettings() {\n return false\n }\n async isBetaBuild() {\n return false\n }\n async mentraJsSpawn(_pkg: string, _polyfill: string, _miniappJs: string) {\n return false\n }\n async mentraJsEvaluate(_pkg: string, _src: string) {\n return null\n }\n async mentraJsKill(_pkg: string) {\n return\n }\n async mentraJsDispatchToJs(_pkg: string, _env: Record<string, unknown>) {\n return\n }\n async mentraJsSetManifest(_pkg: string, _perms: string[]) {\n return\n }\n mentraJsAlivePackages() {\n return []\n }\n async mentraJsDebugForceGC(_pkg: string) {\n return false\n }\n mentraJsLoadPolyfillBundle() {\n return \"\"\n }\n}\n\nexport default registerWebModule(CrustModule, \"CrustModule\")\n"]}
|
package/ios/CrustModule.swift
CHANGED
|
@@ -8,6 +8,59 @@ private enum MentraSyncedMediaAlbum {
|
|
|
8
8
|
static let localizedTitle = "Mentra"
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
/// Serializes callback and timeout delivery for PhotoKit APIs that may call back more than once.
|
|
12
|
+
private final class CheckedContinuationGate<Value>: @unchecked Sendable {
|
|
13
|
+
private let lock = NSLock()
|
|
14
|
+
private var continuation: CheckedContinuation<Value, Never>?
|
|
15
|
+
|
|
16
|
+
init(_ continuation: CheckedContinuation<Value, Never>) {
|
|
17
|
+
self.continuation = continuation
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
func resume(returning value: Value) {
|
|
21
|
+
lock.lock()
|
|
22
|
+
guard let continuation else {
|
|
23
|
+
lock.unlock()
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
self.continuation = nil
|
|
27
|
+
lock.unlock()
|
|
28
|
+
continuation.resume(returning: value)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private final class PhotoLibrarySaveState: @unchecked Sendable {
|
|
33
|
+
private let lock = NSLock()
|
|
34
|
+
private var assetIdentifier: String?
|
|
35
|
+
private var creationFailed = false
|
|
36
|
+
|
|
37
|
+
func setAssetIdentifier(_ identifier: String) {
|
|
38
|
+
lock.lock()
|
|
39
|
+
assetIdentifier = identifier
|
|
40
|
+
lock.unlock()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
func markCreationFailed() {
|
|
44
|
+
lock.lock()
|
|
45
|
+
creationFailed = true
|
|
46
|
+
lock.unlock()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func snapshot() -> (assetIdentifier: String?, creationFailed: Bool) {
|
|
50
|
+
lock.lock()
|
|
51
|
+
defer { lock.unlock() }
|
|
52
|
+
return (assetIdentifier, creationFailed)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private struct PhotoLibrarySaveResult {
|
|
57
|
+
let succeeded: Bool
|
|
58
|
+
let assetIdentifier: String?
|
|
59
|
+
let creationFailed: Bool
|
|
60
|
+
let errorMessage: String?
|
|
61
|
+
let timedOut: Bool
|
|
62
|
+
}
|
|
63
|
+
|
|
11
64
|
public class CrustModule: Module {
|
|
12
65
|
public func definition() -> ModuleDefinition {
|
|
13
66
|
Name("Crust")
|
|
@@ -206,6 +259,10 @@ public class CrustModule: Module {
|
|
|
206
259
|
return false
|
|
207
260
|
}
|
|
208
261
|
|
|
262
|
+
AsyncFunction("refreshNotificationListener") { () -> Bool in
|
|
263
|
+
return false
|
|
264
|
+
}
|
|
265
|
+
|
|
209
266
|
AsyncFunction("openNotificationListenerSettings") { () -> Bool in
|
|
210
267
|
return false
|
|
211
268
|
}
|
|
@@ -411,66 +468,135 @@ public class CrustModule: Module {
|
|
|
411
468
|
// MARK: - Media Library Commands
|
|
412
469
|
|
|
413
470
|
AsyncFunction("saveToGalleryWithDate") {
|
|
414
|
-
(
|
|
471
|
+
(
|
|
472
|
+
filePath: String,
|
|
473
|
+
captureTimeMillis: Int64?,
|
|
474
|
+
displayName: String?
|
|
475
|
+
) -> [String: Any] in
|
|
415
476
|
let fileURL = URL(fileURLWithPath: filePath)
|
|
416
477
|
|
|
417
478
|
guard FileManager.default.fileExists(atPath: filePath) else {
|
|
418
479
|
return ["success": false, "error": "File does not exist"]
|
|
419
480
|
}
|
|
420
481
|
|
|
421
|
-
|
|
422
|
-
let
|
|
423
|
-
|
|
424
|
-
|
|
482
|
+
let pathExtension = fileURL.pathExtension.lowercased()
|
|
483
|
+
let isVideo = ["mp4", "mov", "avi", "m4v"].contains(pathExtension)
|
|
484
|
+
let assetFileName = displayName?.isEmpty == false
|
|
485
|
+
? displayName!
|
|
486
|
+
: fileURL.lastPathComponent
|
|
487
|
+
let captureDate = captureTimeMillis.map {
|
|
488
|
+
Date(timeIntervalSince1970: TimeInterval($0) / 1000.0)
|
|
489
|
+
}
|
|
490
|
+
let candidateFileNames = [assetFileName, fileURL.lastPathComponent]
|
|
491
|
+
.filter { !$0.isEmpty }
|
|
492
|
+
let stableFileName = assetFileName.hasPrefix("IMG_") || assetFileName.hasPrefix("VID_")
|
|
493
|
+
? assetFileName
|
|
494
|
+
: nil
|
|
495
|
+
let fileAttributes = try? FileManager.default.attributesOfItem(atPath: filePath)
|
|
496
|
+
let expectedFileSize = (fileAttributes?[.size] as? NSNumber)?.int64Value
|
|
497
|
+
let authorizationStatus: PHAuthorizationStatus
|
|
498
|
+
if #available(iOS 14, *) {
|
|
499
|
+
authorizationStatus = PHPhotoLibrary.authorizationStatus(for: .readWrite)
|
|
500
|
+
} else {
|
|
501
|
+
authorizationStatus = PHPhotoLibrary.authorizationStatus()
|
|
502
|
+
}
|
|
503
|
+
let hasLimitedAccess: Bool
|
|
504
|
+
if #available(iOS 14, *) {
|
|
505
|
+
hasLimitedAccess = authorizationStatus == .limited
|
|
506
|
+
} else {
|
|
507
|
+
hasLimitedAccess = false
|
|
508
|
+
}
|
|
425
509
|
|
|
426
|
-
|
|
427
|
-
|
|
510
|
+
// The JS ledger normally supplies the previous PhotoKit receipt. If the app was
|
|
511
|
+
// terminated after Photos committed but before that receipt was persisted, reconcile
|
|
512
|
+
// by our stable capture filename/date before creating another asset.
|
|
513
|
+
if let existingIdentifier = await self.findExistingGalleryAssetIdentifier(
|
|
514
|
+
fileNames: candidateFileNames,
|
|
515
|
+
stableFileName: stableFileName,
|
|
516
|
+
expectedFileSize: expectedFileSize,
|
|
517
|
+
captureDate: captureDate,
|
|
518
|
+
isVideo: isVideo
|
|
519
|
+
) {
|
|
520
|
+
NSLog("CrustModule: Reusing existing gallery asset")
|
|
521
|
+
return [
|
|
522
|
+
"success": true,
|
|
523
|
+
"identifier": existingIdentifier,
|
|
524
|
+
"existing": true,
|
|
525
|
+
]
|
|
526
|
+
}
|
|
428
527
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
528
|
+
let saveResult = await self.createGalleryAsset(
|
|
529
|
+
fileURL: fileURL,
|
|
530
|
+
assetFileName: assetFileName,
|
|
531
|
+
captureDate: captureDate,
|
|
532
|
+
isVideo: isVideo,
|
|
533
|
+
hasLimitedAccess: hasLimitedAccess
|
|
534
|
+
)
|
|
535
|
+
if saveResult.timedOut {
|
|
536
|
+
return [
|
|
537
|
+
"success": false,
|
|
538
|
+
"error": "Photo library save timed out; retry will reconcile the result",
|
|
539
|
+
]
|
|
540
|
+
}
|
|
541
|
+
if saveResult.creationFailed {
|
|
542
|
+
return ["success": false, "error": "Failed to create PhotoKit asset placeholder"]
|
|
543
|
+
}
|
|
544
|
+
if let errorMessage = saveResult.errorMessage {
|
|
545
|
+
NSLog("CrustModule: Error saving to gallery: \(errorMessage)")
|
|
546
|
+
return ["success": false, "error": errorMessage]
|
|
547
|
+
}
|
|
548
|
+
guard saveResult.succeeded else {
|
|
549
|
+
return ["success": false, "error": "Photo library did not commit the asset"]
|
|
550
|
+
}
|
|
451
551
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
552
|
+
NSLog("CrustModule: Successfully saved to gallery with proper creation date")
|
|
553
|
+
return ["success": true, "identifier": saveResult.assetIdentifier ?? ""]
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
private func createGalleryAsset(
|
|
558
|
+
fileURL: URL,
|
|
559
|
+
assetFileName: String,
|
|
560
|
+
captureDate: Date?,
|
|
561
|
+
isVideo: Bool,
|
|
562
|
+
hasLimitedAccess: Bool
|
|
563
|
+
) async -> PhotoLibrarySaveResult {
|
|
564
|
+
await withCheckedContinuation { continuation in
|
|
565
|
+
let gate = CheckedContinuationGate(continuation)
|
|
566
|
+
let state = PhotoLibrarySaveState()
|
|
567
|
+
|
|
568
|
+
PHPhotoLibrary.shared().performChanges {
|
|
569
|
+
let creationRequest = PHAssetCreationRequest.forAsset()
|
|
570
|
+
let resourceOptions = PHAssetResourceCreationOptions()
|
|
571
|
+
resourceOptions.originalFilename = assetFileName
|
|
572
|
+
creationRequest.addResource(
|
|
573
|
+
with: isVideo ? .video : .photo,
|
|
574
|
+
fileURL: fileURL,
|
|
575
|
+
options: resourceOptions
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
if let captureDate {
|
|
456
579
|
creationRequest.creationDate = captureDate
|
|
457
580
|
NSLog("CrustModule: Setting creation date to: \(captureDate)")
|
|
458
581
|
}
|
|
459
582
|
|
|
460
583
|
guard let assetPlaceholder = creationRequest.placeholderForCreatedAsset else {
|
|
461
584
|
NSLog("CrustModule: Missing placeholder for created asset")
|
|
462
|
-
|
|
585
|
+
state.markCreationFailed()
|
|
463
586
|
return
|
|
464
587
|
}
|
|
588
|
+
state.setAssetIdentifier(assetPlaceholder.localIdentifier)
|
|
465
589
|
|
|
466
|
-
|
|
590
|
+
// Limited-library access permits creating an asset, but does not reliably
|
|
591
|
+
// permit enumerating or mutating arbitrary user albums. Save to the camera
|
|
592
|
+
// roll and skip Mentra album mutation in that mode.
|
|
593
|
+
guard !hasLimitedAccess else { return }
|
|
467
594
|
|
|
468
595
|
let albumFetch = PHFetchOptions()
|
|
469
596
|
albumFetch.predicate = NSPredicate(
|
|
470
597
|
format: "localizedTitle == %@", MentraSyncedMediaAlbum.localizedTitle
|
|
471
598
|
)
|
|
472
599
|
albumFetch.fetchLimit = 1
|
|
473
|
-
|
|
474
600
|
let existingAlbums = PHAssetCollection.fetchAssetCollections(
|
|
475
601
|
with: .album,
|
|
476
602
|
subtype: .albumRegular,
|
|
@@ -491,26 +617,141 @@ public class CrustModule: Module {
|
|
|
491
617
|
"CrustModule: Mentra album exists but is not writable; asset saved to library only"
|
|
492
618
|
)
|
|
493
619
|
}
|
|
494
|
-
} completionHandler: {
|
|
495
|
-
|
|
496
|
-
|
|
620
|
+
} completionHandler: { succeeded, error in
|
|
621
|
+
let snapshot = state.snapshot()
|
|
622
|
+
gate.resume(returning: PhotoLibrarySaveResult(
|
|
623
|
+
succeeded: succeeded,
|
|
624
|
+
assetIdentifier: snapshot.assetIdentifier,
|
|
625
|
+
creationFailed: snapshot.creationFailed,
|
|
626
|
+
errorMessage: error?.localizedDescription,
|
|
627
|
+
timedOut: false
|
|
628
|
+
))
|
|
497
629
|
}
|
|
498
630
|
|
|
499
|
-
|
|
631
|
+
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + 120) {
|
|
632
|
+
let snapshot = state.snapshot()
|
|
633
|
+
gate.resume(returning: PhotoLibrarySaveResult(
|
|
634
|
+
succeeded: false,
|
|
635
|
+
assetIdentifier: snapshot.assetIdentifier,
|
|
636
|
+
creationFailed: snapshot.creationFailed,
|
|
637
|
+
errorMessage: nil,
|
|
638
|
+
timedOut: true
|
|
639
|
+
))
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
500
643
|
|
|
501
|
-
|
|
502
|
-
|
|
644
|
+
private func findExistingGalleryAssetIdentifier(
|
|
645
|
+
fileNames: [String],
|
|
646
|
+
stableFileName: String?,
|
|
647
|
+
expectedFileSize: Int64?,
|
|
648
|
+
captureDate: Date?,
|
|
649
|
+
isVideo: Bool
|
|
650
|
+
) async -> String? {
|
|
651
|
+
guard !fileNames.isEmpty else { return nil }
|
|
652
|
+
let options = PHFetchOptions()
|
|
653
|
+
let mediaType = isVideo ? PHAssetMediaType.video : PHAssetMediaType.image
|
|
654
|
+
if let captureDate {
|
|
655
|
+
options.predicate = NSPredicate(
|
|
656
|
+
format: "mediaType == %d AND creationDate >= %@ AND creationDate <= %@",
|
|
657
|
+
mediaType.rawValue,
|
|
658
|
+
captureDate.addingTimeInterval(-1) as NSDate,
|
|
659
|
+
captureDate.addingTimeInterval(1) as NSDate
|
|
660
|
+
)
|
|
661
|
+
} else {
|
|
662
|
+
options.predicate = NSPredicate(format: "mediaType == %d", mediaType.rawValue)
|
|
663
|
+
options.fetchLimit = 200
|
|
664
|
+
}
|
|
665
|
+
options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
|
|
666
|
+
|
|
667
|
+
let assets = PHAsset.fetchAssets(with: options)
|
|
668
|
+
var stableCandidates: [PHAsset] = []
|
|
669
|
+
var legacyCandidates: [PHAsset] = []
|
|
670
|
+
assets.enumerateObjects { asset, _, _ in
|
|
671
|
+
let resources = PHAssetResource.assetResources(for: asset)
|
|
672
|
+
let stableMatch = stableFileName.map { stableName in
|
|
673
|
+
resources.contains { $0.originalFilename == stableName }
|
|
674
|
+
} ?? false
|
|
675
|
+
if stableMatch {
|
|
676
|
+
stableCandidates.append(asset)
|
|
677
|
+
return
|
|
678
|
+
}
|
|
679
|
+
let legacyNameMatch = resources.contains {
|
|
680
|
+
guard fileNames.contains($0.originalFilename), $0.originalFilename != stableFileName else {
|
|
681
|
+
return false
|
|
682
|
+
}
|
|
683
|
+
return true
|
|
503
684
|
}
|
|
685
|
+
if legacyNameMatch { legacyCandidates.append(asset) }
|
|
686
|
+
}
|
|
687
|
+
guard let expectedFileSize else { return nil }
|
|
688
|
+
// PhotoKit's original-resource lookups are asynchronous. Resolve candidates after
|
|
689
|
+
// enumeration so the fetch callback never blocks the Photos framework. Stable names
|
|
690
|
+
// may repeat across a restored/older library; require the generation's exact byte size
|
|
691
|
+
// and reuse the newest matching completed asset.
|
|
692
|
+
for asset in stableCandidates {
|
|
693
|
+
if await self.assetOriginalFile(asset, matchesByteCount: expectedFileSize) {
|
|
694
|
+
return asset.localIdentifier
|
|
695
|
+
}
|
|
696
|
+
}
|
|
504
697
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
698
|
+
var legacyIdentifiers: [String] = []
|
|
699
|
+
for asset in legacyCandidates {
|
|
700
|
+
if await self.assetOriginalFile(asset, matchesByteCount: expectedFileSize) {
|
|
701
|
+
legacyIdentifiers.append(asset.localIdentifier)
|
|
508
702
|
}
|
|
703
|
+
}
|
|
704
|
+
// Legacy base names remain safe only when name + capture time + media type + exact
|
|
705
|
+
// original byte size identify one asset.
|
|
706
|
+
return legacyIdentifiers.count == 1 ? legacyIdentifiers[0] : nil
|
|
707
|
+
}
|
|
509
708
|
|
|
510
|
-
|
|
511
|
-
|
|
709
|
+
/// PhotoKit does not expose resource byte size on the deployment targets we support.
|
|
710
|
+
/// Inspect a video's local original URL or an image's original bytes asynchronously.
|
|
711
|
+
/// Network access stays disabled: if the original is only in iCloud, reconciliation fails
|
|
712
|
+
/// closed and the caller creates a fresh local-library asset instead of risking data loss.
|
|
713
|
+
private func assetOriginalFile(
|
|
714
|
+
_ asset: PHAsset,
|
|
715
|
+
matchesByteCount expectedByteCount: Int64
|
|
716
|
+
) async -> Bool {
|
|
717
|
+
let manager = PHImageManager.default()
|
|
718
|
+
if asset.mediaType == .video {
|
|
719
|
+
let options = PHVideoRequestOptions()
|
|
720
|
+
options.isNetworkAccessAllowed = false
|
|
721
|
+
options.version = .original
|
|
722
|
+
return await withCheckedContinuation { continuation in
|
|
723
|
+
let gate = CheckedContinuationGate(continuation)
|
|
724
|
+
let requestID = manager.requestAVAsset(forVideo: asset, options: options) { avAsset, _, _ in
|
|
725
|
+
guard let originalURL = (avAsset as? AVURLAsset)?.url,
|
|
726
|
+
let attributes = try? FileManager.default.attributesOfItem(atPath: originalURL.path),
|
|
727
|
+
let byteCount = attributes[.size] as? NSNumber else {
|
|
728
|
+
gate.resume(returning: false)
|
|
729
|
+
return
|
|
730
|
+
}
|
|
731
|
+
gate.resume(returning: byteCount.int64Value == expectedByteCount)
|
|
732
|
+
}
|
|
733
|
+
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + 30) {
|
|
734
|
+
manager.cancelImageRequest(requestID)
|
|
735
|
+
gate.resume(returning: false)
|
|
736
|
+
}
|
|
737
|
+
}
|
|
512
738
|
}
|
|
513
739
|
|
|
740
|
+
let options = PHImageRequestOptions()
|
|
741
|
+
options.isNetworkAccessAllowed = false
|
|
742
|
+
options.version = .original
|
|
743
|
+
options.deliveryMode = .highQualityFormat
|
|
744
|
+
return await withCheckedContinuation { continuation in
|
|
745
|
+
let gate = CheckedContinuationGate(continuation)
|
|
746
|
+
let requestID = manager.requestImageDataAndOrientation(for: asset, options: options) { data, _, _, info in
|
|
747
|
+
if info?[PHImageResultIsDegradedKey] as? Bool == true { return }
|
|
748
|
+
gate.resume(returning: data.map { Int64($0.count) == expectedByteCount } ?? false)
|
|
749
|
+
}
|
|
750
|
+
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + 30) {
|
|
751
|
+
manager.cancelImageRequest(requestID)
|
|
752
|
+
gate.resume(returning: false)
|
|
753
|
+
}
|
|
754
|
+
}
|
|
514
755
|
}
|
|
515
756
|
}
|
|
516
757
|
|