@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,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background Sync — registers an expo-task-manager task for periodic
|
|
3
|
+
* data sync when the app is in the background, and exposes a one-shot
|
|
4
|
+
* `syncDomains()` for silent-push triggered syncs.
|
|
5
|
+
*
|
|
6
|
+
* iOS minimum background fetch interval is ~15 minutes.
|
|
7
|
+
* Android allows more frequent background execution via WorkManager.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* import { registerBackgroundSync, syncDomains } from '@mobile-pa/device-bridge';
|
|
11
|
+
*
|
|
12
|
+
* // Periodic background fetch — call once at app startup, outside any component
|
|
13
|
+
* registerBackgroundSync({
|
|
14
|
+
* relayUrl: 'https://relay.example.com',
|
|
15
|
+
* deviceId: 'device-123',
|
|
16
|
+
* tenant: 'mobile-pa',
|
|
17
|
+
* actorId: 'user-abc',
|
|
18
|
+
* apiKey: 'key-xxx',
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* // Silent-push triggered sync — from FCM background message handler
|
|
22
|
+
* await syncDomains(config, ['location', 'calendar']);
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import * as TaskManager from 'expo-task-manager';
|
|
26
|
+
import * as BackgroundFetch from 'expo-background-fetch';
|
|
27
|
+
import { RelayClient } from './relay-client';
|
|
28
|
+
import * as caps from './capabilities';
|
|
29
|
+
import type { Capability } from './types';
|
|
30
|
+
|
|
31
|
+
export const BACKGROUND_TASK_NAME = 'DEVICE_BRIDGE_BACKGROUND_SYNC';
|
|
32
|
+
|
|
33
|
+
export interface BackgroundSyncConfig {
|
|
34
|
+
relayUrl: string;
|
|
35
|
+
deviceId: string;
|
|
36
|
+
tenant: string;
|
|
37
|
+
actorId?: string;
|
|
38
|
+
apiKey: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const CAPABILITY_FACTORIES: Record<string, () => Capability> = {
|
|
42
|
+
calendar: caps.calendarCapability,
|
|
43
|
+
contacts: caps.contactsCapability,
|
|
44
|
+
location: caps.locationCapability,
|
|
45
|
+
battery: caps.batteryCapability,
|
|
46
|
+
connectivity: caps.connectivityCapability,
|
|
47
|
+
notifications: caps.notificationsCapability,
|
|
48
|
+
health: caps.healthCapability,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Register the periodic background-fetch task.
|
|
53
|
+
* Call once at app startup, outside any React component.
|
|
54
|
+
*/
|
|
55
|
+
export async function registerBackgroundSync(
|
|
56
|
+
config: BackgroundSyncConfig
|
|
57
|
+
): Promise<void> {
|
|
58
|
+
TaskManager.defineTask(BACKGROUND_TASK_NAME, async () => {
|
|
59
|
+
try {
|
|
60
|
+
const hasNew = await syncDomains(config, ['location', 'battery', 'calendar']);
|
|
61
|
+
return hasNew
|
|
62
|
+
? BackgroundFetch.BackgroundFetchResult.NewData
|
|
63
|
+
: BackgroundFetch.BackgroundFetchResult.NoData;
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error('[DeviceBridge] Background sync failed:', error);
|
|
66
|
+
return BackgroundFetch.BackgroundFetchResult.Failed;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
await BackgroundFetch.registerTaskAsync(BACKGROUND_TASK_NAME, {
|
|
71
|
+
minimumInterval: 300, // 5 minutes (iOS will clamp to ~15 min)
|
|
72
|
+
stopOnTerminate: false,
|
|
73
|
+
startOnBoot: true,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Unregister the background sync task.
|
|
79
|
+
*/
|
|
80
|
+
export async function unregisterBackgroundSync(): Promise<void> {
|
|
81
|
+
const isRegistered = await TaskManager.isTaskRegisteredAsync(
|
|
82
|
+
BACKGROUND_TASK_NAME
|
|
83
|
+
);
|
|
84
|
+
if (isRegistered) {
|
|
85
|
+
await BackgroundFetch.unregisterTaskAsync(BACKGROUND_TASK_NAME);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Collect and push the listed domains once. Used by both the periodic
|
|
91
|
+
* background-fetch task and the silent-push handler.
|
|
92
|
+
*
|
|
93
|
+
* Returns true if at least one domain pushed new data.
|
|
94
|
+
*/
|
|
95
|
+
export async function syncDomains(
|
|
96
|
+
config: BackgroundSyncConfig,
|
|
97
|
+
domains: string[]
|
|
98
|
+
): Promise<boolean> {
|
|
99
|
+
const relay = new RelayClient({
|
|
100
|
+
relayUrl: config.relayUrl,
|
|
101
|
+
deviceId: config.deviceId,
|
|
102
|
+
tenant: config.tenant,
|
|
103
|
+
actorId: config.actorId,
|
|
104
|
+
apiKey: config.apiKey,
|
|
105
|
+
timeout: 15_000, // longer than foreground — background tasks are budget-limited but not latency-sensitive
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const results = await Promise.allSettled(
|
|
109
|
+
domains.map(d => collectAndPush(relay, d))
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
return results.some(r => r.status === 'fulfilled' && r.value);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ─── Internal ───
|
|
116
|
+
|
|
117
|
+
async function collectAndPush(
|
|
118
|
+
relay: RelayClient,
|
|
119
|
+
domain: string
|
|
120
|
+
): Promise<boolean> {
|
|
121
|
+
const factory = CAPABILITY_FACTORIES[domain];
|
|
122
|
+
if (!factory) {
|
|
123
|
+
console.warn(`[DeviceBridge] Unknown sync domain: ${domain}`);
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const cap = factory();
|
|
128
|
+
|
|
129
|
+
if (cap.requiresPermission) {
|
|
130
|
+
const granted = await cap.checkPermission();
|
|
131
|
+
if (!granted) return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const data = await cap.collect();
|
|
135
|
+
const result = await relay.pushDomain(domain, data);
|
|
136
|
+
return result.ok;
|
|
137
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device-bridge: Biometrics (Face ID / Touch ID / fingerprint)
|
|
3
|
+
*
|
|
4
|
+
* Used by solutions that need a biometric gate before sensitive actions
|
|
5
|
+
* (e.g. "unlock payment", "confirm financial transfer", "reveal stored
|
|
6
|
+
* credentials"). Also useful for protecting session cookies at rest.
|
|
7
|
+
*
|
|
8
|
+
* Wraps expo-local-authentication.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as LocalAuthentication from 'expo-local-authentication';
|
|
12
|
+
|
|
13
|
+
export type BiometricType = 'fingerprint' | 'face' | 'iris' | 'none';
|
|
14
|
+
|
|
15
|
+
export interface BiometricCapabilities {
|
|
16
|
+
hasHardware: boolean;
|
|
17
|
+
isEnrolled: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Never contains 'none' — the mapper filters it out, since 'none' means
|
|
20
|
+
* "the OS reported an auth type we do not recognise" and is not a
|
|
21
|
+
* biometric a caller can offer. Typed as the guarantee the code makes.
|
|
22
|
+
*/
|
|
23
|
+
supportedTypes: Exclude<BiometricType, 'none'>[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** What can the device do right now? */
|
|
27
|
+
export async function capabilities(): Promise<BiometricCapabilities> {
|
|
28
|
+
const [hasHardware, isEnrolled, types] = await Promise.all([
|
|
29
|
+
LocalAuthentication.hasHardwareAsync(),
|
|
30
|
+
LocalAuthentication.isEnrolledAsync(),
|
|
31
|
+
LocalAuthentication.supportedAuthenticationTypesAsync(),
|
|
32
|
+
]);
|
|
33
|
+
const supportedTypes = types.map((t) => {
|
|
34
|
+
if (t === LocalAuthentication.AuthenticationType.FINGERPRINT) return 'fingerprint';
|
|
35
|
+
if (t === LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION) return 'face';
|
|
36
|
+
if (t === LocalAuthentication.AuthenticationType.IRIS) return 'iris';
|
|
37
|
+
return 'none';
|
|
38
|
+
}).filter((t) => t !== 'none') as Exclude<BiometricType, 'none'>[];
|
|
39
|
+
return { hasHardware, isEnrolled, supportedTypes };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface AuthenticateOptions {
|
|
43
|
+
/** User-visible reason shown in the system dialog. */
|
|
44
|
+
reason: string;
|
|
45
|
+
/** Label on the "cancel" button (iOS only; Android has system default). */
|
|
46
|
+
cancelLabel?: string;
|
|
47
|
+
/** If biometrics aren't enrolled, fall back to device passcode. Default true. */
|
|
48
|
+
fallbackToPasscode?: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Prompt the user for biometric auth. Resolves { success } or { success:false, error }. */
|
|
52
|
+
export async function authenticate(opts: AuthenticateOptions): Promise<{ success: boolean; error?: string }> {
|
|
53
|
+
try {
|
|
54
|
+
const res = await LocalAuthentication.authenticateAsync({
|
|
55
|
+
promptMessage: opts.reason,
|
|
56
|
+
cancelLabel: opts.cancelLabel,
|
|
57
|
+
disableDeviceFallback: opts.fallbackToPasscode === false,
|
|
58
|
+
});
|
|
59
|
+
if (res.success) return { success: true };
|
|
60
|
+
return { success: false, error: (res as any).error || 'unknown' };
|
|
61
|
+
} catch (e: any) {
|
|
62
|
+
return { success: false, error: String(e?.message || e) };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in Capabilities — wraps each collector as a registerable Capability.
|
|
3
|
+
*
|
|
4
|
+
* Each capability defines:
|
|
5
|
+
* - id, name, description, reason (for UI)
|
|
6
|
+
* - permission check/request
|
|
7
|
+
* - collect function
|
|
8
|
+
*
|
|
9
|
+
* The host app can register these selectively, or add custom ones.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as calendar from './collectors/calendar';
|
|
13
|
+
import * as contacts from './collectors/contacts';
|
|
14
|
+
import * as location from './collectors/location';
|
|
15
|
+
import * as battery from './collectors/battery';
|
|
16
|
+
import * as connectivity from './collectors/connectivity';
|
|
17
|
+
import * as notifications from './collectors/notifications';
|
|
18
|
+
import * as health from './collectors/health';
|
|
19
|
+
import * as mood from './collectors/mood';
|
|
20
|
+
import * as camera from './collectors/camera';
|
|
21
|
+
import type { Capability } from './types';
|
|
22
|
+
|
|
23
|
+
// ─── Factory functions — call these to get a Capability instance ───
|
|
24
|
+
|
|
25
|
+
export function calendarCapability(): Capability {
|
|
26
|
+
return {
|
|
27
|
+
id: 'calendar',
|
|
28
|
+
name: 'Calendar',
|
|
29
|
+
description: 'Syncs your calendar events so your assistant knows your schedule.',
|
|
30
|
+
reason: 'Needed for scheduling, meeting prep, commute alerts, and daily briefings.',
|
|
31
|
+
icon: 'calendar',
|
|
32
|
+
requiresPermission: true,
|
|
33
|
+
permissionState: 'not_requested',
|
|
34
|
+
requestPermission: calendar.requestPermission,
|
|
35
|
+
checkPermission: calendar.hasPermission,
|
|
36
|
+
collect: async () => {
|
|
37
|
+
const [today, upcoming] = await Promise.all([
|
|
38
|
+
calendar.collectToday(),
|
|
39
|
+
calendar.collectUpcoming(7),
|
|
40
|
+
]);
|
|
41
|
+
return { today, upcoming };
|
|
42
|
+
},
|
|
43
|
+
defaultInterval: 60_000, // 1 min
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function contactsCapability(): Capability {
|
|
48
|
+
return {
|
|
49
|
+
id: 'contacts',
|
|
50
|
+
name: 'Contacts',
|
|
51
|
+
description: 'Syncs your contacts so your assistant can find people and relationships.',
|
|
52
|
+
reason: 'Needed for messaging, birthday reminders, and relationship insights.',
|
|
53
|
+
icon: 'people',
|
|
54
|
+
requiresPermission: true,
|
|
55
|
+
permissionState: 'not_requested',
|
|
56
|
+
requestPermission: contacts.requestPermission,
|
|
57
|
+
checkPermission: contacts.hasPermission,
|
|
58
|
+
collect: async () => {
|
|
59
|
+
const list = await contacts.collectAll();
|
|
60
|
+
// null = no changes since last successful sync → bridge skips push.
|
|
61
|
+
// Critical for users with 6000+ contacts: full payload is ~1MB and
|
|
62
|
+
// would otherwise re-upload every 5 min with no actual change.
|
|
63
|
+
if (list === null) return null as any;
|
|
64
|
+
return { contacts: list };
|
|
65
|
+
},
|
|
66
|
+
defaultInterval: 300_000, // 5 min
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function locationCapability(): Capability {
|
|
71
|
+
return {
|
|
72
|
+
id: 'location',
|
|
73
|
+
name: 'Location',
|
|
74
|
+
description: 'Syncs your location for weather, commute, and context-aware assistance.',
|
|
75
|
+
reason: 'Needed for commute estimates, weather at your location, and situation detection.',
|
|
76
|
+
icon: 'location',
|
|
77
|
+
requiresPermission: true,
|
|
78
|
+
permissionState: 'not_requested',
|
|
79
|
+
requestPermission: location.requestPermission,
|
|
80
|
+
checkPermission: location.hasPermission,
|
|
81
|
+
collect: location.collectCurrent,
|
|
82
|
+
defaultInterval: 30_000, // 30 sec
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function batteryCapability(): Capability {
|
|
87
|
+
return {
|
|
88
|
+
id: 'battery',
|
|
89
|
+
name: 'Battery',
|
|
90
|
+
description: 'Syncs battery level and charging status.',
|
|
91
|
+
reason: 'Helps your assistant adjust behavior when battery is low.',
|
|
92
|
+
icon: 'battery',
|
|
93
|
+
requiresPermission: false,
|
|
94
|
+
permissionState: 'not_needed',
|
|
95
|
+
requestPermission: async () => true,
|
|
96
|
+
checkPermission: async () => true,
|
|
97
|
+
collect: battery.collectBattery,
|
|
98
|
+
defaultInterval: 60_000, // 1 min
|
|
99
|
+
subscribe: battery.subscribeToBattery,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function connectivityCapability(): Capability {
|
|
104
|
+
return {
|
|
105
|
+
id: 'connectivity',
|
|
106
|
+
name: 'Connectivity',
|
|
107
|
+
description: 'Syncs WiFi/cellular status.',
|
|
108
|
+
reason: 'Helps your assistant know if you have internet access.',
|
|
109
|
+
icon: 'wifi',
|
|
110
|
+
requiresPermission: false,
|
|
111
|
+
permissionState: 'not_needed',
|
|
112
|
+
requestPermission: async () => true,
|
|
113
|
+
checkPermission: async () => true,
|
|
114
|
+
collect: connectivity.collectConnectivity,
|
|
115
|
+
defaultInterval: 30_000, // 30 sec
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function healthCapability(): Capability {
|
|
120
|
+
return {
|
|
121
|
+
id: 'health',
|
|
122
|
+
name: 'Health',
|
|
123
|
+
description: 'Reads daily steps, floors, calories, and distance from Apple Health.',
|
|
124
|
+
reason: 'Powers the personal health coach and pairs activity data with nutrition tracking.',
|
|
125
|
+
icon: 'heart',
|
|
126
|
+
requiresPermission: true,
|
|
127
|
+
permissionState: 'not_requested',
|
|
128
|
+
requestPermission: health.requestPermission,
|
|
129
|
+
checkPermission: health.hasPermission,
|
|
130
|
+
collect: async () => {
|
|
131
|
+
// Push today AND the last 30 days every sync. The connector slices
|
|
132
|
+
// `days` to whatever range the caller asked for. ~30 sequential
|
|
133
|
+
// HealthKit reads on iOS take <300ms total — cheap.
|
|
134
|
+
const days = await health.collectHistory(30);
|
|
135
|
+
const today = days[0] || (await health.collectToday());
|
|
136
|
+
return { today, days };
|
|
137
|
+
},
|
|
138
|
+
defaultInterval: 300_000, // 5 min
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function moodCapability(): Capability {
|
|
143
|
+
return {
|
|
144
|
+
id: 'mood',
|
|
145
|
+
name: 'Mood',
|
|
146
|
+
description: 'Sends a daily behavioral-signal summary (activity, routine) to infer mood — not a medical diagnosis.',
|
|
147
|
+
reason: 'Lets ada gently notice changes in energy, stress, and routine and check in.',
|
|
148
|
+
icon: 'heart',
|
|
149
|
+
// No OS permission of its own — it derives from collectors that gate
|
|
150
|
+
// themselves (e.g. health). Treated as not_needed so the bridge syncs it
|
|
151
|
+
// whenever the user has the Mood toggle on.
|
|
152
|
+
requiresPermission: false,
|
|
153
|
+
permissionState: 'not_needed',
|
|
154
|
+
requestPermission: async () => true,
|
|
155
|
+
checkPermission: async () => true,
|
|
156
|
+
collect: async () => {
|
|
157
|
+
// One compact daily summary (window/timezone/collected_at/today).
|
|
158
|
+
// Pushed as the `mood` relay domain; backend runs /api/mood/analyze.
|
|
159
|
+
return mood.collectToday();
|
|
160
|
+
},
|
|
161
|
+
defaultInterval: 3_600_000, // hourly — it's a daily summary, no need to spam
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function cameraCapability(): Capability {
|
|
166
|
+
return {
|
|
167
|
+
id: 'camera',
|
|
168
|
+
name: 'Camera',
|
|
169
|
+
description:
|
|
170
|
+
"Lets ada see through your camera to guide you — reading text and describing what's in " +
|
|
171
|
+
'front of you. Frames are analysed on the phone; nothing is stored or uploaded.',
|
|
172
|
+
reason: 'Powers visual guidance (Ada Guide): a live view with on-device text and scene understanding.',
|
|
173
|
+
icon: 'camera',
|
|
174
|
+
requiresPermission: true,
|
|
175
|
+
permissionState: 'not_requested',
|
|
176
|
+
requestPermission: camera.requestPermission,
|
|
177
|
+
checkPermission: camera.hasPermission,
|
|
178
|
+
// LIVE, not synced: the camera is used in the moment (deviceState.visual / a
|
|
179
|
+
// widget's preview) and never collected to the relay. So collect never
|
|
180
|
+
// pushes — this capability's value is the USER GRANT + OS permission, exposed
|
|
181
|
+
// to the backend like every other, not a data payload. `null` = nothing to
|
|
182
|
+
// push, which the bridge skips (same contract contacts uses when unchanged).
|
|
183
|
+
collect: async () => null as any,
|
|
184
|
+
defaultInterval: 3_600_000, // never really collects; keep the poll rare
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function notificationsCapability(): Capability {
|
|
189
|
+
return {
|
|
190
|
+
id: 'notifications',
|
|
191
|
+
name: 'Notifications',
|
|
192
|
+
description: 'Syncs your notifications for triage and smart grouping.',
|
|
193
|
+
reason: 'Needed for notification triage, priority filtering, and Do Not Disturb.',
|
|
194
|
+
icon: 'notifications',
|
|
195
|
+
requiresPermission: true,
|
|
196
|
+
permissionState: 'not_requested',
|
|
197
|
+
requestPermission: notifications.requestPermission,
|
|
198
|
+
checkPermission: notifications.hasPermission,
|
|
199
|
+
collect: notifications.collectNotifications,
|
|
200
|
+
defaultInterval: 15_000, // 15 sec
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ─── Convenience: get all built-in capabilities ───
|
|
205
|
+
|
|
206
|
+
export function allBuiltIn(): Capability[] {
|
|
207
|
+
return [
|
|
208
|
+
calendarCapability(),
|
|
209
|
+
contactsCapability(),
|
|
210
|
+
locationCapability(),
|
|
211
|
+
batteryCapability(),
|
|
212
|
+
connectivityCapability(),
|
|
213
|
+
notificationsCapability(),
|
|
214
|
+
healthCapability(),
|
|
215
|
+
moodCapability(),
|
|
216
|
+
cameraCapability(),
|
|
217
|
+
];
|
|
218
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App Engagement Collector — our OWN usage analytics (no OS permission, fully
|
|
3
|
+
* available on every platform). Feeds the mood summary's app_sessions +
|
|
4
|
+
* app_active_min, which iOS otherwise blocks for third-party apps.
|
|
5
|
+
*
|
|
6
|
+
* app_sessions — count of foreground sessions today (a background→active
|
|
7
|
+
* transition, or a cold start, starts a session).
|
|
8
|
+
* app_active_min — total minutes the app was in the foreground today.
|
|
9
|
+
*
|
|
10
|
+
* Counters are kept per local day and persisted to AsyncStorage so they
|
|
11
|
+
* survive relaunches. start() must be called once at app boot to attach the
|
|
12
|
+
* AppState listener; getToday() returns the current day's tallies.
|
|
13
|
+
*/
|
|
14
|
+
import { AppState, type AppStateStatus } from 'react-native';
|
|
15
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
16
|
+
|
|
17
|
+
const KEY = 'ateam.appEngagement.v1';
|
|
18
|
+
|
|
19
|
+
interface DayTally {
|
|
20
|
+
date: string; // local YYYY-MM-DD
|
|
21
|
+
sessions: number;
|
|
22
|
+
activeMs: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let _tally: DayTally | null = null;
|
|
26
|
+
let _foregroundSince: number | null = null;
|
|
27
|
+
let _started = false;
|
|
28
|
+
let _sub: { remove: () => void } | null = null;
|
|
29
|
+
|
|
30
|
+
function todayKey(): string {
|
|
31
|
+
const d = new Date();
|
|
32
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function fresh(): DayTally {
|
|
36
|
+
return { date: todayKey(), sessions: 0, activeMs: 0 };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function load(): Promise<void> {
|
|
40
|
+
try {
|
|
41
|
+
const raw = await AsyncStorage.getItem(KEY);
|
|
42
|
+
if (raw) {
|
|
43
|
+
const parsed = JSON.parse(raw) as DayTally;
|
|
44
|
+
_tally = parsed?.date === todayKey() ? parsed : fresh();
|
|
45
|
+
} else {
|
|
46
|
+
_tally = fresh();
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
_tally = fresh();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function persist(): void {
|
|
54
|
+
if (_tally) AsyncStorage.setItem(KEY, JSON.stringify(_tally)).catch(() => {});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function rollIfNewDay(): void {
|
|
58
|
+
if (!_tally || _tally.date !== todayKey()) {
|
|
59
|
+
_tally = fresh();
|
|
60
|
+
persist();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Fold the time spent foregrounded since the last checkpoint into activeMs. */
|
|
65
|
+
function flushForeground(): void {
|
|
66
|
+
if (_foregroundSince == null || !_tally) return;
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
const delta = now - _foregroundSince;
|
|
69
|
+
if (delta > 0) _tally.activeMs += delta;
|
|
70
|
+
_foregroundSince = now;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function onChange(state: AppStateStatus): void {
|
|
74
|
+
rollIfNewDay();
|
|
75
|
+
if (state === 'active') {
|
|
76
|
+
// New foreground session.
|
|
77
|
+
if (_tally) _tally.sessions += 1;
|
|
78
|
+
_foregroundSince = Date.now();
|
|
79
|
+
persist();
|
|
80
|
+
} else {
|
|
81
|
+
// Leaving foreground — bank the elapsed active time.
|
|
82
|
+
flushForeground();
|
|
83
|
+
_foregroundSince = null;
|
|
84
|
+
persist();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Attach the AppState listener. Idempotent; call once at app boot. */
|
|
89
|
+
export async function start(): Promise<void> {
|
|
90
|
+
if (_started) return;
|
|
91
|
+
_started = true;
|
|
92
|
+
await load();
|
|
93
|
+
// Count the launch itself as the first active session.
|
|
94
|
+
rollIfNewDay();
|
|
95
|
+
if (_tally) _tally.sessions += 1;
|
|
96
|
+
_foregroundSince = Date.now();
|
|
97
|
+
persist();
|
|
98
|
+
_sub = AppState.addEventListener('change', onChange);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function stop(): void {
|
|
102
|
+
flushForeground();
|
|
103
|
+
persist();
|
|
104
|
+
_sub?.remove();
|
|
105
|
+
_sub = null;
|
|
106
|
+
_started = false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Current day's tallies for the mood summary. */
|
|
110
|
+
export async function getToday(): Promise<{ app_sessions: number | null; app_active_min: number | null }> {
|
|
111
|
+
if (!_tally) await load();
|
|
112
|
+
rollIfNewDay();
|
|
113
|
+
flushForeground(); // include the in-progress foreground stretch
|
|
114
|
+
persist();
|
|
115
|
+
if (!_tally) return { app_sessions: null, app_active_min: null };
|
|
116
|
+
return {
|
|
117
|
+
app_sessions: _tally.sessions,
|
|
118
|
+
app_active_min: Math.round(_tally.activeMs / 60000),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Battery Collector — reads battery status via expo-battery.
|
|
3
|
+
* Output format matches device-mock-mcp's device.battery schema.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as ExpoBattery from 'expo-battery';
|
|
7
|
+
// Type-only alias: the value namespace above cannot be used in type
|
|
8
|
+
// position, so `ExpoBattery.Calendar`-style annotations need this.
|
|
9
|
+
// BatteryState is not re-exported from the package entry — derive it from
|
|
10
|
+
// the API that returns it so we never hand-maintain a copy of the enum.
|
|
11
|
+
type BatteryState = Awaited<ReturnType<typeof ExpoBattery.getBatteryStateAsync>>;
|
|
12
|
+
import type { BatteryData } from '../types';
|
|
13
|
+
|
|
14
|
+
// No permission needed for battery
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Collect current battery status.
|
|
18
|
+
* Matches the device.battery tool output schema.
|
|
19
|
+
*/
|
|
20
|
+
export async function collectBattery(): Promise<BatteryData> {
|
|
21
|
+
const level = await ExpoBattery.getBatteryLevelAsync();
|
|
22
|
+
const state = await ExpoBattery.getBatteryStateAsync();
|
|
23
|
+
const lowPower = await ExpoBattery.isLowPowerModeEnabledAsync();
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
ok: true,
|
|
27
|
+
timestamp: new Date().toISOString(),
|
|
28
|
+
battery: {
|
|
29
|
+
level: Math.round(level * 100), // expo-battery returns 0-1
|
|
30
|
+
state: mapBatteryState(state),
|
|
31
|
+
low_power_mode: lowPower,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Subscribe to OS-level battery changes (level + charging state).
|
|
38
|
+
*
|
|
39
|
+
* iOS suspends JS timers when the app is backgrounded, so a pure setInterval
|
|
40
|
+
* sync drifts: the relay ends up stuck on whatever level we last sampled
|
|
41
|
+
* before the app went to sleep (commonly 100% from an overnight charge).
|
|
42
|
+
* expo-battery's listeners fire from the native side and survive that gap,
|
|
43
|
+
* so we get a fresh push as soon as the OS notices a change.
|
|
44
|
+
*
|
|
45
|
+
* Level changes are throttled to >=5pp deltas — without that, iOS emits a
|
|
46
|
+
* level event for every 1% tick which would spam the relay.
|
|
47
|
+
*/
|
|
48
|
+
export function subscribeToBattery(callback: () => void): () => void {
|
|
49
|
+
let lastLevel: number | null = null;
|
|
50
|
+
|
|
51
|
+
const levelSub = ExpoBattery.addBatteryLevelListener(({ batteryLevel }) => {
|
|
52
|
+
const pct = Math.round(batteryLevel * 100);
|
|
53
|
+
if (lastLevel === null || Math.abs(pct - lastLevel) >= 5) {
|
|
54
|
+
lastLevel = pct;
|
|
55
|
+
callback();
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const stateSub = ExpoBattery.addBatteryStateListener(() => {
|
|
60
|
+
callback();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return () => {
|
|
64
|
+
levelSub?.remove?.();
|
|
65
|
+
stateSub?.remove?.();
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ─── Helpers ───
|
|
70
|
+
|
|
71
|
+
function mapBatteryState(
|
|
72
|
+
state: BatteryState
|
|
73
|
+
): 'charging' | 'unplugged' | 'full' {
|
|
74
|
+
switch (state) {
|
|
75
|
+
case ExpoBattery.BatteryState.CHARGING:
|
|
76
|
+
return 'charging';
|
|
77
|
+
case ExpoBattery.BatteryState.FULL:
|
|
78
|
+
return 'full';
|
|
79
|
+
case ExpoBattery.BatteryState.UNPLUGGED:
|
|
80
|
+
default:
|
|
81
|
+
return 'unplugged';
|
|
82
|
+
}
|
|
83
|
+
}
|