@rejourneyco/react-native 1.0.2 → 1.0.3
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/src/main/java/com/rejourney/RejourneyModuleImpl.kt +38 -363
- package/android/src/main/java/com/rejourney/capture/CaptureEngine.kt +11 -113
- package/android/src/main/java/com/rejourney/capture/SegmentUploader.kt +1 -15
- package/android/src/main/java/com/rejourney/capture/VideoEncoder.kt +1 -61
- package/android/src/main/java/com/rejourney/capture/ViewHierarchyScanner.kt +3 -1
- package/android/src/main/java/com/rejourney/lifecycle/SessionLifecycleService.kt +1 -22
- package/android/src/main/java/com/rejourney/network/DeviceAuthManager.kt +3 -26
- package/android/src/main/java/com/rejourney/network/NetworkMonitor.kt +0 -2
- package/android/src/main/java/com/rejourney/network/UploadManager.kt +7 -93
- package/android/src/main/java/com/rejourney/network/UploadWorker.kt +5 -41
- package/android/src/main/java/com/rejourney/privacy/PrivacyMask.kt +2 -58
- package/android/src/main/java/com/rejourney/touch/TouchInterceptor.kt +4 -4
- package/android/src/main/java/com/rejourney/utils/EventBuffer.kt +36 -7
- package/ios/Capture/RJViewHierarchyScanner.m +68 -51
- package/ios/Core/RJLifecycleManager.m +0 -14
- package/ios/Core/Rejourney.mm +24 -37
- package/ios/Network/RJDeviceAuthManager.m +0 -2
- package/ios/Network/RJUploadManager.h +8 -0
- package/ios/Network/RJUploadManager.m +45 -0
- package/ios/Privacy/RJPrivacyMask.m +5 -31
- package/ios/Rejourney.h +0 -14
- package/ios/Touch/RJTouchInterceptor.m +21 -15
- package/ios/Utils/RJEventBuffer.m +57 -69
- package/ios/Utils/RJWindowUtils.m +87 -86
- package/lib/commonjs/index.js +42 -30
- package/lib/commonjs/sdk/autoTracking.js +0 -3
- package/lib/commonjs/sdk/networkInterceptor.js +0 -11
- package/lib/commonjs/sdk/utils.js +73 -14
- package/lib/module/index.js +42 -30
- package/lib/module/sdk/autoTracking.js +0 -3
- package/lib/module/sdk/networkInterceptor.js +0 -11
- package/lib/module/sdk/utils.js +73 -14
- package/lib/typescript/sdk/utils.d.ts +31 -1
- package/package.json +16 -4
- package/src/index.ts +40 -19
- package/src/sdk/autoTracking.ts +0 -2
- package/src/sdk/constants.ts +13 -13
- package/src/sdk/networkInterceptor.ts +0 -9
- package/src/sdk/utils.ts +76 -14
|
@@ -64,7 +64,6 @@ class UploadWorker(
|
|
|
64
64
|
TimeUnit.SECONDS
|
|
65
65
|
)
|
|
66
66
|
|
|
67
|
-
// For urgent uploads (app going to background), use expedited work
|
|
68
67
|
if (expedited) {
|
|
69
68
|
workRequestBuilder.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
|
|
70
69
|
}
|
|
@@ -107,13 +106,13 @@ class UploadWorker(
|
|
|
107
106
|
30,
|
|
108
107
|
TimeUnit.SECONDS
|
|
109
108
|
)
|
|
110
|
-
.setInitialDelay(2, TimeUnit.SECONDS)
|
|
109
|
+
.setInitialDelay(2, TimeUnit.SECONDS)
|
|
111
110
|
.build()
|
|
112
111
|
|
|
113
112
|
WorkManager.getInstance(context)
|
|
114
113
|
.enqueueUniqueWork(
|
|
115
114
|
RECOVERY_WORK_NAME,
|
|
116
|
-
ExistingWorkPolicy.KEEP,
|
|
115
|
+
ExistingWorkPolicy.KEEP,
|
|
117
116
|
workRequest
|
|
118
117
|
)
|
|
119
118
|
|
|
@@ -146,7 +145,6 @@ class UploadWorker(
|
|
|
146
145
|
|
|
147
146
|
try {
|
|
148
147
|
if (isRecovery) {
|
|
149
|
-
// Recovery mode: process all pending sessions
|
|
150
148
|
val success = performRecoveryUpload()
|
|
151
149
|
return@withContext if (success) Result.success() else Result.retry()
|
|
152
150
|
}
|
|
@@ -156,7 +154,6 @@ class UploadWorker(
|
|
|
156
154
|
return@withContext Result.failure()
|
|
157
155
|
}
|
|
158
156
|
|
|
159
|
-
// Upload pending data for this specific session
|
|
160
157
|
Logger.debug("[UploadWorker] ===== CALLING performSessionUpload =====")
|
|
161
158
|
Logger.debug("[UploadWorker] About to upload session: $sessionId, isFinal=$isFinal")
|
|
162
159
|
val success = performSessionUpload(sessionId, isFinal)
|
|
@@ -168,7 +165,6 @@ class UploadWorker(
|
|
|
168
165
|
Result.success()
|
|
169
166
|
} else {
|
|
170
167
|
Logger.debug("[UploadWorker] ===== UPLOAD FAILED =====")
|
|
171
|
-
// Retry up to 5 times with exponential backoff
|
|
172
168
|
if (runAttemptCount < 5) {
|
|
173
169
|
Logger.warning("[UploadWorker] ⚠️ Upload failed, will retry (attempt $runAttemptCount)")
|
|
174
170
|
Result.retry()
|
|
@@ -178,13 +174,11 @@ class UploadWorker(
|
|
|
178
174
|
}
|
|
179
175
|
}
|
|
180
176
|
} catch (e: CancellationException) {
|
|
181
|
-
// WorkManager cancellation is expected, not an error
|
|
182
177
|
Logger.debug("[UploadWorker] Work cancelled")
|
|
183
|
-
throw e
|
|
178
|
+
throw e
|
|
184
179
|
} catch (e: Exception) {
|
|
185
180
|
Logger.error("[UploadWorker] Upload error", e)
|
|
186
181
|
|
|
187
|
-
// Retry on transient errors
|
|
188
182
|
if (runAttemptCount < 5) {
|
|
189
183
|
Result.retry()
|
|
190
184
|
} else {
|
|
@@ -201,14 +195,11 @@ class UploadWorker(
|
|
|
201
195
|
private suspend fun performSessionUpload(sessionId: String, isFinal: Boolean): Boolean {
|
|
202
196
|
Logger.debug("[UploadWorker] performSessionUpload START (sessionId=$sessionId, isFinal=$isFinal)")
|
|
203
197
|
|
|
204
|
-
// EventBuffer stores events in cache directory
|
|
205
198
|
val eventBufferDir = File(appContext.cacheDir, "rj_pending/$sessionId")
|
|
206
|
-
// UploadManager stores session metadata in files directory
|
|
207
199
|
val uploadManagerDir = File(appContext.filesDir, "rejourney/pending_uploads/$sessionId")
|
|
208
200
|
|
|
209
201
|
Logger.debug("[UploadWorker] Checking directories: eventBufferDir=${eventBufferDir.exists()}, uploadManagerDir=${uploadManagerDir.exists()}")
|
|
210
202
|
|
|
211
|
-
// Read events from EventBuffer's disk storage
|
|
212
203
|
val eventsFile = File(eventBufferDir, "events.jsonl")
|
|
213
204
|
Logger.debug("[UploadWorker] ===== READING EVENTS FROM DISK =====")
|
|
214
205
|
Logger.debug("[UploadWorker] Events file path: ${eventsFile.absolutePath}")
|
|
@@ -224,7 +215,7 @@ class UploadWorker(
|
|
|
224
215
|
if (eventBufferDir.exists()) {
|
|
225
216
|
Logger.warning("[UploadWorker] EventBuffer directory contents: ${eventBufferDir.listFiles()?.map { it.name }?.joinToString(", ") ?: "empty"}")
|
|
226
217
|
}
|
|
227
|
-
return true
|
|
218
|
+
return true
|
|
228
219
|
}
|
|
229
220
|
|
|
230
221
|
Logger.debug("[UploadWorker] Reading events from file...")
|
|
@@ -245,7 +236,6 @@ class UploadWorker(
|
|
|
245
236
|
|
|
246
237
|
if (events.isEmpty()) {
|
|
247
238
|
Logger.warning("[UploadWorker] ⚠️ No events to upload for session: $sessionId (file exists but is empty or unreadable)")
|
|
248
|
-
// Clean up empty events file
|
|
249
239
|
eventsFile.delete()
|
|
250
240
|
File(eventBufferDir, "buffer_meta.json").delete()
|
|
251
241
|
if (eventBufferDir.listFiles()?.isEmpty() == true) {
|
|
@@ -256,25 +246,19 @@ class UploadWorker(
|
|
|
256
246
|
|
|
257
247
|
Logger.debug("[UploadWorker] ===== FOUND ${events.size} EVENTS TO UPLOAD =====")
|
|
258
248
|
|
|
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
249
|
val authManager = DeviceAuthManager.getInstance(appContext)
|
|
263
250
|
Logger.debug("[UploadWorker] Ensuring valid auth token before upload...")
|
|
264
251
|
|
|
265
252
|
val tokenValid = authManager.ensureValidToken()
|
|
266
253
|
if (!tokenValid) {
|
|
267
254
|
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
255
|
} else {
|
|
270
256
|
Logger.debug("[UploadWorker] Auth token is valid, proceeding with upload")
|
|
271
257
|
}
|
|
272
258
|
|
|
273
|
-
// Create upload manager for this upload (reads session metadata from uploadManagerDir)
|
|
274
259
|
val uploadManager = createUploadManager(sessionId, uploadManagerDir)
|
|
275
260
|
Logger.debug("[UploadWorker] UploadManager created (apiUrl=${uploadManager.apiUrl}, publicKey=${uploadManager.publicKey.take(8)}...)")
|
|
276
261
|
|
|
277
|
-
// Perform upload
|
|
278
262
|
Logger.debug("[UploadWorker] ===== STARTING EVENT UPLOAD =====")
|
|
279
263
|
Logger.debug("[UploadWorker] Calling uploadBatch for ${events.size} events (isFinal=$isFinal, sessionId=$sessionId)")
|
|
280
264
|
val uploadStartTime = System.currentTimeMillis()
|
|
@@ -284,28 +268,23 @@ class UploadWorker(
|
|
|
284
268
|
|
|
285
269
|
if (uploadSuccess) {
|
|
286
270
|
Logger.debug("[UploadWorker] Upload SUCCESS - cleaning up local files")
|
|
287
|
-
// Clear the events file after successful upload
|
|
288
271
|
eventsFile.delete()
|
|
289
272
|
File(eventBufferDir, "buffer_meta.json").delete()
|
|
290
273
|
|
|
291
|
-
// Clean up EventBuffer directory if empty
|
|
292
274
|
if (eventBufferDir.listFiles()?.isEmpty() == true) {
|
|
293
275
|
eventBufferDir.delete()
|
|
294
276
|
}
|
|
295
277
|
|
|
296
|
-
// If final upload, send session end
|
|
297
278
|
if (isFinal) {
|
|
298
279
|
Logger.debug("[UploadWorker] Sending session end signal...")
|
|
299
280
|
val endSuccess = uploadManager.endSession()
|
|
300
281
|
Logger.debug("[UploadWorker] Session end signal: ${if (endSuccess) "SUCCESS" else "FAILED"}")
|
|
301
|
-
// Clean up UploadManager directory
|
|
302
282
|
uploadManagerDir.deleteRecursively()
|
|
303
283
|
}
|
|
304
284
|
} else {
|
|
305
285
|
Logger.error("[UploadWorker] Upload FAILED for session $sessionId - will retry")
|
|
306
286
|
}
|
|
307
287
|
|
|
308
|
-
// Video Recovery: Check for crash segment
|
|
309
288
|
checkAndUploadCrashSegment(uploadManager, sessionId)
|
|
310
289
|
|
|
311
290
|
return uploadSuccess
|
|
@@ -324,7 +303,6 @@ class UploadWorker(
|
|
|
324
303
|
val metaJson = JSONObject(metaFile.readText())
|
|
325
304
|
val metaSessionId = metaJson.optString("sessionId")
|
|
326
305
|
|
|
327
|
-
// Only process if it matches our session
|
|
328
306
|
if (metaSessionId != sessionId) return
|
|
329
307
|
|
|
330
308
|
Logger.debug("[UploadWorker] Found pending crash segment for session $sessionId")
|
|
@@ -346,7 +324,6 @@ class UploadWorker(
|
|
|
346
324
|
|
|
347
325
|
if (success) {
|
|
348
326
|
Logger.debug("[UploadWorker] Crash segment recovered successfully")
|
|
349
|
-
// Delete metadata and file
|
|
350
327
|
metaFile.delete()
|
|
351
328
|
segmentFile.delete()
|
|
352
329
|
} else {
|
|
@@ -354,7 +331,7 @@ class UploadWorker(
|
|
|
354
331
|
}
|
|
355
332
|
} else {
|
|
356
333
|
Logger.warning("[UploadWorker] Crash segment file missing or empty: $segmentPath")
|
|
357
|
-
metaFile.delete()
|
|
334
|
+
metaFile.delete()
|
|
358
335
|
}
|
|
359
336
|
} catch (e: Exception) {
|
|
360
337
|
Logger.error("[UploadWorker] Error processing crash segment", e)
|
|
@@ -366,20 +343,15 @@ class UploadWorker(
|
|
|
366
343
|
* Scans both the EventBuffer cache directory and UploadManager pending directory.
|
|
367
344
|
*/
|
|
368
345
|
private suspend fun performRecoveryUpload(): Boolean {
|
|
369
|
-
// EventBuffer stores events in cache directory
|
|
370
346
|
val eventBufferRootDir = File(appContext.cacheDir, "rj_pending")
|
|
371
|
-
// UploadManager stores session metadata in files directory
|
|
372
347
|
val uploadManagerRootDir = File(appContext.filesDir, "rejourney/pending_uploads")
|
|
373
348
|
|
|
374
|
-
// Collect all session IDs from both locations
|
|
375
349
|
val sessionIds = mutableSetOf<String>()
|
|
376
350
|
|
|
377
|
-
// Get sessions from EventBuffer directory (has events.jsonl)
|
|
378
351
|
eventBufferRootDir.listFiles()?.filter { it.isDirectory }?.forEach {
|
|
379
352
|
sessionIds.add(it.name)
|
|
380
353
|
}
|
|
381
354
|
|
|
382
|
-
// Get sessions from UploadManager directory (has session.json metadata)
|
|
383
355
|
uploadManagerRootDir.listFiles()?.filter { it.isDirectory }?.forEach {
|
|
384
356
|
sessionIds.add(it.name)
|
|
385
357
|
}
|
|
@@ -398,7 +370,6 @@ class UploadWorker(
|
|
|
398
370
|
|
|
399
371
|
Logger.debug("[UploadWorker] Checking session for recovery: $sessionId")
|
|
400
372
|
|
|
401
|
-
// Skip current active session (marked by session.json with recent timestamp)
|
|
402
373
|
val sessionMetaFile = File(uploadManagerRootDir, "$sessionId/session.json")
|
|
403
374
|
if (sessionMetaFile.exists()) {
|
|
404
375
|
try {
|
|
@@ -406,14 +377,11 @@ class UploadWorker(
|
|
|
406
377
|
val updatedAt = meta.optLong("updatedAt", 0)
|
|
407
378
|
val age = System.currentTimeMillis() - updatedAt
|
|
408
379
|
|
|
409
|
-
// Skip if session was active less than 60 seconds ago
|
|
410
|
-
// (likely current active session)
|
|
411
380
|
if (age < 60_000) {
|
|
412
381
|
Logger.debug("[UploadWorker] Skipping recent session: $sessionId (age=${age}ms)")
|
|
413
382
|
continue
|
|
414
383
|
}
|
|
415
384
|
} catch (e: Exception) {
|
|
416
|
-
// Continue with recovery
|
|
417
385
|
}
|
|
418
386
|
}
|
|
419
387
|
|
|
@@ -444,7 +412,6 @@ class UploadWorker(
|
|
|
444
412
|
}
|
|
445
413
|
events.add(map)
|
|
446
414
|
} catch (e: Exception) {
|
|
447
|
-
// Skip malformed lines
|
|
448
415
|
}
|
|
449
416
|
}
|
|
450
417
|
}
|
|
@@ -460,7 +427,6 @@ class UploadWorker(
|
|
|
460
427
|
* Create an UploadManager configured for the given session.
|
|
461
428
|
*/
|
|
462
429
|
private fun createUploadManager(sessionId: String, sessionDir: File): UploadManager {
|
|
463
|
-
// Read session metadata
|
|
464
430
|
val sessionMetaFile = File(sessionDir, "session.json")
|
|
465
431
|
var sessionStartTime = System.currentTimeMillis()
|
|
466
432
|
var totalBackgroundTimeMs = 0L
|
|
@@ -471,11 +437,9 @@ class UploadWorker(
|
|
|
471
437
|
sessionStartTime = meta.optLong("sessionStartTime", sessionStartTime)
|
|
472
438
|
totalBackgroundTimeMs = meta.optLong("totalBackgroundTimeMs", 0)
|
|
473
439
|
} catch (e: Exception) {
|
|
474
|
-
// Use defaults
|
|
475
440
|
}
|
|
476
441
|
}
|
|
477
442
|
|
|
478
|
-
// Get auth details from DeviceAuthManager
|
|
479
443
|
val authManager = DeviceAuthManager.getInstance(appContext)
|
|
480
444
|
val publicKey = authManager.getCurrentPublicKey() ?: ""
|
|
481
445
|
val deviceHash = authManager.getCurrentDeviceHash() ?: ""
|
|
@@ -30,19 +30,18 @@ object PrivacyMask {
|
|
|
30
30
|
var maskVideoLayers: Boolean = true
|
|
31
31
|
|
|
32
32
|
private val maskPaint = Paint().apply {
|
|
33
|
-
color = Color.BLACK
|
|
33
|
+
color = Color.BLACK
|
|
34
34
|
style = Paint.Style.FILL
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
private val textPaint = Paint().apply {
|
|
38
38
|
color = Color.WHITE
|
|
39
|
-
textSize = 32f
|
|
39
|
+
textSize = 32f
|
|
40
40
|
textAlign = Paint.Align.CENTER
|
|
41
41
|
typeface = Typeface.DEFAULT_BOLD
|
|
42
42
|
isAntiAlias = true
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
// Set of nativeIDs that should be manually masked
|
|
46
45
|
private val maskedNativeIDs = mutableSetOf<String>()
|
|
47
46
|
|
|
48
47
|
/**
|
|
@@ -90,16 +89,13 @@ object PrivacyMask {
|
|
|
90
89
|
if (sensitiveRects.isEmpty()) return bitmap
|
|
91
90
|
|
|
92
91
|
return try {
|
|
93
|
-
// Create mutable copy if needed - use ARGB_8888 as fallback if config is null
|
|
94
92
|
val config = bitmap.config ?: Bitmap.Config.ARGB_8888
|
|
95
93
|
val mutableBitmap = if (bitmap.isMutable) bitmap else bitmap.copy(config, true)
|
|
96
94
|
val canvas = Canvas(mutableBitmap)
|
|
97
95
|
|
|
98
|
-
// Calculate scale factor
|
|
99
96
|
val scaleX = mutableBitmap.width.toFloat() / rootWidth
|
|
100
97
|
val scaleY = mutableBitmap.height.toFloat() / rootHeight
|
|
101
98
|
|
|
102
|
-
// Draw masks over sensitive areas
|
|
103
99
|
for (rect in sensitiveRects) {
|
|
104
100
|
val scaledRect = Rect(
|
|
105
101
|
(rect.left * scaleX).toInt(),
|
|
@@ -109,9 +105,7 @@ object PrivacyMask {
|
|
|
109
105
|
)
|
|
110
106
|
canvas.drawRect(scaledRect, maskPaint)
|
|
111
107
|
|
|
112
|
-
// Draw stars centered
|
|
113
108
|
val centerX = scaledRect.centerX().toFloat()
|
|
114
|
-
// Approximated vertical centering
|
|
115
109
|
val centerY = scaledRect.centerY().toFloat() + (textPaint.textSize / 3)
|
|
116
110
|
canvas.drawText("********", centerX, centerY, textPaint)
|
|
117
111
|
}
|
|
@@ -130,9 +124,6 @@ object PrivacyMask {
|
|
|
130
124
|
fun findSensitiveRects(rootView: View): List<Rect> {
|
|
131
125
|
val rects = mutableListOf<Rect>()
|
|
132
126
|
|
|
133
|
-
// IMPORTANT: The capture bitmap is in the Activity window/decorView coordinate space,
|
|
134
|
-
// but View.getLocationOnScreen() returns absolute screen coordinates.
|
|
135
|
-
// Convert to coordinates relative to the provided rootView to ensure masks land correctly.
|
|
136
127
|
val rootLocationOnScreen = IntArray(2)
|
|
137
128
|
rootView.getLocationOnScreen(rootLocationOnScreen)
|
|
138
129
|
|
|
@@ -144,9 +135,6 @@ object PrivacyMask {
|
|
|
144
135
|
rootHeight = rootView.height
|
|
145
136
|
)
|
|
146
137
|
|
|
147
|
-
// Fail-closed fallback: if the scan returns nothing but there is a focused sensitive view,
|
|
148
|
-
// at least mask that focused view. This protects against edge cases where parts of the
|
|
149
|
-
// hierarchy are not reachable (e.g., some dialog/modal roots) or transient layout states.
|
|
150
138
|
if (rects.isEmpty()) {
|
|
151
139
|
try {
|
|
152
140
|
val focused = rootView.findFocus()
|
|
@@ -174,7 +162,6 @@ object PrivacyMask {
|
|
|
174
162
|
}
|
|
175
163
|
}
|
|
176
164
|
} catch (e: Exception) {
|
|
177
|
-
// Best-effort only
|
|
178
165
|
}
|
|
179
166
|
}
|
|
180
167
|
return rects
|
|
@@ -184,12 +171,6 @@ object PrivacyMask {
|
|
|
184
171
|
* Find all sensitive views across ALL visible windows.
|
|
185
172
|
* This includes the main Activity window plus any dialogs, modals, or popup windows.
|
|
186
173
|
*
|
|
187
|
-
* CRITICAL: React Native modals/sheets may create views in separate windows
|
|
188
|
-
* that are not children of the main decorView. This method scans all windows
|
|
189
|
-
* to ensure text inputs in modals are properly masked.
|
|
190
|
-
*
|
|
191
|
-
* Must be run on Main Thread.
|
|
192
|
-
*
|
|
193
174
|
* @param activity The current Activity (to access window and system windows)
|
|
194
175
|
* @param primaryRootView The primary decorView for coordinate reference
|
|
195
176
|
* @return List of sensitive view rects in primaryRootView coordinate space
|
|
@@ -199,12 +180,10 @@ object PrivacyMask {
|
|
|
199
180
|
val scannedViews = mutableSetOf<View>()
|
|
200
181
|
var didBailOutEarly = false
|
|
201
182
|
|
|
202
|
-
// Get primary window location for coordinate conversion
|
|
203
183
|
val primaryLocationOnScreen = IntArray(2)
|
|
204
184
|
primaryRootView.getLocationOnScreen(primaryLocationOnScreen)
|
|
205
185
|
|
|
206
186
|
try {
|
|
207
|
-
// 1. Scan the primary decorView (main window)
|
|
208
187
|
findSensitiveViewsRecursive(
|
|
209
188
|
view = primaryRootView,
|
|
210
189
|
rects = rects,
|
|
@@ -214,8 +193,6 @@ object PrivacyMask {
|
|
|
214
193
|
)
|
|
215
194
|
scannedViews.add(primaryRootView)
|
|
216
195
|
|
|
217
|
-
// 2. Access all visible windows using reflection on WindowManager
|
|
218
|
-
// This catches Dialogs, PopupWindows, and React Native modals
|
|
219
196
|
val additionalWindows = getAllVisibleWindowRoots(activity)
|
|
220
197
|
|
|
221
198
|
for (windowRoot in additionalWindows) {
|
|
@@ -226,8 +203,6 @@ object PrivacyMask {
|
|
|
226
203
|
|
|
227
204
|
scannedViews.add(windowRoot)
|
|
228
205
|
|
|
229
|
-
// Scan this window for sensitive views
|
|
230
|
-
// Convert coordinates relative to the primary window for proper masking
|
|
231
206
|
val hitLimit = findSensitiveViewsInWindowRelativeTo(
|
|
232
207
|
windowRoot = windowRoot,
|
|
233
208
|
rects = rects,
|
|
@@ -245,14 +220,11 @@ object PrivacyMask {
|
|
|
245
220
|
|
|
246
221
|
} catch (e: Exception) {
|
|
247
222
|
Logger.warning("PrivacyMask: Multi-window scan failed: ${e.message}")
|
|
248
|
-
// Fall back to single-window scan if multi-window fails
|
|
249
223
|
if (rects.isEmpty()) {
|
|
250
224
|
return findSensitiveRects(primaryRootView)
|
|
251
225
|
}
|
|
252
226
|
}
|
|
253
227
|
|
|
254
|
-
// If we bailed out early and found nothing, do a targeted fallback
|
|
255
|
-
// with a higher per-window budget to avoid missing inputs on complex screens.
|
|
256
228
|
if (rects.isEmpty() && didBailOutEarly) {
|
|
257
229
|
try {
|
|
258
230
|
for (windowRoot in getAllVisibleWindowRoots(activity)) {
|
|
@@ -271,14 +243,11 @@ object PrivacyMask {
|
|
|
271
243
|
Logger.warning("PrivacyMask: Fallback scan recovered ${rects.size} sensitive views after early bailout")
|
|
272
244
|
}
|
|
273
245
|
} catch (_: Exception) {
|
|
274
|
-
// Best-effort only
|
|
275
246
|
}
|
|
276
247
|
}
|
|
277
248
|
|
|
278
|
-
// Fail-closed fallback for focused view
|
|
279
249
|
if (rects.isEmpty()) {
|
|
280
250
|
try {
|
|
281
|
-
// Check focused view across all windows
|
|
282
251
|
val focusedView = activity.currentFocus
|
|
283
252
|
if (focusedView != null && focusedView.isShown && isSensitiveView(focusedView) &&
|
|
284
253
|
focusedView.width > 0 && focusedView.height > 0
|
|
@@ -304,7 +273,6 @@ object PrivacyMask {
|
|
|
304
273
|
}
|
|
305
274
|
}
|
|
306
275
|
} catch (e: Exception) {
|
|
307
|
-
// Best-effort only
|
|
308
276
|
}
|
|
309
277
|
}
|
|
310
278
|
|
|
@@ -320,12 +288,9 @@ object PrivacyMask {
|
|
|
320
288
|
val roots = mutableListOf<View>()
|
|
321
289
|
|
|
322
290
|
try {
|
|
323
|
-
// Method 1: Use WindowManager's internal mViews array via reflection
|
|
324
|
-
// This is how system tools and accessibility services access all windows
|
|
325
291
|
val wmgClass = Class.forName("android.view.WindowManagerGlobal")
|
|
326
292
|
val wmgInstance = wmgClass.getMethod("getInstance").invoke(null)
|
|
327
293
|
|
|
328
|
-
// Try to get mViews field (array of DecorViews for all windows)
|
|
329
294
|
val viewsField = wmgClass.getDeclaredField("mViews")
|
|
330
295
|
viewsField.isAccessible = true
|
|
331
296
|
val views = viewsField.get(wmgInstance)
|
|
@@ -349,8 +314,6 @@ object PrivacyMask {
|
|
|
349
314
|
} catch (e: Exception) {
|
|
350
315
|
Logger.debug("PrivacyMask: Reflection method failed (${e.message}), using fallback")
|
|
351
316
|
|
|
352
|
-
// Method 2: Fallback - just scan the activity's own windows
|
|
353
|
-
// This won't catch all dialogs but is safer
|
|
354
317
|
try {
|
|
355
318
|
activity.window?.decorView?.let { roots.add(it) }
|
|
356
319
|
} catch (_: Exception) {}
|
|
@@ -385,13 +348,11 @@ object PrivacyMask {
|
|
|
385
348
|
val location = IntArray(2)
|
|
386
349
|
view.getLocationOnScreen(location)
|
|
387
350
|
|
|
388
|
-
// Convert to primary window coordinates
|
|
389
351
|
val left = location[0] - primaryLocationOnScreen[0]
|
|
390
352
|
val top = location[1] - primaryLocationOnScreen[1]
|
|
391
353
|
val right = left + view.width
|
|
392
354
|
val bottom = top + view.height
|
|
393
355
|
|
|
394
|
-
// Clip to primary window bounds
|
|
395
356
|
val clipped = Rect(
|
|
396
357
|
left.coerceAtLeast(0),
|
|
397
358
|
top.coerceAtLeast(0),
|
|
@@ -427,19 +388,16 @@ object PrivacyMask {
|
|
|
427
388
|
) {
|
|
428
389
|
if (!view.isShown) return
|
|
429
390
|
|
|
430
|
-
// Check if this is a sensitive view
|
|
431
391
|
if (isSensitiveView(view)) {
|
|
432
392
|
if (view.width > 0 && view.height > 0) {
|
|
433
393
|
val location = IntArray(2)
|
|
434
394
|
view.getLocationOnScreen(location)
|
|
435
395
|
|
|
436
|
-
// Convert absolute screen coords -> rootView-relative coords
|
|
437
396
|
val left = location[0] - rootLocationOnScreen[0]
|
|
438
397
|
val top = location[1] - rootLocationOnScreen[1]
|
|
439
398
|
val right = left + view.width
|
|
440
399
|
val bottom = top + view.height
|
|
441
400
|
|
|
442
|
-
// Clip to root bounds to avoid huge/offscreen rects
|
|
443
401
|
val clipped = Rect(
|
|
444
402
|
left.coerceAtLeast(0),
|
|
445
403
|
top.coerceAtLeast(0),
|
|
@@ -453,7 +411,6 @@ object PrivacyMask {
|
|
|
453
411
|
}
|
|
454
412
|
}
|
|
455
413
|
|
|
456
|
-
// Recurse into children
|
|
457
414
|
if (view is ViewGroup) {
|
|
458
415
|
for (i in 0 until view.childCount) {
|
|
459
416
|
findSensitiveViewsRecursive(
|
|
@@ -471,18 +428,14 @@ object PrivacyMask {
|
|
|
471
428
|
* Determine if a view should be masked.
|
|
472
429
|
*/
|
|
473
430
|
internal fun isSensitiveView(view: View): Boolean {
|
|
474
|
-
// Check for manual masking tag first
|
|
475
431
|
if (hasPrivacyTag(view)) return true
|
|
476
432
|
|
|
477
|
-
// Check if view's nativeID is in the masked set
|
|
478
433
|
val viewNativeID = view.getTag(com.facebook.react.R.id.view_tag_native_id)
|
|
479
434
|
if (viewNativeID is String && maskedNativeIDs.contains(viewNativeID)) {
|
|
480
435
|
Logger.debug("PrivacyMask: Found masked nativeID: $viewNativeID")
|
|
481
436
|
return true
|
|
482
437
|
}
|
|
483
438
|
|
|
484
|
-
// Check immediate children for nativeID - React Native nests views so
|
|
485
|
-
// the nativeID may be on a child wrapper rather than the parent container
|
|
486
439
|
if (view is ViewGroup) {
|
|
487
440
|
for (i in view.childCount - 1 downTo 0) {
|
|
488
441
|
val child = view.getChildAt(i)
|
|
@@ -494,10 +447,8 @@ object PrivacyMask {
|
|
|
494
447
|
}
|
|
495
448
|
}
|
|
496
449
|
|
|
497
|
-
// EditText and subclasses (password fields, etc.)
|
|
498
450
|
if (maskTextInputs && view is EditText) return true
|
|
499
451
|
|
|
500
|
-
// Check for WebView (both Android native and React Native)
|
|
501
452
|
if (maskWebViews && isWebViewSurface(view)) return true
|
|
502
453
|
if (maskCameraViews && isCameraPreview(view)) return true
|
|
503
454
|
if (maskVideoLayers && isVideoLayerView(view)) return true
|
|
@@ -579,7 +530,6 @@ object PrivacyMask {
|
|
|
579
530
|
}
|
|
580
531
|
}
|
|
581
532
|
|
|
582
|
-
// Class cache for prewarm (matching iOS prewarmClassCaches)
|
|
583
533
|
@Volatile
|
|
584
534
|
private var classesPrewarmed = false
|
|
585
535
|
|
|
@@ -595,14 +545,11 @@ object PrivacyMask {
|
|
|
595
545
|
classesPrewarmed = true
|
|
596
546
|
|
|
597
547
|
try {
|
|
598
|
-
// Force class loading for common sensitive view types
|
|
599
|
-
// These class lookups are cached by the JVM after first access
|
|
600
548
|
EditText::class.java
|
|
601
549
|
android.widget.TextView::class.java
|
|
602
550
|
android.widget.Button::class.java
|
|
603
551
|
ViewGroup::class.java
|
|
604
552
|
|
|
605
|
-
// Force loading of WebView class name strings (used in isSensitiveView)
|
|
606
553
|
val dummyClassNames = listOf(
|
|
607
554
|
"android.webkit.webview",
|
|
608
555
|
"webview",
|
|
@@ -613,14 +560,11 @@ object PrivacyMask {
|
|
|
613
560
|
"password",
|
|
614
561
|
"securetext"
|
|
615
562
|
)
|
|
616
|
-
// Touch each string to ensure interning
|
|
617
563
|
dummyClassNames.forEach { it.lowercase() }
|
|
618
564
|
|
|
619
|
-
// Pre-load React Native tag ID lookup (if available)
|
|
620
565
|
try {
|
|
621
566
|
com.facebook.react.R.id.view_tag_native_id
|
|
622
567
|
} catch (_: Exception) {
|
|
623
|
-
// May not be available in all configurations
|
|
624
568
|
}
|
|
625
569
|
|
|
626
570
|
Logger.debug("PrivacyMask: Class caches pre-warmed")
|
|
@@ -29,8 +29,8 @@ import kotlin.math.sqrt
|
|
|
29
29
|
* Touch point data matching iOS RJTouchPoint.
|
|
30
30
|
*/
|
|
31
31
|
data class TouchPoint(
|
|
32
|
-
val x: Float,
|
|
33
|
-
val y: Float,
|
|
32
|
+
val x: Float,
|
|
33
|
+
val y: Float,
|
|
34
34
|
val timestamp: Long,
|
|
35
35
|
val force: Float // Pressure (0.0-1.0)
|
|
36
36
|
) {
|
|
@@ -63,8 +63,8 @@ interface TouchInterceptorDelegate {
|
|
|
63
63
|
type: String, // "scroll", "swipe", "pan"
|
|
64
64
|
t0: Long, // Start timestamp
|
|
65
65
|
t1: Long, // End timestamp
|
|
66
|
-
dx: Float, // Delta X
|
|
67
|
-
dy: Float, // Delta Y
|
|
66
|
+
dx: Float, // Delta X
|
|
67
|
+
dy: Float, // Delta Y
|
|
68
68
|
v0: Float, // Initial velocity
|
|
69
69
|
v1: Float, // Final velocity
|
|
70
70
|
curve: String // "linear", "exponential_decay", "ease_out"
|
|
@@ -219,14 +219,43 @@ class EventBuffer(
|
|
|
219
219
|
}
|
|
220
220
|
|
|
221
221
|
fun readEventsAfterBatchNumber(afterBatchNumber: Int): List<Map<String, Any?>> {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
222
|
+
return lock.withLock {
|
|
223
|
+
try {
|
|
224
|
+
if (!eventsFile.exists()) {
|
|
225
|
+
return@withLock emptyList()
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
val events = mutableListOf<Map<String, Any?>>()
|
|
229
|
+
val targetIndex = maxOf(uploadedEventCount, maxOf(0, afterBatchNumber))
|
|
230
|
+
var currentIndex = 0
|
|
231
|
+
|
|
232
|
+
// Use BufferedReader to stream the file line by line
|
|
233
|
+
// This avoids loading the whole file into memory just to skip lines
|
|
234
|
+
BufferedReader(FileReader(eventsFile)).use { reader ->
|
|
235
|
+
reader.forEachLine { line ->
|
|
236
|
+
if (line.isNotBlank()) {
|
|
237
|
+
// Only parse JSON if we are past the skip threshold
|
|
238
|
+
if (currentIndex >= targetIndex) {
|
|
239
|
+
try {
|
|
240
|
+
val json = JSONObject(line)
|
|
241
|
+
val map = mutableMapOf<String, Any?>()
|
|
242
|
+
json.keys().forEach { key ->
|
|
243
|
+
map[key] = json.opt(key)
|
|
244
|
+
}
|
|
245
|
+
events.add(map)
|
|
246
|
+
} catch (_: Exception) {
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
currentIndex++
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
events
|
|
254
|
+
} catch (e: Exception) {
|
|
255
|
+
Logger.error("[EventBuffer] readEventsAfterBatchNumber: Failed to read events", e)
|
|
256
|
+
emptyList()
|
|
257
|
+
}
|
|
228
258
|
}
|
|
229
|
-
return allEvents.subList(startIndex, allEvents.size)
|
|
230
259
|
}
|
|
231
260
|
|
|
232
261
|
fun readPendingEvents(): List<Map<String, Any?>> {
|