@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,375 @@
1
+ package com.rejourney.capture
2
+
3
+
4
+ enum class CaptureAction {
5
+ RenderNow,
6
+ Defer,
7
+ ReuseLast
8
+ }
9
+
10
+ enum class CaptureReason {
11
+ RenderNow,
12
+ DeferTouch,
13
+ DeferScroll,
14
+ DeferBounce,
15
+ DeferRefresh,
16
+ DeferTransition,
17
+ DeferKeyboard,
18
+ DeferMap,
19
+ DeferBigAnimation,
20
+ ReuseSignatureUnchanged,
21
+ DeadlineExpired,
22
+ RenderFailedReuse
23
+ }
24
+
25
+ data class CaptureDecision(
26
+ val action: CaptureAction,
27
+ val reason: CaptureReason,
28
+ val deferUntilMs: Long = 0L
29
+ )
30
+
31
+ class CaptureHeuristics {
32
+ val captureGraceMs: Long = CAPTURE_GRACE_MS
33
+ val pollIntervalMs: Long = POLL_INTERVAL_MS
34
+ val maxStaleMs: Long = MAX_STALE_MS
35
+
36
+ var keyboardAnimating: Boolean = false
37
+ private set
38
+ var scrollActive: Boolean = false
39
+ private set
40
+ var animationBlocking: Boolean = false
41
+ private set
42
+
43
+ private var refreshActive: Boolean = false
44
+ private var mapActive: Boolean = false
45
+
46
+ private var lastTouchTime = 0L
47
+ private var lastScrollTime = 0L
48
+ private var lastBounceTime = 0L
49
+ private var lastRefreshTime = 0L
50
+ private var lastMapTime = 0L
51
+ private var lastTransitionTime = 0L
52
+ private var lastKeyboardTime = 0L
53
+ private var lastAnimationTime = 0L
54
+ private var mapSettleUntilMs = 0L
55
+
56
+ private var lastRenderedTime = 0L
57
+ private var lastRenderedSignature: String? = null
58
+
59
+ private var lastObservedSignature: String? = null
60
+ private var lastObservedSignatureTime = 0L
61
+ private var signatureChurnCount = 0
62
+ private var lastSignatureChurnTime = 0L
63
+ private var churnBlocking = false
64
+
65
+ private var hasVideoSurface = false
66
+ private var hasWebSurface = false
67
+ private var hasCameraSurface = false
68
+
69
+ private var bonusCaptureTime = 0L
70
+ private var pendingKeyframes = 0
71
+ private var lastKeyframeRenderTime = 0L
72
+
73
+ private var keyboardAnimatingUntil = 0L
74
+
75
+ fun reset() {
76
+ lastTouchTime = 0L
77
+ lastScrollTime = 0L
78
+ lastBounceTime = 0L
79
+ lastRefreshTime = 0L
80
+ lastMapTime = 0L
81
+ lastTransitionTime = 0L
82
+ lastKeyboardTime = 0L
83
+ lastAnimationTime = 0L
84
+ mapSettleUntilMs = 0L
85
+ lastRenderedTime = 0L
86
+ lastRenderedSignature = null
87
+ lastObservedSignature = null
88
+ lastObservedSignatureTime = 0L
89
+ signatureChurnCount = 0
90
+ lastSignatureChurnTime = 0L
91
+ churnBlocking = false
92
+ keyboardAnimating = false
93
+ scrollActive = false
94
+ animationBlocking = false
95
+ refreshActive = false
96
+ mapActive = false
97
+ hasVideoSurface = false
98
+ hasWebSurface = false
99
+ hasCameraSurface = false
100
+ bonusCaptureTime = 0L
101
+ pendingKeyframes = 0
102
+ lastKeyframeRenderTime = 0L
103
+ keyboardAnimatingUntil = 0L
104
+ }
105
+
106
+ fun invalidateSignature() {
107
+ lastRenderedSignature = null
108
+ lastRenderedTime = 0L
109
+ }
110
+
111
+ fun recordTouchEventAtTime(nowMs: Long) {
112
+ lastTouchTime = nowMs
113
+ scheduleBonusCaptureAfterDelay(BONUS_INTERACTION_DELAY_MS, nowMs)
114
+ }
115
+
116
+ fun recordInteractionEventAtTime(nowMs: Long) {
117
+ recordTouchEventAtTime(nowMs)
118
+ }
119
+
120
+ fun recordMapInteractionAtTime(nowMs: Long) {
121
+ lastMapTime = nowMs
122
+ val candidate = nowMs + MAP_SETTLE_MS
123
+ if (candidate > mapSettleUntilMs) {
124
+ mapSettleUntilMs = candidate
125
+ }
126
+ }
127
+
128
+ fun recordNavigationEventAtTime(nowMs: Long) {
129
+ lastTransitionTime = nowMs
130
+ scheduleBonusCaptureAfterDelay(BONUS_TRANSITION_DELAY_MS, nowMs)
131
+ }
132
+
133
+ fun recordKeyboardEventAtTime(nowMs: Long) {
134
+ lastKeyboardTime = nowMs
135
+ keyboardAnimating = true
136
+ keyboardAnimatingUntil = nowMs + QUIET_KEYBOARD_MS
137
+ scheduleBonusCaptureAfterDelay(BONUS_KEYBOARD_DELAY_MS, nowMs)
138
+ }
139
+
140
+ fun recordRenderedSignature(signature: String?, nowMs: Long) {
141
+ lastRenderedSignature = signature?.takeIf { it.isNotEmpty() }
142
+ lastRenderedTime = nowMs
143
+ if (pendingKeyframes > 0) {
144
+ pendingKeyframes -= 1
145
+ lastKeyframeRenderTime = nowMs
146
+ if (pendingKeyframes > 0) {
147
+ bonusCaptureTime = nowMs + KEYFRAME_SPACING_MS
148
+ return
149
+ }
150
+ }
151
+ bonusCaptureTime = 0L
152
+ }
153
+
154
+ fun updateWithScanResult(scanResult: ViewHierarchyScanResult?, nowMs: Long) {
155
+ if (scanResult == null) {
156
+ updateKeyboardState(nowMs)
157
+ return
158
+ }
159
+
160
+ updateKeyboardState(nowMs)
161
+
162
+ val currentSignature = scanResult.layoutSignature.orEmpty()
163
+ val lastSignature = lastObservedSignature.orEmpty()
164
+ val signatureChanged = currentSignature != lastSignature
165
+ if (signatureChanged) {
166
+ val delta = nowMs - lastObservedSignatureTime
167
+ signatureChurnCount = if (delta < SIGNATURE_CHURN_WINDOW_MS) {
168
+ signatureChurnCount + 1
169
+ } else {
170
+ 1
171
+ }
172
+ lastObservedSignatureTime = nowMs
173
+ lastSignatureChurnTime = nowMs
174
+ lastObservedSignature = currentSignature.takeIf { it.isNotEmpty() }
175
+ } else if (lastSignatureChurnTime > 0 &&
176
+ (nowMs - lastSignatureChurnTime) > SIGNATURE_CHURN_WINDOW_MS
177
+ ) {
178
+ signatureChurnCount = 0
179
+ }
180
+ churnBlocking = signatureChurnCount >= 2 &&
181
+ (nowMs - lastSignatureChurnTime) < SIGNATURE_CHURN_WINDOW_MS
182
+
183
+ hasVideoSurface = scanResult.videoFrames.isNotEmpty()
184
+ hasWebSurface = scanResult.webViewFrames.isNotEmpty()
185
+ hasCameraSurface = scanResult.cameraFrames.isNotEmpty()
186
+
187
+ if (scanResult.scrollActive) {
188
+ lastScrollTime = nowMs
189
+ }
190
+ if (scanResult.bounceActive) {
191
+ lastBounceTime = nowMs
192
+ }
193
+ if (scanResult.refreshActive) {
194
+ lastRefreshTime = nowMs
195
+ }
196
+ if (scanResult.mapActive) {
197
+ recordMapInteractionAtTime(nowMs)
198
+ }
199
+
200
+ updateScrollActiveState(scanResult.scrollActive, scanResult.refreshActive, scanResult.mapActive, nowMs)
201
+
202
+ val blockingAnimation = scanResult.hasAnyAnimations &&
203
+ scanResult.animationAreaRatio >= ANIMATION_SMALL_AREA_ALLOWED
204
+
205
+ val recentSignatureChange = signatureChanged ||
206
+ (signatureChurnCount > 0 && (nowMs - lastSignatureChurnTime) < SIGNATURE_CHURN_WINDOW_MS)
207
+ val bailoutBlocking = scanResult.didBailOutEarly && recentSignatureChange
208
+ val shouldBlock = blockingAnimation || churnBlocking || bailoutBlocking
209
+
210
+ val wasBlocking = animationBlocking
211
+ animationBlocking = shouldBlock
212
+ if (shouldBlock) {
213
+ lastAnimationTime = nowMs
214
+ } else if (wasBlocking) {
215
+ scheduleBonusCaptureAfterDelay(BONUS_ANIMATION_DELAY_MS, nowMs)
216
+ }
217
+ }
218
+
219
+ fun decisionForSignature(signature: String?, nowMs: Long, hasLastFrame: Boolean): CaptureDecision {
220
+ var earliestSafeTime = nowMs
221
+ var blockerReason = CaptureReason.RenderNow
222
+
223
+ considerBlockerSince(lastTouchTime, QUIET_TOUCH_MS, nowMs, earliestSafeTime)?.let {
224
+ earliestSafeTime = it
225
+ blockerReason = CaptureReason.DeferTouch
226
+ }
227
+ considerBlockerSince(lastScrollTime, QUIET_SCROLL_MS, nowMs, earliestSafeTime)?.let {
228
+ earliestSafeTime = it
229
+ blockerReason = CaptureReason.DeferScroll
230
+ }
231
+ considerBlockerSince(lastBounceTime, QUIET_BOUNCE_MS, nowMs, earliestSafeTime)?.let {
232
+ earliestSafeTime = it
233
+ blockerReason = CaptureReason.DeferBounce
234
+ }
235
+ considerBlockerSince(lastRefreshTime, QUIET_REFRESH_MS, nowMs, earliestSafeTime)?.let {
236
+ earliestSafeTime = it
237
+ blockerReason = CaptureReason.DeferRefresh
238
+ }
239
+ considerBlockerSince(lastTransitionTime, QUIET_TRANSITION_MS, nowMs, earliestSafeTime)?.let {
240
+ earliestSafeTime = it
241
+ blockerReason = CaptureReason.DeferTransition
242
+ }
243
+
244
+ if (keyboardAnimating) {
245
+ lastKeyboardTime = nowMs
246
+ }
247
+ considerBlockerSince(lastKeyboardTime, QUIET_KEYBOARD_MS, nowMs, earliestSafeTime)?.let {
248
+ earliestSafeTime = it
249
+ blockerReason = CaptureReason.DeferKeyboard
250
+ }
251
+
252
+ considerBlockerSince(lastMapTime, QUIET_MAP_MS, nowMs, earliestSafeTime)?.let {
253
+ earliestSafeTime = it
254
+ blockerReason = CaptureReason.DeferMap
255
+ }
256
+
257
+ if (mapSettleUntilMs > nowMs && mapSettleUntilMs > earliestSafeTime) {
258
+ earliestSafeTime = mapSettleUntilMs
259
+ blockerReason = CaptureReason.DeferMap
260
+ }
261
+
262
+ if (animationBlocking) {
263
+ considerBlockerSince(lastAnimationTime, QUIET_ANIMATION_MS, nowMs, earliestSafeTime)?.let {
264
+ earliestSafeTime = it
265
+ blockerReason = CaptureReason.DeferBigAnimation
266
+ }
267
+ }
268
+
269
+ if (earliestSafeTime > nowMs) {
270
+ return CaptureDecision(CaptureAction.Defer, blockerReason, earliestSafeTime)
271
+ }
272
+
273
+ val signatureChanged = signature.isNullOrEmpty() || signature != lastRenderedSignature
274
+ val stale = lastRenderedTime <= 0 || (nowMs - lastRenderedTime) > MAX_STALE_MS
275
+ val bonusDue = bonusCaptureTime > 0 && nowMs >= bonusCaptureTime
276
+ val keyframeDue = bonusDue && pendingKeyframes > 0 &&
277
+ (nowMs - lastKeyframeRenderTime) >= KEYFRAME_SPACING_MS
278
+ val staleOnly = stale && hasLastFrame && !signatureChanged && !keyframeDue
279
+ val suppressStaleRender = staleOnly && (hasVideoSurface || hasWebSurface || hasCameraSurface)
280
+
281
+ if (suppressStaleRender) {
282
+ return CaptureDecision(CaptureAction.ReuseLast, CaptureReason.ReuseSignatureUnchanged)
283
+ }
284
+
285
+ if (!hasLastFrame || signatureChanged || stale || keyframeDue) {
286
+ return CaptureDecision(CaptureAction.RenderNow, CaptureReason.RenderNow)
287
+ }
288
+
289
+ return CaptureDecision(CaptureAction.ReuseLast, CaptureReason.ReuseSignatureUnchanged)
290
+ }
291
+
292
+ private fun updateScrollActiveState(scrollActive: Boolean, refreshActive: Boolean, mapActive: Boolean, nowMs: Long) {
293
+ if (this.scrollActive && !scrollActive) {
294
+ scheduleBonusCaptureAfterDelay(BONUS_SCROLL_DELAY_MS, nowMs)
295
+ }
296
+ if (this.refreshActive && !refreshActive) {
297
+ scheduleBonusCaptureAfterDelay(BONUS_REFRESH_DELAY_MS, nowMs)
298
+ }
299
+ if (this.mapActive && !mapActive) {
300
+ scheduleBonusCaptureAfterDelay(BONUS_MAP_DELAY_MS, nowMs)
301
+ val candidate = nowMs + MAP_SETTLE_MS
302
+ if (candidate > mapSettleUntilMs) {
303
+ mapSettleUntilMs = candidate
304
+ }
305
+ }
306
+
307
+ this.scrollActive = scrollActive
308
+ this.refreshActive = refreshActive
309
+ this.mapActive = mapActive
310
+ }
311
+
312
+ private fun scheduleBonusCaptureAfterDelay(delayMs: Long, nowMs: Long) {
313
+ if (pendingKeyframes < MAX_PENDING_KEYFRAMES) {
314
+ pendingKeyframes += 1
315
+ }
316
+ val candidate = nowMs + delayMs
317
+ if (bonusCaptureTime <= 0 || candidate < bonusCaptureTime) {
318
+ bonusCaptureTime = candidate
319
+ }
320
+ }
321
+
322
+ private fun updateKeyboardState(nowMs: Long) {
323
+ if (keyboardAnimating && nowMs >= keyboardAnimatingUntil) {
324
+ keyboardAnimating = false
325
+ }
326
+ }
327
+
328
+ private fun considerBlockerSince(
329
+ timestamp: Long,
330
+ quietInterval: Long,
331
+ nowMs: Long,
332
+ currentEarliest: Long
333
+ ): Long? {
334
+ if (timestamp <= 0L) return null
335
+
336
+ val readyTime = timestamp + quietInterval
337
+ return if (readyTime > nowMs && readyTime > currentEarliest) {
338
+ readyTime
339
+ } else {
340
+ null
341
+ }
342
+ }
343
+
344
+ companion object {
345
+ private const val CAPTURE_GRACE_MS = 900L
346
+ private const val POLL_INTERVAL_MS = 80L
347
+ private const val MAX_STALE_MS = 5_000L
348
+
349
+ private const val QUIET_TOUCH_MS = 120L
350
+ private const val QUIET_SCROLL_MS = 200L
351
+ private const val QUIET_BOUNCE_MS = 200L
352
+ private const val QUIET_REFRESH_MS = 220L
353
+ private const val QUIET_MAP_MS = 550L
354
+ private const val QUIET_TRANSITION_MS = 200L
355
+ private const val QUIET_KEYBOARD_MS = 250L
356
+ private const val QUIET_ANIMATION_MS = 250L
357
+
358
+ private const val MAP_SETTLE_MS = 800L
359
+
360
+ private const val SIGNATURE_CHURN_WINDOW_MS = 250L
361
+
362
+ private const val BONUS_SCROLL_DELAY_MS = 120L
363
+ private const val BONUS_MAP_DELAY_MS = 350L
364
+ private const val BONUS_REFRESH_DELAY_MS = 200L
365
+ private const val BONUS_INTERACTION_DELAY_MS = 150L
366
+ private const val BONUS_TRANSITION_DELAY_MS = 200L
367
+ private const val BONUS_KEYBOARD_DELAY_MS = 200L
368
+ private const val BONUS_ANIMATION_DELAY_MS = 200L
369
+
370
+ private const val ANIMATION_SMALL_AREA_ALLOWED = 0.03f
371
+
372
+ private const val KEYFRAME_SPACING_MS = 250L
373
+ private const val MAX_PENDING_KEYFRAMES = 3
374
+ }
375
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Crash detection and reporting handler.
3
+ * Ported from iOS RJCrashHandler.
4
+ */
5
+ package com.rejourney.capture
6
+
7
+ import android.content.Context
8
+ import android.content.SharedPreferences
9
+ import com.rejourney.core.Logger
10
+ import org.json.JSONObject
11
+ import java.io.PrintWriter
12
+ import java.io.StringWriter
13
+
14
+ class CrashHandler private constructor(private val context: Context) : Thread.UncaughtExceptionHandler {
15
+
16
+ companion object {
17
+ @Volatile
18
+ private var instance: CrashHandler? = null
19
+
20
+ private const val PREFS_NAME = "rejourney_crash"
21
+ private const val KEY_CRASH_REPORT = "pending_crash_report"
22
+ private const val KEY_HAS_PENDING = "has_pending_crash"
23
+
24
+ fun getInstance(context: Context): CrashHandler {
25
+ return instance ?: synchronized(this) {
26
+ instance ?: CrashHandler(context.applicationContext).also { instance = it }
27
+ }
28
+ }
29
+ }
30
+
31
+ private var defaultHandler: Thread.UncaughtExceptionHandler? = null
32
+ private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
33
+
34
+ fun startMonitoring() {
35
+ defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
36
+ Thread.setDefaultUncaughtExceptionHandler(this)
37
+ Logger.debug("Crash monitoring started")
38
+ }
39
+
40
+ override fun uncaughtException(thread: Thread, throwable: Throwable) {
41
+ try {
42
+ Logger.debug("[CRASH] EXCEPTION DETECTED: ${throwable.javaClass.name} - ${throwable.message}")
43
+
44
+ // Get session info from main prefs
45
+ val mainPrefs = context.getSharedPreferences("rejourney", Context.MODE_PRIVATE)
46
+ val sessionId = mainPrefs.getString("rj_current_session_id", null)
47
+
48
+ // Build crash report
49
+ val crashReport = buildCrashReport(thread, throwable, sessionId)
50
+
51
+ // Persist to SharedPreferences (synchronous write for reliability)
52
+ prefs.edit()
53
+ .putString(KEY_CRASH_REPORT, crashReport.toString())
54
+ .putBoolean(KEY_HAS_PENDING, true)
55
+ .commit()
56
+
57
+ Logger.debug("[CRASH] Report persisted (sessionId=$sessionId)")
58
+ } catch (e: Exception) {
59
+ // Don't let crash handling cause another crash
60
+ Logger.error("Failed to capture crash", e)
61
+ } finally {
62
+ // Call the default handler to continue normal crash behavior
63
+ defaultHandler?.uncaughtException(thread, throwable)
64
+ }
65
+ }
66
+
67
+ private fun buildCrashReport(thread: Thread, throwable: Throwable, sessionId: String?): JSONObject {
68
+ val sw = StringWriter()
69
+ throwable.printStackTrace(PrintWriter(sw))
70
+ val stackTrace = sw.toString()
71
+
72
+ // Generate fingerprint for crash deduplication (industry standard)
73
+ val fingerprint = generateFingerprint(throwable, stackTrace)
74
+
75
+ return JSONObject().apply {
76
+ put("timestamp", System.currentTimeMillis())
77
+ put("sessionId", sessionId)
78
+ put("threadName", thread.name)
79
+ put("exceptionType", throwable.javaClass.name)
80
+ put("exceptionMessage", throwable.message)
81
+ put("stackTrace", stackTrace)
82
+ put("fingerprint", fingerprint)
83
+ put("platform", "android")
84
+ put("sdkVersion", com.rejourney.core.Constants.SDK_VERSION)
85
+
86
+ // Add device info
87
+ put("deviceInfo", JSONObject().apply {
88
+ put("manufacturer", android.os.Build.MANUFACTURER)
89
+ put("model", android.os.Build.MODEL)
90
+ put("osVersion", android.os.Build.VERSION.RELEASE)
91
+ put("sdkInt", android.os.Build.VERSION.SDK_INT)
92
+ })
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Generate a fingerprint for crash deduplication (industry standard).
98
+ * Based on exception type + top stack frames.
99
+ */
100
+ private fun generateFingerprint(throwable: Throwable, stackTrace: String): String {
101
+ val input = StringBuilder(throwable.javaClass.name)
102
+
103
+ // Use top 5 stack trace lines for fingerprint
104
+ val lines = stackTrace.lines().take(6) // First line is message, next 5 are frames
105
+ for (line in lines) {
106
+ // Remove memory addresses and hash codes (they change between runs)
107
+ val cleaned = line.replace(Regex("@[0-9a-fA-F]+"), "")
108
+ input.append(cleaned)
109
+ }
110
+
111
+ // Create SHA256 hash
112
+ val digest = java.security.MessageDigest.getInstance("SHA-256")
113
+ val hash = digest.digest(input.toString().toByteArray())
114
+
115
+ // Return first 16 chars of hex
116
+ return hash.take(8).joinToString("") { "%02x".format(it) }
117
+ }
118
+
119
+ /**
120
+ * Check if there's a pending crash report from previous session.
121
+ */
122
+ fun hasPendingCrashReport(): Boolean {
123
+ return prefs.getBoolean(KEY_HAS_PENDING, false)
124
+ }
125
+
126
+ /**
127
+ * Load and purge the pending crash report.
128
+ * Returns the crash report as a map, or null if none exists.
129
+ */
130
+ fun loadAndPurgePendingCrashReport(): Map<String, Any?>? {
131
+ if (!hasPendingCrashReport()) return null
132
+
133
+ return try {
134
+ val jsonString = prefs.getString(KEY_CRASH_REPORT, null) ?: return null
135
+ val json = JSONObject(jsonString)
136
+
137
+ // Clear the pending report
138
+ prefs.edit()
139
+ .remove(KEY_CRASH_REPORT)
140
+ .putBoolean(KEY_HAS_PENDING, false)
141
+ .apply()
142
+
143
+ // Convert to map
144
+ json.keys().asSequence().associateWith { key ->
145
+ json.opt(key)
146
+ }
147
+ } catch (e: Exception) {
148
+ Logger.error("Failed to load pending crash report", e)
149
+ prefs.edit().putBoolean(KEY_HAS_PENDING, false).apply()
150
+ null
151
+ }
152
+ }
153
+ }