@rejourneyco/react-native 1.0.7 → 1.0.9
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/README.md +1 -1
- package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +109 -26
- package/android/src/main/java/com/rejourney/engine/DeviceRegistrar.kt +18 -3
- package/android/src/main/java/com/rejourney/engine/RejourneyImpl.kt +69 -17
- package/android/src/main/java/com/rejourney/recording/AnrSentinel.kt +27 -2
- package/android/src/main/java/com/rejourney/recording/InteractionRecorder.kt +30 -0
- package/android/src/main/java/com/rejourney/recording/RejourneyNetworkInterceptor.kt +100 -0
- package/android/src/main/java/com/rejourney/recording/ReplayOrchestrator.kt +260 -174
- package/android/src/main/java/com/rejourney/recording/SegmentDispatcher.kt +246 -34
- package/android/src/main/java/com/rejourney/recording/SpecialCases.kt +572 -0
- package/android/src/main/java/com/rejourney/recording/StabilityMonitor.kt +3 -0
- package/android/src/main/java/com/rejourney/recording/TelemetryPipeline.kt +19 -4
- package/android/src/main/java/com/rejourney/recording/ViewHierarchyScanner.kt +8 -0
- package/android/src/main/java/com/rejourney/recording/VisualCapture.kt +251 -85
- package/android/src/newarch/java/com/rejourney/RejourneyModule.kt +14 -0
- package/android/src/oldarch/java/com/rejourney/RejourneyModule.kt +18 -0
- package/ios/Engine/DeviceRegistrar.swift +13 -3
- package/ios/Engine/RejourneyImpl.swift +202 -133
- package/ios/Recording/AnrSentinel.swift +58 -25
- package/ios/Recording/InteractionRecorder.swift +29 -0
- package/ios/Recording/RejourneyURLProtocol.swift +168 -0
- package/ios/Recording/ReplayOrchestrator.swift +241 -147
- package/ios/Recording/SegmentDispatcher.swift +155 -13
- package/ios/Recording/SpecialCases.swift +614 -0
- package/ios/Recording/StabilityMonitor.swift +42 -34
- package/ios/Recording/TelemetryPipeline.swift +38 -3
- package/ios/Recording/ViewHierarchyScanner.swift +1 -0
- package/ios/Recording/VisualCapture.swift +104 -28
- package/ios/Rejourney.mm +27 -8
- package/ios/Utility/ImageBlur.swift +0 -1
- package/lib/commonjs/index.js +32 -20
- package/lib/commonjs/sdk/autoTracking.js +162 -11
- package/lib/commonjs/sdk/constants.js +2 -2
- package/lib/commonjs/sdk/networkInterceptor.js +84 -4
- package/lib/commonjs/sdk/utils.js +1 -1
- package/lib/module/index.js +32 -20
- package/lib/module/sdk/autoTracking.js +162 -11
- package/lib/module/sdk/constants.js +2 -2
- package/lib/module/sdk/networkInterceptor.js +84 -4
- package/lib/module/sdk/utils.js +1 -1
- package/lib/typescript/NativeRejourney.d.ts +5 -2
- package/lib/typescript/sdk/autoTracking.d.ts +3 -1
- package/lib/typescript/sdk/constants.d.ts +2 -2
- package/lib/typescript/types/index.d.ts +15 -8
- package/package.json +4 -4
- package/src/NativeRejourney.ts +8 -5
- package/src/index.ts +46 -29
- package/src/sdk/autoTracking.ts +176 -11
- package/src/sdk/constants.ts +2 -2
- package/src/sdk/networkInterceptor.ts +110 -1
- package/src/sdk/utils.ts +1 -1
- package/src/types/index.ts +16 -9
|
@@ -26,8 +26,11 @@ import android.graphics.Rect
|
|
|
26
26
|
import android.os.Handler
|
|
27
27
|
import android.os.Looper
|
|
28
28
|
import android.os.SystemClock
|
|
29
|
+
import android.view.TextureView
|
|
29
30
|
import android.view.View
|
|
31
|
+
import android.view.ViewGroup
|
|
30
32
|
import android.view.WindowManager
|
|
33
|
+
import android.widget.EditText
|
|
31
34
|
import com.rejourney.engine.DiagnosticLog
|
|
32
35
|
import com.rejourney.utility.gzipCompress
|
|
33
36
|
import java.io.ByteArrayOutputStream
|
|
@@ -60,7 +63,7 @@ class VisualCapture private constructor(private val context: Context) {
|
|
|
60
63
|
get() = instance
|
|
61
64
|
}
|
|
62
65
|
|
|
63
|
-
var snapshotInterval: Double = 0
|
|
66
|
+
var snapshotInterval: Double = 1.0
|
|
64
67
|
var quality: Float = 0.5f
|
|
65
68
|
|
|
66
69
|
val isCapturing: Boolean
|
|
@@ -91,18 +94,17 @@ class VisualCapture private constructor(private val context: Context) {
|
|
|
91
94
|
|
|
92
95
|
// Current activity reference
|
|
93
96
|
private var currentActivity: WeakReference<Activity>? = null
|
|
97
|
+
|
|
94
98
|
|
|
95
99
|
fun setCurrentActivity(activity: Activity?) {
|
|
96
100
|
currentActivity = if (activity != null) WeakReference(activity) else null
|
|
97
|
-
DiagnosticLog.
|
|
101
|
+
DiagnosticLog.trace("[VisualCapture] setCurrentActivity: ${activity?.javaClass?.simpleName ?: "null"}")
|
|
98
102
|
}
|
|
99
103
|
|
|
100
104
|
fun beginCapture(sessionOrigin: Long) {
|
|
101
|
-
DiagnosticLog.
|
|
102
|
-
DiagnosticLog.trace("[VisualCapture] beginCapture called, currentActivity=${currentActivity?.get()?.javaClass?.simpleName ?: "null"}")
|
|
105
|
+
DiagnosticLog.trace("[VisualCapture] beginCapture called, currentActivity=${currentActivity?.get()?.javaClass?.simpleName ?: "null"}, state=${stateMachine.currentState}")
|
|
103
106
|
if (!stateMachine.transition(CaptureState.CAPTURING)) {
|
|
104
|
-
DiagnosticLog.
|
|
105
|
-
DiagnosticLog.trace("[VisualCapture] beginCapture failed - state transition rejected")
|
|
107
|
+
DiagnosticLog.trace("[VisualCapture] beginCapture REJECTED - state transition failed from ${stateMachine.currentState}")
|
|
106
108
|
return
|
|
107
109
|
}
|
|
108
110
|
sessionEpoch = sessionOrigin
|
|
@@ -116,7 +118,6 @@ class VisualCapture private constructor(private val context: Context) {
|
|
|
116
118
|
}
|
|
117
119
|
}
|
|
118
120
|
|
|
119
|
-
DiagnosticLog.notice("[VisualCapture] Starting capture timer with interval=${snapshotInterval}s")
|
|
120
121
|
DiagnosticLog.trace("[VisualCapture] Starting capture timer with interval=${snapshotInterval}s")
|
|
121
122
|
startCaptureTimer()
|
|
122
123
|
}
|
|
@@ -138,6 +139,21 @@ class VisualCapture private constructor(private val context: Context) {
|
|
|
138
139
|
flushBufferToDisk()
|
|
139
140
|
}
|
|
140
141
|
|
|
142
|
+
/** Submit any buffered frames to the upload pipeline immediately
|
|
143
|
+
* (regardless of batch size threshold). Packages synchronously to
|
|
144
|
+
* avoid race conditions during backgrounding. */
|
|
145
|
+
fun flushBufferToNetwork() {
|
|
146
|
+
// Take frames from buffer synchronously (not via async sendScreenshots)
|
|
147
|
+
val images = stateLock.withLock {
|
|
148
|
+
val copy = screenshots.toList()
|
|
149
|
+
screenshots.clear()
|
|
150
|
+
copy
|
|
151
|
+
}
|
|
152
|
+
if (images.isEmpty()) return
|
|
153
|
+
// Package and submit synchronously on this thread
|
|
154
|
+
packageAndShip(images, sessionEpoch)
|
|
155
|
+
}
|
|
156
|
+
|
|
141
157
|
fun activateDeferredMode() {
|
|
142
158
|
deferredUntilCommit = true
|
|
143
159
|
}
|
|
@@ -155,6 +171,10 @@ class VisualCapture private constructor(private val context: Context) {
|
|
|
155
171
|
redactionMask.remove(view)
|
|
156
172
|
}
|
|
157
173
|
|
|
174
|
+
fun invalidateMaskCache() {
|
|
175
|
+
redactionMask.invalidateCache()
|
|
176
|
+
}
|
|
177
|
+
|
|
158
178
|
fun configure(snapshotInterval: Double, jpegQuality: Double) {
|
|
159
179
|
this.snapshotInterval = snapshotInterval
|
|
160
180
|
this.quality = jpegQuality.toFloat()
|
|
@@ -165,14 +185,14 @@ class VisualCapture private constructor(private val context: Context) {
|
|
|
165
185
|
}
|
|
166
186
|
|
|
167
187
|
fun snapshotNow() {
|
|
168
|
-
mainHandler.post { captureFrame() }
|
|
188
|
+
mainHandler.post { captureFrame(force = true) }
|
|
169
189
|
}
|
|
170
190
|
|
|
171
191
|
private fun startCaptureTimer() {
|
|
172
192
|
stopCaptureTimer()
|
|
173
193
|
captureRunnable = object : Runnable {
|
|
174
194
|
override fun run() {
|
|
175
|
-
captureFrame()
|
|
195
|
+
captureFrame(force = false)
|
|
176
196
|
mainHandler.postDelayed(this, (snapshotInterval * 1000).toLong())
|
|
177
197
|
}
|
|
178
198
|
}
|
|
@@ -184,101 +204,178 @@ class VisualCapture private constructor(private val context: Context) {
|
|
|
184
204
|
captureRunnable = null
|
|
185
205
|
}
|
|
186
206
|
|
|
187
|
-
private fun captureFrame() {
|
|
207
|
+
private fun captureFrame(force: Boolean = false) {
|
|
188
208
|
val currentFrameNum = frameCounter.get()
|
|
189
|
-
// Log first 3 frames at notice level
|
|
190
209
|
if (currentFrameNum < 3) {
|
|
191
|
-
DiagnosticLog.
|
|
210
|
+
DiagnosticLog.trace("[VisualCapture] captureFrame #$currentFrameNum, state=${stateMachine.currentState}, activity=${currentActivity?.get()?.javaClass?.simpleName ?: "null"}")
|
|
192
211
|
}
|
|
193
212
|
|
|
194
213
|
if (stateMachine.currentState != CaptureState.CAPTURING) {
|
|
195
|
-
DiagnosticLog.notice("[VisualCapture] captureFrame skipped - state=${stateMachine.currentState}")
|
|
196
214
|
DiagnosticLog.trace("[VisualCapture] captureFrame skipped - state=${stateMachine.currentState}")
|
|
197
215
|
return
|
|
198
216
|
}
|
|
199
217
|
|
|
200
218
|
val activity = currentActivity?.get()
|
|
201
219
|
if (activity == null) {
|
|
202
|
-
if (currentFrameNum < 3) {
|
|
203
|
-
DiagnosticLog.notice("[VisualCapture] captureFrame skipped - NO ACTIVITY")
|
|
204
|
-
}
|
|
205
220
|
DiagnosticLog.trace("[VisualCapture] captureFrame skipped - no activity")
|
|
206
221
|
return
|
|
207
222
|
}
|
|
208
223
|
|
|
224
|
+
// Refresh map detection state (very cheap shallow walk)
|
|
225
|
+
SpecialCases.shared.refreshMapState(activity)
|
|
226
|
+
|
|
227
|
+
// Map stutter prevention: when a map view is visible and its camera
|
|
228
|
+
// is still moving (user gesture or animation), skip decorView.draw()
|
|
229
|
+
// entirely — this call triggers GPU readback on SurfaceView/TextureView
|
|
230
|
+
// map tiles which causes visible stutter. We resume capture at 1 FPS
|
|
231
|
+
// once the map SDK reports idle.
|
|
232
|
+
if (!force && SpecialCases.shared.mapVisible && !SpecialCases.shared.mapIdle) {
|
|
233
|
+
if (currentFrameNum < 3 || currentFrameNum % 30 == 0L) {
|
|
234
|
+
DiagnosticLog.trace("[VisualCapture] SKIPPING capture - map moving (mapIdle=false)")
|
|
235
|
+
}
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
|
|
209
239
|
val frameStart = SystemClock.elapsedRealtime()
|
|
210
240
|
|
|
211
241
|
try {
|
|
212
|
-
val
|
|
242
|
+
val window = activity.window ?: return
|
|
243
|
+
val decorView = window.decorView
|
|
213
244
|
val bounds = Rect()
|
|
214
245
|
decorView.getWindowVisibleDisplayFrame(bounds)
|
|
215
246
|
|
|
216
247
|
if (bounds.width() <= 0 || bounds.height() <= 0) return
|
|
217
248
|
|
|
218
|
-
val redactRects = redactionMask.computeRects()
|
|
249
|
+
val redactRects = redactionMask.computeRects(decorView)
|
|
219
250
|
|
|
220
|
-
// Use lower scale to reduce encoding time significantly
|
|
221
251
|
val screenScale = 1.25f
|
|
222
252
|
val scaledWidth = (bounds.width() / screenScale).toInt()
|
|
223
253
|
val scaledHeight = (bounds.height() / screenScale).toInt()
|
|
224
254
|
|
|
255
|
+
// 1. Draw the View tree (captures everything except GPU surfaces)
|
|
225
256
|
val bitmap = Bitmap.createBitmap(scaledWidth, scaledHeight, Bitmap.Config.ARGB_8888)
|
|
226
257
|
val canvas = Canvas(bitmap)
|
|
227
258
|
canvas.scale(1f / screenScale, 1f / screenScale)
|
|
228
|
-
|
|
229
259
|
decorView.draw(canvas)
|
|
230
260
|
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
style = Paint.Style.FILL
|
|
236
|
-
}
|
|
237
|
-
for (rect in redactRects) {
|
|
238
|
-
if (rect.width() > 0 && rect.height() > 0) {
|
|
239
|
-
canvas.drawRect(
|
|
240
|
-
rect.left / screenScale,
|
|
241
|
-
rect.top / screenScale,
|
|
242
|
-
rect.right / screenScale,
|
|
243
|
-
rect.bottom / screenScale,
|
|
244
|
-
paint
|
|
245
|
-
)
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
261
|
+
// 2. Composite GPU surfaces (TextureView/SurfaceView) on top.
|
|
262
|
+
// decorView.draw() renders these as black; we grab their pixels
|
|
263
|
+
// directly and paint them at the correct position.
|
|
264
|
+
compositeGpuSurfaces(decorView, canvas, screenScale)
|
|
249
265
|
|
|
250
|
-
|
|
251
|
-
val stream = ByteArrayOutputStream()
|
|
252
|
-
bitmap.compress(Bitmap.CompressFormat.JPEG, (quality * 100).toInt(), stream)
|
|
253
|
-
bitmap.recycle()
|
|
266
|
+
processCapture(bitmap, redactRects, screenScale, frameStart, force)
|
|
254
267
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
268
|
+
} catch (e: Exception) {
|
|
269
|
+
DiagnosticLog.fault("Frame capture failed: ${e.message}")
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Find all TextureView instances in the hierarchy and draw their GPU-rendered
|
|
275
|
+
* content onto the capture canvas at the correct position. decorView.draw()
|
|
276
|
+
* renders TextureView/SurfaceView as black; this fills in the actual pixels.
|
|
277
|
+
*
|
|
278
|
+
* Mapbox uses SurfaceView by default, so we use MapView.snapshot() to capture
|
|
279
|
+
* the map and composite it at the correct position.
|
|
280
|
+
*/
|
|
281
|
+
private fun compositeGpuSurfaces(root: View, canvas: Canvas, screenScale: Float) {
|
|
282
|
+
findTextureViews(root) { tv ->
|
|
283
|
+
try {
|
|
284
|
+
val tvBitmap = tv.bitmap ?: return@findTextureViews
|
|
285
|
+
val loc = IntArray(2)
|
|
286
|
+
tv.getLocationInWindow(loc)
|
|
287
|
+
canvas.drawBitmap(tvBitmap, loc[0].toFloat(), loc[1].toFloat(), null)
|
|
288
|
+
tvBitmap.recycle()
|
|
289
|
+
} catch (_: Exception) {
|
|
290
|
+
// Safety: never crash if TextureView.getBitmap() fails
|
|
262
291
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
292
|
+
}
|
|
293
|
+
compositeMapboxSnapshot(root, canvas)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Mapbox MapView uses SurfaceView; decorView.draw() renders it black.
|
|
298
|
+
* Use MapView.snapshot() (Mapbox SDK API) to capture the map and composite it.
|
|
299
|
+
*/
|
|
300
|
+
private fun compositeMapboxSnapshot(root: View, canvas: Canvas) {
|
|
301
|
+
val mapView = SpecialCases.shared.getMapboxMapViewForSnapshot(root) ?: return
|
|
302
|
+
try {
|
|
303
|
+
val snapshot = mapView.javaClass.getMethod("snapshot").invoke(mapView)
|
|
304
|
+
val bitmap = snapshot as? Bitmap ?: return
|
|
305
|
+
val loc = IntArray(2)
|
|
306
|
+
mapView.getLocationInWindow(loc)
|
|
307
|
+
canvas.drawBitmap(bitmap, loc[0].toFloat(), loc[1].toFloat(), null)
|
|
308
|
+
bitmap.recycle()
|
|
309
|
+
} catch (e: Exception) {
|
|
310
|
+
DiagnosticLog.trace("[VisualCapture] Mapbox snapshot failed: ${e.message}")
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private fun findTextureViews(view: View, action: (TextureView) -> Unit) {
|
|
315
|
+
if (view is TextureView && view.isAvailable) {
|
|
316
|
+
action(view)
|
|
317
|
+
}
|
|
318
|
+
if (view is ViewGroup) {
|
|
319
|
+
for (i in 0 until view.childCount) {
|
|
320
|
+
findTextureViews(view.getChildAt(i), action)
|
|
267
321
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private fun processCapture(
|
|
326
|
+
bitmap: Bitmap,
|
|
327
|
+
redactRects: List<Rect>,
|
|
328
|
+
screenScale: Float,
|
|
329
|
+
frameStart: Long,
|
|
330
|
+
force: Boolean
|
|
331
|
+
) {
|
|
332
|
+
// Apply redactions
|
|
333
|
+
if (redactRects.isNotEmpty()) {
|
|
334
|
+
val canvas = Canvas(bitmap)
|
|
335
|
+
val paint = Paint().apply {
|
|
336
|
+
color = Color.BLACK
|
|
337
|
+
style = Paint.Style.FILL
|
|
338
|
+
}
|
|
339
|
+
for (rect in redactRects) {
|
|
340
|
+
if (rect.width() > 0 && rect.height() > 0) {
|
|
341
|
+
canvas.drawRect(
|
|
342
|
+
rect.left / screenScale,
|
|
343
|
+
rect.top / screenScale,
|
|
344
|
+
rect.right / screenScale,
|
|
345
|
+
rect.bottom / screenScale,
|
|
346
|
+
paint
|
|
347
|
+
)
|
|
277
348
|
}
|
|
278
349
|
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Compress to JPEG
|
|
353
|
+
val stream = ByteArrayOutputStream()
|
|
354
|
+
bitmap.compress(Bitmap.CompressFormat.JPEG, (quality * 100).toInt(), stream)
|
|
355
|
+
bitmap.recycle()
|
|
356
|
+
|
|
357
|
+
val data = stream.toByteArray()
|
|
358
|
+
val captureTs = System.currentTimeMillis()
|
|
359
|
+
val frameNum = frameCounter.incrementAndGet()
|
|
360
|
+
|
|
361
|
+
if (frameNum == 1L) {
|
|
362
|
+
DiagnosticLog.trace("[VisualCapture] First frame captured! size=${data.size} bytes")
|
|
363
|
+
}
|
|
364
|
+
if (frameNum % 30 == 0L) {
|
|
365
|
+
val frameDurationMs = (SystemClock.elapsedRealtime() - frameStart).toDouble()
|
|
366
|
+
val isMainThread = Looper.myLooper() == Looper.getMainLooper()
|
|
367
|
+
DiagnosticLog.perfFrame("screenshot", frameDurationMs, frameNum.toInt(), isMainThread)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Store in buffer
|
|
371
|
+
stateLock.withLock {
|
|
372
|
+
screenshots.add(Pair(data, captureTs))
|
|
373
|
+
enforceScreenshotCaps()
|
|
374
|
+
val shouldSend = !deferredUntilCommit && screenshots.size >= batchSize
|
|
279
375
|
|
|
280
|
-
|
|
281
|
-
|
|
376
|
+
if (shouldSend) {
|
|
377
|
+
sendScreenshots()
|
|
378
|
+
}
|
|
282
379
|
}
|
|
283
380
|
}
|
|
284
381
|
|
|
@@ -302,7 +399,7 @@ class VisualCapture private constructor(private val context: Context) {
|
|
|
302
399
|
return
|
|
303
400
|
}
|
|
304
401
|
|
|
305
|
-
DiagnosticLog.
|
|
402
|
+
DiagnosticLog.trace("[VisualCapture] sendScreenshots: sending ${images.size} frames")
|
|
306
403
|
|
|
307
404
|
// All heavy work happens in background
|
|
308
405
|
encodeExecutor.execute {
|
|
@@ -374,12 +471,18 @@ class VisualCapture private constructor(private val context: Context) {
|
|
|
374
471
|
sendScreenshots()
|
|
375
472
|
}
|
|
376
473
|
|
|
377
|
-
fun uploadPendingFrames(sessionId: String) {
|
|
474
|
+
fun uploadPendingFrames(sessionId: String, completion: ((Boolean) -> Unit)? = null) {
|
|
378
475
|
val framesPath = File(context.cacheDir, "rj_pending/$sessionId/frames")
|
|
379
476
|
|
|
380
|
-
if (!framesPath.exists())
|
|
477
|
+
if (!framesPath.exists()) {
|
|
478
|
+
completion?.invoke(true)
|
|
479
|
+
return
|
|
480
|
+
}
|
|
381
481
|
|
|
382
|
-
val frameFiles = framesPath.listFiles()?.sortedBy { it.name } ?:
|
|
482
|
+
val frameFiles = framesPath.listFiles()?.sortedBy { it.name } ?: run {
|
|
483
|
+
completion?.invoke(true)
|
|
484
|
+
return
|
|
485
|
+
}
|
|
383
486
|
|
|
384
487
|
val frames = mutableListOf<Pair<ByteArray, Long>>()
|
|
385
488
|
for (file in frameFiles) {
|
|
@@ -389,9 +492,15 @@ class VisualCapture private constructor(private val context: Context) {
|
|
|
389
492
|
frames.add(Pair(data, ts))
|
|
390
493
|
}
|
|
391
494
|
|
|
392
|
-
if (frames.isEmpty())
|
|
495
|
+
if (frames.isEmpty()) {
|
|
496
|
+
completion?.invoke(true)
|
|
497
|
+
return
|
|
498
|
+
}
|
|
393
499
|
|
|
394
|
-
val bundle = packageFrameBundle(frames, frames.first().second) ?:
|
|
500
|
+
val bundle = packageFrameBundle(frames, frames.first().second) ?: run {
|
|
501
|
+
completion?.invoke(false)
|
|
502
|
+
return
|
|
503
|
+
}
|
|
395
504
|
|
|
396
505
|
SegmentDispatcher.shared.transmitFrameBundle(
|
|
397
506
|
payload = bundle,
|
|
@@ -404,6 +513,7 @@ class VisualCapture private constructor(private val context: Context) {
|
|
|
404
513
|
frameFiles.forEach { it.delete() }
|
|
405
514
|
framesPath.delete()
|
|
406
515
|
}
|
|
516
|
+
completion?.invoke(ok)
|
|
407
517
|
}
|
|
408
518
|
}
|
|
409
519
|
}
|
|
@@ -438,6 +548,10 @@ private class CaptureStateMachine {
|
|
|
438
548
|
private class RedactionMask {
|
|
439
549
|
private val views = CopyOnWriteArrayList<WeakReference<View>>()
|
|
440
550
|
|
|
551
|
+
private val cachedAutoRects = mutableListOf<Rect>()
|
|
552
|
+
private var lastScanTime = 0L
|
|
553
|
+
private val scanCacheDurationMs = 500L
|
|
554
|
+
|
|
441
555
|
fun add(view: View) {
|
|
442
556
|
views.add(WeakReference(view))
|
|
443
557
|
}
|
|
@@ -446,29 +560,81 @@ private class RedactionMask {
|
|
|
446
560
|
views.removeIf { it.get() === view || it.get() == null }
|
|
447
561
|
}
|
|
448
562
|
|
|
449
|
-
fun
|
|
563
|
+
fun invalidateCache() {
|
|
564
|
+
lastScanTime = 0L
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
fun computeRects(decorView: View? = null): List<Rect> {
|
|
450
568
|
val rects = mutableListOf<Rect>()
|
|
451
569
|
views.removeIf { it.get() == null }
|
|
452
570
|
|
|
453
571
|
for (ref in views) {
|
|
454
572
|
val view = ref.get() ?: continue
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
val
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
)
|
|
466
|
-
|
|
467
|
-
if (rect.width() > 0 && rect.height() > 0) {
|
|
468
|
-
rects.add(rect)
|
|
573
|
+
val rect = getViewRect(view)
|
|
574
|
+
if (rect != null) rects.add(rect)
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (decorView != null) {
|
|
578
|
+
val now = SystemClock.elapsedRealtime()
|
|
579
|
+
if (now - lastScanTime >= scanCacheDurationMs) {
|
|
580
|
+
cachedAutoRects.clear()
|
|
581
|
+
scanForSensitiveViews(decorView, cachedAutoRects)
|
|
582
|
+
lastScanTime = now
|
|
469
583
|
}
|
|
584
|
+
rects.addAll(cachedAutoRects)
|
|
470
585
|
}
|
|
471
586
|
|
|
472
587
|
return rects
|
|
473
588
|
}
|
|
589
|
+
|
|
590
|
+
private fun getViewRect(view: View): Rect? {
|
|
591
|
+
if (!view.isShown || view.width <= 0 || view.height <= 0) return null
|
|
592
|
+
val location = IntArray(2)
|
|
593
|
+
view.getLocationOnScreen(location)
|
|
594
|
+
val rect = Rect(
|
|
595
|
+
location[0],
|
|
596
|
+
location[1],
|
|
597
|
+
location[0] + view.width,
|
|
598
|
+
location[1] + view.height
|
|
599
|
+
)
|
|
600
|
+
if (rect.width() > 0 && rect.height() > 0) return rect
|
|
601
|
+
return null
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
private fun scanForSensitiveViews(view: View, rects: MutableList<Rect>, depth: Int = 0) {
|
|
605
|
+
if (depth > 20) return
|
|
606
|
+
if (!view.isShown || view.alpha <= 0.01f || view.width <= 0 || view.height <= 0) return
|
|
607
|
+
|
|
608
|
+
if (shouldMask(view)) {
|
|
609
|
+
val rect = getViewRect(view)
|
|
610
|
+
if (rect != null) {
|
|
611
|
+
rects.add(rect)
|
|
612
|
+
return
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (view is ViewGroup) {
|
|
617
|
+
for (i in 0 until view.childCount) {
|
|
618
|
+
scanForSensitiveViews(view.getChildAt(i), rects, depth + 1)
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
private fun shouldMask(view: View): Boolean {
|
|
624
|
+
if (view.contentDescription?.toString() == "rejourney_occlude") return true
|
|
625
|
+
|
|
626
|
+
try {
|
|
627
|
+
val hint = view.getTag(com.facebook.react.R.id.accessibility_hint) as? String
|
|
628
|
+
if (hint == "rejourney_occlude") return true
|
|
629
|
+
} catch (_: Exception) { }
|
|
630
|
+
|
|
631
|
+
if (view is EditText) return true
|
|
632
|
+
|
|
633
|
+
val className = view.javaClass.simpleName.lowercase(java.util.Locale.US)
|
|
634
|
+
if (className.contains("camera") || (className.contains("surfaceview") && className.contains("preview"))) {
|
|
635
|
+
return true
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
return false
|
|
639
|
+
}
|
|
474
640
|
}
|
|
@@ -191,6 +191,20 @@ class RejourneyModule(reactContext: ReactApplicationContext) :
|
|
|
191
191
|
instance.getUserIdentity(promise)
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
@ReactMethod
|
|
195
|
+
@DoNotStrip
|
|
196
|
+
override fun setAnonymousId(anonymousId: String, promise: Promise) {
|
|
197
|
+
val instance = getImplOrReject(promise) ?: return
|
|
198
|
+
instance.setAnonymousId(anonymousId, promise)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
@ReactMethod
|
|
202
|
+
@DoNotStrip
|
|
203
|
+
override fun getAnonymousId(promise: Promise) {
|
|
204
|
+
val instance = getImplOrReject(promise) ?: return
|
|
205
|
+
instance.getAnonymousId(promise)
|
|
206
|
+
}
|
|
207
|
+
|
|
194
208
|
@ReactMethod
|
|
195
209
|
@DoNotStrip
|
|
196
210
|
override fun setDebugMode(enabled: Boolean, promise: Promise) {
|
|
@@ -224,6 +224,24 @@ class RejourneyModule(reactContext: ReactApplicationContext) :
|
|
|
224
224
|
}
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
@ReactMethod
|
|
228
|
+
fun setAnonymousId(anonymousId: String, promise: Promise) {
|
|
229
|
+
try {
|
|
230
|
+
impl.setAnonymousId(anonymousId, promise)
|
|
231
|
+
} catch (e: Exception) {
|
|
232
|
+
promise.resolve(createErrorMap("Module initialization failed: ${e.message}"))
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
@ReactMethod
|
|
237
|
+
fun getAnonymousId(promise: Promise) {
|
|
238
|
+
try {
|
|
239
|
+
impl.getAnonymousId(promise)
|
|
240
|
+
} catch (e: Exception) {
|
|
241
|
+
promise.resolve(null)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
227
245
|
@ReactMethod
|
|
228
246
|
fun setSDKVersion(version: String) {
|
|
229
247
|
try {
|
|
@@ -37,6 +37,7 @@ public final class DeviceRegistrar: NSObject {
|
|
|
37
37
|
// MARK: Private State
|
|
38
38
|
|
|
39
39
|
private let _keychainId = "com.rejourney.device.fingerprint"
|
|
40
|
+
private let _fallbackIdKey = "com.rejourney.device.fallbackId"
|
|
40
41
|
|
|
41
42
|
private lazy var _httpSession: URLSession = {
|
|
42
43
|
let config = URLSessionConfiguration.default
|
|
@@ -124,13 +125,22 @@ public final class DeviceRegistrar: NSObject {
|
|
|
124
125
|
|
|
125
126
|
var composite = bundleId
|
|
126
127
|
composite += device.model
|
|
127
|
-
composite += device.
|
|
128
|
-
composite += device.systemVersion
|
|
129
|
-
composite += device.identifierForVendor?.uuidString ?? UUID().uuidString
|
|
128
|
+
composite += device.identifierForVendor?.uuidString ?? _stableDeviceFallback()
|
|
130
129
|
|
|
131
130
|
return _sha256(composite)
|
|
132
131
|
}
|
|
133
132
|
|
|
133
|
+
/// Returns a keychain-persisted UUID so the fingerprint stays stable even when
|
|
134
|
+
/// identifierForVendor is temporarily nil (early boot, App Clips, extensions).
|
|
135
|
+
private func _stableDeviceFallback() -> String {
|
|
136
|
+
if let existing = _keychainLoad(_fallbackIdKey) {
|
|
137
|
+
return existing
|
|
138
|
+
}
|
|
139
|
+
let fresh = UUID().uuidString
|
|
140
|
+
_keychainSave(_fallbackIdKey, value: fresh)
|
|
141
|
+
return fresh
|
|
142
|
+
}
|
|
143
|
+
|
|
134
144
|
// MARK: Server Communication
|
|
135
145
|
|
|
136
146
|
private func _fetchServerCredential(fingerprint: String, apiToken: String, completion: @escaping (Bool, String?) -> Void) {
|