@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.
- package/android/build.gradle.kts +135 -0
- package/android/consumer-rules.pro +10 -0
- package/android/proguard-rules.pro +1 -0
- package/android/src/main/AndroidManifest.xml +15 -0
- package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +2981 -0
- package/android/src/main/java/com/rejourney/capture/ANRHandler.kt +206 -0
- package/android/src/main/java/com/rejourney/capture/ActivityTracker.kt +98 -0
- package/android/src/main/java/com/rejourney/capture/CaptureEngine.kt +1553 -0
- package/android/src/main/java/com/rejourney/capture/CaptureHeuristics.kt +375 -0
- package/android/src/main/java/com/rejourney/capture/CrashHandler.kt +153 -0
- package/android/src/main/java/com/rejourney/capture/MotionEvent.kt +215 -0
- package/android/src/main/java/com/rejourney/capture/SegmentUploader.kt +512 -0
- package/android/src/main/java/com/rejourney/capture/VideoEncoder.kt +773 -0
- package/android/src/main/java/com/rejourney/capture/ViewHierarchyScanner.kt +633 -0
- package/android/src/main/java/com/rejourney/capture/ViewSerializer.kt +286 -0
- package/android/src/main/java/com/rejourney/core/Constants.kt +117 -0
- package/android/src/main/java/com/rejourney/core/Logger.kt +93 -0
- package/android/src/main/java/com/rejourney/core/Types.kt +124 -0
- package/android/src/main/java/com/rejourney/lifecycle/SessionLifecycleService.kt +162 -0
- package/android/src/main/java/com/rejourney/network/DeviceAuthManager.kt +747 -0
- package/android/src/main/java/com/rejourney/network/HttpClientProvider.kt +16 -0
- package/android/src/main/java/com/rejourney/network/NetworkMonitor.kt +272 -0
- package/android/src/main/java/com/rejourney/network/UploadManager.kt +1363 -0
- package/android/src/main/java/com/rejourney/network/UploadWorker.kt +492 -0
- package/android/src/main/java/com/rejourney/privacy/PrivacyMask.kt +645 -0
- package/android/src/main/java/com/rejourney/touch/GestureClassifier.kt +233 -0
- package/android/src/main/java/com/rejourney/touch/KeyboardTracker.kt +158 -0
- package/android/src/main/java/com/rejourney/touch/TextInputTracker.kt +181 -0
- package/android/src/main/java/com/rejourney/touch/TouchInterceptor.kt +591 -0
- package/android/src/main/java/com/rejourney/utils/EventBuffer.kt +284 -0
- package/android/src/main/java/com/rejourney/utils/OEMDetector.kt +154 -0
- package/android/src/main/java/com/rejourney/utils/PerfTiming.kt +235 -0
- package/android/src/main/java/com/rejourney/utils/Telemetry.kt +297 -0
- package/android/src/main/java/com/rejourney/utils/WindowUtils.kt +84 -0
- package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +187 -0
- package/android/src/newarch/java/com/rejourney/RejourneyPackage.kt +40 -0
- package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +218 -0
- package/android/src/oldarch/java/com/rejourney/RejourneyPackage.kt +23 -0
- package/ios/Capture/RJANRHandler.h +42 -0
- package/ios/Capture/RJANRHandler.m +328 -0
- package/ios/Capture/RJCaptureEngine.h +275 -0
- package/ios/Capture/RJCaptureEngine.m +2062 -0
- package/ios/Capture/RJCaptureHeuristics.h +80 -0
- package/ios/Capture/RJCaptureHeuristics.m +903 -0
- package/ios/Capture/RJCrashHandler.h +46 -0
- package/ios/Capture/RJCrashHandler.m +313 -0
- package/ios/Capture/RJMotionEvent.h +183 -0
- package/ios/Capture/RJMotionEvent.m +183 -0
- package/ios/Capture/RJPerformanceManager.h +100 -0
- package/ios/Capture/RJPerformanceManager.m +373 -0
- package/ios/Capture/RJPixelBufferDownscaler.h +42 -0
- package/ios/Capture/RJPixelBufferDownscaler.m +85 -0
- package/ios/Capture/RJSegmentUploader.h +146 -0
- package/ios/Capture/RJSegmentUploader.m +778 -0
- package/ios/Capture/RJVideoEncoder.h +247 -0
- package/ios/Capture/RJVideoEncoder.m +1036 -0
- package/ios/Capture/RJViewControllerTracker.h +73 -0
- package/ios/Capture/RJViewControllerTracker.m +508 -0
- package/ios/Capture/RJViewHierarchyScanner.h +215 -0
- package/ios/Capture/RJViewHierarchyScanner.m +1464 -0
- package/ios/Capture/RJViewSerializer.h +119 -0
- package/ios/Capture/RJViewSerializer.m +498 -0
- package/ios/Core/RJConstants.h +124 -0
- package/ios/Core/RJConstants.m +88 -0
- package/ios/Core/RJLifecycleManager.h +85 -0
- package/ios/Core/RJLifecycleManager.m +308 -0
- package/ios/Core/RJLogger.h +61 -0
- package/ios/Core/RJLogger.m +211 -0
- package/ios/Core/RJTypes.h +176 -0
- package/ios/Core/RJTypes.m +66 -0
- package/ios/Core/Rejourney.h +64 -0
- package/ios/Core/Rejourney.mm +2495 -0
- package/ios/Network/RJDeviceAuthManager.h +94 -0
- package/ios/Network/RJDeviceAuthManager.m +967 -0
- package/ios/Network/RJNetworkMonitor.h +68 -0
- package/ios/Network/RJNetworkMonitor.m +267 -0
- package/ios/Network/RJRetryManager.h +73 -0
- package/ios/Network/RJRetryManager.m +325 -0
- package/ios/Network/RJUploadManager.h +267 -0
- package/ios/Network/RJUploadManager.m +2296 -0
- package/ios/Privacy/RJPrivacyMask.h +163 -0
- package/ios/Privacy/RJPrivacyMask.m +922 -0
- package/ios/Rejourney.h +63 -0
- package/ios/Touch/RJGestureClassifier.h +130 -0
- package/ios/Touch/RJGestureClassifier.m +333 -0
- package/ios/Touch/RJTouchInterceptor.h +169 -0
- package/ios/Touch/RJTouchInterceptor.m +772 -0
- package/ios/Utils/RJEventBuffer.h +112 -0
- package/ios/Utils/RJEventBuffer.m +358 -0
- package/ios/Utils/RJGzipUtils.h +33 -0
- package/ios/Utils/RJGzipUtils.m +89 -0
- package/ios/Utils/RJKeychainManager.h +48 -0
- package/ios/Utils/RJKeychainManager.m +111 -0
- package/ios/Utils/RJPerfTiming.h +209 -0
- package/ios/Utils/RJPerfTiming.m +264 -0
- package/ios/Utils/RJTelemetry.h +92 -0
- package/ios/Utils/RJTelemetry.m +320 -0
- package/ios/Utils/RJWindowUtils.h +66 -0
- package/ios/Utils/RJWindowUtils.m +133 -0
- package/lib/commonjs/NativeRejourney.js +40 -0
- package/lib/commonjs/components/Mask.js +79 -0
- package/lib/commonjs/index.js +1381 -0
- package/lib/commonjs/sdk/autoTracking.js +1259 -0
- package/lib/commonjs/sdk/constants.js +151 -0
- package/lib/commonjs/sdk/errorTracking.js +199 -0
- package/lib/commonjs/sdk/index.js +50 -0
- package/lib/commonjs/sdk/metricsTracking.js +204 -0
- package/lib/commonjs/sdk/navigation.js +151 -0
- package/lib/commonjs/sdk/networkInterceptor.js +412 -0
- package/lib/commonjs/sdk/utils.js +363 -0
- package/lib/commonjs/types/expo-router.d.js +2 -0
- package/lib/commonjs/types/index.js +2 -0
- package/lib/module/NativeRejourney.js +38 -0
- package/lib/module/components/Mask.js +72 -0
- package/lib/module/index.js +1284 -0
- package/lib/module/sdk/autoTracking.js +1233 -0
- package/lib/module/sdk/constants.js +145 -0
- package/lib/module/sdk/errorTracking.js +189 -0
- package/lib/module/sdk/index.js +12 -0
- package/lib/module/sdk/metricsTracking.js +187 -0
- package/lib/module/sdk/navigation.js +143 -0
- package/lib/module/sdk/networkInterceptor.js +401 -0
- package/lib/module/sdk/utils.js +342 -0
- package/lib/module/types/expo-router.d.js +2 -0
- package/lib/module/types/index.js +2 -0
- package/lib/typescript/NativeRejourney.d.ts +147 -0
- package/lib/typescript/components/Mask.d.ts +39 -0
- package/lib/typescript/index.d.ts +117 -0
- package/lib/typescript/sdk/autoTracking.d.ts +204 -0
- package/lib/typescript/sdk/constants.d.ts +120 -0
- package/lib/typescript/sdk/errorTracking.d.ts +32 -0
- package/lib/typescript/sdk/index.d.ts +9 -0
- package/lib/typescript/sdk/metricsTracking.d.ts +58 -0
- package/lib/typescript/sdk/navigation.d.ts +33 -0
- package/lib/typescript/sdk/networkInterceptor.d.ts +47 -0
- package/lib/typescript/sdk/utils.d.ts +148 -0
- package/lib/typescript/types/index.d.ts +624 -0
- package/package.json +102 -0
- package/rejourney.podspec +21 -0
- package/src/NativeRejourney.ts +165 -0
- package/src/components/Mask.tsx +80 -0
- package/src/index.ts +1459 -0
- package/src/sdk/autoTracking.ts +1373 -0
- package/src/sdk/constants.ts +134 -0
- package/src/sdk/errorTracking.ts +231 -0
- package/src/sdk/index.ts +11 -0
- package/src/sdk/metricsTracking.ts +232 -0
- package/src/sdk/navigation.ts +157 -0
- package/src/sdk/networkInterceptor.ts +440 -0
- package/src/sdk/utils.ts +369 -0
- package/src/types/expo-router.d.ts +7 -0
- 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
|
+
}
|