@ateam-ai-mobile/expo 0.1.4 → 0.1.7
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/android/build.gradle +16 -0
- package/android/src/main/java/expo/modules/adavision/AdaVisionModule.kt +75 -0
- package/dist/attachmentCache-DhQTqi2E.d.mts +18 -0
- package/dist/attachmentCache-DhQTqi2E.d.ts +18 -0
- package/dist/bootstrap.d.mts +95 -0
- package/dist/bootstrap.d.ts +95 -0
- package/dist/bootstrap.js +2473 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/bootstrap.mjs +64 -0
- package/dist/bootstrap.mjs.map +1 -0
- package/dist/chunk-4BMWVN25.mjs +91 -0
- package/dist/chunk-4BMWVN25.mjs.map +1 -0
- package/dist/chunk-4XYXJYN5.mjs +1513 -0
- package/dist/chunk-4XYXJYN5.mjs.map +1 -0
- package/dist/chunk-5HM6UA2K.mjs +571 -0
- package/dist/chunk-5HM6UA2K.mjs.map +1 -0
- package/dist/chunk-6LXF3JVX.mjs +96 -0
- package/dist/chunk-6LXF3JVX.mjs.map +1 -0
- package/dist/chunk-FF6ZZZXH.mjs +71 -0
- package/dist/chunk-FF6ZZZXH.mjs.map +1 -0
- package/dist/chunk-KOXF2FZM.mjs +92 -0
- package/dist/chunk-KOXF2FZM.mjs.map +1 -0
- package/dist/chunk-L4V7OCJ3.mjs +71 -0
- package/dist/chunk-L4V7OCJ3.mjs.map +1 -0
- package/dist/chunk-TZXWVYYR.mjs +67 -0
- package/dist/chunk-TZXWVYYR.mjs.map +1 -0
- package/dist/chunk-VQJPSLHP.mjs +1801 -0
- package/dist/chunk-VQJPSLHP.mjs.map +1 -0
- package/dist/chunk-WMJKH4XE.mjs +34 -0
- package/dist/chunk-WMJKH4XE.mjs.map +1 -0
- package/dist/chunk-XQSY6MHP.mjs +213 -0
- package/dist/chunk-XQSY6MHP.mjs.map +1 -0
- package/dist/createBridge.mjs +2 -0
- package/dist/createBridge.mjs.map +1 -1
- package/dist/device/createDeviceProvider.d.mts +23 -0
- package/dist/device/createDeviceProvider.d.ts +23 -0
- package/dist/device/createDeviceProvider.js +967 -0
- package/dist/device/createDeviceProvider.js.map +1 -0
- package/dist/device/createDeviceProvider.mjs +17 -0
- package/dist/device/createDeviceProvider.mjs.map +1 -0
- package/dist/device-bridge/collectors/appEngagement.d.mts +10 -0
- package/dist/device-bridge/collectors/appEngagement.d.ts +10 -0
- package/dist/device-bridge/collectors/appEngagement.js +128 -0
- package/dist/device-bridge/collectors/appEngagement.js.map +1 -0
- package/dist/device-bridge/collectors/appEngagement.mjs +12 -0
- package/dist/device-bridge/collectors/appEngagement.mjs.map +1 -0
- package/dist/device-bridge/collectors/health.d.mts +24 -0
- package/dist/device-bridge/collectors/health.d.ts +24 -0
- package/dist/device-bridge/collectors/health.js +238 -0
- package/dist/device-bridge/collectors/health.js.map +1 -0
- package/dist/device-bridge/collectors/health.mjs +18 -0
- package/dist/device-bridge/collectors/health.mjs.map +1 -0
- package/dist/device-bridge/cookies.d.mts +49 -0
- package/dist/device-bridge/cookies.d.ts +49 -0
- package/dist/device-bridge/cookies.js +90 -0
- package/dist/device-bridge/cookies.js.map +1 -0
- package/dist/device-bridge/cookies.mjs +14 -0
- package/dist/device-bridge/cookies.mjs.map +1 -0
- package/dist/device-bridge/index.d.mts +483 -0
- package/dist/device-bridge/index.d.ts +483 -0
- package/dist/device-bridge/index.js +2084 -0
- package/dist/device-bridge/index.js.map +1 -0
- package/dist/device-bridge/index.mjs +32 -0
- package/dist/device-bridge/index.mjs.map +1 -0
- package/dist/device-bridge/nfc.d.mts +30 -0
- package/dist/device-bridge/nfc.d.ts +30 -0
- package/dist/device-bridge/nfc.js +109 -0
- package/dist/device-bridge/nfc.js.map +1 -0
- package/dist/device-bridge/nfc.mjs +14 -0
- package/dist/device-bridge/nfc.mjs.map +1 -0
- package/dist/index.d.mts +493 -18
- package/dist/index.d.ts +493 -18
- package/dist/index.js +1409 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +23 -194
- package/dist/index.mjs.map +1 -1
- package/dist/oauth/expoOAuth.d.mts +18 -0
- package/dist/oauth/expoOAuth.d.ts +18 -0
- package/dist/oauth/expoOAuth.js +102 -0
- package/dist/oauth/expoOAuth.js.map +1 -0
- package/dist/oauth/expoOAuth.mjs +8 -0
- package/dist/oauth/expoOAuth.mjs.map +1 -0
- package/dist/types-BRanJYTn.d.mts +138 -0
- package/dist/types-BRanJYTn.d.ts +138 -0
- package/dist/vision/index.d.mts +81 -0
- package/dist/vision/index.d.ts +81 -0
- package/dist/vision/index.js +101 -0
- package/dist/vision/index.js.map +1 -0
- package/dist/vision/index.mjs +28 -0
- package/dist/vision/index.mjs.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/AdaVision.podspec +28 -0
- package/ios/AdaVisionModule.swift +432 -0
- package/ios/Resources/yolov8n-oiv7.mlmodelc/analytics/coremldata.bin +0 -0
- package/ios/Resources/yolov8n-oiv7.mlmodelc/coremldata.bin +0 -0
- package/ios/Resources/yolov8n-oiv7.mlmodelc/metadata.json +773 -0
- package/ios/Resources/yolov8n-oiv7.mlmodelc/model0/analytics/coremldata.bin +0 -0
- package/ios/Resources/yolov8n-oiv7.mlmodelc/model0/coremldata.bin +0 -0
- package/ios/Resources/yolov8n-oiv7.mlmodelc/model0/model.mil +720 -0
- package/ios/Resources/yolov8n-oiv7.mlmodelc/model0/weights/weight.bin +0 -0
- package/ios/Resources/yolov8n-oiv7.mlmodelc/model1/coremldata.bin +0 -0
- package/package.json +107 -7
- package/src/bootstrap.ts +159 -0
- package/src/camera/CameraTaskHost.tsx +175 -0
- package/src/camera/cameraTasks.ts +129 -0
- package/src/device/createDeviceProvider.ts +832 -0
- package/src/device-bridge/actions/calendar-actions.ts +217 -0
- package/src/device-bridge/actions/dnd.ts +74 -0
- package/src/device-bridge/actions/health-actions.ts +58 -0
- package/src/device-bridge/actions/index.ts +12 -0
- package/src/device-bridge/actions/messaging.ts +74 -0
- package/src/device-bridge/actions/navigation.ts +88 -0
- package/src/device-bridge/actions/queue-poller.ts +151 -0
- package/src/device-bridge/background.ts +137 -0
- package/src/device-bridge/biometrics.ts +64 -0
- package/src/device-bridge/capabilities.ts +218 -0
- package/src/device-bridge/collectors/appEngagement.ts +120 -0
- package/src/device-bridge/collectors/battery.ts +83 -0
- package/src/device-bridge/collectors/calendar.ts +162 -0
- package/src/device-bridge/collectors/camera.ts +27 -0
- package/src/device-bridge/collectors/connectivity.ts +60 -0
- package/src/device-bridge/collectors/contacts.ts +200 -0
- package/src/device-bridge/collectors/health.ts +299 -0
- package/src/device-bridge/collectors/index.ts +12 -0
- package/src/device-bridge/collectors/location.ts +224 -0
- package/src/device-bridge/collectors/mood.ts +141 -0
- package/src/device-bridge/collectors/notifications.ts +70 -0
- package/src/device-bridge/cookies.ts +89 -0
- package/src/device-bridge/dynamic-connector.ts +422 -0
- package/src/device-bridge/index.ts +467 -0
- package/src/device-bridge/nfc.ts +85 -0
- package/src/device-bridge/plugin.ts +71 -0
- package/src/device-bridge/relay-client.ts +237 -0
- package/src/device-bridge/scanner.ts +42 -0
- package/src/device-bridge/sharing.ts +35 -0
- package/src/device-bridge/types.ts +326 -0
- package/src/device-state/activityClassifier.ts +151 -0
- package/src/device-state/activityEngine.ts +252 -0
- package/src/device-state/collectors.ts +31 -0
- package/src/device-state/domains/activity.ts +93 -0
- package/src/device-state/domains/capability.ts +60 -0
- package/src/device-state/domains/connectivity.ts +60 -0
- package/src/device-state/domains/deviceContext.ts +55 -0
- package/src/device-state/domains/interaction.ts +59 -0
- package/src/device-state/domains/spatial.ts +283 -0
- package/src/device-state/domains/visual.ts +100 -0
- package/src/device-state/headingSource.ts +84 -0
- package/src/device-state/index.ts +77 -0
- package/src/device-state/quality.ts +60 -0
- package/src/device-state/replay.ts +220 -0
- package/src/device-state/turnDetector.ts +121 -0
- package/src/device-state/visualEngine.ts +341 -0
- package/src/haptics.ts +4 -0
- package/src/index.ts +37 -0
- package/src/oauth/expoOAuth.tsx +113 -0
- package/src/vision/analysis.js +80 -0
- package/src/vision/index.ts +144 -0
- package/src/vision/ring.js +35 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notifications Collector — reads notification state via expo-notifications.
|
|
3
|
+
* Output format matches device-mock-mcp's notifications.list schema.
|
|
4
|
+
*
|
|
5
|
+
* Note: On iOS/Android, expo-notifications only gives access to notifications
|
|
6
|
+
* that YOUR app has posted or received. System-wide notification access
|
|
7
|
+
* requires platform-specific APIs (NotificationListenerService on Android,
|
|
8
|
+
* UNUserNotificationCenter on iOS with app extensions).
|
|
9
|
+
*
|
|
10
|
+
* For the PA, the relay approach means we capture notifications delivered
|
|
11
|
+
* TO our app (from the PA system), not FROM other apps.
|
|
12
|
+
* Other-app notifications would need a native module or accessibility service.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as ExpoNotifications from 'expo-notifications';
|
|
16
|
+
import type { Notification, NotificationsData } from '../types';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Request notification permissions.
|
|
20
|
+
*/
|
|
21
|
+
export async function requestPermission(): Promise<boolean> {
|
|
22
|
+
const { status } = await ExpoNotifications.requestPermissionsAsync();
|
|
23
|
+
return status === 'granted';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Check if notification permission is granted.
|
|
28
|
+
*/
|
|
29
|
+
export async function hasPermission(): Promise<boolean> {
|
|
30
|
+
const { status } = await ExpoNotifications.getPermissionsAsync();
|
|
31
|
+
return status === 'granted';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Collect delivered notifications from the notification tray.
|
|
36
|
+
* Only captures notifications delivered to THIS app.
|
|
37
|
+
* Matches the device.notifications.list tool output schema.
|
|
38
|
+
*/
|
|
39
|
+
export async function collectNotifications(): Promise<NotificationsData> {
|
|
40
|
+
const presented = await ExpoNotifications.getPresentedNotificationsAsync();
|
|
41
|
+
|
|
42
|
+
const notifications: Notification[] = presented.map((n, i) => ({
|
|
43
|
+
id: n.request.identifier || `n${i}`,
|
|
44
|
+
app: (n.request.content.data?.app as string) || 'A-Team',
|
|
45
|
+
title: n.request.content.title || '',
|
|
46
|
+
body: n.request.content.body || '',
|
|
47
|
+
timestamp: n.date ? new Date(n.date).toISOString() : new Date().toISOString(),
|
|
48
|
+
sender: (n.request.content.data?.sender as string) || null,
|
|
49
|
+
priority: mapPriority(n.request.content.data?.priority as string),
|
|
50
|
+
read: false, // Presented notifications are unread by definition
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
const unread = notifications.filter(n => !n.read).length;
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
ok: true,
|
|
57
|
+
timestamp: new Date().toISOString(),
|
|
58
|
+
total: notifications.length,
|
|
59
|
+
unread,
|
|
60
|
+
notifications,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── Helpers ───
|
|
65
|
+
|
|
66
|
+
function mapPriority(p: string | undefined): 'high' | 'medium' | 'low' {
|
|
67
|
+
if (p === 'high' || p === 'max') return 'high';
|
|
68
|
+
if (p === 'low' || p === 'min') return 'low';
|
|
69
|
+
return 'medium';
|
|
70
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device-bridge: Cookies API
|
|
3
|
+
*
|
|
4
|
+
* Reads the platform's shared cookie jar (NSHTTPCookieStorage on iOS,
|
|
5
|
+
* CookieManager on Android) so upper layers (plugin-sdk → RN plugins) can
|
|
6
|
+
* extract session cookies captured during a native WebView login flow.
|
|
7
|
+
*
|
|
8
|
+
* Wraps `@react-native-cookies/cookies` so upper layers never import the
|
|
9
|
+
* native module directly. If we swap implementations, only this file changes.
|
|
10
|
+
*
|
|
11
|
+
* Why device-bridge owns this: cookies are a device-level capability (tied
|
|
12
|
+
* to the platform's shared HTTP stack). Plugins should consume a clean
|
|
13
|
+
* abstraction; the native wiring lives here.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import CookieManager from '@react-native-cookies/cookies';
|
|
17
|
+
|
|
18
|
+
export interface DeviceCookie {
|
|
19
|
+
name: string;
|
|
20
|
+
value: string;
|
|
21
|
+
domain?: string;
|
|
22
|
+
path?: string;
|
|
23
|
+
/** Unix seconds. Omitted for session cookies. */
|
|
24
|
+
expires?: number;
|
|
25
|
+
httpOnly?: boolean;
|
|
26
|
+
secure?: boolean;
|
|
27
|
+
sameSite?: 'Lax' | 'Strict' | 'None';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get every cookie attached to the URL's effective domain.
|
|
32
|
+
* Set `includeHttpOnly` true to include HttpOnly cookies (required for
|
|
33
|
+
* LinkedIn/Google session tokens).
|
|
34
|
+
*/
|
|
35
|
+
export async function get(url: string, includeHttpOnly: boolean = true): Promise<DeviceCookie[]> {
|
|
36
|
+
const raw = (await CookieManager.get(url, includeHttpOnly)) as Record<string, any>;
|
|
37
|
+
const out: DeviceCookie[] = [];
|
|
38
|
+
for (const [name, c] of Object.entries(raw || {})) {
|
|
39
|
+
if (!c || typeof c !== 'object') continue;
|
|
40
|
+
const value = typeof c.value === 'string' ? c.value : String(c.value ?? '');
|
|
41
|
+
const cookie: DeviceCookie = { name, value };
|
|
42
|
+
if (c.domain) cookie.domain = String(c.domain);
|
|
43
|
+
if (c.path) cookie.path = String(c.path);
|
|
44
|
+
if (c.expires) {
|
|
45
|
+
const t = Date.parse(c.expires);
|
|
46
|
+
if (!Number.isNaN(t)) cookie.expires = Math.floor(t / 1000);
|
|
47
|
+
}
|
|
48
|
+
if (typeof c.httpOnly === 'boolean') cookie.httpOnly = c.httpOnly;
|
|
49
|
+
if (typeof c.secure === 'boolean') cookie.secure = c.secure;
|
|
50
|
+
if (c.sameSite === 'Lax' || c.sameSite === 'Strict' || c.sameSite === 'None') {
|
|
51
|
+
cookie.sameSite = c.sameSite;
|
|
52
|
+
}
|
|
53
|
+
out.push(cookie);
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Wipe the shared cookie jar. Cross-platform scoped clear isn't supported
|
|
60
|
+
* uniformly; `clearAll` is the safe operation on both iOS and Android.
|
|
61
|
+
*/
|
|
62
|
+
export async function clearAll(): Promise<{ ok: boolean; error?: string }> {
|
|
63
|
+
try {
|
|
64
|
+
await CookieManager.clearAll(true);
|
|
65
|
+
return { ok: true };
|
|
66
|
+
} catch (e: any) {
|
|
67
|
+
return { ok: false, error: String(e?.message || e) };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Set a cookie programmatically. Useful when replaying a saved session on
|
|
73
|
+
* the device's shared jar before opening a WebView.
|
|
74
|
+
*/
|
|
75
|
+
export async function set(url: string, cookie: DeviceCookie): Promise<{ ok: boolean; error?: string }> {
|
|
76
|
+
try {
|
|
77
|
+
await CookieManager.set(url, {
|
|
78
|
+
name: cookie.name,
|
|
79
|
+
value: cookie.value,
|
|
80
|
+
domain: cookie.domain,
|
|
81
|
+
path: cookie.path || '/',
|
|
82
|
+
httpOnly: cookie.httpOnly,
|
|
83
|
+
secure: cookie.secure,
|
|
84
|
+
} as any, true);
|
|
85
|
+
return { ok: true };
|
|
86
|
+
} catch (e: any) {
|
|
87
|
+
return { ok: false, error: String(e?.message || e) };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device Bridge — Dynamic Connector
|
|
3
|
+
*
|
|
4
|
+
* Pure JavaScript implementation (zero native imports).
|
|
5
|
+
* Uses bridge APIs for all device operations.
|
|
6
|
+
* Can be downloaded and executed at runtime.
|
|
7
|
+
*
|
|
8
|
+
* This file compiles to pure JS that gets bundled as device-bridge-connector.js
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Structural Bridge type. The old import pointed at '@ateam-mobile/bridge',
|
|
12
|
+
// a package that does not exist here — it resolved to `any` and hid whatever
|
|
13
|
+
// this file actually needs from the host.
|
|
14
|
+
type Bridge = Record<string, any>;
|
|
15
|
+
|
|
16
|
+
interface PendingAction {
|
|
17
|
+
id: string;
|
|
18
|
+
type: string;
|
|
19
|
+
params: Record<string, any>;
|
|
20
|
+
created_at: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface ActionResult {
|
|
24
|
+
id: string;
|
|
25
|
+
ok: boolean;
|
|
26
|
+
result?: Record<string, any>;
|
|
27
|
+
error?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface RelayPushResult {
|
|
31
|
+
ok: boolean;
|
|
32
|
+
error?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Main dynamic connector export
|
|
37
|
+
*/
|
|
38
|
+
export default {
|
|
39
|
+
id: 'device-bridge',
|
|
40
|
+
name: 'Device Bridge',
|
|
41
|
+
version: '1.0.0',
|
|
42
|
+
|
|
43
|
+
// State (persisted across syncs)
|
|
44
|
+
_config: {} as Record<string, string>,
|
|
45
|
+
// ReturnType<typeof setInterval>, not NodeJS.Timeout: this runs in React
|
|
46
|
+
// Native, where setInterval returns a number. @types/node is present in the
|
|
47
|
+
// workspace and made the Node type look right, so the annotation disagreed
|
|
48
|
+
// with every value ever assigned to it.
|
|
49
|
+
_syncTimers: new Map<string, ReturnType<typeof setInterval>>(),
|
|
50
|
+
_actionPollTimer: undefined as ReturnType<typeof setInterval> | undefined,
|
|
51
|
+
_lastSync: new Map<string, string>(),
|
|
52
|
+
_isRunning: false,
|
|
53
|
+
|
|
54
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
55
|
+
// LIFECYCLE: onStart — Request permissions and start syncing
|
|
56
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
async onStart(bridge: Bridge, config: Record<string, string>) {
|
|
59
|
+
this._config = config;
|
|
60
|
+
this._isRunning = true;
|
|
61
|
+
|
|
62
|
+
bridge.log.info('device-bridge starting', {
|
|
63
|
+
relay: config.relay_url,
|
|
64
|
+
deviceId: config.device_id,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Request permissions for all capabilities
|
|
68
|
+
const capabilities = ['calendar', 'contacts', 'location', 'notifications'];
|
|
69
|
+
for (const cap of capabilities) {
|
|
70
|
+
try {
|
|
71
|
+
const granted = await bridge.permissions.request(cap);
|
|
72
|
+
bridge.log.info(`Permission requested: ${cap} = ${granted ? 'granted' : 'denied'}`);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
bridge.log.warn(`Permission request failed for ${cap}:`, err);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Do initial sync
|
|
79
|
+
await this.onSync!(bridge);
|
|
80
|
+
|
|
81
|
+
// Start periodic sync
|
|
82
|
+
this._setupSyncTimers(bridge);
|
|
83
|
+
|
|
84
|
+
// Start action polling
|
|
85
|
+
this._startActionPoll(bridge);
|
|
86
|
+
|
|
87
|
+
bridge.log.info('device-bridge started');
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
91
|
+
// LIFECYCLE: onSync — Collect all device data and push to relay
|
|
92
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
async onSync(bridge: Bridge) {
|
|
95
|
+
if (!this._isRunning) return;
|
|
96
|
+
|
|
97
|
+
bridge.log.info('device-bridge syncing');
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
// Collect all domains in parallel
|
|
101
|
+
const results = await Promise.allSettled([
|
|
102
|
+
this._collectCalendar(bridge),
|
|
103
|
+
this._collectContacts(bridge),
|
|
104
|
+
this._collectLocation(bridge),
|
|
105
|
+
this._collectBattery(bridge),
|
|
106
|
+
this._collectNetwork(bridge),
|
|
107
|
+
this._collectNotifications(bridge),
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
// Push each domain to relay
|
|
111
|
+
const domains = [
|
|
112
|
+
{ name: 'calendar', result: results[0] },
|
|
113
|
+
{ name: 'contacts', result: results[1] },
|
|
114
|
+
{ name: 'location', result: results[2] },
|
|
115
|
+
{ name: 'battery', result: results[3] },
|
|
116
|
+
{ name: 'connectivity', result: results[4] },
|
|
117
|
+
{ name: 'notifications', result: results[5] },
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
for (const { name, result } of domains) {
|
|
121
|
+
if (result.status !== 'fulfilled' || !result.value) {
|
|
122
|
+
bridge.log.warn(`Skipping ${name} (collection failed)`);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const pushResult = await this._pushDomain(bridge, name, result.value);
|
|
127
|
+
if (pushResult.ok) {
|
|
128
|
+
this._lastSync.set(name, new Date().toISOString());
|
|
129
|
+
bridge.log.info(`Synced ${name}`);
|
|
130
|
+
} else {
|
|
131
|
+
bridge.log.error(`Failed to sync ${name}: ${pushResult.error}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
} catch (err) {
|
|
135
|
+
bridge.log.error('Sync failed', err);
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
140
|
+
// LIFECYCLE: onAction — Execute inbound actions from relay
|
|
141
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
async onAction(bridge: Bridge, action: PendingAction): Promise<ActionResult> {
|
|
144
|
+
try {
|
|
145
|
+
bridge.log.info(`Executing action: ${action.type}`, action.params);
|
|
146
|
+
|
|
147
|
+
switch (action.type) {
|
|
148
|
+
// Messaging
|
|
149
|
+
case 'messaging.send': {
|
|
150
|
+
const { phone, message } = action.params;
|
|
151
|
+
const result = await bridge.sms.send(phone, message);
|
|
152
|
+
return {
|
|
153
|
+
id: action.id,
|
|
154
|
+
ok: result.ok,
|
|
155
|
+
result: { status: result.status },
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Calendar — Create
|
|
160
|
+
case 'calendar.create': {
|
|
161
|
+
const { title, date, start_time, end_time, location, notes } = action.params;
|
|
162
|
+
const result = await bridge.calendar.create({
|
|
163
|
+
title,
|
|
164
|
+
date,
|
|
165
|
+
start_time,
|
|
166
|
+
end_time: end_time || undefined,
|
|
167
|
+
location: location || undefined,
|
|
168
|
+
notes: notes || undefined,
|
|
169
|
+
});
|
|
170
|
+
return {
|
|
171
|
+
id: action.id,
|
|
172
|
+
ok: true,
|
|
173
|
+
result,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Calendar — Reschedule
|
|
178
|
+
case 'calendar.reschedule': {
|
|
179
|
+
const result = await bridge.calendar.reschedule(action.params as any);
|
|
180
|
+
return {
|
|
181
|
+
id: action.id,
|
|
182
|
+
ok: result.ok,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Calendar — Cancel
|
|
187
|
+
case 'calendar.cancel': {
|
|
188
|
+
const { event_id } = action.params;
|
|
189
|
+
const result = await bridge.calendar.cancel(event_id);
|
|
190
|
+
return {
|
|
191
|
+
id: action.id,
|
|
192
|
+
ok: result.ok,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Navigation
|
|
197
|
+
case 'navigation.start': {
|
|
198
|
+
const { destination, mode } = action.params;
|
|
199
|
+
const result = await bridge.maps.navigate(destination, mode || 'driving');
|
|
200
|
+
return {
|
|
201
|
+
id: action.id,
|
|
202
|
+
ok: result.ok,
|
|
203
|
+
result: { status: result.status },
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// DND — Store as preference (app-level, not system-level)
|
|
208
|
+
case 'dnd.set': {
|
|
209
|
+
await bridge.storage.set('dnd-state', JSON.stringify(action.params));
|
|
210
|
+
return {
|
|
211
|
+
id: action.id,
|
|
212
|
+
ok: true,
|
|
213
|
+
result: { stored: true },
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
default:
|
|
218
|
+
return {
|
|
219
|
+
id: action.id,
|
|
220
|
+
ok: false,
|
|
221
|
+
error: `Unknown action type: ${action.type}`,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
} catch (err) {
|
|
225
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
226
|
+
return {
|
|
227
|
+
id: action.id,
|
|
228
|
+
ok: false,
|
|
229
|
+
error: message,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
235
|
+
// LIFECYCLE: onStop — Cleanup
|
|
236
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
async onStop(bridge: Bridge) {
|
|
239
|
+
bridge.log.info('device-bridge stopping');
|
|
240
|
+
this._isRunning = false;
|
|
241
|
+
|
|
242
|
+
// Clear all timers
|
|
243
|
+
for (const timer of this._syncTimers.values()) {
|
|
244
|
+
clearInterval(timer);
|
|
245
|
+
}
|
|
246
|
+
this._syncTimers.clear();
|
|
247
|
+
|
|
248
|
+
if (this._actionPollTimer) {
|
|
249
|
+
clearInterval(this._actionPollTimer);
|
|
250
|
+
this._actionPollTimer = undefined;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
bridge.log.info('device-bridge stopped');
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
257
|
+
// LIFECYCLE: onForeground — Immediate sync on app wake
|
|
258
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
async onForeground(bridge: Bridge) {
|
|
261
|
+
bridge.log.info('device-bridge foreground — immediate sync');
|
|
262
|
+
await this.onSync!(bridge);
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
266
|
+
// INTERNAL: Data Collection (uses bridge APIs only)
|
|
267
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
async _collectCalendar(bridge: Bridge) {
|
|
270
|
+
try {
|
|
271
|
+
const [today, upcoming] = await Promise.all([
|
|
272
|
+
bridge.calendar.getToday(),
|
|
273
|
+
bridge.calendar.getUpcoming(7),
|
|
274
|
+
]);
|
|
275
|
+
return { today, upcoming };
|
|
276
|
+
} catch (err) {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
async _collectContacts(bridge: Bridge) {
|
|
282
|
+
try {
|
|
283
|
+
const result = await bridge.contacts.getAll();
|
|
284
|
+
return { ok: true, contacts: result.contacts, count: result.count };
|
|
285
|
+
} catch (err) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
async _collectLocation(bridge: Bridge) {
|
|
291
|
+
try {
|
|
292
|
+
return await bridge.location.getCurrent();
|
|
293
|
+
} catch (err) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
|
|
298
|
+
async _collectBattery(bridge: Bridge) {
|
|
299
|
+
try {
|
|
300
|
+
return await bridge.battery.getStatus();
|
|
301
|
+
} catch (err) {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
|
|
306
|
+
async _collectNetwork(bridge: Bridge) {
|
|
307
|
+
try {
|
|
308
|
+
return await bridge.network.getStatus();
|
|
309
|
+
} catch (err) {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
|
|
314
|
+
async _collectNotifications(bridge: Bridge) {
|
|
315
|
+
try {
|
|
316
|
+
const result = await bridge.notifications.list();
|
|
317
|
+
return result;
|
|
318
|
+
} catch (err) {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
324
|
+
// INTERNAL: Relay Communication (uses bridge.http only)
|
|
325
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
async _pushDomain(bridge: Bridge, domain: string, data: any): Promise<RelayPushResult> {
|
|
328
|
+
try {
|
|
329
|
+
const url = `${this._config.relay_url}/api/device/${this._config.device_id}/sync/${domain}`;
|
|
330
|
+
const response = await bridge.http.post(
|
|
331
|
+
url,
|
|
332
|
+
{ data, timestamp: new Date().toISOString() },
|
|
333
|
+
{ 'X-Device-Key': this._config.api_key }
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
if (!response.ok) {
|
|
337
|
+
return { ok: false, error: `HTTP ${response.status}` };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return { ok: true };
|
|
341
|
+
} catch (err) {
|
|
342
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
343
|
+
return { ok: false, error: message };
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
|
|
347
|
+
async _getActions(bridge: Bridge): Promise<PendingAction[]> {
|
|
348
|
+
try {
|
|
349
|
+
const url = `${this._config.relay_url}/api/device/${this._config.device_id}/actions`;
|
|
350
|
+
const response = await bridge.http.get(url, {
|
|
351
|
+
'X-Device-Key': this._config.api_key,
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
if (!response.ok || !response.data?.actions) {
|
|
355
|
+
return [];
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return response.data.actions.filter((a: any) => a.status === 'pending');
|
|
359
|
+
} catch (err) {
|
|
360
|
+
return [];
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
|
|
364
|
+
async _reportActionResult(
|
|
365
|
+
bridge: Bridge,
|
|
366
|
+
result: ActionResult
|
|
367
|
+
): Promise<void> {
|
|
368
|
+
try {
|
|
369
|
+
const url = `${this._config.relay_url}/api/device/${this._config.device_id}/actions/${result.id}`;
|
|
370
|
+
await bridge.http.delete(
|
|
371
|
+
url,
|
|
372
|
+
{ ...result, executed_at: new Date().toISOString() },
|
|
373
|
+
{ 'X-Device-Key': this._config.api_key }
|
|
374
|
+
);
|
|
375
|
+
} catch (err) {
|
|
376
|
+
// Silently fail — action was executed, just couldn't report back
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
|
|
380
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
381
|
+
// INTERNAL: Timers and Polling
|
|
382
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
383
|
+
|
|
384
|
+
_setupSyncTimers(bridge: Bridge) {
|
|
385
|
+
const intervals = this._config.sync_intervals
|
|
386
|
+
? JSON.parse(this._config.sync_intervals)
|
|
387
|
+
: {};
|
|
388
|
+
|
|
389
|
+
// Default intervals (ms)
|
|
390
|
+
const defaultIntervals: Record<string, number> = {
|
|
391
|
+
calendar: 60_000,
|
|
392
|
+
contacts: 300_000,
|
|
393
|
+
location: 30_000,
|
|
394
|
+
battery: 60_000,
|
|
395
|
+
connectivity: 30_000,
|
|
396
|
+
notifications: 15_000,
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
for (const [domain, defaultInterval] of Object.entries(defaultIntervals)) {
|
|
400
|
+
const interval = intervals[domain] || defaultInterval;
|
|
401
|
+
const timer = setInterval(() => this.onSync!(bridge), interval);
|
|
402
|
+
this._syncTimers.set(domain, timer);
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
|
|
406
|
+
_startActionPoll(bridge: Bridge) {
|
|
407
|
+
const pollInterval = 5_000; // 5 seconds
|
|
408
|
+
|
|
409
|
+
this._actionPollTimer = setInterval(async () => {
|
|
410
|
+
try {
|
|
411
|
+
const actions = await this._getActions(bridge);
|
|
412
|
+
|
|
413
|
+
for (const action of actions) {
|
|
414
|
+
const result = await this.onAction!(bridge, action);
|
|
415
|
+
await this._reportActionResult(bridge, result);
|
|
416
|
+
}
|
|
417
|
+
} catch (err) {
|
|
418
|
+
bridge.log.warn('Action poll failed', err);
|
|
419
|
+
}
|
|
420
|
+
}, pollInterval);
|
|
421
|
+
},
|
|
422
|
+
};
|