therms-device-tracker 1.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +145 -0
- package/CHANGELOG.md +26 -0
- package/LICENSE +21 -0
- package/README.md +386 -0
- package/android/build.gradle +25 -0
- package/android/src/main/AndroidManifest.xml +23 -0
- package/android/src/main/java/expo/modules/thermsdevicetracker/ActivityRecognitionProvider.kt +109 -0
- package/android/src/main/java/expo/modules/thermsdevicetracker/GeofenceProvider.kt +184 -0
- package/android/src/main/java/expo/modules/thermsdevicetracker/GeofenceTransitionReceiver.kt +34 -0
- package/android/src/main/java/expo/modules/thermsdevicetracker/LocationProvider.kt +84 -0
- package/android/src/main/java/expo/modules/thermsdevicetracker/LocationStore.kt +150 -0
- package/android/src/main/java/expo/modules/thermsdevicetracker/ScheduleProvider.kt +55 -0
- package/android/src/main/java/expo/modules/thermsdevicetracker/SyncProvider.kt +292 -0
- package/android/src/main/java/expo/modules/thermsdevicetracker/ThermsDeviceTrackerModule.kt +726 -0
- package/android/src/main/java/expo/modules/thermsdevicetracker/ThermsDeviceTrackerModuleSharedObject.kt +23 -0
- package/android/src/main/java/expo/modules/thermsdevicetracker/ThermsLocationService.kt +129 -0
- package/app.plugin.js +1 -0
- package/build/DeviceSettings.d.ts +14 -0
- package/build/DeviceSettings.d.ts.map +1 -0
- package/build/DeviceSettings.js +24 -0
- package/build/DeviceSettings.js.map +1 -0
- package/build/Logger.d.ts +13 -0
- package/build/Logger.d.ts.map +1 -0
- package/build/Logger.js +27 -0
- package/build/Logger.js.map +1 -0
- package/build/NativeModule.d.ts +51 -0
- package/build/NativeModule.d.ts.map +1 -0
- package/build/NativeModule.js +159 -0
- package/build/NativeModule.js.map +1 -0
- package/build/ThermsDeviceTracker.types.d.ts +204 -0
- package/build/ThermsDeviceTracker.types.d.ts.map +1 -0
- package/build/ThermsDeviceTracker.types.js +34 -0
- package/build/ThermsDeviceTracker.types.js.map +1 -0
- package/build/ThermsDeviceTrackerModule.d.ts +43 -0
- package/build/ThermsDeviceTrackerModule.d.ts.map +1 -0
- package/build/ThermsDeviceTrackerModule.js +3 -0
- package/build/ThermsDeviceTrackerModule.js.map +1 -0
- package/build/ThermsDeviceTrackerModule.web.d.ts +47 -0
- package/build/ThermsDeviceTrackerModule.web.d.ts.map +1 -0
- package/build/ThermsDeviceTrackerModule.web.js +132 -0
- package/build/ThermsDeviceTrackerModule.web.js.map +1 -0
- package/build/ThermsDeviceTrackerModuleSharedObject.d.ts +46 -0
- package/build/ThermsDeviceTrackerModuleSharedObject.d.ts.map +1 -0
- package/build/ThermsDeviceTrackerModuleSharedObject.js +24 -0
- package/build/ThermsDeviceTrackerModuleSharedObject.js.map +1 -0
- package/build/index.d.ts +101 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +221 -0
- package/build/index.js.map +1 -0
- package/build/plugin/index.d.ts +14 -0
- package/build/plugin/index.d.ts.map +1 -0
- package/build/plugin/index.js +83 -0
- package/build/plugin/index.js.map +1 -0
- package/build/tsconfig.tsbuildinfo +1 -0
- package/expo-module.config.json +9 -0
- package/ios/GeofenceManager.swift +221 -0
- package/ios/LocationProvider.swift +32 -0
- package/ios/LocationStore.swift +98 -0
- package/ios/MotionActivityProvider.swift +109 -0
- package/ios/ProviderMonitor.swift +33 -0
- package/ios/ScheduleManager.swift +33 -0
- package/ios/SyncManager.swift +186 -0
- package/ios/ThermsDeviceTracker.podspec +24 -0
- package/ios/ThermsDeviceTrackerModule.swift +632 -0
- package/ios/ThermsDeviceTrackerModuleSharedObject.swift +17 -0
- package/ios/ThermsGeofenceTests.swift +474 -0
- package/package.json +95 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Simple protocol for Sync data source seam (testability, mirrors GeofenceRegionMonitoring).
|
|
4
|
+
/// Allows injecting mock stores in SyncManager tests without concrete LocationStore dep.
|
|
5
|
+
/// Both batch and immediate use live getAll() at perform time.
|
|
6
|
+
protocol SyncDataSource: AnyObject {
|
|
7
|
+
func getAll() -> [[String: Any]]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/// Focused manager for opt-in native background HTTP data sync.
|
|
11
|
+
/// Mirrors GeofenceManager + ScheduleManager seams for consistency (callbacks, injection, testability).
|
|
12
|
+
///
|
|
13
|
+
/// Responsibilities:
|
|
14
|
+
/// - Accept sync config (url, method, batch/immediate, interval, etc.) from JS via module.
|
|
15
|
+
/// - Pull data from injected LocationStore (primarily locations; geofence data not included in sync payloads).
|
|
16
|
+
/// - Perform native HTTP POST/PUT using URLSession (background config + temp file body for suspended/killed app durability).
|
|
17
|
+
/// - Support immediate (on data) or periodic batch.
|
|
18
|
+
/// - Emit via onSync callback (success/fail, count, etc.). No direct sendEvent coupling.
|
|
19
|
+
///
|
|
20
|
+
/// Key invariants (do not regress):
|
|
21
|
+
/// - Live `getAll()` from the injected SyncDataSource at perform time for both batch and immediate.
|
|
22
|
+
/// - Consumer responsibility: on success we only emit; caller decides to destroyLocations().
|
|
23
|
+
/// - onSync callback only; module owns sendEvent.
|
|
24
|
+
///
|
|
25
|
+
/// This keeps ThermsDeviceTrackerModule thin. Callback pattern: owner (module) sets `onSync` closure;
|
|
26
|
+
/// manager never calls sendEvent directly. See GeofenceManager/ScheduleManager for same seam.
|
|
27
|
+
///
|
|
28
|
+
/// Config (from ThermsConfig.sync):
|
|
29
|
+
/// - enabled, url (required), method, headers, batch, interval, maxBatchSize.
|
|
30
|
+
///
|
|
31
|
+
/// Data: reuses persisted records from LocationStore (see insert/getAll). Module controls when
|
|
32
|
+
/// to call syncNow() (e.g. after location insert if immediate, or on schedule heartbeat).
|
|
33
|
+
///
|
|
34
|
+
/// ---
|
|
35
|
+
/// iOS PERIODIC BATCH LIMITATIONS + REQUIREMENTS (CRITICAL - READ):
|
|
36
|
+
/// - **Current impl**: periodic batch uses a repeating `Timer` (scheduled in `start`). This ONLY fires
|
|
37
|
+
/// while the app process is alive (foreground, or limited bg execution after user activity).
|
|
38
|
+
/// It does NOT wake a suspended or terminated app.
|
|
39
|
+
/// - **For true background periodic sync on iOS** (while suspended/killed):
|
|
40
|
+
/// 1. Ensure "Background fetch" capability / UIBackgroundModes includes "fetch" (config plugin adds this
|
|
41
|
+
/// automatically when isIosBackgroundLocationEnabled, along with 'location').
|
|
42
|
+
/// 2. Pass `iosBackgroundTaskIdentifier` (e.g. "com.yourcompany.therms.sync") to the config plugin in app.json.
|
|
43
|
+
/// The plugin (see plugin/src/index.ts + ThermsDeviceTrackerPluginProps) appends it to BGTaskSchedulerPermittedIdentifiers.
|
|
44
|
+
/// 3. In your AppDelegate (or SceneDelegate equivalent), BEFORE app finishes launching:
|
|
45
|
+
/// `BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.yourcompany.therms.sync", using: nil) { task in
|
|
46
|
+
/// // Your handler: call into your extended module or use expo-task-manager + JS ThermsDeviceTracker.startSync().
|
|
47
|
+
/// // Then: task.setTaskCompleted(success: true)
|
|
48
|
+
/// // Optionally schedule next: let req = BGAppRefreshTaskRequest(identifier: ...); try? BGTaskScheduler.shared.submit(req)
|
|
49
|
+
/// }`
|
|
50
|
+
/// The identifier **must match exactly** the iosBackgroundTaskIdentifier supplied to the plugin.
|
|
51
|
+
/// 4. Submit a refresh task request from active code or prior handler.
|
|
52
|
+
/// - The `performSync` itself *does* use a background-configured URLSession (uploadTask with temp file body), which CAN continue
|
|
53
|
+
/// uploads even if the app is suspended/killed (OS will manage the session). But *scheduling* the batch
|
|
54
|
+
/// call requires BGTaskScheduler (or the demo Timer).
|
|
55
|
+
/// - See Apple "BGTaskScheduler", "Background Modes", "URLSessionConfiguration.background".
|
|
56
|
+
/// - In practice for Expo apps, many use expo-task-manager or a host AppDelegate wrapper for registration.
|
|
57
|
+
/// Without the above, batch falls back to Timer-only behavior.
|
|
58
|
+
/// - Registration seam notes: See the full AppDelegate Swift example + expo-task-manager alternative in README.md
|
|
59
|
+
/// ("iOS Background Sync..." section, which includes use of the identifier and cross-refs example/App.tsx).
|
|
60
|
+
/// The example/App.tsx has explicit comments + small helper demonstrating registration using iosBackgroundTaskIdentifier.
|
|
61
|
+
/// The module does not expose a public `shared` singleton or direct `syncManager` by default (thin coordinator design).
|
|
62
|
+
/// For pure native BGTask handlers you would extend the native module yourself to add static access + a trigger.
|
|
63
|
+
/// Recommended for Expo/JS apps: use expo-task-manager + ThermsDeviceTracker.startSync() (wakes JS, calls through public API).
|
|
64
|
+
/// This triggers the manager (see example/App.tsx).
|
|
65
|
+
/// ---
|
|
66
|
+
final class SyncManager {
|
|
67
|
+
// Callback supplied by owner (module) for events like onSync.
|
|
68
|
+
// Pattern: consistent onSync callback (no direct sendEvent). Mirrors other *Manager seams.
|
|
69
|
+
var onSync: (([String: Any]) -> Void)?
|
|
70
|
+
|
|
71
|
+
// Injected for data access + test seams (like store in GeofenceManager).
|
|
72
|
+
private weak var store: SyncDataSource?
|
|
73
|
+
|
|
74
|
+
private var config: [String: Any]?
|
|
75
|
+
private var isEnabled = false
|
|
76
|
+
private var timer: Timer? // fallback/demo; real bg uses BGTaskScheduler
|
|
77
|
+
|
|
78
|
+
init(store: SyncDataSource? = nil) {
|
|
79
|
+
self.store = store
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// Start/configure sync. Called from module on ready/start when sync.enabled.
|
|
83
|
+
/// Mirrors scheduleManager.start(config).
|
|
84
|
+
func start(config: [String: Any]?) {
|
|
85
|
+
stop()
|
|
86
|
+
self.config = config
|
|
87
|
+
isEnabled = (config?["enabled"] as? Bool) ?? false
|
|
88
|
+
guard isEnabled, let url = config?["url"] as? String, !url.isEmpty else {
|
|
89
|
+
onSync?(["state": "disabled", "reason": "no url or disabled"])
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
onSync?(["state": "started", "url": url])
|
|
93
|
+
|
|
94
|
+
let batch = (config?["batch"] as? Bool) ?? true
|
|
95
|
+
if batch {
|
|
96
|
+
let interval = (config?["interval"] as? TimeInterval) ?? 60.0
|
|
97
|
+
// Demo/fallback periodic via Timer (process must be alive). See top-of-file CRITICAL section (updated)
|
|
98
|
+
// for full BGTaskScheduler + iosBackgroundTaskIdentifier (via plugin) + AppDelegate registration requirements
|
|
99
|
+
// to get OS bg periodic execution. (Without host registration, this Timer path is the only periodic mechanism.)
|
|
100
|
+
// Note: unlike Android WorkManager (15min min for periodic), Timer accepts small intervals here.
|
|
101
|
+
timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in
|
|
102
|
+
self?.performSync()
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
// Immediate mode: caller (module) should invoke syncNow() after data events.
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
func stop() {
|
|
110
|
+
timer?.invalidate()
|
|
111
|
+
timer = nil
|
|
112
|
+
isEnabled = false
|
|
113
|
+
config = nil
|
|
114
|
+
onSync?(["state": "stopped"])
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// Trigger a sync (called by module on location if immediate, or from schedule).
|
|
118
|
+
/// Pulls from store (locations primarily).
|
|
119
|
+
func syncNow() {
|
|
120
|
+
guard isEnabled else { return }
|
|
121
|
+
performSync()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private func performSync() {
|
|
125
|
+
guard let urlStr = config?["url"] as? String,
|
|
126
|
+
let url = URL(string: urlStr),
|
|
127
|
+
let store = store else { return }
|
|
128
|
+
|
|
129
|
+
let items = store.getAll()
|
|
130
|
+
guard !items.isEmpty else {
|
|
131
|
+
onSync?(["state": "noop", "count": 0])
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let maxBatchSize = (config?["maxBatchSize"] as? Int) ?? 0
|
|
136
|
+
let payload: [[String: Any]] = maxBatchSize > 0 ? Array(items.prefix(maxBatchSize)) : items
|
|
137
|
+
// maxBatchSize <= 0 (or absent) means "send all available" (matches Android SyncWorker behavior).
|
|
138
|
+
|
|
139
|
+
let method = (config?["method"] as? String)?.uppercased() ?? "POST"
|
|
140
|
+
var req = URLRequest(url: url)
|
|
141
|
+
req.httpMethod = method
|
|
142
|
+
if let headers = config?["headers"] as? [String: String] {
|
|
143
|
+
for (k, v) in headers { req.setValue(v, forHTTPHeaderField: k) }
|
|
144
|
+
}
|
|
145
|
+
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
146
|
+
|
|
147
|
+
// Serialize payload. For background URLSession uploads (esp. when app may suspend/kill),
|
|
148
|
+
// write to a temp file and use uploadTask(with:fromFile:) instead of in-memory body.
|
|
149
|
+
// This follows Apple guidance for background transfer service durability.
|
|
150
|
+
let fileURL: URL
|
|
151
|
+
do {
|
|
152
|
+
let bodyData = try JSONSerialization.data(withJSONObject: payload)
|
|
153
|
+
let tmpDir = FileManager.default.temporaryDirectory
|
|
154
|
+
fileURL = tmpDir.appendingPathComponent("therms-sync-\(UUID().uuidString).json")
|
|
155
|
+
try bodyData.write(to: fileURL)
|
|
156
|
+
} catch {
|
|
157
|
+
onSync?(["state": "error", "message": "serialize failed"])
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Use background-capable session for bg execution.
|
|
162
|
+
let bgConfig = URLSessionConfiguration.background(withIdentifier: "therms.sync.\(UUID().uuidString)")
|
|
163
|
+
let session = URLSession(configuration: bgConfig)
|
|
164
|
+
|
|
165
|
+
let task = session.uploadTask(with: req, fromFile: fileURL) { [weak self] data, response, error in
|
|
166
|
+
let httpResp = response as? HTTPURLResponse
|
|
167
|
+
let success = (error == nil) && (httpResp?.statusCode ?? 500) < 300
|
|
168
|
+
self?.onSync?([
|
|
169
|
+
"state": success ? "success" : "error",
|
|
170
|
+
"success": success,
|
|
171
|
+
"status": httpResp?.statusCode ?? -1,
|
|
172
|
+
"count": payload.count,
|
|
173
|
+
"message": error?.localizedDescription
|
|
174
|
+
])
|
|
175
|
+
|
|
176
|
+
// Best-effort cleanup of temp body file (harmless if already removed).
|
|
177
|
+
try? FileManager.default.removeItem(at: fileURL)
|
|
178
|
+
|
|
179
|
+
if success {
|
|
180
|
+
// Best-effort: after successful send of batch prefix, caller/module can destroy if desired.
|
|
181
|
+
// For now emit; consumer decides (or future auto after ack).
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
task.resume()
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Pod::Spec.new do |s|
|
|
2
|
+
s.name = 'ThermsDeviceTracker'
|
|
3
|
+
s.version = '1.0.0'
|
|
4
|
+
s.summary = 'Device physical activity and geolocation tracking for THERMS'
|
|
5
|
+
s.description = 'Expo native module providing unified access to location updates, physical activity recognition (walking/running/etc), and pedometer data with foreground and background support.'
|
|
6
|
+
s.author = 'Cory Robinson <cory@therms.io>'
|
|
7
|
+
s.homepage = 'https://www.therms.io'
|
|
8
|
+
s.platforms = {
|
|
9
|
+
:ios => '16.4',
|
|
10
|
+
:tvos => '16.4'
|
|
11
|
+
}
|
|
12
|
+
s.source = { git: '' }
|
|
13
|
+
s.static_framework = true
|
|
14
|
+
|
|
15
|
+
s.dependency 'ExpoModulesCore'
|
|
16
|
+
|
|
17
|
+
# Swift/Objective-C compatibility
|
|
18
|
+
s.pod_target_xcconfig = {
|
|
19
|
+
'DEFINES_MODULE' => 'YES',
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
|
|
23
|
+
s.exclude_files = "**/*Tests.{swift,h,m,mm}"
|
|
24
|
+
end
|