@rejourneyco/react-native 1.0.0

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.
Files changed (152) hide show
  1. package/android/build.gradle.kts +135 -0
  2. package/android/consumer-rules.pro +10 -0
  3. package/android/proguard-rules.pro +1 -0
  4. package/android/src/main/AndroidManifest.xml +15 -0
  5. package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +2981 -0
  6. package/android/src/main/java/com/rejourney/capture/ANRHandler.kt +206 -0
  7. package/android/src/main/java/com/rejourney/capture/ActivityTracker.kt +98 -0
  8. package/android/src/main/java/com/rejourney/capture/CaptureEngine.kt +1553 -0
  9. package/android/src/main/java/com/rejourney/capture/CaptureHeuristics.kt +375 -0
  10. package/android/src/main/java/com/rejourney/capture/CrashHandler.kt +153 -0
  11. package/android/src/main/java/com/rejourney/capture/MotionEvent.kt +215 -0
  12. package/android/src/main/java/com/rejourney/capture/SegmentUploader.kt +512 -0
  13. package/android/src/main/java/com/rejourney/capture/VideoEncoder.kt +773 -0
  14. package/android/src/main/java/com/rejourney/capture/ViewHierarchyScanner.kt +633 -0
  15. package/android/src/main/java/com/rejourney/capture/ViewSerializer.kt +286 -0
  16. package/android/src/main/java/com/rejourney/core/Constants.kt +117 -0
  17. package/android/src/main/java/com/rejourney/core/Logger.kt +93 -0
  18. package/android/src/main/java/com/rejourney/core/Types.kt +124 -0
  19. package/android/src/main/java/com/rejourney/lifecycle/SessionLifecycleService.kt +162 -0
  20. package/android/src/main/java/com/rejourney/network/DeviceAuthManager.kt +747 -0
  21. package/android/src/main/java/com/rejourney/network/HttpClientProvider.kt +16 -0
  22. package/android/src/main/java/com/rejourney/network/NetworkMonitor.kt +272 -0
  23. package/android/src/main/java/com/rejourney/network/UploadManager.kt +1363 -0
  24. package/android/src/main/java/com/rejourney/network/UploadWorker.kt +492 -0
  25. package/android/src/main/java/com/rejourney/privacy/PrivacyMask.kt +645 -0
  26. package/android/src/main/java/com/rejourney/touch/GestureClassifier.kt +233 -0
  27. package/android/src/main/java/com/rejourney/touch/KeyboardTracker.kt +158 -0
  28. package/android/src/main/java/com/rejourney/touch/TextInputTracker.kt +181 -0
  29. package/android/src/main/java/com/rejourney/touch/TouchInterceptor.kt +591 -0
  30. package/android/src/main/java/com/rejourney/utils/EventBuffer.kt +284 -0
  31. package/android/src/main/java/com/rejourney/utils/OEMDetector.kt +154 -0
  32. package/android/src/main/java/com/rejourney/utils/PerfTiming.kt +235 -0
  33. package/android/src/main/java/com/rejourney/utils/Telemetry.kt +297 -0
  34. package/android/src/main/java/com/rejourney/utils/WindowUtils.kt +84 -0
  35. package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +187 -0
  36. package/android/src/newarch/java/com/rejourney/RejourneyPackage.kt +40 -0
  37. package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +218 -0
  38. package/android/src/oldarch/java/com/rejourney/RejourneyPackage.kt +23 -0
  39. package/ios/Capture/RJANRHandler.h +42 -0
  40. package/ios/Capture/RJANRHandler.m +328 -0
  41. package/ios/Capture/RJCaptureEngine.h +275 -0
  42. package/ios/Capture/RJCaptureEngine.m +2062 -0
  43. package/ios/Capture/RJCaptureHeuristics.h +80 -0
  44. package/ios/Capture/RJCaptureHeuristics.m +903 -0
  45. package/ios/Capture/RJCrashHandler.h +46 -0
  46. package/ios/Capture/RJCrashHandler.m +313 -0
  47. package/ios/Capture/RJMotionEvent.h +183 -0
  48. package/ios/Capture/RJMotionEvent.m +183 -0
  49. package/ios/Capture/RJPerformanceManager.h +100 -0
  50. package/ios/Capture/RJPerformanceManager.m +373 -0
  51. package/ios/Capture/RJPixelBufferDownscaler.h +42 -0
  52. package/ios/Capture/RJPixelBufferDownscaler.m +85 -0
  53. package/ios/Capture/RJSegmentUploader.h +146 -0
  54. package/ios/Capture/RJSegmentUploader.m +778 -0
  55. package/ios/Capture/RJVideoEncoder.h +247 -0
  56. package/ios/Capture/RJVideoEncoder.m +1036 -0
  57. package/ios/Capture/RJViewControllerTracker.h +73 -0
  58. package/ios/Capture/RJViewControllerTracker.m +508 -0
  59. package/ios/Capture/RJViewHierarchyScanner.h +215 -0
  60. package/ios/Capture/RJViewHierarchyScanner.m +1464 -0
  61. package/ios/Capture/RJViewSerializer.h +119 -0
  62. package/ios/Capture/RJViewSerializer.m +498 -0
  63. package/ios/Core/RJConstants.h +124 -0
  64. package/ios/Core/RJConstants.m +88 -0
  65. package/ios/Core/RJLifecycleManager.h +85 -0
  66. package/ios/Core/RJLifecycleManager.m +308 -0
  67. package/ios/Core/RJLogger.h +61 -0
  68. package/ios/Core/RJLogger.m +211 -0
  69. package/ios/Core/RJTypes.h +176 -0
  70. package/ios/Core/RJTypes.m +66 -0
  71. package/ios/Core/Rejourney.h +64 -0
  72. package/ios/Core/Rejourney.mm +2495 -0
  73. package/ios/Network/RJDeviceAuthManager.h +94 -0
  74. package/ios/Network/RJDeviceAuthManager.m +967 -0
  75. package/ios/Network/RJNetworkMonitor.h +68 -0
  76. package/ios/Network/RJNetworkMonitor.m +267 -0
  77. package/ios/Network/RJRetryManager.h +73 -0
  78. package/ios/Network/RJRetryManager.m +325 -0
  79. package/ios/Network/RJUploadManager.h +267 -0
  80. package/ios/Network/RJUploadManager.m +2296 -0
  81. package/ios/Privacy/RJPrivacyMask.h +163 -0
  82. package/ios/Privacy/RJPrivacyMask.m +922 -0
  83. package/ios/Rejourney.h +63 -0
  84. package/ios/Touch/RJGestureClassifier.h +130 -0
  85. package/ios/Touch/RJGestureClassifier.m +333 -0
  86. package/ios/Touch/RJTouchInterceptor.h +169 -0
  87. package/ios/Touch/RJTouchInterceptor.m +772 -0
  88. package/ios/Utils/RJEventBuffer.h +112 -0
  89. package/ios/Utils/RJEventBuffer.m +358 -0
  90. package/ios/Utils/RJGzipUtils.h +33 -0
  91. package/ios/Utils/RJGzipUtils.m +89 -0
  92. package/ios/Utils/RJKeychainManager.h +48 -0
  93. package/ios/Utils/RJKeychainManager.m +111 -0
  94. package/ios/Utils/RJPerfTiming.h +209 -0
  95. package/ios/Utils/RJPerfTiming.m +264 -0
  96. package/ios/Utils/RJTelemetry.h +92 -0
  97. package/ios/Utils/RJTelemetry.m +320 -0
  98. package/ios/Utils/RJWindowUtils.h +66 -0
  99. package/ios/Utils/RJWindowUtils.m +133 -0
  100. package/lib/commonjs/NativeRejourney.js +40 -0
  101. package/lib/commonjs/components/Mask.js +79 -0
  102. package/lib/commonjs/index.js +1381 -0
  103. package/lib/commonjs/sdk/autoTracking.js +1259 -0
  104. package/lib/commonjs/sdk/constants.js +151 -0
  105. package/lib/commonjs/sdk/errorTracking.js +199 -0
  106. package/lib/commonjs/sdk/index.js +50 -0
  107. package/lib/commonjs/sdk/metricsTracking.js +204 -0
  108. package/lib/commonjs/sdk/navigation.js +151 -0
  109. package/lib/commonjs/sdk/networkInterceptor.js +412 -0
  110. package/lib/commonjs/sdk/utils.js +363 -0
  111. package/lib/commonjs/types/expo-router.d.js +2 -0
  112. package/lib/commonjs/types/index.js +2 -0
  113. package/lib/module/NativeRejourney.js +38 -0
  114. package/lib/module/components/Mask.js +72 -0
  115. package/lib/module/index.js +1284 -0
  116. package/lib/module/sdk/autoTracking.js +1233 -0
  117. package/lib/module/sdk/constants.js +145 -0
  118. package/lib/module/sdk/errorTracking.js +189 -0
  119. package/lib/module/sdk/index.js +12 -0
  120. package/lib/module/sdk/metricsTracking.js +187 -0
  121. package/lib/module/sdk/navigation.js +143 -0
  122. package/lib/module/sdk/networkInterceptor.js +401 -0
  123. package/lib/module/sdk/utils.js +342 -0
  124. package/lib/module/types/expo-router.d.js +2 -0
  125. package/lib/module/types/index.js +2 -0
  126. package/lib/typescript/NativeRejourney.d.ts +147 -0
  127. package/lib/typescript/components/Mask.d.ts +39 -0
  128. package/lib/typescript/index.d.ts +117 -0
  129. package/lib/typescript/sdk/autoTracking.d.ts +204 -0
  130. package/lib/typescript/sdk/constants.d.ts +120 -0
  131. package/lib/typescript/sdk/errorTracking.d.ts +32 -0
  132. package/lib/typescript/sdk/index.d.ts +9 -0
  133. package/lib/typescript/sdk/metricsTracking.d.ts +58 -0
  134. package/lib/typescript/sdk/navigation.d.ts +33 -0
  135. package/lib/typescript/sdk/networkInterceptor.d.ts +47 -0
  136. package/lib/typescript/sdk/utils.d.ts +148 -0
  137. package/lib/typescript/types/index.d.ts +624 -0
  138. package/package.json +102 -0
  139. package/rejourney.podspec +21 -0
  140. package/src/NativeRejourney.ts +165 -0
  141. package/src/components/Mask.tsx +80 -0
  142. package/src/index.ts +1459 -0
  143. package/src/sdk/autoTracking.ts +1373 -0
  144. package/src/sdk/constants.ts +134 -0
  145. package/src/sdk/errorTracking.ts +231 -0
  146. package/src/sdk/index.ts +11 -0
  147. package/src/sdk/metricsTracking.ts +232 -0
  148. package/src/sdk/navigation.ts +157 -0
  149. package/src/sdk/networkInterceptor.ts +440 -0
  150. package/src/sdk/utils.ts +369 -0
  151. package/src/types/expo-router.d.ts +7 -0
  152. package/src/types/index.ts +739 -0
@@ -0,0 +1,492 @@
1
+ /**
2
+ * WorkManager-based upload worker for guaranteed background uploads.
3
+ *
4
+ * This worker survives process death and device restarts, ensuring that
5
+ * analytics events are always uploaded even when the app is killed.
6
+ *
7
+ * Key features:
8
+ * - Reads pending events from disk-persisted EventBuffer
9
+ * - Automatic retry with exponential backoff on failure
10
+ * - Network constraint ensures uploads only happen when connected
11
+ * - Unique work per session prevents duplicate uploads
12
+ */
13
+ package com.rejourney.network
14
+
15
+ import android.content.Context
16
+ import androidx.work.*
17
+ import com.rejourney.core.Logger
18
+ import kotlinx.coroutines.CancellationException
19
+ import kotlinx.coroutines.Dispatchers
20
+ import kotlinx.coroutines.withContext
21
+ import org.json.JSONObject
22
+ import java.io.File
23
+ import java.util.concurrent.TimeUnit
24
+
25
+ class UploadWorker(
26
+ private val appContext: Context,
27
+ workerParams: WorkerParameters
28
+ ) : CoroutineWorker(appContext, workerParams) {
29
+
30
+ companion object {
31
+ const val KEY_SESSION_ID = "sessionId"
32
+ const val KEY_IS_FINAL = "isFinal"
33
+ const val KEY_IS_RECOVERY = "isRecovery"
34
+
35
+ private const val WORK_NAME_PREFIX = "rejourney_upload_"
36
+ private const val RECOVERY_WORK_NAME = "rejourney_recovery_upload"
37
+
38
+ /**
39
+ * Schedule an upload for a specific session.
40
+ * Uses unique work to prevent duplicate uploads for the same session.
41
+ */
42
+ fun scheduleUpload(
43
+ context: Context,
44
+ sessionId: String,
45
+ isFinal: Boolean = false,
46
+ expedited: Boolean = false
47
+ ) {
48
+ val constraints = Constraints.Builder()
49
+ .setRequiredNetworkType(androidx.work.NetworkType.CONNECTED)
50
+ .build()
51
+
52
+ val inputData = workDataOf(
53
+ KEY_SESSION_ID to sessionId,
54
+ KEY_IS_FINAL to isFinal,
55
+ KEY_IS_RECOVERY to false
56
+ )
57
+
58
+ val workRequestBuilder = OneTimeWorkRequestBuilder<UploadWorker>()
59
+ .setConstraints(constraints)
60
+ .setInputData(inputData)
61
+ .setBackoffCriteria(
62
+ BackoffPolicy.EXPONENTIAL,
63
+ 30,
64
+ TimeUnit.SECONDS
65
+ )
66
+
67
+ // For urgent uploads (app going to background), use expedited work
68
+ if (expedited) {
69
+ workRequestBuilder.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
70
+ }
71
+
72
+ val workRequest = workRequestBuilder.build()
73
+
74
+ Logger.debug("[UploadWorker] scheduleUpload: Enqueuing work for session: $sessionId")
75
+ Logger.debug("[UploadWorker] scheduleUpload: Work name: $WORK_NAME_PREFIX$sessionId")
76
+ Logger.debug("[UploadWorker] scheduleUpload: Constraints: network=CONNECTED, expedited=$expedited")
77
+
78
+ WorkManager.getInstance(context)
79
+ .enqueueUniqueWork(
80
+ "$WORK_NAME_PREFIX$sessionId",
81
+ ExistingWorkPolicy.REPLACE,
82
+ workRequest
83
+ )
84
+
85
+ Logger.debug("[UploadWorker] ✅ Scheduled upload for session: $sessionId (final=$isFinal, expedited=$expedited)")
86
+ Logger.debug("[UploadWorker] scheduleUpload: WorkManager should execute this work when network is available")
87
+ }
88
+
89
+ /**
90
+ * Schedule recovery of any pending uploads from previous sessions.
91
+ * Called on app startup to ensure no data is lost.
92
+ */
93
+ fun scheduleRecoveryUpload(context: Context) {
94
+ val constraints = Constraints.Builder()
95
+ .setRequiredNetworkType(androidx.work.NetworkType.CONNECTED)
96
+ .build()
97
+
98
+ val inputData = workDataOf(
99
+ KEY_IS_RECOVERY to true
100
+ )
101
+
102
+ val workRequest = OneTimeWorkRequestBuilder<UploadWorker>()
103
+ .setConstraints(constraints)
104
+ .setInputData(inputData)
105
+ .setBackoffCriteria(
106
+ BackoffPolicy.EXPONENTIAL,
107
+ 30,
108
+ TimeUnit.SECONDS
109
+ )
110
+ .setInitialDelay(2, TimeUnit.SECONDS) // Small delay to let app initialize
111
+ .build()
112
+
113
+ WorkManager.getInstance(context)
114
+ .enqueueUniqueWork(
115
+ RECOVERY_WORK_NAME,
116
+ ExistingWorkPolicy.KEEP, // Don't replace if already running
117
+ workRequest
118
+ )
119
+
120
+ Logger.debug("[UploadWorker] Scheduled recovery upload")
121
+ }
122
+
123
+ /**
124
+ * Cancel any pending upload work for a session.
125
+ */
126
+ fun cancelUpload(context: Context, sessionId: String) {
127
+ WorkManager.getInstance(context)
128
+ .cancelUniqueWork("$WORK_NAME_PREFIX$sessionId")
129
+ }
130
+ }
131
+
132
+ override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
133
+ Logger.debug("[UploadWorker] ========================================")
134
+ Logger.debug("[UploadWorker] ===== DO WORK CALLED BY WORKMANAGER =====")
135
+ Logger.debug("[UploadWorker] ========================================")
136
+ Logger.debug("[UploadWorker] WorkManager execution started at ${System.currentTimeMillis()}")
137
+
138
+ val sessionId = inputData.getString(KEY_SESSION_ID)
139
+ val isFinal = inputData.getBoolean(KEY_IS_FINAL, false)
140
+ val isRecovery = inputData.getBoolean(KEY_IS_RECOVERY, false)
141
+
142
+ Logger.debug("[UploadWorker] ===== STARTING WORK =====")
143
+ Logger.debug("[UploadWorker] Starting work (sessionId=$sessionId, isFinal=$isFinal, isRecovery=$isRecovery, attempt=${runAttemptCount})")
144
+ Logger.debug("[UploadWorker] Input data keys: ${inputData.keyValueMap.keys.joinToString(", ")}")
145
+ Logger.debug("[UploadWorker] Input data values: sessionId=$sessionId, isFinal=$isFinal, isRecovery=$isRecovery")
146
+
147
+ try {
148
+ if (isRecovery) {
149
+ // Recovery mode: process all pending sessions
150
+ val success = performRecoveryUpload()
151
+ return@withContext if (success) Result.success() else Result.retry()
152
+ }
153
+
154
+ if (sessionId.isNullOrEmpty()) {
155
+ Logger.warning("[UploadWorker] No session ID provided")
156
+ return@withContext Result.failure()
157
+ }
158
+
159
+ // Upload pending data for this specific session
160
+ Logger.debug("[UploadWorker] ===== CALLING performSessionUpload =====")
161
+ Logger.debug("[UploadWorker] About to upload session: $sessionId, isFinal=$isFinal")
162
+ val success = performSessionUpload(sessionId, isFinal)
163
+ Logger.debug("[UploadWorker] performSessionUpload returned: $success")
164
+
165
+ if (success) {
166
+ Logger.debug("[UploadWorker] ===== UPLOAD SUCCESS =====")
167
+ Logger.debug("[UploadWorker] ✅ Upload succeeded for session: $sessionId")
168
+ Result.success()
169
+ } else {
170
+ Logger.debug("[UploadWorker] ===== UPLOAD FAILED =====")
171
+ // Retry up to 5 times with exponential backoff
172
+ if (runAttemptCount < 5) {
173
+ Logger.warning("[UploadWorker] ⚠️ Upload failed, will retry (attempt $runAttemptCount)")
174
+ Result.retry()
175
+ } else {
176
+ Logger.error("[UploadWorker] ❌ Upload failed after $runAttemptCount attempts")
177
+ Result.failure()
178
+ }
179
+ }
180
+ } catch (e: CancellationException) {
181
+ // WorkManager cancellation is expected, not an error
182
+ Logger.debug("[UploadWorker] Work cancelled")
183
+ throw e // Re-throw to let WorkManager handle cancellation
184
+ } catch (e: Exception) {
185
+ Logger.error("[UploadWorker] Upload error", e)
186
+
187
+ // Retry on transient errors
188
+ if (runAttemptCount < 5) {
189
+ Result.retry()
190
+ } else {
191
+ Result.failure()
192
+ }
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Upload pending data for a specific session.
198
+ * Events are stored in cacheDir/rj_pending/<sessionId>/events.jsonl by EventBuffer.
199
+ * Session metadata is stored in filesDir/rejourney/pending_uploads/<sessionId>/session.json by UploadManager.
200
+ */
201
+ private suspend fun performSessionUpload(sessionId: String, isFinal: Boolean): Boolean {
202
+ Logger.debug("[UploadWorker] performSessionUpload START (sessionId=$sessionId, isFinal=$isFinal)")
203
+
204
+ // EventBuffer stores events in cache directory
205
+ val eventBufferDir = File(appContext.cacheDir, "rj_pending/$sessionId")
206
+ // UploadManager stores session metadata in files directory
207
+ val uploadManagerDir = File(appContext.filesDir, "rejourney/pending_uploads/$sessionId")
208
+
209
+ Logger.debug("[UploadWorker] Checking directories: eventBufferDir=${eventBufferDir.exists()}, uploadManagerDir=${uploadManagerDir.exists()}")
210
+
211
+ // Read events from EventBuffer's disk storage
212
+ val eventsFile = File(eventBufferDir, "events.jsonl")
213
+ Logger.debug("[UploadWorker] ===== READING EVENTS FROM DISK =====")
214
+ Logger.debug("[UploadWorker] Events file path: ${eventsFile.absolutePath}")
215
+ Logger.debug("[UploadWorker] Events file exists: ${eventsFile.exists()}")
216
+
217
+ if (eventsFile.exists()) {
218
+ Logger.debug("[UploadWorker] Events file size: ${eventsFile.length()} bytes")
219
+ }
220
+
221
+ if (!eventsFile.exists()) {
222
+ Logger.warning("[UploadWorker] ⚠️ No events file for session: $sessionId - nothing to upload")
223
+ Logger.warning("[UploadWorker] EventBuffer directory exists: ${eventBufferDir.exists()}")
224
+ if (eventBufferDir.exists()) {
225
+ Logger.warning("[UploadWorker] EventBuffer directory contents: ${eventBufferDir.listFiles()?.map { it.name }?.joinToString(", ") ?: "empty"}")
226
+ }
227
+ return true // Nothing to upload is success
228
+ }
229
+
230
+ Logger.debug("[UploadWorker] Reading events from file...")
231
+ val readStartTime = System.currentTimeMillis()
232
+ val events = readEventsFromFile(eventsFile)
233
+ val readDuration = System.currentTimeMillis() - readStartTime
234
+
235
+ Logger.debug("[UploadWorker] ✅ Read ${events.size} events from file in ${readDuration}ms (sessionId=$sessionId)")
236
+
237
+ if (events.isNotEmpty()) {
238
+ val eventTypes = events.mapNotNull { it["type"]?.toString() }.distinct()
239
+ Logger.debug("[UploadWorker] Event types found: ${eventTypes.joinToString(", ")}")
240
+ Logger.debug("[UploadWorker] First event: type=${events.first()["type"]}, timestamp=${events.first()["timestamp"]}")
241
+ if (events.size > 1) {
242
+ Logger.debug("[UploadWorker] Last event: type=${events.last()["type"]}, timestamp=${events.last()["timestamp"]}")
243
+ }
244
+ }
245
+
246
+ if (events.isEmpty()) {
247
+ Logger.warning("[UploadWorker] ⚠️ No events to upload for session: $sessionId (file exists but is empty or unreadable)")
248
+ // Clean up empty events file
249
+ eventsFile.delete()
250
+ File(eventBufferDir, "buffer_meta.json").delete()
251
+ if (eventBufferDir.listFiles()?.isEmpty() == true) {
252
+ eventBufferDir.delete()
253
+ }
254
+ return true
255
+ }
256
+
257
+ Logger.debug("[UploadWorker] ===== FOUND ${events.size} EVENTS TO UPLOAD =====")
258
+
259
+ // CRITICAL FIX: Ensure valid auth token before attempting any upload!
260
+ // This was the root cause of silent upload failures - the token would be
261
+ // expired/missing and presign requests would fail silently.
262
+ val authManager = DeviceAuthManager.getInstance(appContext)
263
+ Logger.debug("[UploadWorker] Ensuring valid auth token before upload...")
264
+
265
+ val tokenValid = authManager.ensureValidToken()
266
+ if (!tokenValid) {
267
+ Logger.error("[UploadWorker] FAILED to obtain valid auth token - upload will likely fail!")
268
+ // Continue anyway, the upload will fail but at least we'll get detailed error logs
269
+ } else {
270
+ Logger.debug("[UploadWorker] Auth token is valid, proceeding with upload")
271
+ }
272
+
273
+ // Create upload manager for this upload (reads session metadata from uploadManagerDir)
274
+ val uploadManager = createUploadManager(sessionId, uploadManagerDir)
275
+ Logger.debug("[UploadWorker] UploadManager created (apiUrl=${uploadManager.apiUrl}, publicKey=${uploadManager.publicKey.take(8)}...)")
276
+
277
+ // Perform upload
278
+ Logger.debug("[UploadWorker] ===== STARTING EVENT UPLOAD =====")
279
+ Logger.debug("[UploadWorker] Calling uploadBatch for ${events.size} events (isFinal=$isFinal, sessionId=$sessionId)")
280
+ val uploadStartTime = System.currentTimeMillis()
281
+ val uploadSuccess = uploadManager.uploadBatch(events, isFinal = isFinal)
282
+ val uploadDuration = System.currentTimeMillis() - uploadStartTime
283
+ Logger.debug("[UploadWorker] uploadBatch returned: $uploadSuccess (duration=${uploadDuration}ms)")
284
+
285
+ if (uploadSuccess) {
286
+ Logger.debug("[UploadWorker] Upload SUCCESS - cleaning up local files")
287
+ // Clear the events file after successful upload
288
+ eventsFile.delete()
289
+ File(eventBufferDir, "buffer_meta.json").delete()
290
+
291
+ // Clean up EventBuffer directory if empty
292
+ if (eventBufferDir.listFiles()?.isEmpty() == true) {
293
+ eventBufferDir.delete()
294
+ }
295
+
296
+ // If final upload, send session end
297
+ if (isFinal) {
298
+ Logger.debug("[UploadWorker] Sending session end signal...")
299
+ val endSuccess = uploadManager.endSession()
300
+ Logger.debug("[UploadWorker] Session end signal: ${if (endSuccess) "SUCCESS" else "FAILED"}")
301
+ // Clean up UploadManager directory
302
+ uploadManagerDir.deleteRecursively()
303
+ }
304
+ } else {
305
+ Logger.error("[UploadWorker] Upload FAILED for session $sessionId - will retry")
306
+ }
307
+
308
+ // Video Recovery: Check for crash segment
309
+ checkAndUploadCrashSegment(uploadManager, sessionId)
310
+
311
+ return uploadSuccess
312
+ }
313
+
314
+ /**
315
+ * Check for and upload a pending crash segment (saved by emergencyFlush).
316
+ */
317
+ private suspend fun checkAndUploadCrashSegment(uploadManager: UploadManager, sessionId: String) {
318
+ val segmentsDir = File(appContext.filesDir, "rejourney/segments")
319
+ val metaFile = File(segmentsDir, "pending_crash_segment.json")
320
+
321
+ if (!metaFile.exists()) return
322
+
323
+ try {
324
+ val metaJson = JSONObject(metaFile.readText())
325
+ val metaSessionId = metaJson.optString("sessionId")
326
+
327
+ // Only process if it matches our session
328
+ if (metaSessionId != sessionId) return
329
+
330
+ Logger.debug("[UploadWorker] Found pending crash segment for session $sessionId")
331
+
332
+ val segmentPath = metaJson.optString("segmentFile")
333
+ val startTime = metaJson.optLong("startTime")
334
+ val endTime = metaJson.optLong("endTime")
335
+ val frameCount = metaJson.optInt("frameCount")
336
+ val segmentFile = File(segmentPath)
337
+
338
+ if (segmentFile.exists() && segmentFile.length() > 0) {
339
+ Logger.debug("[UploadWorker] Recovering crash segment: ${segmentFile.name} ($frameCount frames)")
340
+ val success = uploadManager.uploadVideoSegment(
341
+ segmentFile = segmentFile,
342
+ startTime = startTime,
343
+ endTime = endTime,
344
+ frameCount = frameCount
345
+ )
346
+
347
+ if (success) {
348
+ Logger.debug("[UploadWorker] Crash segment recovered successfully")
349
+ // Delete metadata and file
350
+ metaFile.delete()
351
+ segmentFile.delete()
352
+ } else {
353
+ Logger.warning("[UploadWorker] Failed to upload crash segment")
354
+ }
355
+ } else {
356
+ Logger.warning("[UploadWorker] Crash segment file missing or empty: $segmentPath")
357
+ metaFile.delete() // Clean up invalid metadata
358
+ }
359
+ } catch (e: Exception) {
360
+ Logger.error("[UploadWorker] Error processing crash segment", e)
361
+ }
362
+ }
363
+
364
+ /**
365
+ * Recover and upload data from all pending sessions.
366
+ * Scans both the EventBuffer cache directory and UploadManager pending directory.
367
+ */
368
+ private suspend fun performRecoveryUpload(): Boolean {
369
+ // EventBuffer stores events in cache directory
370
+ val eventBufferRootDir = File(appContext.cacheDir, "rj_pending")
371
+ // UploadManager stores session metadata in files directory
372
+ val uploadManagerRootDir = File(appContext.filesDir, "rejourney/pending_uploads")
373
+
374
+ // Collect all session IDs from both locations
375
+ val sessionIds = mutableSetOf<String>()
376
+
377
+ // Get sessions from EventBuffer directory (has events.jsonl)
378
+ eventBufferRootDir.listFiles()?.filter { it.isDirectory }?.forEach {
379
+ sessionIds.add(it.name)
380
+ }
381
+
382
+ // Get sessions from UploadManager directory (has session.json metadata)
383
+ uploadManagerRootDir.listFiles()?.filter { it.isDirectory }?.forEach {
384
+ sessionIds.add(it.name)
385
+ }
386
+
387
+ if (sessionIds.isEmpty()) {
388
+ Logger.debug("[UploadWorker] No pending sessions to recover")
389
+ return true
390
+ }
391
+
392
+ Logger.debug("[UploadWorker] Found ${sessionIds.size} sessions to check for recovery")
393
+
394
+ var allSuccess = true
395
+
396
+ for (sessionId in sessionIds) {
397
+ if (sessionId.isBlank()) continue
398
+
399
+ Logger.debug("[UploadWorker] Checking session for recovery: $sessionId")
400
+
401
+ // Skip current active session (marked by session.json with recent timestamp)
402
+ val sessionMetaFile = File(uploadManagerRootDir, "$sessionId/session.json")
403
+ if (sessionMetaFile.exists()) {
404
+ try {
405
+ val meta = JSONObject(sessionMetaFile.readText())
406
+ val updatedAt = meta.optLong("updatedAt", 0)
407
+ val age = System.currentTimeMillis() - updatedAt
408
+
409
+ // Skip if session was active less than 60 seconds ago
410
+ // (likely current active session)
411
+ if (age < 60_000) {
412
+ Logger.debug("[UploadWorker] Skipping recent session: $sessionId (age=${age}ms)")
413
+ continue
414
+ }
415
+ } catch (e: Exception) {
416
+ // Continue with recovery
417
+ }
418
+ }
419
+
420
+ val success = performSessionUpload(sessionId, isFinal = true)
421
+ if (!success) {
422
+ allSuccess = false
423
+ }
424
+ }
425
+
426
+ return allSuccess
427
+ }
428
+
429
+ /**
430
+ * Read events from a JSONL file.
431
+ */
432
+ private fun readEventsFromFile(file: File): List<Map<String, Any?>> {
433
+ val events = mutableListOf<Map<String, Any?>>()
434
+
435
+ try {
436
+ file.bufferedReader().useLines { lines ->
437
+ lines.forEach { line ->
438
+ if (line.isNotBlank()) {
439
+ try {
440
+ val json = JSONObject(line)
441
+ val map = mutableMapOf<String, Any?>()
442
+ json.keys().forEach { key ->
443
+ map[key] = json.opt(key)
444
+ }
445
+ events.add(map)
446
+ } catch (e: Exception) {
447
+ // Skip malformed lines
448
+ }
449
+ }
450
+ }
451
+ }
452
+ } catch (e: Exception) {
453
+ Logger.warning("[UploadWorker] Failed to read events file: ${e.message}")
454
+ }
455
+
456
+ return events
457
+ }
458
+
459
+ /**
460
+ * Create an UploadManager configured for the given session.
461
+ */
462
+ private fun createUploadManager(sessionId: String, sessionDir: File): UploadManager {
463
+ // Read session metadata
464
+ val sessionMetaFile = File(sessionDir, "session.json")
465
+ var sessionStartTime = System.currentTimeMillis()
466
+ var totalBackgroundTimeMs = 0L
467
+
468
+ if (sessionMetaFile.exists()) {
469
+ try {
470
+ val meta = JSONObject(sessionMetaFile.readText())
471
+ sessionStartTime = meta.optLong("sessionStartTime", sessionStartTime)
472
+ totalBackgroundTimeMs = meta.optLong("totalBackgroundTimeMs", 0)
473
+ } catch (e: Exception) {
474
+ // Use defaults
475
+ }
476
+ }
477
+
478
+ // Get auth details from DeviceAuthManager
479
+ val authManager = DeviceAuthManager.getInstance(appContext)
480
+ val publicKey = authManager.getCurrentPublicKey() ?: ""
481
+ val deviceHash = authManager.getCurrentDeviceHash() ?: ""
482
+ val apiUrl = authManager.getCurrentApiUrl() ?: "https://api.rejourney.co"
483
+
484
+ return UploadManager(appContext, apiUrl).apply {
485
+ this.sessionId = sessionId
486
+ this.publicKey = publicKey
487
+ this.deviceHash = deviceHash
488
+ this.sessionStartTime = sessionStartTime
489
+ this.totalBackgroundTimeMs = totalBackgroundTimeMs
490
+ }
491
+ }
492
+ }