@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,2084 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/device-bridge/index.ts
|
|
31
|
+
var device_bridge_exports = {};
|
|
32
|
+
__export(device_bridge_exports, {
|
|
33
|
+
BACKGROUND_TASK_NAME: () => BACKGROUND_TASK_NAME,
|
|
34
|
+
DeviceBridge: () => DeviceBridge,
|
|
35
|
+
QueuePoller: () => QueuePoller,
|
|
36
|
+
RelayClient: () => RelayClient,
|
|
37
|
+
biometrics: () => biometrics_exports,
|
|
38
|
+
capabilities: () => capabilities_exports,
|
|
39
|
+
location: () => location_exports,
|
|
40
|
+
registerBackgroundSync: () => registerBackgroundSync,
|
|
41
|
+
scanner: () => scanner_exports,
|
|
42
|
+
sharing: () => sharing_exports,
|
|
43
|
+
syncDomains: () => syncDomains,
|
|
44
|
+
unregisterBackgroundSync: () => unregisterBackgroundSync
|
|
45
|
+
});
|
|
46
|
+
module.exports = __toCommonJS(device_bridge_exports);
|
|
47
|
+
|
|
48
|
+
// src/device-bridge/relay-client.ts
|
|
49
|
+
var RelayClient = class {
|
|
50
|
+
constructor(config) {
|
|
51
|
+
this.offlineQueue = [];
|
|
52
|
+
this.baseUrl = config.relayUrl.replace(/\/$/, "");
|
|
53
|
+
this.deviceId = config.deviceId;
|
|
54
|
+
this.tenant = config.tenant;
|
|
55
|
+
this.actorId = config.actorId ?? "default";
|
|
56
|
+
this.apiKey = config.apiKey ?? "";
|
|
57
|
+
this.timeout = config.timeout ?? 1e4;
|
|
58
|
+
}
|
|
59
|
+
// ─── Data Sync ───
|
|
60
|
+
/**
|
|
61
|
+
* Push a full device data bundle to the relay.
|
|
62
|
+
* If offline, buffers the bundle and retries on next sync.
|
|
63
|
+
*/
|
|
64
|
+
async pushData(bundle) {
|
|
65
|
+
try {
|
|
66
|
+
const response = await this.fetch(
|
|
67
|
+
`/api/device/${this.deviceId}/sync`,
|
|
68
|
+
{
|
|
69
|
+
method: "POST",
|
|
70
|
+
body: JSON.stringify(bundle)
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
if (response.ok) {
|
|
74
|
+
await this.flushOfflineQueue();
|
|
75
|
+
return { ok: true };
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
ok: false,
|
|
79
|
+
error: `Relay returned ${response.status}: ${await response.text()}`
|
|
80
|
+
};
|
|
81
|
+
} catch (error) {
|
|
82
|
+
this.offlineQueue.push(bundle);
|
|
83
|
+
return {
|
|
84
|
+
ok: false,
|
|
85
|
+
error: `Offline \u2014 buffered (${this.offlineQueue.length} pending)`
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Push a single domain update (partial sync).
|
|
91
|
+
*/
|
|
92
|
+
async pushDomain(domain, data) {
|
|
93
|
+
try {
|
|
94
|
+
const response = await this.fetch(
|
|
95
|
+
`/api/device/${this.deviceId}/sync/${domain}`,
|
|
96
|
+
{
|
|
97
|
+
method: "POST",
|
|
98
|
+
body: JSON.stringify({ domain, data, timestamp: (/* @__PURE__ */ new Date()).toISOString(), actorId: this.actorId })
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
if (response.ok) return { ok: true };
|
|
102
|
+
return {
|
|
103
|
+
ok: false,
|
|
104
|
+
error: `Relay returned ${response.status}`
|
|
105
|
+
};
|
|
106
|
+
} catch (error) {
|
|
107
|
+
return { ok: false, error: "Network error \u2014 domain sync failed" };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// ─── Action Queue ───
|
|
111
|
+
/**
|
|
112
|
+
* Poll for pending actions the phone needs to execute.
|
|
113
|
+
*/
|
|
114
|
+
async pollActions() {
|
|
115
|
+
try {
|
|
116
|
+
const response = await this.fetch(
|
|
117
|
+
`/api/device/${this.deviceId}/actions`,
|
|
118
|
+
{ method: "GET" }
|
|
119
|
+
);
|
|
120
|
+
if (response.ok) {
|
|
121
|
+
const data = await response.json();
|
|
122
|
+
return { ok: true, data: data.actions ?? [] };
|
|
123
|
+
}
|
|
124
|
+
return { ok: false, error: `Relay returned ${response.status}` };
|
|
125
|
+
} catch (error) {
|
|
126
|
+
return { ok: false, error: "Network error \u2014 action poll failed" };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Report action execution result and remove from queue.
|
|
131
|
+
*/
|
|
132
|
+
async completeAction(result) {
|
|
133
|
+
try {
|
|
134
|
+
const response = await this.fetch(
|
|
135
|
+
`/api/device/${this.deviceId}/actions/${result.id}`,
|
|
136
|
+
{
|
|
137
|
+
method: "DELETE",
|
|
138
|
+
body: JSON.stringify(result)
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
if (response.ok) return { ok: true };
|
|
142
|
+
return { ok: false, error: `Relay returned ${response.status}` };
|
|
143
|
+
} catch (error) {
|
|
144
|
+
return { ok: false, error: "Network error \u2014 action complete failed" };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// ─── Health ───
|
|
148
|
+
/**
|
|
149
|
+
* Ping the relay to check connectivity.
|
|
150
|
+
*/
|
|
151
|
+
async ping() {
|
|
152
|
+
try {
|
|
153
|
+
const response = await this.fetch(
|
|
154
|
+
`/api/health`,
|
|
155
|
+
{ method: "GET" },
|
|
156
|
+
3e3
|
|
157
|
+
// short timeout for health check
|
|
158
|
+
);
|
|
159
|
+
return response.ok;
|
|
160
|
+
} catch {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Get count of buffered offline bundles.
|
|
166
|
+
*/
|
|
167
|
+
getOfflineQueueSize() {
|
|
168
|
+
return this.offlineQueue.length;
|
|
169
|
+
}
|
|
170
|
+
// ─── Internal ───
|
|
171
|
+
async fetch(path, init, timeout) {
|
|
172
|
+
const controller = new AbortController();
|
|
173
|
+
const timer = setTimeout(
|
|
174
|
+
() => controller.abort(),
|
|
175
|
+
timeout ?? this.timeout
|
|
176
|
+
);
|
|
177
|
+
try {
|
|
178
|
+
return await fetch(`${this.baseUrl}${path}`, {
|
|
179
|
+
...init,
|
|
180
|
+
signal: controller.signal,
|
|
181
|
+
headers: {
|
|
182
|
+
"Content-Type": "application/json",
|
|
183
|
+
"X-ADAS-TENANT": this.tenant,
|
|
184
|
+
"X-ADAS-ACTOR": this.actorId,
|
|
185
|
+
"X-Device-Id": this.deviceId,
|
|
186
|
+
// The relay (post-2026-04-25) gates every /api/device/* call on
|
|
187
|
+
// a shared secret. Send it as `Authorization: Bearer` so the
|
|
188
|
+
// server's relayAuth middleware accepts the request. Empty/missing
|
|
189
|
+
// secret = 401 from the relay (correct fail-closed behavior).
|
|
190
|
+
...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {},
|
|
191
|
+
...init.headers || {}
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
} finally {
|
|
195
|
+
clearTimeout(timer);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
async flushOfflineQueue() {
|
|
199
|
+
if (this.offlineQueue.length === 0) return;
|
|
200
|
+
const queue = [...this.offlineQueue];
|
|
201
|
+
this.offlineQueue = [];
|
|
202
|
+
for (const bundle of queue) {
|
|
203
|
+
try {
|
|
204
|
+
const response = await this.fetch(
|
|
205
|
+
`/api/device/${this.deviceId}/sync`,
|
|
206
|
+
{
|
|
207
|
+
method: "POST",
|
|
208
|
+
body: JSON.stringify(bundle)
|
|
209
|
+
}
|
|
210
|
+
);
|
|
211
|
+
if (!response.ok) {
|
|
212
|
+
this.offlineQueue.push(bundle);
|
|
213
|
+
}
|
|
214
|
+
} catch {
|
|
215
|
+
this.offlineQueue.push(bundle);
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// src/device-bridge/actions/messaging.ts
|
|
223
|
+
var SMS = __toESM(require("expo-sms"));
|
|
224
|
+
async function execute(actionId, params) {
|
|
225
|
+
const phone = params.phone;
|
|
226
|
+
const message = params.message;
|
|
227
|
+
if (!phone || !message) {
|
|
228
|
+
return {
|
|
229
|
+
id: actionId,
|
|
230
|
+
ok: false,
|
|
231
|
+
error: "Missing phone or message parameter",
|
|
232
|
+
executed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
const available = await SMS.isAvailableAsync();
|
|
237
|
+
if (!available) {
|
|
238
|
+
return {
|
|
239
|
+
id: actionId,
|
|
240
|
+
ok: false,
|
|
241
|
+
error: "SMS not available on this device",
|
|
242
|
+
executed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
const { result } = await SMS.sendSMSAsync([phone], message);
|
|
246
|
+
return {
|
|
247
|
+
id: actionId,
|
|
248
|
+
ok: result === "sent" || result === "unknown",
|
|
249
|
+
// 'unknown' on Android means it was sent
|
|
250
|
+
result: {
|
|
251
|
+
to: params.contact_name || phone,
|
|
252
|
+
phone,
|
|
253
|
+
message,
|
|
254
|
+
status: result,
|
|
255
|
+
sent_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
256
|
+
},
|
|
257
|
+
executed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
258
|
+
};
|
|
259
|
+
} catch (error) {
|
|
260
|
+
return {
|
|
261
|
+
id: actionId,
|
|
262
|
+
ok: false,
|
|
263
|
+
error: `SMS send failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
264
|
+
executed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// src/device-bridge/actions/calendar-actions.ts
|
|
270
|
+
var ExpoCalendar = __toESM(require("expo-calendar"));
|
|
271
|
+
async function getDefaultCalendarId() {
|
|
272
|
+
const calendars = await ExpoCalendar.getCalendarsAsync(ExpoCalendar.EntityTypes.EVENT);
|
|
273
|
+
const defaultCal = calendars.find((c) => c.isPrimary) || calendars[0];
|
|
274
|
+
return defaultCal?.id || null;
|
|
275
|
+
}
|
|
276
|
+
async function executeCreate(actionId, params) {
|
|
277
|
+
const title = params.title;
|
|
278
|
+
const date = params.date;
|
|
279
|
+
const startTime = params.start_time;
|
|
280
|
+
const endTime = params.end_time;
|
|
281
|
+
if (!title || !date || !startTime) {
|
|
282
|
+
return {
|
|
283
|
+
id: actionId,
|
|
284
|
+
ok: false,
|
|
285
|
+
error: "Missing title, date, or start_time",
|
|
286
|
+
executed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
const calendarId = await getDefaultCalendarId();
|
|
291
|
+
if (!calendarId) {
|
|
292
|
+
return {
|
|
293
|
+
id: actionId,
|
|
294
|
+
ok: false,
|
|
295
|
+
error: "No calendar found on device",
|
|
296
|
+
executed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
const [year, month, day] = date.split("-").map(Number);
|
|
300
|
+
const [startH, startM] = startTime.split(":").map(Number);
|
|
301
|
+
const startDate = new Date(year, month - 1, day, startH, startM);
|
|
302
|
+
let endDate;
|
|
303
|
+
if (endTime) {
|
|
304
|
+
const [endH, endM] = endTime.split(":").map(Number);
|
|
305
|
+
endDate = new Date(year, month - 1, day, endH, endM);
|
|
306
|
+
} else {
|
|
307
|
+
endDate = new Date(startDate.getTime() + 60 * 60 * 1e3);
|
|
308
|
+
}
|
|
309
|
+
const eventId = await ExpoCalendar.createEventAsync(calendarId, {
|
|
310
|
+
title,
|
|
311
|
+
startDate,
|
|
312
|
+
endDate,
|
|
313
|
+
location: params.location || void 0,
|
|
314
|
+
notes: params.notes || void 0,
|
|
315
|
+
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
316
|
+
});
|
|
317
|
+
return {
|
|
318
|
+
id: actionId,
|
|
319
|
+
ok: true,
|
|
320
|
+
result: {
|
|
321
|
+
event_id: eventId,
|
|
322
|
+
title,
|
|
323
|
+
date,
|
|
324
|
+
start: startTime,
|
|
325
|
+
end: endTime || formatTime(endDate),
|
|
326
|
+
message: `Event "${title}" created successfully`
|
|
327
|
+
},
|
|
328
|
+
executed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
329
|
+
};
|
|
330
|
+
} catch (error) {
|
|
331
|
+
return {
|
|
332
|
+
id: actionId,
|
|
333
|
+
ok: false,
|
|
334
|
+
error: `Calendar create failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
335
|
+
executed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
async function executeReschedule(actionId, params) {
|
|
340
|
+
const eventId = params.event_id;
|
|
341
|
+
if (!eventId) {
|
|
342
|
+
return {
|
|
343
|
+
id: actionId,
|
|
344
|
+
ok: false,
|
|
345
|
+
error: "Missing event_id",
|
|
346
|
+
executed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
try {
|
|
350
|
+
const updates = {};
|
|
351
|
+
if (params.new_date && params.new_start_time) {
|
|
352
|
+
const [year, month, day] = params.new_date.split("-").map(Number);
|
|
353
|
+
const [h, m] = params.new_start_time.split(":").map(Number);
|
|
354
|
+
updates.startDate = new Date(year, month - 1, day, h, m);
|
|
355
|
+
if (params.new_end_time) {
|
|
356
|
+
const [eh, em] = params.new_end_time.split(":").map(Number);
|
|
357
|
+
updates.endDate = new Date(year, month - 1, day, eh, em);
|
|
358
|
+
} else {
|
|
359
|
+
updates.endDate = new Date(updates.startDate.getTime() + 60 * 60 * 1e3);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
await ExpoCalendar.updateEventAsync(eventId, updates);
|
|
363
|
+
return {
|
|
364
|
+
id: actionId,
|
|
365
|
+
ok: true,
|
|
366
|
+
result: { event_id: eventId, message: "Event rescheduled successfully" },
|
|
367
|
+
executed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
368
|
+
};
|
|
369
|
+
} catch (error) {
|
|
370
|
+
return {
|
|
371
|
+
id: actionId,
|
|
372
|
+
ok: false,
|
|
373
|
+
error: `Calendar reschedule failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
374
|
+
executed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
async function executeCancel(actionId, params) {
|
|
379
|
+
const eventId = params.event_id;
|
|
380
|
+
if (!eventId) {
|
|
381
|
+
return {
|
|
382
|
+
id: actionId,
|
|
383
|
+
ok: false,
|
|
384
|
+
error: "Missing event_id",
|
|
385
|
+
executed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
try {
|
|
389
|
+
await ExpoCalendar.deleteEventAsync(eventId);
|
|
390
|
+
return {
|
|
391
|
+
id: actionId,
|
|
392
|
+
ok: true,
|
|
393
|
+
result: { event_id: eventId, message: "Event cancelled successfully" },
|
|
394
|
+
executed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
395
|
+
};
|
|
396
|
+
} catch (error) {
|
|
397
|
+
return {
|
|
398
|
+
id: actionId,
|
|
399
|
+
ok: false,
|
|
400
|
+
error: `Calendar cancel failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
401
|
+
executed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
function formatTime(date) {
|
|
406
|
+
return date.toLocaleTimeString("en-US", {
|
|
407
|
+
hour: "2-digit",
|
|
408
|
+
minute: "2-digit",
|
|
409
|
+
hour12: false
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// src/device-bridge/actions/dnd.ts
|
|
414
|
+
var currentDndState = {
|
|
415
|
+
enabled: false,
|
|
416
|
+
schedule: { start: "22:00", end: "07:00" },
|
|
417
|
+
allowlist: []
|
|
418
|
+
};
|
|
419
|
+
async function execute2(actionId, params) {
|
|
420
|
+
try {
|
|
421
|
+
if (params.enabled !== void 0) {
|
|
422
|
+
currentDndState.enabled = params.enabled;
|
|
423
|
+
}
|
|
424
|
+
if (params.schedule_start) {
|
|
425
|
+
currentDndState.schedule.start = params.schedule_start;
|
|
426
|
+
}
|
|
427
|
+
if (params.schedule_end) {
|
|
428
|
+
currentDndState.schedule.end = params.schedule_end;
|
|
429
|
+
}
|
|
430
|
+
if (params.allowlist) {
|
|
431
|
+
currentDndState.allowlist = params.allowlist;
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
id: actionId,
|
|
435
|
+
ok: true,
|
|
436
|
+
result: {
|
|
437
|
+
message: `DND ${currentDndState.enabled ? "enabled" : "disabled"}`,
|
|
438
|
+
dnd: currentDndState
|
|
439
|
+
},
|
|
440
|
+
executed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
441
|
+
};
|
|
442
|
+
} catch (error) {
|
|
443
|
+
return {
|
|
444
|
+
id: actionId,
|
|
445
|
+
ok: false,
|
|
446
|
+
error: `DND set failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
447
|
+
executed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// src/device-bridge/actions/navigation.ts
|
|
453
|
+
var Linking = __toESM(require("expo-linking"));
|
|
454
|
+
var import_react_native = require("react-native");
|
|
455
|
+
async function execute3(actionId, params) {
|
|
456
|
+
const destination = params.destination;
|
|
457
|
+
const mode = params.mode || "driving";
|
|
458
|
+
if (!destination) {
|
|
459
|
+
return {
|
|
460
|
+
id: actionId,
|
|
461
|
+
ok: false,
|
|
462
|
+
error: "Missing destination parameter",
|
|
463
|
+
executed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
try {
|
|
467
|
+
const encodedDest = encodeURIComponent(destination);
|
|
468
|
+
let url;
|
|
469
|
+
if (import_react_native.Platform.OS === "ios") {
|
|
470
|
+
const modeMap = {
|
|
471
|
+
driving: "d",
|
|
472
|
+
transit: "r",
|
|
473
|
+
walking: "w"
|
|
474
|
+
};
|
|
475
|
+
url = `maps://maps.apple.com/?daddr=${encodedDest}&dirflg=${modeMap[mode] || "d"}`;
|
|
476
|
+
} else {
|
|
477
|
+
const modeMap = {
|
|
478
|
+
driving: "driving",
|
|
479
|
+
transit: "transit",
|
|
480
|
+
walking: "walking"
|
|
481
|
+
};
|
|
482
|
+
url = `google.navigation:q=${encodedDest}&mode=${modeMap[mode] || "driving"}`;
|
|
483
|
+
}
|
|
484
|
+
const canOpen = await Linking.canOpenURL(url);
|
|
485
|
+
if (!canOpen) {
|
|
486
|
+
const webModeMap = {
|
|
487
|
+
driving: "driving",
|
|
488
|
+
transit: "transit",
|
|
489
|
+
walking: "walking"
|
|
490
|
+
};
|
|
491
|
+
url = `https://www.google.com/maps/dir/?api=1&destination=${encodedDest}&travelmode=${webModeMap[mode] || "driving"}`;
|
|
492
|
+
}
|
|
493
|
+
await Linking.openURL(url);
|
|
494
|
+
return {
|
|
495
|
+
id: actionId,
|
|
496
|
+
ok: true,
|
|
497
|
+
result: {
|
|
498
|
+
message: `Navigation started to ${destination} via ${mode}`,
|
|
499
|
+
destination,
|
|
500
|
+
mode,
|
|
501
|
+
status: "navigating"
|
|
502
|
+
},
|
|
503
|
+
executed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
504
|
+
};
|
|
505
|
+
} catch (error) {
|
|
506
|
+
return {
|
|
507
|
+
id: actionId,
|
|
508
|
+
ok: false,
|
|
509
|
+
error: `Navigation failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
510
|
+
executed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// src/device-bridge/collectors/health.ts
|
|
516
|
+
var import_react_native2 = require("react-native");
|
|
517
|
+
var _HK = null;
|
|
518
|
+
var _hkProbed = false;
|
|
519
|
+
function getHK() {
|
|
520
|
+
if (import_react_native2.Platform.OS !== "ios") return null;
|
|
521
|
+
if (_hkProbed) return _HK;
|
|
522
|
+
_hkProbed = true;
|
|
523
|
+
try {
|
|
524
|
+
const mod = require("@kingstinct/react-native-healthkit");
|
|
525
|
+
if (mod && typeof mod.requestAuthorization === "function") {
|
|
526
|
+
_HK = mod;
|
|
527
|
+
} else {
|
|
528
|
+
console.warn("[health] @kingstinct/react-native-healthkit native bridge unavailable \u2014 HealthKit disabled (Expo Go?)");
|
|
529
|
+
}
|
|
530
|
+
} catch (e) {
|
|
531
|
+
console.warn("[health] @kingstinct/react-native-healthkit require failed:", e);
|
|
532
|
+
}
|
|
533
|
+
return _HK;
|
|
534
|
+
}
|
|
535
|
+
var lastHealthInitError = null;
|
|
536
|
+
var READ_TYPES = [
|
|
537
|
+
"HKQuantityTypeIdentifierStepCount",
|
|
538
|
+
"HKQuantityTypeIdentifierFlightsClimbed",
|
|
539
|
+
"HKQuantityTypeIdentifierActiveEnergyBurned",
|
|
540
|
+
"HKQuantityTypeIdentifierDistanceWalkingRunning",
|
|
541
|
+
// Added for mood signals — all read-only, covered by the same auth sheet.
|
|
542
|
+
"HKQuantityTypeIdentifierRestingHeartRate",
|
|
543
|
+
"HKQuantityTypeIdentifierAppleExerciseTime",
|
|
544
|
+
"HKCategoryTypeIdentifierSleepAnalysis"
|
|
545
|
+
];
|
|
546
|
+
var _initPromise = null;
|
|
547
|
+
async function ensureInit() {
|
|
548
|
+
const HK = getHK();
|
|
549
|
+
if (!HK) {
|
|
550
|
+
lastHealthInitError = "@kingstinct/react-native-healthkit native module not loaded";
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
if (_initPromise) return _initPromise;
|
|
554
|
+
_initPromise = (async () => {
|
|
555
|
+
try {
|
|
556
|
+
await HK.requestAuthorization({ toRead: READ_TYPES, toWrite: [] });
|
|
557
|
+
lastHealthInitError = null;
|
|
558
|
+
return true;
|
|
559
|
+
} catch (e) {
|
|
560
|
+
lastHealthInitError = `requestAuthorization threw: ${e?.message || String(e)}`;
|
|
561
|
+
console.warn("[health]", lastHealthInitError);
|
|
562
|
+
_initPromise = null;
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
})();
|
|
566
|
+
return _initPromise;
|
|
567
|
+
}
|
|
568
|
+
async function hasPermission() {
|
|
569
|
+
return ensureInit();
|
|
570
|
+
}
|
|
571
|
+
async function requestPermission() {
|
|
572
|
+
return ensureInit();
|
|
573
|
+
}
|
|
574
|
+
function startOfDay(d) {
|
|
575
|
+
const out = new Date(d);
|
|
576
|
+
out.setHours(0, 0, 0, 0);
|
|
577
|
+
return out;
|
|
578
|
+
}
|
|
579
|
+
function dateKey(d) {
|
|
580
|
+
const y = d.getFullYear();
|
|
581
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
582
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
583
|
+
return `${y}-${m}-${day}`;
|
|
584
|
+
}
|
|
585
|
+
async function readDailySum(identifier, unit, start, end) {
|
|
586
|
+
const HK = getHK();
|
|
587
|
+
if (!HK?.queryStatisticsForQuantity) return 0;
|
|
588
|
+
try {
|
|
589
|
+
const result = await HK.queryStatisticsForQuantity(
|
|
590
|
+
identifier,
|
|
591
|
+
["cumulativeSum"],
|
|
592
|
+
{
|
|
593
|
+
unit,
|
|
594
|
+
filter: { date: { startDate: start, endDate: end } }
|
|
595
|
+
}
|
|
596
|
+
);
|
|
597
|
+
const q = result?.sumQuantity?.quantity;
|
|
598
|
+
return typeof q === "number" ? q : 0;
|
|
599
|
+
} catch {
|
|
600
|
+
return 0;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
async function readDailyAverage(identifier, unit, start, end) {
|
|
604
|
+
const HK = getHK();
|
|
605
|
+
if (!HK?.queryStatisticsForQuantity) return null;
|
|
606
|
+
try {
|
|
607
|
+
const result = await HK.queryStatisticsForQuantity(
|
|
608
|
+
identifier,
|
|
609
|
+
["discreteAverage"],
|
|
610
|
+
{ unit, filter: { date: { startDate: start, endDate: end } } }
|
|
611
|
+
);
|
|
612
|
+
const q = result?.averageQuantity?.quantity;
|
|
613
|
+
return typeof q === "number" ? q : null;
|
|
614
|
+
} catch {
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
async function readSleepMinutes(start, end) {
|
|
619
|
+
const HK = getHK();
|
|
620
|
+
const query = HK?.queryCategorySamples || HK?.queryCategorySamplesAsync || HK?.querySamples;
|
|
621
|
+
if (!query) return null;
|
|
622
|
+
try {
|
|
623
|
+
const samples = await query.call(
|
|
624
|
+
HK,
|
|
625
|
+
"HKCategoryTypeIdentifierSleepAnalysis",
|
|
626
|
+
{ filter: { date: { startDate: start, endDate: end } } }
|
|
627
|
+
);
|
|
628
|
+
if (!Array.isArray(samples) || samples.length === 0) return null;
|
|
629
|
+
let ms = 0;
|
|
630
|
+
for (const s of samples) {
|
|
631
|
+
const v = s?.value;
|
|
632
|
+
const isAsleep = v !== 0 && v !== 2;
|
|
633
|
+
if (!isAsleep) continue;
|
|
634
|
+
const a = new Date(s?.startDate).getTime();
|
|
635
|
+
const b = new Date(s?.endDate).getTime();
|
|
636
|
+
if (Number.isFinite(a) && Number.isFinite(b) && b > a) ms += b - a;
|
|
637
|
+
}
|
|
638
|
+
return ms > 0 ? Math.round(ms / 6e4) : null;
|
|
639
|
+
} catch {
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
async function collectMoodExtras() {
|
|
644
|
+
const ok = await ensureInit();
|
|
645
|
+
if (!ok) return { sleep_min: null, resting_hr: null, active_energy_kcal: null, exercise_min: null };
|
|
646
|
+
const start = startOfDay(/* @__PURE__ */ new Date());
|
|
647
|
+
const end = new Date(start);
|
|
648
|
+
end.setDate(end.getDate() + 1);
|
|
649
|
+
const sleepStart = new Date(start);
|
|
650
|
+
sleepStart.setHours(sleepStart.getHours() - 6);
|
|
651
|
+
const [sleep_min, resting_hr, energy, exercise] = await Promise.all([
|
|
652
|
+
readSleepMinutes(sleepStart, end),
|
|
653
|
+
readDailyAverage("HKQuantityTypeIdentifierRestingHeartRate", "count/min", start, end),
|
|
654
|
+
readDailySum("HKQuantityTypeIdentifierActiveEnergyBurned", "kcal", start, end),
|
|
655
|
+
readDailySum("HKQuantityTypeIdentifierAppleExerciseTime", "min", start, end)
|
|
656
|
+
]);
|
|
657
|
+
return {
|
|
658
|
+
sleep_min,
|
|
659
|
+
resting_hr: resting_hr != null ? Math.round(resting_hr) : null,
|
|
660
|
+
active_energy_kcal: energy ? Math.round(energy) : null,
|
|
661
|
+
exercise_min: exercise ? Math.round(exercise) : null
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
async function readDay(date) {
|
|
665
|
+
const start = startOfDay(date);
|
|
666
|
+
const end = new Date(start);
|
|
667
|
+
end.setDate(end.getDate() + 1);
|
|
668
|
+
const [steps, floors, energy, distance] = await Promise.all([
|
|
669
|
+
readDailySum("HKQuantityTypeIdentifierStepCount", "count", start, end),
|
|
670
|
+
readDailySum("HKQuantityTypeIdentifierFlightsClimbed", "count", start, end),
|
|
671
|
+
readDailySum("HKQuantityTypeIdentifierActiveEnergyBurned", "kcal", start, end),
|
|
672
|
+
// Ask for kilometers directly so we don't have to convert from meters.
|
|
673
|
+
readDailySum("HKQuantityTypeIdentifierDistanceWalkingRunning", "km", start, end)
|
|
674
|
+
]);
|
|
675
|
+
return {
|
|
676
|
+
date: dateKey(start),
|
|
677
|
+
available: true,
|
|
678
|
+
steps: Math.round(steps),
|
|
679
|
+
floors: Math.round(floors),
|
|
680
|
+
active_energy_kcal: Math.round(energy),
|
|
681
|
+
distance_km: Math.round(distance * 100) / 100
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
function unavailableDay(date) {
|
|
685
|
+
return {
|
|
686
|
+
date: dateKey(date),
|
|
687
|
+
available: false,
|
|
688
|
+
steps: null,
|
|
689
|
+
floors: null,
|
|
690
|
+
active_energy_kcal: null,
|
|
691
|
+
distance_km: null
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
async function collectToday() {
|
|
695
|
+
const ok = await ensureInit();
|
|
696
|
+
if (!ok) {
|
|
697
|
+
return unavailableDay(/* @__PURE__ */ new Date());
|
|
698
|
+
}
|
|
699
|
+
return readDay(/* @__PURE__ */ new Date());
|
|
700
|
+
}
|
|
701
|
+
async function collectHistory(days) {
|
|
702
|
+
const n = Math.max(1, Math.min(30, Math.floor(days)));
|
|
703
|
+
const ok = await ensureInit();
|
|
704
|
+
if (!ok) return [];
|
|
705
|
+
const today = startOfDay(/* @__PURE__ */ new Date());
|
|
706
|
+
const out = [];
|
|
707
|
+
for (let i = 0; i < n; i++) {
|
|
708
|
+
const d = new Date(today);
|
|
709
|
+
d.setDate(d.getDate() - i);
|
|
710
|
+
out.push(await readDay(d));
|
|
711
|
+
}
|
|
712
|
+
return out;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// src/device-bridge/actions/health-actions.ts
|
|
716
|
+
async function executeToday(actionId) {
|
|
717
|
+
try {
|
|
718
|
+
const today = await collectToday();
|
|
719
|
+
return {
|
|
720
|
+
id: actionId,
|
|
721
|
+
ok: true,
|
|
722
|
+
result: { today },
|
|
723
|
+
executed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
724
|
+
};
|
|
725
|
+
} catch (e) {
|
|
726
|
+
return {
|
|
727
|
+
id: actionId,
|
|
728
|
+
ok: false,
|
|
729
|
+
error: String(e?.message || e),
|
|
730
|
+
executed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
async function executeHistory(actionId, params) {
|
|
735
|
+
try {
|
|
736
|
+
const days = Number(params?.days) || 30;
|
|
737
|
+
const list = await collectHistory(days);
|
|
738
|
+
return {
|
|
739
|
+
id: actionId,
|
|
740
|
+
ok: true,
|
|
741
|
+
result: { days: list },
|
|
742
|
+
executed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
743
|
+
};
|
|
744
|
+
} catch (e) {
|
|
745
|
+
return {
|
|
746
|
+
id: actionId,
|
|
747
|
+
ok: false,
|
|
748
|
+
error: String(e?.message || e),
|
|
749
|
+
executed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// src/device-bridge/actions/queue-poller.ts
|
|
755
|
+
var QueuePoller = class {
|
|
756
|
+
constructor(config) {
|
|
757
|
+
this.timer = null;
|
|
758
|
+
this.running = false;
|
|
759
|
+
this.relayClient = config.relayClient;
|
|
760
|
+
this.pollInterval = config.pollInterval ?? 5e3;
|
|
761
|
+
this.onActionExecuted = config.onActionExecuted;
|
|
762
|
+
this.onError = config.onError;
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Start polling for actions.
|
|
766
|
+
*/
|
|
767
|
+
start() {
|
|
768
|
+
if (this.running) return;
|
|
769
|
+
this.running = true;
|
|
770
|
+
this.poll();
|
|
771
|
+
this.timer = setInterval(() => this.poll(), this.pollInterval);
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Stop polling.
|
|
775
|
+
*/
|
|
776
|
+
stop() {
|
|
777
|
+
this.running = false;
|
|
778
|
+
if (this.timer) {
|
|
779
|
+
clearInterval(this.timer);
|
|
780
|
+
this.timer = null;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Check if poller is running.
|
|
785
|
+
*/
|
|
786
|
+
isRunning() {
|
|
787
|
+
return this.running;
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Execute a single poll cycle.
|
|
791
|
+
*/
|
|
792
|
+
async poll() {
|
|
793
|
+
if (!this.running) return;
|
|
794
|
+
try {
|
|
795
|
+
const response = await this.relayClient.pollActions();
|
|
796
|
+
if (!response.ok || !response.data) return;
|
|
797
|
+
for (const action of response.data) {
|
|
798
|
+
if (action.status && action.status !== "pending") continue;
|
|
799
|
+
const result = await this.executeAction(action);
|
|
800
|
+
await this.relayClient.completeAction(result);
|
|
801
|
+
this.onActionExecuted?.(result);
|
|
802
|
+
}
|
|
803
|
+
} catch (error) {
|
|
804
|
+
this.onError?.(
|
|
805
|
+
error instanceof Error ? error : new Error(String(error))
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Route an action to the appropriate executor.
|
|
811
|
+
*/
|
|
812
|
+
async executeAction(action) {
|
|
813
|
+
const { id, type, params } = action;
|
|
814
|
+
try {
|
|
815
|
+
switch (type) {
|
|
816
|
+
case "messaging.send":
|
|
817
|
+
return await execute(id, params);
|
|
818
|
+
case "calendar.create":
|
|
819
|
+
return await executeCreate(id, params);
|
|
820
|
+
case "calendar.reschedule":
|
|
821
|
+
return await executeReschedule(id, params);
|
|
822
|
+
case "calendar.cancel":
|
|
823
|
+
return await executeCancel(id, params);
|
|
824
|
+
case "dnd.set":
|
|
825
|
+
return await execute2(id, params);
|
|
826
|
+
case "navigation.start":
|
|
827
|
+
return await execute3(id, params);
|
|
828
|
+
case "health.today":
|
|
829
|
+
return await executeToday(id);
|
|
830
|
+
case "health.history":
|
|
831
|
+
return await executeHistory(id, params);
|
|
832
|
+
default:
|
|
833
|
+
return {
|
|
834
|
+
id,
|
|
835
|
+
ok: false,
|
|
836
|
+
error: `Unknown action type: ${type}`,
|
|
837
|
+
executed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
} catch (error) {
|
|
841
|
+
return {
|
|
842
|
+
id,
|
|
843
|
+
ok: false,
|
|
844
|
+
error: `Action execution failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
845
|
+
executed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
|
|
851
|
+
// src/device-bridge/background.ts
|
|
852
|
+
var TaskManager = __toESM(require("expo-task-manager"));
|
|
853
|
+
var BackgroundFetch = __toESM(require("expo-background-fetch"));
|
|
854
|
+
|
|
855
|
+
// src/device-bridge/capabilities.ts
|
|
856
|
+
var capabilities_exports = {};
|
|
857
|
+
__export(capabilities_exports, {
|
|
858
|
+
allBuiltIn: () => allBuiltIn,
|
|
859
|
+
batteryCapability: () => batteryCapability,
|
|
860
|
+
calendarCapability: () => calendarCapability,
|
|
861
|
+
cameraCapability: () => cameraCapability,
|
|
862
|
+
connectivityCapability: () => connectivityCapability,
|
|
863
|
+
contactsCapability: () => contactsCapability,
|
|
864
|
+
healthCapability: () => healthCapability,
|
|
865
|
+
locationCapability: () => locationCapability,
|
|
866
|
+
moodCapability: () => moodCapability,
|
|
867
|
+
notificationsCapability: () => notificationsCapability
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
// src/device-bridge/collectors/calendar.ts
|
|
871
|
+
var ExpoCalendar2 = __toESM(require("expo-calendar"));
|
|
872
|
+
async function requestPermission2() {
|
|
873
|
+
const { status } = await ExpoCalendar2.requestCalendarPermissionsAsync();
|
|
874
|
+
return status === "granted";
|
|
875
|
+
}
|
|
876
|
+
async function hasPermission2() {
|
|
877
|
+
const { status } = await ExpoCalendar2.getCalendarPermissionsAsync();
|
|
878
|
+
return status === "granted";
|
|
879
|
+
}
|
|
880
|
+
async function collectToday2() {
|
|
881
|
+
const now = /* @__PURE__ */ new Date();
|
|
882
|
+
const startOfDay2 = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
883
|
+
const endOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59);
|
|
884
|
+
const calendars = await ExpoCalendar2.getCalendarsAsync(ExpoCalendar2.EntityTypes.EVENT);
|
|
885
|
+
const userCalendarIds = getUserCalendarIds(calendars);
|
|
886
|
+
const events = await ExpoCalendar2.getEventsAsync(userCalendarIds, startOfDay2, endOfDay);
|
|
887
|
+
const filtered = filterHolidays(events);
|
|
888
|
+
const mapped = filtered.map((e) => ({
|
|
889
|
+
id: e.id,
|
|
890
|
+
title: e.title || "Untitled",
|
|
891
|
+
date: startOfDay2.toISOString().slice(0, 10),
|
|
892
|
+
start: formatTime2(new Date(e.startDate)),
|
|
893
|
+
end: formatTime2(new Date(e.endDate)),
|
|
894
|
+
location: e.location || "",
|
|
895
|
+
attendees: (e.attendees || []).map((a) => a.name || a.email || "Unknown"),
|
|
896
|
+
notes: e.notes || ""
|
|
897
|
+
}));
|
|
898
|
+
mapped.sort((a, b) => a.start.localeCompare(b.start));
|
|
899
|
+
return {
|
|
900
|
+
ok: true,
|
|
901
|
+
date: startOfDay2.toISOString().slice(0, 10),
|
|
902
|
+
day_of_week: now.toLocaleDateString("en-US", { weekday: "long" }),
|
|
903
|
+
event_count: mapped.length,
|
|
904
|
+
events: mapped
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
async function collectUpcoming(daysAhead = 3) {
|
|
908
|
+
const now = /* @__PURE__ */ new Date();
|
|
909
|
+
const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
|
|
910
|
+
const endDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() + daysAhead + 1);
|
|
911
|
+
const calendars = await ExpoCalendar2.getCalendarsAsync(ExpoCalendar2.EntityTypes.EVENT);
|
|
912
|
+
const userCalendarIds = getUserCalendarIds(calendars);
|
|
913
|
+
const events = await ExpoCalendar2.getEventsAsync(userCalendarIds, tomorrow, endDate);
|
|
914
|
+
const filtered = filterHolidays(events);
|
|
915
|
+
const mapped = filtered.map((e) => {
|
|
916
|
+
const startDt = new Date(e.startDate);
|
|
917
|
+
return {
|
|
918
|
+
id: e.id,
|
|
919
|
+
title: e.title || "Untitled",
|
|
920
|
+
date: startDt.toISOString().slice(0, 10),
|
|
921
|
+
start: formatTime2(startDt),
|
|
922
|
+
end: formatTime2(new Date(e.endDate)),
|
|
923
|
+
location: e.location || "",
|
|
924
|
+
attendees: (e.attendees || []).map((a) => a.name || a.email || "Unknown"),
|
|
925
|
+
notes: e.notes || ""
|
|
926
|
+
};
|
|
927
|
+
});
|
|
928
|
+
mapped.sort(
|
|
929
|
+
(a, b) => a.date === b.date ? a.start.localeCompare(b.start) : a.date.localeCompare(b.date)
|
|
930
|
+
);
|
|
931
|
+
return {
|
|
932
|
+
ok: true,
|
|
933
|
+
from: tomorrow.toISOString().slice(0, 10),
|
|
934
|
+
days_ahead: daysAhead,
|
|
935
|
+
event_count: mapped.length,
|
|
936
|
+
events: mapped
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
function getUserCalendarIds(calendars) {
|
|
940
|
+
const SKIP_SOURCES = ["birthdays", "other"];
|
|
941
|
+
const SKIP_TYPES = ["birthdays", "other"];
|
|
942
|
+
return calendars.filter((c) => {
|
|
943
|
+
const sourceType = (c.source?.type || "").toLowerCase();
|
|
944
|
+
const calType = (c.type || "").toLowerCase();
|
|
945
|
+
if (SKIP_SOURCES.includes(sourceType)) return false;
|
|
946
|
+
if (SKIP_TYPES.includes(calType)) return false;
|
|
947
|
+
return true;
|
|
948
|
+
}).map((c) => c.id);
|
|
949
|
+
}
|
|
950
|
+
function filterHolidays(events) {
|
|
951
|
+
const HOLIDAY_PATTERNS = [
|
|
952
|
+
/holiday/i,
|
|
953
|
+
/observance/i,
|
|
954
|
+
/eve\b.*\btentative\b/i,
|
|
955
|
+
/\btentative\b/i
|
|
956
|
+
];
|
|
957
|
+
return events.filter((e) => {
|
|
958
|
+
const isAllDay = e.allDay === true;
|
|
959
|
+
const notes = (e.notes || "").toLowerCase();
|
|
960
|
+
if (isAllDay && HOLIDAY_PATTERNS.some((p) => p.test(notes))) return false;
|
|
961
|
+
if (isAllDay && HOLIDAY_PATTERNS.some((p) => p.test(e.title || ""))) return false;
|
|
962
|
+
return true;
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
function formatTime2(date) {
|
|
966
|
+
return date.toLocaleTimeString("en-US", {
|
|
967
|
+
hour: "2-digit",
|
|
968
|
+
minute: "2-digit",
|
|
969
|
+
hour12: false
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// src/device-bridge/collectors/contacts.ts
|
|
974
|
+
var ExpoContacts = __toESM(require("expo-contacts"));
|
|
975
|
+
var import_async_storage = __toESM(require("@react-native-async-storage/async-storage"));
|
|
976
|
+
var HASH_STORE_KEY = "device-bridge.contacts.hashes.v1";
|
|
977
|
+
async function requestPermission3() {
|
|
978
|
+
const { status } = await ExpoContacts.requestPermissionsAsync();
|
|
979
|
+
return status === "granted";
|
|
980
|
+
}
|
|
981
|
+
async function hasPermission3() {
|
|
982
|
+
const { status } = await ExpoContacts.getPermissionsAsync();
|
|
983
|
+
return status === "granted";
|
|
984
|
+
}
|
|
985
|
+
var lastContactsStats = null;
|
|
986
|
+
async function collectAll() {
|
|
987
|
+
const result = await ExpoContacts.getContactsAsync({
|
|
988
|
+
fields: [
|
|
989
|
+
ExpoContacts.Fields.Name,
|
|
990
|
+
ExpoContacts.Fields.PhoneNumbers,
|
|
991
|
+
ExpoContacts.Fields.Emails,
|
|
992
|
+
ExpoContacts.Fields.Birthday
|
|
993
|
+
],
|
|
994
|
+
sort: ExpoContacts.SortTypes.LastName
|
|
995
|
+
});
|
|
996
|
+
const raw = Array.isArray(result?.data) ? result.data : [];
|
|
997
|
+
const contacts = [];
|
|
998
|
+
const currentHashes = {};
|
|
999
|
+
for (let i = 0; i < raw.length; i++) {
|
|
1000
|
+
const c = raw[i];
|
|
1001
|
+
const id = c.id || `c${i}`;
|
|
1002
|
+
const mapped = {
|
|
1003
|
+
id,
|
|
1004
|
+
name: c.name || `${c.firstName || ""} ${c.lastName || ""}`.trim() || "Unknown",
|
|
1005
|
+
relationship: "",
|
|
1006
|
+
// dropped — was the iOS Unauthorized-Keys trigger
|
|
1007
|
+
phone: c.phoneNumbers?.[0]?.number || null,
|
|
1008
|
+
email: c.emails?.[0]?.email || null,
|
|
1009
|
+
birthday: formatBirthday(c.birthday),
|
|
1010
|
+
notes: ""
|
|
1011
|
+
// dropped — Notes is restricted on iOS 16+
|
|
1012
|
+
};
|
|
1013
|
+
contacts.push(mapped);
|
|
1014
|
+
currentHashes[id] = hashContact(mapped);
|
|
1015
|
+
}
|
|
1016
|
+
let stored = {};
|
|
1017
|
+
try {
|
|
1018
|
+
const blob = await import_async_storage.default.getItem(HASH_STORE_KEY);
|
|
1019
|
+
if (blob) stored = JSON.parse(blob);
|
|
1020
|
+
} catch {
|
|
1021
|
+
}
|
|
1022
|
+
const fullSync = Object.keys(stored).length === 0;
|
|
1023
|
+
let added = 0;
|
|
1024
|
+
let updated = 0;
|
|
1025
|
+
let unchanged = 0;
|
|
1026
|
+
for (const id of Object.keys(currentHashes)) {
|
|
1027
|
+
const prev = stored[id];
|
|
1028
|
+
if (prev === void 0) added++;
|
|
1029
|
+
else if (prev !== currentHashes[id]) updated++;
|
|
1030
|
+
else unchanged++;
|
|
1031
|
+
}
|
|
1032
|
+
let removed = 0;
|
|
1033
|
+
for (const id of Object.keys(stored)) {
|
|
1034
|
+
if (currentHashes[id] === void 0) removed++;
|
|
1035
|
+
}
|
|
1036
|
+
lastContactsStats = {
|
|
1037
|
+
total: contacts.length,
|
|
1038
|
+
added,
|
|
1039
|
+
updated,
|
|
1040
|
+
removed,
|
|
1041
|
+
unchanged,
|
|
1042
|
+
full_sync: fullSync
|
|
1043
|
+
};
|
|
1044
|
+
if (!fullSync && added === 0 && updated === 0 && removed === 0) {
|
|
1045
|
+
return null;
|
|
1046
|
+
}
|
|
1047
|
+
try {
|
|
1048
|
+
await import_async_storage.default.setItem(HASH_STORE_KEY, JSON.stringify(currentHashes));
|
|
1049
|
+
} catch {
|
|
1050
|
+
}
|
|
1051
|
+
return contacts;
|
|
1052
|
+
}
|
|
1053
|
+
function fnv1a(str) {
|
|
1054
|
+
let h = 2166136261;
|
|
1055
|
+
for (let i = 0; i < str.length; i++) {
|
|
1056
|
+
h ^= str.charCodeAt(i);
|
|
1057
|
+
h = Math.imul(h, 16777619);
|
|
1058
|
+
}
|
|
1059
|
+
return (h >>> 0).toString(16).padStart(8, "0");
|
|
1060
|
+
}
|
|
1061
|
+
function hashContact(c) {
|
|
1062
|
+
const parts = [
|
|
1063
|
+
c.id,
|
|
1064
|
+
(c.name || "").trim(),
|
|
1065
|
+
c.phone || "\0",
|
|
1066
|
+
(c.email || "\0").toLowerCase(),
|
|
1067
|
+
c.birthday || "\0"
|
|
1068
|
+
];
|
|
1069
|
+
return fnv1a(parts.join(""));
|
|
1070
|
+
}
|
|
1071
|
+
function formatBirthday(birthday) {
|
|
1072
|
+
if (!birthday) return null;
|
|
1073
|
+
const { year, month, day } = birthday;
|
|
1074
|
+
if (month === void 0 || day === void 0) return null;
|
|
1075
|
+
const m = String(month + 1).padStart(2, "0");
|
|
1076
|
+
const d = String(day).padStart(2, "0");
|
|
1077
|
+
if (!year) return `--${m}-${d}`;
|
|
1078
|
+
return `${year}-${m}-${d}`;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// src/device-bridge/collectors/location.ts
|
|
1082
|
+
var location_exports = {};
|
|
1083
|
+
__export(location_exports, {
|
|
1084
|
+
collectCurrent: () => collectCurrent,
|
|
1085
|
+
getPosition: () => getPosition,
|
|
1086
|
+
hasPermission: () => hasPermission4,
|
|
1087
|
+
requestPermission: () => requestPermission4,
|
|
1088
|
+
watchPosition: () => watchPosition
|
|
1089
|
+
});
|
|
1090
|
+
var ExpoLocation = __toESM(require("expo-location"));
|
|
1091
|
+
async function requestPermission4() {
|
|
1092
|
+
const { status: fg } = await ExpoLocation.requestForegroundPermissionsAsync();
|
|
1093
|
+
if (fg !== "granted") return false;
|
|
1094
|
+
try {
|
|
1095
|
+
await ExpoLocation.requestBackgroundPermissionsAsync();
|
|
1096
|
+
} catch {
|
|
1097
|
+
}
|
|
1098
|
+
return fg === "granted";
|
|
1099
|
+
}
|
|
1100
|
+
async function hasPermission4() {
|
|
1101
|
+
const { status } = await ExpoLocation.getForegroundPermissionsAsync();
|
|
1102
|
+
return status === "granted";
|
|
1103
|
+
}
|
|
1104
|
+
async function collectCurrent() {
|
|
1105
|
+
const location = await ExpoLocation.getCurrentPositionAsync({
|
|
1106
|
+
accuracy: ExpoLocation.Accuracy.Balanced
|
|
1107
|
+
});
|
|
1108
|
+
let place = "Current Location";
|
|
1109
|
+
let address = `${location.coords.latitude.toFixed(4)}, ${location.coords.longitude.toFixed(4)}`;
|
|
1110
|
+
let locationType = "unknown";
|
|
1111
|
+
try {
|
|
1112
|
+
const [geocoded] = await ExpoLocation.reverseGeocodeAsync({
|
|
1113
|
+
latitude: location.coords.latitude,
|
|
1114
|
+
longitude: location.coords.longitude
|
|
1115
|
+
});
|
|
1116
|
+
if (geocoded) {
|
|
1117
|
+
place = geocoded.name || geocoded.street || place;
|
|
1118
|
+
address = [
|
|
1119
|
+
geocoded.streetNumber,
|
|
1120
|
+
geocoded.street,
|
|
1121
|
+
geocoded.city,
|
|
1122
|
+
geocoded.region
|
|
1123
|
+
].filter(Boolean).join(", ") || address;
|
|
1124
|
+
locationType = classifyLocation(geocoded);
|
|
1125
|
+
}
|
|
1126
|
+
} catch {
|
|
1127
|
+
}
|
|
1128
|
+
return {
|
|
1129
|
+
ok: true,
|
|
1130
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1131
|
+
location: {
|
|
1132
|
+
place,
|
|
1133
|
+
address,
|
|
1134
|
+
lat: location.coords.latitude,
|
|
1135
|
+
lng: location.coords.longitude,
|
|
1136
|
+
type: locationType
|
|
1137
|
+
}
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
async function ensurePermission() {
|
|
1141
|
+
if (await hasPermission4()) return;
|
|
1142
|
+
const granted = await requestPermission4();
|
|
1143
|
+
if (!granted) {
|
|
1144
|
+
throw new Error(
|
|
1145
|
+
"Location permission was not granted. Enable it for this app in iOS Settings > Privacy > Location Services."
|
|
1146
|
+
);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
async function getPosition(opts = {}) {
|
|
1150
|
+
await ensurePermission();
|
|
1151
|
+
const fix = await ExpoLocation.getCurrentPositionAsync({
|
|
1152
|
+
accuracy: opts.accuracy === "high" ? ExpoLocation.Accuracy.High : ExpoLocation.Accuracy.Balanced
|
|
1153
|
+
});
|
|
1154
|
+
return toPosition(fix);
|
|
1155
|
+
}
|
|
1156
|
+
function toPosition(fix) {
|
|
1157
|
+
return {
|
|
1158
|
+
latitude: fix.coords.latitude,
|
|
1159
|
+
longitude: fix.coords.longitude,
|
|
1160
|
+
altitude: fix.coords.altitude ?? void 0,
|
|
1161
|
+
accuracy: nonNegative(fix.coords.accuracy),
|
|
1162
|
+
timestamp: fix.timestamp ?? Date.now(),
|
|
1163
|
+
speed: nonNegative(fix.coords.speed),
|
|
1164
|
+
course: nonNegative(fix.coords.heading)
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
function nonNegative(v) {
|
|
1168
|
+
return typeof v === "number" && v >= 0 ? v : void 0;
|
|
1169
|
+
}
|
|
1170
|
+
function watchPosition(cb) {
|
|
1171
|
+
let subscription = null;
|
|
1172
|
+
let cancelled = false;
|
|
1173
|
+
(async () => {
|
|
1174
|
+
try {
|
|
1175
|
+
await ensurePermission();
|
|
1176
|
+
if (cancelled) return;
|
|
1177
|
+
subscription = await ExpoLocation.watchPositionAsync(
|
|
1178
|
+
{ accuracy: ExpoLocation.Accuracy.Balanced },
|
|
1179
|
+
(fix) => cb(toPosition(fix))
|
|
1180
|
+
);
|
|
1181
|
+
if (cancelled) {
|
|
1182
|
+
subscription?.remove();
|
|
1183
|
+
subscription = null;
|
|
1184
|
+
}
|
|
1185
|
+
} catch (err) {
|
|
1186
|
+
console.warn("[location] watchPosition failed:", err?.message || String(err));
|
|
1187
|
+
}
|
|
1188
|
+
})();
|
|
1189
|
+
return () => {
|
|
1190
|
+
cancelled = true;
|
|
1191
|
+
subscription?.remove();
|
|
1192
|
+
subscription = null;
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
function classifyLocation(geocoded) {
|
|
1196
|
+
const name = (geocoded.name || "").toLowerCase();
|
|
1197
|
+
const street = (geocoded.street || "").toLowerCase();
|
|
1198
|
+
if (name.includes("home") || name.includes("residence")) return "home";
|
|
1199
|
+
if (name.includes("office") || name.includes("workplace") || name.includes("tech park")) return "work";
|
|
1200
|
+
if (name.includes("school") || name.includes("elementary") || name.includes("university")) return "school";
|
|
1201
|
+
if (name.includes("gym") || name.includes("fitness")) return "fitness";
|
|
1202
|
+
if (name.includes("hospital") || name.includes("clinic") || name.includes("dental")) return "medical";
|
|
1203
|
+
return "other";
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
// src/device-bridge/collectors/battery.ts
|
|
1207
|
+
var ExpoBattery = __toESM(require("expo-battery"));
|
|
1208
|
+
async function collectBattery() {
|
|
1209
|
+
const level = await ExpoBattery.getBatteryLevelAsync();
|
|
1210
|
+
const state = await ExpoBattery.getBatteryStateAsync();
|
|
1211
|
+
const lowPower = await ExpoBattery.isLowPowerModeEnabledAsync();
|
|
1212
|
+
return {
|
|
1213
|
+
ok: true,
|
|
1214
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1215
|
+
battery: {
|
|
1216
|
+
level: Math.round(level * 100),
|
|
1217
|
+
// expo-battery returns 0-1
|
|
1218
|
+
state: mapBatteryState(state),
|
|
1219
|
+
low_power_mode: lowPower
|
|
1220
|
+
}
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
function subscribeToBattery(callback) {
|
|
1224
|
+
let lastLevel = null;
|
|
1225
|
+
const levelSub = ExpoBattery.addBatteryLevelListener(({ batteryLevel }) => {
|
|
1226
|
+
const pct = Math.round(batteryLevel * 100);
|
|
1227
|
+
if (lastLevel === null || Math.abs(pct - lastLevel) >= 5) {
|
|
1228
|
+
lastLevel = pct;
|
|
1229
|
+
callback();
|
|
1230
|
+
}
|
|
1231
|
+
});
|
|
1232
|
+
const stateSub = ExpoBattery.addBatteryStateListener(() => {
|
|
1233
|
+
callback();
|
|
1234
|
+
});
|
|
1235
|
+
return () => {
|
|
1236
|
+
levelSub?.remove?.();
|
|
1237
|
+
stateSub?.remove?.();
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
function mapBatteryState(state) {
|
|
1241
|
+
switch (state) {
|
|
1242
|
+
case ExpoBattery.BatteryState.CHARGING:
|
|
1243
|
+
return "charging";
|
|
1244
|
+
case ExpoBattery.BatteryState.FULL:
|
|
1245
|
+
return "full";
|
|
1246
|
+
case ExpoBattery.BatteryState.UNPLUGGED:
|
|
1247
|
+
default:
|
|
1248
|
+
return "unplugged";
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
// src/device-bridge/collectors/connectivity.ts
|
|
1253
|
+
var ExpoNetwork = __toESM(require("expo-network"));
|
|
1254
|
+
async function collectConnectivity() {
|
|
1255
|
+
const networkState = await ExpoNetwork.getNetworkStateAsync();
|
|
1256
|
+
let type = "none";
|
|
1257
|
+
let wifiName;
|
|
1258
|
+
let cellularType;
|
|
1259
|
+
if (networkState.type === ExpoNetwork.NetworkStateType.WIFI) {
|
|
1260
|
+
type = "wifi";
|
|
1261
|
+
try {
|
|
1262
|
+
wifiName = await getWifiName();
|
|
1263
|
+
} catch {
|
|
1264
|
+
wifiName = void 0;
|
|
1265
|
+
}
|
|
1266
|
+
} else if (networkState.type === ExpoNetwork.NetworkStateType.CELLULAR) {
|
|
1267
|
+
type = "cellular";
|
|
1268
|
+
cellularType = getCellularGeneration();
|
|
1269
|
+
}
|
|
1270
|
+
return {
|
|
1271
|
+
ok: true,
|
|
1272
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1273
|
+
connectivity: {
|
|
1274
|
+
type,
|
|
1275
|
+
is_connected: networkState.isConnected ?? false,
|
|
1276
|
+
...wifiName && { wifi_name: wifiName },
|
|
1277
|
+
...cellularType && { cellular_type: cellularType }
|
|
1278
|
+
}
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
async function getWifiName() {
|
|
1282
|
+
return void 0;
|
|
1283
|
+
}
|
|
1284
|
+
function getCellularGeneration() {
|
|
1285
|
+
return "LTE";
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
// src/device-bridge/collectors/notifications.ts
|
|
1289
|
+
var ExpoNotifications = __toESM(require("expo-notifications"));
|
|
1290
|
+
async function requestPermission5() {
|
|
1291
|
+
const { status } = await ExpoNotifications.requestPermissionsAsync();
|
|
1292
|
+
return status === "granted";
|
|
1293
|
+
}
|
|
1294
|
+
async function hasPermission5() {
|
|
1295
|
+
const { status } = await ExpoNotifications.getPermissionsAsync();
|
|
1296
|
+
return status === "granted";
|
|
1297
|
+
}
|
|
1298
|
+
async function collectNotifications() {
|
|
1299
|
+
const presented = await ExpoNotifications.getPresentedNotificationsAsync();
|
|
1300
|
+
const notifications = presented.map((n, i) => ({
|
|
1301
|
+
id: n.request.identifier || `n${i}`,
|
|
1302
|
+
app: n.request.content.data?.app || "A-Team",
|
|
1303
|
+
title: n.request.content.title || "",
|
|
1304
|
+
body: n.request.content.body || "",
|
|
1305
|
+
timestamp: n.date ? new Date(n.date).toISOString() : (/* @__PURE__ */ new Date()).toISOString(),
|
|
1306
|
+
sender: n.request.content.data?.sender || null,
|
|
1307
|
+
priority: mapPriority(n.request.content.data?.priority),
|
|
1308
|
+
read: false
|
|
1309
|
+
// Presented notifications are unread by definition
|
|
1310
|
+
}));
|
|
1311
|
+
const unread = notifications.filter((n) => !n.read).length;
|
|
1312
|
+
return {
|
|
1313
|
+
ok: true,
|
|
1314
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1315
|
+
total: notifications.length,
|
|
1316
|
+
unread,
|
|
1317
|
+
notifications
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
function mapPriority(p) {
|
|
1321
|
+
if (p === "high" || p === "max") return "high";
|
|
1322
|
+
if (p === "low" || p === "min") return "low";
|
|
1323
|
+
return "medium";
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// src/device-bridge/collectors/appEngagement.ts
|
|
1327
|
+
var import_react_native3 = require("react-native");
|
|
1328
|
+
var import_async_storage2 = __toESM(require("@react-native-async-storage/async-storage"));
|
|
1329
|
+
var KEY = "ateam.appEngagement.v1";
|
|
1330
|
+
var _tally = null;
|
|
1331
|
+
var _foregroundSince = null;
|
|
1332
|
+
function todayKey() {
|
|
1333
|
+
const d = /* @__PURE__ */ new Date();
|
|
1334
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
1335
|
+
}
|
|
1336
|
+
function fresh() {
|
|
1337
|
+
return { date: todayKey(), sessions: 0, activeMs: 0 };
|
|
1338
|
+
}
|
|
1339
|
+
async function load() {
|
|
1340
|
+
try {
|
|
1341
|
+
const raw = await import_async_storage2.default.getItem(KEY);
|
|
1342
|
+
if (raw) {
|
|
1343
|
+
const parsed = JSON.parse(raw);
|
|
1344
|
+
_tally = parsed?.date === todayKey() ? parsed : fresh();
|
|
1345
|
+
} else {
|
|
1346
|
+
_tally = fresh();
|
|
1347
|
+
}
|
|
1348
|
+
} catch {
|
|
1349
|
+
_tally = fresh();
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
function persist() {
|
|
1353
|
+
if (_tally) import_async_storage2.default.setItem(KEY, JSON.stringify(_tally)).catch(() => {
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
function rollIfNewDay() {
|
|
1357
|
+
if (!_tally || _tally.date !== todayKey()) {
|
|
1358
|
+
_tally = fresh();
|
|
1359
|
+
persist();
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
function flushForeground() {
|
|
1363
|
+
if (_foregroundSince == null || !_tally) return;
|
|
1364
|
+
const now = Date.now();
|
|
1365
|
+
const delta = now - _foregroundSince;
|
|
1366
|
+
if (delta > 0) _tally.activeMs += delta;
|
|
1367
|
+
_foregroundSince = now;
|
|
1368
|
+
}
|
|
1369
|
+
async function getToday() {
|
|
1370
|
+
if (!_tally) await load();
|
|
1371
|
+
rollIfNewDay();
|
|
1372
|
+
flushForeground();
|
|
1373
|
+
persist();
|
|
1374
|
+
if (!_tally) return { app_sessions: null, app_active_min: null };
|
|
1375
|
+
return {
|
|
1376
|
+
app_sessions: _tally.sessions,
|
|
1377
|
+
app_active_min: Math.round(_tally.activeMs / 6e4)
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
// src/device-bridge/collectors/mood.ts
|
|
1382
|
+
function emptySignals() {
|
|
1383
|
+
return {
|
|
1384
|
+
steps: null,
|
|
1385
|
+
sleep_min: null,
|
|
1386
|
+
screen_min: null,
|
|
1387
|
+
night_screen_min: null,
|
|
1388
|
+
places_count: null,
|
|
1389
|
+
distance_km: null,
|
|
1390
|
+
social_app_min: null,
|
|
1391
|
+
video_app_min: null,
|
|
1392
|
+
productivity_app_min: null,
|
|
1393
|
+
messages_sent: null,
|
|
1394
|
+
calls_count: null,
|
|
1395
|
+
app_sessions: null,
|
|
1396
|
+
app_active_min: null,
|
|
1397
|
+
checkin_mood: null
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
async function collectToday3() {
|
|
1401
|
+
const today = emptySignals();
|
|
1402
|
+
try {
|
|
1403
|
+
const h = await collectToday();
|
|
1404
|
+
if (h) {
|
|
1405
|
+
today.steps = typeof h.steps === "number" ? h.steps : null;
|
|
1406
|
+
today.distance_km = typeof h.distance_km === "number" ? h.distance_km : null;
|
|
1407
|
+
}
|
|
1408
|
+
} catch {
|
|
1409
|
+
}
|
|
1410
|
+
try {
|
|
1411
|
+
const x = await collectMoodExtras();
|
|
1412
|
+
today.sleep_min = x.sleep_min;
|
|
1413
|
+
today.resting_hr = x.resting_hr;
|
|
1414
|
+
today.active_energy_kcal = x.active_energy_kcal;
|
|
1415
|
+
today.exercise_min = x.exercise_min;
|
|
1416
|
+
} catch {
|
|
1417
|
+
}
|
|
1418
|
+
try {
|
|
1419
|
+
const e = await getToday();
|
|
1420
|
+
today.app_sessions = e.app_sessions;
|
|
1421
|
+
today.app_active_min = e.app_active_min;
|
|
1422
|
+
} catch {
|
|
1423
|
+
}
|
|
1424
|
+
let timezone = "UTC";
|
|
1425
|
+
try {
|
|
1426
|
+
timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
1427
|
+
} catch {
|
|
1428
|
+
}
|
|
1429
|
+
return {
|
|
1430
|
+
window: "today",
|
|
1431
|
+
timezone,
|
|
1432
|
+
collected_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1433
|
+
today
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
// src/device-bridge/collectors/camera.ts
|
|
1438
|
+
var import_expo_camera = require("expo-camera");
|
|
1439
|
+
async function hasPermission6() {
|
|
1440
|
+
try {
|
|
1441
|
+
return (await import_expo_camera.Camera.getCameraPermissionsAsync()).granted;
|
|
1442
|
+
} catch {
|
|
1443
|
+
return false;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
async function requestPermission6() {
|
|
1447
|
+
try {
|
|
1448
|
+
return (await import_expo_camera.Camera.requestCameraPermissionsAsync()).granted;
|
|
1449
|
+
} catch {
|
|
1450
|
+
return false;
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
// src/device-bridge/capabilities.ts
|
|
1455
|
+
function calendarCapability() {
|
|
1456
|
+
return {
|
|
1457
|
+
id: "calendar",
|
|
1458
|
+
name: "Calendar",
|
|
1459
|
+
description: "Syncs your calendar events so your assistant knows your schedule.",
|
|
1460
|
+
reason: "Needed for scheduling, meeting prep, commute alerts, and daily briefings.",
|
|
1461
|
+
icon: "calendar",
|
|
1462
|
+
requiresPermission: true,
|
|
1463
|
+
permissionState: "not_requested",
|
|
1464
|
+
requestPermission: requestPermission2,
|
|
1465
|
+
checkPermission: hasPermission2,
|
|
1466
|
+
collect: async () => {
|
|
1467
|
+
const [today, upcoming] = await Promise.all([
|
|
1468
|
+
collectToday2(),
|
|
1469
|
+
collectUpcoming(7)
|
|
1470
|
+
]);
|
|
1471
|
+
return { today, upcoming };
|
|
1472
|
+
},
|
|
1473
|
+
defaultInterval: 6e4
|
|
1474
|
+
// 1 min
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
function contactsCapability() {
|
|
1478
|
+
return {
|
|
1479
|
+
id: "contacts",
|
|
1480
|
+
name: "Contacts",
|
|
1481
|
+
description: "Syncs your contacts so your assistant can find people and relationships.",
|
|
1482
|
+
reason: "Needed for messaging, birthday reminders, and relationship insights.",
|
|
1483
|
+
icon: "people",
|
|
1484
|
+
requiresPermission: true,
|
|
1485
|
+
permissionState: "not_requested",
|
|
1486
|
+
requestPermission: requestPermission3,
|
|
1487
|
+
checkPermission: hasPermission3,
|
|
1488
|
+
collect: async () => {
|
|
1489
|
+
const list = await collectAll();
|
|
1490
|
+
if (list === null) return null;
|
|
1491
|
+
return { contacts: list };
|
|
1492
|
+
},
|
|
1493
|
+
defaultInterval: 3e5
|
|
1494
|
+
// 5 min
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
function locationCapability() {
|
|
1498
|
+
return {
|
|
1499
|
+
id: "location",
|
|
1500
|
+
name: "Location",
|
|
1501
|
+
description: "Syncs your location for weather, commute, and context-aware assistance.",
|
|
1502
|
+
reason: "Needed for commute estimates, weather at your location, and situation detection.",
|
|
1503
|
+
icon: "location",
|
|
1504
|
+
requiresPermission: true,
|
|
1505
|
+
permissionState: "not_requested",
|
|
1506
|
+
requestPermission: requestPermission4,
|
|
1507
|
+
checkPermission: hasPermission4,
|
|
1508
|
+
collect: collectCurrent,
|
|
1509
|
+
defaultInterval: 3e4
|
|
1510
|
+
// 30 sec
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
function batteryCapability() {
|
|
1514
|
+
return {
|
|
1515
|
+
id: "battery",
|
|
1516
|
+
name: "Battery",
|
|
1517
|
+
description: "Syncs battery level and charging status.",
|
|
1518
|
+
reason: "Helps your assistant adjust behavior when battery is low.",
|
|
1519
|
+
icon: "battery",
|
|
1520
|
+
requiresPermission: false,
|
|
1521
|
+
permissionState: "not_needed",
|
|
1522
|
+
requestPermission: async () => true,
|
|
1523
|
+
checkPermission: async () => true,
|
|
1524
|
+
collect: collectBattery,
|
|
1525
|
+
defaultInterval: 6e4,
|
|
1526
|
+
// 1 min
|
|
1527
|
+
subscribe: subscribeToBattery
|
|
1528
|
+
};
|
|
1529
|
+
}
|
|
1530
|
+
function connectivityCapability() {
|
|
1531
|
+
return {
|
|
1532
|
+
id: "connectivity",
|
|
1533
|
+
name: "Connectivity",
|
|
1534
|
+
description: "Syncs WiFi/cellular status.",
|
|
1535
|
+
reason: "Helps your assistant know if you have internet access.",
|
|
1536
|
+
icon: "wifi",
|
|
1537
|
+
requiresPermission: false,
|
|
1538
|
+
permissionState: "not_needed",
|
|
1539
|
+
requestPermission: async () => true,
|
|
1540
|
+
checkPermission: async () => true,
|
|
1541
|
+
collect: collectConnectivity,
|
|
1542
|
+
defaultInterval: 3e4
|
|
1543
|
+
// 30 sec
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
function healthCapability() {
|
|
1547
|
+
return {
|
|
1548
|
+
id: "health",
|
|
1549
|
+
name: "Health",
|
|
1550
|
+
description: "Reads daily steps, floors, calories, and distance from Apple Health.",
|
|
1551
|
+
reason: "Powers the personal health coach and pairs activity data with nutrition tracking.",
|
|
1552
|
+
icon: "heart",
|
|
1553
|
+
requiresPermission: true,
|
|
1554
|
+
permissionState: "not_requested",
|
|
1555
|
+
requestPermission,
|
|
1556
|
+
checkPermission: hasPermission,
|
|
1557
|
+
collect: async () => {
|
|
1558
|
+
const days = await collectHistory(30);
|
|
1559
|
+
const today = days[0] || await collectToday();
|
|
1560
|
+
return { today, days };
|
|
1561
|
+
},
|
|
1562
|
+
defaultInterval: 3e5
|
|
1563
|
+
// 5 min
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
function moodCapability() {
|
|
1567
|
+
return {
|
|
1568
|
+
id: "mood",
|
|
1569
|
+
name: "Mood",
|
|
1570
|
+
description: "Sends a daily behavioral-signal summary (activity, routine) to infer mood \u2014 not a medical diagnosis.",
|
|
1571
|
+
reason: "Lets ada gently notice changes in energy, stress, and routine and check in.",
|
|
1572
|
+
icon: "heart",
|
|
1573
|
+
// No OS permission of its own — it derives from collectors that gate
|
|
1574
|
+
// themselves (e.g. health). Treated as not_needed so the bridge syncs it
|
|
1575
|
+
// whenever the user has the Mood toggle on.
|
|
1576
|
+
requiresPermission: false,
|
|
1577
|
+
permissionState: "not_needed",
|
|
1578
|
+
requestPermission: async () => true,
|
|
1579
|
+
checkPermission: async () => true,
|
|
1580
|
+
collect: async () => {
|
|
1581
|
+
return collectToday3();
|
|
1582
|
+
},
|
|
1583
|
+
defaultInterval: 36e5
|
|
1584
|
+
// hourly — it's a daily summary, no need to spam
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
function cameraCapability() {
|
|
1588
|
+
return {
|
|
1589
|
+
id: "camera",
|
|
1590
|
+
name: "Camera",
|
|
1591
|
+
description: "Lets ada see through your camera to guide you \u2014 reading text and describing what's in front of you. Frames are analysed on the phone; nothing is stored or uploaded.",
|
|
1592
|
+
reason: "Powers visual guidance (Ada Guide): a live view with on-device text and scene understanding.",
|
|
1593
|
+
icon: "camera",
|
|
1594
|
+
requiresPermission: true,
|
|
1595
|
+
permissionState: "not_requested",
|
|
1596
|
+
requestPermission: requestPermission6,
|
|
1597
|
+
checkPermission: hasPermission6,
|
|
1598
|
+
// LIVE, not synced: the camera is used in the moment (deviceState.visual / a
|
|
1599
|
+
// widget's preview) and never collected to the relay. So collect never
|
|
1600
|
+
// pushes — this capability's value is the USER GRANT + OS permission, exposed
|
|
1601
|
+
// to the backend like every other, not a data payload. `null` = nothing to
|
|
1602
|
+
// push, which the bridge skips (same contract contacts uses when unchanged).
|
|
1603
|
+
collect: async () => null,
|
|
1604
|
+
defaultInterval: 36e5
|
|
1605
|
+
// never really collects; keep the poll rare
|
|
1606
|
+
};
|
|
1607
|
+
}
|
|
1608
|
+
function notificationsCapability() {
|
|
1609
|
+
return {
|
|
1610
|
+
id: "notifications",
|
|
1611
|
+
name: "Notifications",
|
|
1612
|
+
description: "Syncs your notifications for triage and smart grouping.",
|
|
1613
|
+
reason: "Needed for notification triage, priority filtering, and Do Not Disturb.",
|
|
1614
|
+
icon: "notifications",
|
|
1615
|
+
requiresPermission: true,
|
|
1616
|
+
permissionState: "not_requested",
|
|
1617
|
+
requestPermission: requestPermission5,
|
|
1618
|
+
checkPermission: hasPermission5,
|
|
1619
|
+
collect: collectNotifications,
|
|
1620
|
+
defaultInterval: 15e3
|
|
1621
|
+
// 15 sec
|
|
1622
|
+
};
|
|
1623
|
+
}
|
|
1624
|
+
function allBuiltIn() {
|
|
1625
|
+
return [
|
|
1626
|
+
calendarCapability(),
|
|
1627
|
+
contactsCapability(),
|
|
1628
|
+
locationCapability(),
|
|
1629
|
+
batteryCapability(),
|
|
1630
|
+
connectivityCapability(),
|
|
1631
|
+
notificationsCapability(),
|
|
1632
|
+
healthCapability(),
|
|
1633
|
+
moodCapability(),
|
|
1634
|
+
cameraCapability()
|
|
1635
|
+
];
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
// src/device-bridge/background.ts
|
|
1639
|
+
var BACKGROUND_TASK_NAME = "DEVICE_BRIDGE_BACKGROUND_SYNC";
|
|
1640
|
+
var CAPABILITY_FACTORIES = {
|
|
1641
|
+
calendar: calendarCapability,
|
|
1642
|
+
contacts: contactsCapability,
|
|
1643
|
+
location: locationCapability,
|
|
1644
|
+
battery: batteryCapability,
|
|
1645
|
+
connectivity: connectivityCapability,
|
|
1646
|
+
notifications: notificationsCapability,
|
|
1647
|
+
health: healthCapability
|
|
1648
|
+
};
|
|
1649
|
+
async function registerBackgroundSync(config) {
|
|
1650
|
+
TaskManager.defineTask(BACKGROUND_TASK_NAME, async () => {
|
|
1651
|
+
try {
|
|
1652
|
+
const hasNew = await syncDomains(config, ["location", "battery", "calendar"]);
|
|
1653
|
+
return hasNew ? BackgroundFetch.BackgroundFetchResult.NewData : BackgroundFetch.BackgroundFetchResult.NoData;
|
|
1654
|
+
} catch (error) {
|
|
1655
|
+
console.error("[DeviceBridge] Background sync failed:", error);
|
|
1656
|
+
return BackgroundFetch.BackgroundFetchResult.Failed;
|
|
1657
|
+
}
|
|
1658
|
+
});
|
|
1659
|
+
await BackgroundFetch.registerTaskAsync(BACKGROUND_TASK_NAME, {
|
|
1660
|
+
minimumInterval: 300,
|
|
1661
|
+
// 5 minutes (iOS will clamp to ~15 min)
|
|
1662
|
+
stopOnTerminate: false,
|
|
1663
|
+
startOnBoot: true
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1666
|
+
async function unregisterBackgroundSync() {
|
|
1667
|
+
const isRegistered = await TaskManager.isTaskRegisteredAsync(
|
|
1668
|
+
BACKGROUND_TASK_NAME
|
|
1669
|
+
);
|
|
1670
|
+
if (isRegistered) {
|
|
1671
|
+
await BackgroundFetch.unregisterTaskAsync(BACKGROUND_TASK_NAME);
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
async function syncDomains(config, domains) {
|
|
1675
|
+
const relay = new RelayClient({
|
|
1676
|
+
relayUrl: config.relayUrl,
|
|
1677
|
+
deviceId: config.deviceId,
|
|
1678
|
+
tenant: config.tenant,
|
|
1679
|
+
actorId: config.actorId,
|
|
1680
|
+
apiKey: config.apiKey,
|
|
1681
|
+
timeout: 15e3
|
|
1682
|
+
// longer than foreground — background tasks are budget-limited but not latency-sensitive
|
|
1683
|
+
});
|
|
1684
|
+
const results = await Promise.allSettled(
|
|
1685
|
+
domains.map((d) => collectAndPush(relay, d))
|
|
1686
|
+
);
|
|
1687
|
+
return results.some((r) => r.status === "fulfilled" && r.value);
|
|
1688
|
+
}
|
|
1689
|
+
async function collectAndPush(relay, domain) {
|
|
1690
|
+
const factory = CAPABILITY_FACTORIES[domain];
|
|
1691
|
+
if (!factory) {
|
|
1692
|
+
console.warn(`[DeviceBridge] Unknown sync domain: ${domain}`);
|
|
1693
|
+
return false;
|
|
1694
|
+
}
|
|
1695
|
+
const cap = factory();
|
|
1696
|
+
if (cap.requiresPermission) {
|
|
1697
|
+
const granted = await cap.checkPermission();
|
|
1698
|
+
if (!granted) return false;
|
|
1699
|
+
}
|
|
1700
|
+
const data = await cap.collect();
|
|
1701
|
+
const result = await relay.pushDomain(domain, data);
|
|
1702
|
+
return result.ok;
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
// src/device-bridge/biometrics.ts
|
|
1706
|
+
var biometrics_exports = {};
|
|
1707
|
+
__export(biometrics_exports, {
|
|
1708
|
+
authenticate: () => authenticate,
|
|
1709
|
+
capabilities: () => capabilities
|
|
1710
|
+
});
|
|
1711
|
+
var LocalAuthentication = __toESM(require("expo-local-authentication"));
|
|
1712
|
+
async function capabilities() {
|
|
1713
|
+
const [hasHardware, isEnrolled, types] = await Promise.all([
|
|
1714
|
+
LocalAuthentication.hasHardwareAsync(),
|
|
1715
|
+
LocalAuthentication.isEnrolledAsync(),
|
|
1716
|
+
LocalAuthentication.supportedAuthenticationTypesAsync()
|
|
1717
|
+
]);
|
|
1718
|
+
const supportedTypes = types.map((t) => {
|
|
1719
|
+
if (t === LocalAuthentication.AuthenticationType.FINGERPRINT) return "fingerprint";
|
|
1720
|
+
if (t === LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION) return "face";
|
|
1721
|
+
if (t === LocalAuthentication.AuthenticationType.IRIS) return "iris";
|
|
1722
|
+
return "none";
|
|
1723
|
+
}).filter((t) => t !== "none");
|
|
1724
|
+
return { hasHardware, isEnrolled, supportedTypes };
|
|
1725
|
+
}
|
|
1726
|
+
async function authenticate(opts) {
|
|
1727
|
+
try {
|
|
1728
|
+
const res = await LocalAuthentication.authenticateAsync({
|
|
1729
|
+
promptMessage: opts.reason,
|
|
1730
|
+
cancelLabel: opts.cancelLabel,
|
|
1731
|
+
disableDeviceFallback: opts.fallbackToPasscode === false
|
|
1732
|
+
});
|
|
1733
|
+
if (res.success) return { success: true };
|
|
1734
|
+
return { success: false, error: res.error || "unknown" };
|
|
1735
|
+
} catch (e) {
|
|
1736
|
+
return { success: false, error: String(e?.message || e) };
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
// src/device-bridge/sharing.ts
|
|
1741
|
+
var sharing_exports = {};
|
|
1742
|
+
__export(sharing_exports, {
|
|
1743
|
+
isAvailable: () => isAvailable,
|
|
1744
|
+
shareFile: () => shareFile
|
|
1745
|
+
});
|
|
1746
|
+
var Sharing = __toESM(require("expo-sharing"));
|
|
1747
|
+
async function shareFile(fileUri, opts = {}) {
|
|
1748
|
+
try {
|
|
1749
|
+
const available = await Sharing.isAvailableAsync();
|
|
1750
|
+
if (!available) return { ok: false, error: "sharing_unavailable" };
|
|
1751
|
+
await Sharing.shareAsync(fileUri, opts);
|
|
1752
|
+
return { ok: true };
|
|
1753
|
+
} catch (e) {
|
|
1754
|
+
return { ok: false, error: String(e?.message || e) };
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
async function isAvailable() {
|
|
1758
|
+
return Sharing.isAvailableAsync();
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
// src/device-bridge/scanner.ts
|
|
1762
|
+
var scanner_exports = {};
|
|
1763
|
+
__export(scanner_exports, {
|
|
1764
|
+
CameraView: () => import_expo_camera2.CameraView,
|
|
1765
|
+
normalizeResult: () => normalizeResult,
|
|
1766
|
+
requestPermissions: () => requestPermissions,
|
|
1767
|
+
useCameraPermissions: () => import_expo_camera2.useCameraPermissions
|
|
1768
|
+
});
|
|
1769
|
+
var import_expo_camera2 = require("expo-camera");
|
|
1770
|
+
async function requestPermissions() {
|
|
1771
|
+
const { Camera: Camera2 } = await import("expo-camera");
|
|
1772
|
+
const { status } = await Camera2.requestCameraPermissionsAsync();
|
|
1773
|
+
return { granted: status === "granted", status };
|
|
1774
|
+
}
|
|
1775
|
+
function normalizeResult(r) {
|
|
1776
|
+
return { type: r.type, data: r.data };
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
// src/device-bridge/index.ts
|
|
1780
|
+
var DeviceBridge = class {
|
|
1781
|
+
constructor(config) {
|
|
1782
|
+
// Capability registry
|
|
1783
|
+
this.caps = /* @__PURE__ */ new Map();
|
|
1784
|
+
this.syncTimers = /* @__PURE__ */ new Map();
|
|
1785
|
+
this.subscriptions = /* @__PURE__ */ new Map();
|
|
1786
|
+
this.domainStatus = /* @__PURE__ */ new Map();
|
|
1787
|
+
this.running = false;
|
|
1788
|
+
this.config = config;
|
|
1789
|
+
this.relay = new RelayClient({
|
|
1790
|
+
relayUrl: config.relayUrl,
|
|
1791
|
+
deviceId: config.deviceId,
|
|
1792
|
+
tenant: config.tenant,
|
|
1793
|
+
actorId: config.actorId,
|
|
1794
|
+
apiKey: config.apiKey
|
|
1795
|
+
});
|
|
1796
|
+
this.queuePoller = new QueuePoller({
|
|
1797
|
+
relayClient: this.relay,
|
|
1798
|
+
pollInterval: 5e3,
|
|
1799
|
+
onActionExecuted: () => this.notifyStatusChange(),
|
|
1800
|
+
onError: (error) => this.config.onError?.({
|
|
1801
|
+
domain: "actions",
|
|
1802
|
+
message: error.message,
|
|
1803
|
+
code: "ACTION_ERROR",
|
|
1804
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1805
|
+
})
|
|
1806
|
+
});
|
|
1807
|
+
}
|
|
1808
|
+
// ─── Capability Registration ───
|
|
1809
|
+
/**
|
|
1810
|
+
* Register a capability. Does NOT request permission — just makes it known.
|
|
1811
|
+
* Can be called before or after startSync().
|
|
1812
|
+
*/
|
|
1813
|
+
register(cap) {
|
|
1814
|
+
this.caps.set(cap.id, cap);
|
|
1815
|
+
this.domainStatus.set(cap.id, {
|
|
1816
|
+
domain: cap.id,
|
|
1817
|
+
lastSync: null,
|
|
1818
|
+
state: "idle"
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
1821
|
+
/**
|
|
1822
|
+
* Register multiple capabilities at once.
|
|
1823
|
+
*/
|
|
1824
|
+
registerAll(caps) {
|
|
1825
|
+
for (const cap of caps) {
|
|
1826
|
+
this.register(cap);
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
/**
|
|
1830
|
+
* Get a registered capability by ID.
|
|
1831
|
+
* Use this to read name/description/reason for your permission UI.
|
|
1832
|
+
*/
|
|
1833
|
+
getCapability(id) {
|
|
1834
|
+
return this.caps.get(id);
|
|
1835
|
+
}
|
|
1836
|
+
/**
|
|
1837
|
+
* Get all registered capabilities with their current status.
|
|
1838
|
+
*/
|
|
1839
|
+
listCapabilities() {
|
|
1840
|
+
return Array.from(this.caps.values()).map((cap) => ({
|
|
1841
|
+
id: cap.id,
|
|
1842
|
+
name: cap.name,
|
|
1843
|
+
permission: cap.permissionState,
|
|
1844
|
+
sync: this.domainStatus.get(cap.id) || {
|
|
1845
|
+
domain: cap.id,
|
|
1846
|
+
lastSync: null,
|
|
1847
|
+
state: "idle"
|
|
1848
|
+
}
|
|
1849
|
+
}));
|
|
1850
|
+
}
|
|
1851
|
+
// ─── Permissions (host app controls the flow) ───
|
|
1852
|
+
/**
|
|
1853
|
+
* Request permission for ONE capability.
|
|
1854
|
+
* Call after showing your own explanation screen.
|
|
1855
|
+
* If bridge is already running and granted, syncing starts automatically.
|
|
1856
|
+
*/
|
|
1857
|
+
async requestPermission(capId) {
|
|
1858
|
+
const cap = this.caps.get(capId);
|
|
1859
|
+
if (!cap) throw new Error(`Capability "${capId}" not registered`);
|
|
1860
|
+
if (!cap.requiresPermission) {
|
|
1861
|
+
cap.permissionState = "not_needed";
|
|
1862
|
+
return true;
|
|
1863
|
+
}
|
|
1864
|
+
const granted = await cap.requestPermission();
|
|
1865
|
+
cap.permissionState = granted ? "granted" : "denied";
|
|
1866
|
+
if (granted && this.running) {
|
|
1867
|
+
this.startCapabilitySync(cap);
|
|
1868
|
+
}
|
|
1869
|
+
this.notifyStatusChange();
|
|
1870
|
+
return granted;
|
|
1871
|
+
}
|
|
1872
|
+
/**
|
|
1873
|
+
* Check permission WITHOUT prompting.
|
|
1874
|
+
*/
|
|
1875
|
+
async checkPermission(capId) {
|
|
1876
|
+
const cap = this.caps.get(capId);
|
|
1877
|
+
if (!cap) throw new Error(`Capability "${capId}" not registered`);
|
|
1878
|
+
if (!cap.requiresPermission) return "not_needed";
|
|
1879
|
+
const granted = await cap.checkPermission();
|
|
1880
|
+
cap.permissionState = granted ? "granted" : cap.permissionState;
|
|
1881
|
+
return cap.permissionState;
|
|
1882
|
+
}
|
|
1883
|
+
/**
|
|
1884
|
+
* Request ALL permissions sequentially (one at a time, not bombarded).
|
|
1885
|
+
*/
|
|
1886
|
+
async requestAllPermissions() {
|
|
1887
|
+
const results = /* @__PURE__ */ new Map();
|
|
1888
|
+
for (const [id, cap] of this.caps) {
|
|
1889
|
+
if (!cap.requiresPermission) {
|
|
1890
|
+
cap.permissionState = "not_needed";
|
|
1891
|
+
results.set(id, true);
|
|
1892
|
+
continue;
|
|
1893
|
+
}
|
|
1894
|
+
const granted = await cap.requestPermission();
|
|
1895
|
+
cap.permissionState = granted ? "granted" : "denied";
|
|
1896
|
+
results.set(id, granted);
|
|
1897
|
+
}
|
|
1898
|
+
this.notifyStatusChange();
|
|
1899
|
+
return results;
|
|
1900
|
+
}
|
|
1901
|
+
/**
|
|
1902
|
+
* Get capabilities that still need permission.
|
|
1903
|
+
* Use this to drive a "next permission" onboarding flow.
|
|
1904
|
+
*/
|
|
1905
|
+
getPendingPermissions() {
|
|
1906
|
+
return Array.from(this.caps.values()).filter(
|
|
1907
|
+
(cap) => cap.requiresPermission && cap.permissionState === "not_requested"
|
|
1908
|
+
);
|
|
1909
|
+
}
|
|
1910
|
+
// ─── Sync Control ───
|
|
1911
|
+
/**
|
|
1912
|
+
* Start syncing all permitted capabilities + action queue poller.
|
|
1913
|
+
*/
|
|
1914
|
+
async startSync() {
|
|
1915
|
+
if (this.running) return;
|
|
1916
|
+
this.running = true;
|
|
1917
|
+
for (const [, cap] of this.caps) {
|
|
1918
|
+
const permitted = !cap.requiresPermission || cap.permissionState === "granted" || cap.permissionState === "not_needed";
|
|
1919
|
+
if (permitted) {
|
|
1920
|
+
this.startCapabilitySync(cap);
|
|
1921
|
+
} else {
|
|
1922
|
+
this.updateDomainStatus(cap.id, "error", "Permission not granted");
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
this.queuePoller.start();
|
|
1926
|
+
if (this.config.tenant && this.config.apiKey) {
|
|
1927
|
+
registerBackgroundSync({
|
|
1928
|
+
relayUrl: this.config.relayUrl,
|
|
1929
|
+
deviceId: this.config.deviceId,
|
|
1930
|
+
tenant: this.config.tenant,
|
|
1931
|
+
actorId: this.config.actorId,
|
|
1932
|
+
apiKey: this.config.apiKey
|
|
1933
|
+
}).catch((e) => console.warn("[adas/bridge] registerBackgroundSync failed:", e?.message || e));
|
|
1934
|
+
}
|
|
1935
|
+
this.notifyStatusChange();
|
|
1936
|
+
}
|
|
1937
|
+
/**
|
|
1938
|
+
* Stop all syncing.
|
|
1939
|
+
*/
|
|
1940
|
+
stopSync() {
|
|
1941
|
+
this.running = false;
|
|
1942
|
+
for (const [id, timer] of this.syncTimers) {
|
|
1943
|
+
clearInterval(timer);
|
|
1944
|
+
this.updateDomainStatus(id, "stopped");
|
|
1945
|
+
}
|
|
1946
|
+
this.syncTimers.clear();
|
|
1947
|
+
for (const unsub of this.subscriptions.values()) {
|
|
1948
|
+
try {
|
|
1949
|
+
unsub();
|
|
1950
|
+
} catch {
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
this.subscriptions.clear();
|
|
1954
|
+
this.queuePoller.stop();
|
|
1955
|
+
unregisterBackgroundSync().catch((e) => console.warn("[adas/bridge] unregisterBackgroundSync failed:", e?.message || e));
|
|
1956
|
+
this.notifyStatusChange();
|
|
1957
|
+
}
|
|
1958
|
+
/**
|
|
1959
|
+
* Force immediate sync of all permitted capabilities.
|
|
1960
|
+
*/
|
|
1961
|
+
async syncNow() {
|
|
1962
|
+
const promises = Array.from(this.caps.values()).filter((c) => c.permissionState === "granted" || c.permissionState === "not_needed").map((c) => this.syncCapability(c));
|
|
1963
|
+
await Promise.allSettled(promises);
|
|
1964
|
+
}
|
|
1965
|
+
/**
|
|
1966
|
+
* Force immediate sync of a single capability by id. Used by the host to
|
|
1967
|
+
* refresh fast-moving sources (battery) on app-foreground so the relay
|
|
1968
|
+
* gets a fresh reading the moment the user opens the app.
|
|
1969
|
+
*/
|
|
1970
|
+
async syncCapabilityNow(capId) {
|
|
1971
|
+
const cap = this.caps.get(capId);
|
|
1972
|
+
if (!cap) return;
|
|
1973
|
+
const permitted = !cap.requiresPermission || cap.permissionState === "granted" || cap.permissionState === "not_needed";
|
|
1974
|
+
if (!permitted) return;
|
|
1975
|
+
await this.syncCapability(cap);
|
|
1976
|
+
}
|
|
1977
|
+
/**
|
|
1978
|
+
* Get current bridge status.
|
|
1979
|
+
*/
|
|
1980
|
+
getStatus() {
|
|
1981
|
+
return {
|
|
1982
|
+
running: this.running,
|
|
1983
|
+
capabilities: this.listCapabilities(),
|
|
1984
|
+
lastFullSync: this.getLastFullSync(),
|
|
1985
|
+
pendingActions: this.relay.getOfflineQueueSize()
|
|
1986
|
+
};
|
|
1987
|
+
}
|
|
1988
|
+
/**
|
|
1989
|
+
* Check if the relay is reachable.
|
|
1990
|
+
*/
|
|
1991
|
+
async checkConnectivity() {
|
|
1992
|
+
return this.relay.ping();
|
|
1993
|
+
}
|
|
1994
|
+
// ─── Internal ───
|
|
1995
|
+
startCapabilitySync(cap) {
|
|
1996
|
+
if (this.syncTimers.has(cap.id)) return;
|
|
1997
|
+
const interval = this.config.syncIntervals?.[cap.id] ?? cap.defaultInterval;
|
|
1998
|
+
this.syncCapability(cap);
|
|
1999
|
+
const timer = setInterval(() => this.syncCapability(cap), interval);
|
|
2000
|
+
this.syncTimers.set(cap.id, timer);
|
|
2001
|
+
if (cap.subscribe && !this.subscriptions.has(cap.id)) {
|
|
2002
|
+
try {
|
|
2003
|
+
const unsub = cap.subscribe(() => {
|
|
2004
|
+
if (this.running) this.syncCapability(cap);
|
|
2005
|
+
});
|
|
2006
|
+
this.subscriptions.set(cap.id, unsub);
|
|
2007
|
+
} catch (err) {
|
|
2008
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2009
|
+
this.config.onError?.({
|
|
2010
|
+
domain: cap.id,
|
|
2011
|
+
message: `subscribe failed: ${message}`,
|
|
2012
|
+
code: "SUBSCRIBE_ERROR",
|
|
2013
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2014
|
+
});
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
async syncCapability(cap) {
|
|
2019
|
+
if (!this.running) return;
|
|
2020
|
+
this.updateDomainStatus(cap.id, "syncing");
|
|
2021
|
+
try {
|
|
2022
|
+
const data = await cap.collect();
|
|
2023
|
+
if (data === null) {
|
|
2024
|
+
this.updateDomainStatus(cap.id, "idle");
|
|
2025
|
+
return;
|
|
2026
|
+
}
|
|
2027
|
+
const result = await this.relay.pushDomain(cap.id, data);
|
|
2028
|
+
if (result.ok) {
|
|
2029
|
+
this.updateDomainStatus(cap.id, "idle");
|
|
2030
|
+
} else {
|
|
2031
|
+
this.updateDomainStatus(cap.id, "error", result.error);
|
|
2032
|
+
this.config.onError?.({
|
|
2033
|
+
domain: cap.id,
|
|
2034
|
+
message: result.error || "Push failed",
|
|
2035
|
+
code: "RELAY_ERROR",
|
|
2036
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2037
|
+
});
|
|
2038
|
+
}
|
|
2039
|
+
} catch (error) {
|
|
2040
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2041
|
+
this.updateDomainStatus(cap.id, "error", message);
|
|
2042
|
+
this.config.onError?.({
|
|
2043
|
+
domain: cap.id,
|
|
2044
|
+
message,
|
|
2045
|
+
code: "COLLECTOR_ERROR",
|
|
2046
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2047
|
+
});
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
updateDomainStatus(id, state, error) {
|
|
2051
|
+
const current = this.domainStatus.get(id);
|
|
2052
|
+
this.domainStatus.set(id, {
|
|
2053
|
+
domain: id,
|
|
2054
|
+
lastSync: state === "idle" ? (/* @__PURE__ */ new Date()).toISOString() : current?.lastSync ?? null,
|
|
2055
|
+
state,
|
|
2056
|
+
error,
|
|
2057
|
+
itemCount: current?.itemCount
|
|
2058
|
+
});
|
|
2059
|
+
}
|
|
2060
|
+
getLastFullSync() {
|
|
2061
|
+
const syncs = Array.from(this.domainStatus.values()).map((d) => d.lastSync).filter((s) => s !== null);
|
|
2062
|
+
if (syncs.length === 0) return null;
|
|
2063
|
+
return syncs.sort()[0];
|
|
2064
|
+
}
|
|
2065
|
+
notifyStatusChange() {
|
|
2066
|
+
this.config.onStatusChange?.(this.getStatus());
|
|
2067
|
+
}
|
|
2068
|
+
};
|
|
2069
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2070
|
+
0 && (module.exports = {
|
|
2071
|
+
BACKGROUND_TASK_NAME,
|
|
2072
|
+
DeviceBridge,
|
|
2073
|
+
QueuePoller,
|
|
2074
|
+
RelayClient,
|
|
2075
|
+
biometrics,
|
|
2076
|
+
capabilities,
|
|
2077
|
+
location,
|
|
2078
|
+
registerBackgroundSync,
|
|
2079
|
+
scanner,
|
|
2080
|
+
sharing,
|
|
2081
|
+
syncDomains,
|
|
2082
|
+
unregisterBackgroundSync
|
|
2083
|
+
});
|
|
2084
|
+
//# sourceMappingURL=index.js.map
|