@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,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
|
+
}
|