@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,284 @@
|
|
|
1
|
+
package com.rejourney.utils
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import com.rejourney.core.Logger
|
|
5
|
+
import org.json.JSONObject
|
|
6
|
+
import java.io.BufferedReader
|
|
7
|
+
import java.io.File
|
|
8
|
+
import java.io.FileOutputStream
|
|
9
|
+
import java.io.FileReader
|
|
10
|
+
import java.io.OutputStreamWriter
|
|
11
|
+
import java.util.concurrent.locks.ReentrantLock
|
|
12
|
+
import kotlin.concurrent.withLock
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Write-first event buffer for crash-safe event persistence.
|
|
16
|
+
*
|
|
17
|
+
* Android implementation aligned with iOS RJEventBuffer:
|
|
18
|
+
* - Events are written synchronously to disk on append.
|
|
19
|
+
* - JSONL format (one JSON object per line).
|
|
20
|
+
* - Thread-safe via a single lock.
|
|
21
|
+
*/
|
|
22
|
+
class EventBuffer(
|
|
23
|
+
private val context: Context,
|
|
24
|
+
private val sessionId: String,
|
|
25
|
+
private val pendingRootPath: File
|
|
26
|
+
) {
|
|
27
|
+
private val lock = ReentrantLock()
|
|
28
|
+
private val eventsFile: File
|
|
29
|
+
private val metaFile: File
|
|
30
|
+
private var fileWriter: OutputStreamWriter? = null
|
|
31
|
+
|
|
32
|
+
var eventCount: Int = 0
|
|
33
|
+
private set
|
|
34
|
+
|
|
35
|
+
var lastEventTimestamp: Long = 0
|
|
36
|
+
private set
|
|
37
|
+
|
|
38
|
+
private var uploadedEventCount: Int = 0
|
|
39
|
+
private var isShutdown = false
|
|
40
|
+
|
|
41
|
+
init {
|
|
42
|
+
val sessionDir = File(pendingRootPath, sessionId).apply { mkdirs() }
|
|
43
|
+
eventsFile = File(sessionDir, "events.jsonl")
|
|
44
|
+
metaFile = File(sessionDir, "buffer_meta.json")
|
|
45
|
+
|
|
46
|
+
if (!eventsFile.exists()) {
|
|
47
|
+
eventsFile.createNewFile()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
countExistingEvents()
|
|
51
|
+
openFileWriter()
|
|
52
|
+
|
|
53
|
+
Logger.debug("Event buffer ready: ${eventsFile.absolutePath} ($eventCount existing events)")
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private fun countExistingEvents() {
|
|
57
|
+
lock.withLock {
|
|
58
|
+
try {
|
|
59
|
+
if (!eventsFile.exists()) {
|
|
60
|
+
eventCount = 0
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
var count = 0
|
|
65
|
+
var lastTs = 0L
|
|
66
|
+
|
|
67
|
+
BufferedReader(FileReader(eventsFile)).use { reader ->
|
|
68
|
+
reader.lineSequence().forEach { line ->
|
|
69
|
+
if (line.isNotBlank()) {
|
|
70
|
+
try {
|
|
71
|
+
val event = JSONObject(line)
|
|
72
|
+
count++
|
|
73
|
+
val ts = event.optLong("timestamp", 0)
|
|
74
|
+
if (ts > lastTs) {
|
|
75
|
+
lastTs = ts
|
|
76
|
+
}
|
|
77
|
+
} catch (_: Exception) {
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
eventCount = count
|
|
84
|
+
lastEventTimestamp = lastTs
|
|
85
|
+
|
|
86
|
+
if (metaFile.exists()) {
|
|
87
|
+
try {
|
|
88
|
+
val meta = JSONObject(metaFile.readText())
|
|
89
|
+
uploadedEventCount = meta.optInt("uploadedEventCount", 0)
|
|
90
|
+
} catch (_: Exception) {
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} catch (e: Exception) {
|
|
94
|
+
Logger.warning("Failed to count existing events: ${e.message}")
|
|
95
|
+
eventCount = 0
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private fun openFileWriter() {
|
|
101
|
+
lock.withLock {
|
|
102
|
+
try {
|
|
103
|
+
fileWriter = OutputStreamWriter(
|
|
104
|
+
FileOutputStream(eventsFile, true),
|
|
105
|
+
Charsets.UTF_8
|
|
106
|
+
)
|
|
107
|
+
} catch (e: Exception) {
|
|
108
|
+
Logger.error("Failed to open events file for writing", e)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
fun appendEvent(event: Map<String, Any?>): Boolean {
|
|
114
|
+
if (isShutdown) {
|
|
115
|
+
Logger.warning("[EventBuffer] appendEvent: Buffer is shutdown, rejecting event type=${event["type"]}")
|
|
116
|
+
return false
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return lock.withLock {
|
|
120
|
+
writeEventToDisk(event)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
fun appendEvents(events: List<Map<String, Any?>>): Boolean {
|
|
125
|
+
if (events.isEmpty()) return true
|
|
126
|
+
if (isShutdown) return false
|
|
127
|
+
|
|
128
|
+
return lock.withLock {
|
|
129
|
+
var allSuccess = true
|
|
130
|
+
events.forEach { event ->
|
|
131
|
+
if (!writeEventToDisk(event)) {
|
|
132
|
+
allSuccess = false
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
allSuccess
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private fun writeEventToDisk(event: Map<String, Any?>): Boolean {
|
|
140
|
+
val writer = fileWriter ?: return false
|
|
141
|
+
|
|
142
|
+
return try {
|
|
143
|
+
val jsonObject = JSONObject()
|
|
144
|
+
event.forEach { (key, value) ->
|
|
145
|
+
when (value) {
|
|
146
|
+
is Map<*, *> -> {
|
|
147
|
+
val nested = JSONObject()
|
|
148
|
+
value.forEach { (nestedKey, nestedValue) ->
|
|
149
|
+
if (nestedKey is String) {
|
|
150
|
+
nested.put(nestedKey, nestedValue)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
jsonObject.put(key, nested)
|
|
154
|
+
}
|
|
155
|
+
is List<*> -> jsonObject.put(key, org.json.JSONArray(value))
|
|
156
|
+
else -> jsonObject.put(key, value)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
val line = jsonObject.toString() + "\n"
|
|
161
|
+
writer.write(line)
|
|
162
|
+
writer.flush()
|
|
163
|
+
|
|
164
|
+
eventCount++
|
|
165
|
+
val ts = event["timestamp"] as? Long ?: (event["timestamp"] as? Number)?.toLong()
|
|
166
|
+
if (ts != null) {
|
|
167
|
+
lastEventTimestamp = ts
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
true
|
|
171
|
+
} catch (e: Exception) {
|
|
172
|
+
Logger.warning("Failed to write event: ${e.message}")
|
|
173
|
+
false
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
fun flush(): Boolean {
|
|
178
|
+
return lock.withLock {
|
|
179
|
+
try {
|
|
180
|
+
fileWriter?.flush()
|
|
181
|
+
true
|
|
182
|
+
} catch (e: Exception) {
|
|
183
|
+
Logger.error("[EventBuffer] flush: Failed to flush events", e)
|
|
184
|
+
false
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
fun readAllEvents(): List<Map<String, Any?>> {
|
|
190
|
+
return lock.withLock {
|
|
191
|
+
try {
|
|
192
|
+
if (!eventsFile.exists()) {
|
|
193
|
+
return@withLock emptyList()
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
val events = mutableListOf<Map<String, Any?>>()
|
|
197
|
+
BufferedReader(FileReader(eventsFile)).use { reader ->
|
|
198
|
+
reader.lineSequence().forEach { line ->
|
|
199
|
+
if (line.isNotBlank()) {
|
|
200
|
+
try {
|
|
201
|
+
val json = JSONObject(line)
|
|
202
|
+
val map = mutableMapOf<String, Any?>()
|
|
203
|
+
json.keys().forEach { key ->
|
|
204
|
+
map[key] = json.opt(key)
|
|
205
|
+
}
|
|
206
|
+
events.add(map)
|
|
207
|
+
} catch (_: Exception) {
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
events
|
|
214
|
+
} catch (e: Exception) {
|
|
215
|
+
Logger.error("[EventBuffer] readAllEvents: Failed to read events", e)
|
|
216
|
+
emptyList()
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
fun readEventsAfterBatchNumber(afterBatchNumber: Int): List<Map<String, Any?>> {
|
|
222
|
+
val allEvents = readAllEvents()
|
|
223
|
+
val startIndex = lock.withLock {
|
|
224
|
+
maxOf(uploadedEventCount, maxOf(0, afterBatchNumber))
|
|
225
|
+
}
|
|
226
|
+
if (startIndex >= allEvents.size) {
|
|
227
|
+
return emptyList()
|
|
228
|
+
}
|
|
229
|
+
return allEvents.subList(startIndex, allEvents.size)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
fun readPendingEvents(): List<Map<String, Any?>> {
|
|
233
|
+
val allEvents = readAllEvents()
|
|
234
|
+
if (uploadedEventCount >= allEvents.size) {
|
|
235
|
+
return emptyList()
|
|
236
|
+
}
|
|
237
|
+
return allEvents.subList(uploadedEventCount, allEvents.size)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
fun markEventsUploadedUpToIndex(eventIndex: Int) {
|
|
241
|
+
lock.withLock {
|
|
242
|
+
uploadedEventCount = eventIndex
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
val meta = JSONObject().apply {
|
|
246
|
+
put("uploadedEventCount", uploadedEventCount)
|
|
247
|
+
put("lastEventTimestamp", lastEventTimestamp)
|
|
248
|
+
}
|
|
249
|
+
metaFile.writeText(meta.toString())
|
|
250
|
+
} catch (e: Exception) {
|
|
251
|
+
Logger.warning("Failed to save buffer meta: ${e.message}")
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
fun clearAllEvents() {
|
|
257
|
+
lock.withLock {
|
|
258
|
+
closeFileWriter()
|
|
259
|
+
eventsFile.delete()
|
|
260
|
+
metaFile.delete()
|
|
261
|
+
eventCount = 0
|
|
262
|
+
uploadedEventCount = 0
|
|
263
|
+
lastEventTimestamp = 0
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
fun close() {
|
|
268
|
+
lock.withLock {
|
|
269
|
+
isShutdown = true
|
|
270
|
+
closeFileWriter()
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private fun closeFileWriter() {
|
|
275
|
+
try {
|
|
276
|
+
fileWriter?.close()
|
|
277
|
+
} catch (_: Exception) {
|
|
278
|
+
} finally {
|
|
279
|
+
fileWriter = null
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
fun getLastEventTimestampMs(): Long = lastEventTimestamp
|
|
284
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility to detect Android OEM (Original Equipment Manufacturer) and handle
|
|
3
|
+
* OEM-specific quirks and behaviors.
|
|
4
|
+
*
|
|
5
|
+
* Different OEMs have different behaviors for app lifecycle, especially around
|
|
6
|
+
* task removal and service callbacks. This utility helps detect and handle
|
|
7
|
+
* these differences.
|
|
8
|
+
*/
|
|
9
|
+
package com.rejourney.utils
|
|
10
|
+
|
|
11
|
+
import android.os.Build
|
|
12
|
+
import com.rejourney.core.Logger
|
|
13
|
+
|
|
14
|
+
object OEMDetector {
|
|
15
|
+
|
|
16
|
+
enum class OEM {
|
|
17
|
+
SAMSUNG,
|
|
18
|
+
XIAOMI,
|
|
19
|
+
HUAWEI,
|
|
20
|
+
ONEPLUS,
|
|
21
|
+
OPPO,
|
|
22
|
+
VIVO,
|
|
23
|
+
PIXEL,
|
|
24
|
+
STOCK_ANDROID,
|
|
25
|
+
UNKNOWN
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private val oem: OEM by lazy {
|
|
29
|
+
detectOEM()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get the detected OEM.
|
|
34
|
+
*/
|
|
35
|
+
fun getOEM(): OEM = oem
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Check if running on Samsung device.
|
|
39
|
+
* Samsung has known bugs with onTaskRemoved() firing incorrectly.
|
|
40
|
+
*/
|
|
41
|
+
fun isSamsung(): Boolean = oem == OEM.SAMSUNG
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check if running on Pixel or stock Android.
|
|
45
|
+
* These devices generally have more reliable lifecycle callbacks.
|
|
46
|
+
*/
|
|
47
|
+
fun isPixelOrStock(): Boolean = oem == OEM.PIXEL || oem == OEM.STOCK_ANDROID
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Check if running on OEMs with aggressive task killing.
|
|
51
|
+
* These OEMs may not reliably call onTaskRemoved().
|
|
52
|
+
*/
|
|
53
|
+
fun hasAggressiveTaskKilling(): Boolean {
|
|
54
|
+
return oem == OEM.XIAOMI ||
|
|
55
|
+
oem == OEM.HUAWEI ||
|
|
56
|
+
oem == OEM.OPPO ||
|
|
57
|
+
oem == OEM.VIVO
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Check if onTaskRemoved() is likely to work reliably on this device.
|
|
62
|
+
*/
|
|
63
|
+
fun isTaskRemovedReliable(): Boolean {
|
|
64
|
+
// Pixel/Stock Android: Generally reliable
|
|
65
|
+
if (isPixelOrStock()) return true
|
|
66
|
+
|
|
67
|
+
// Samsung: Has bugs but sometimes works
|
|
68
|
+
if (isSamsung()) return true // We'll add validation to filter false positives
|
|
69
|
+
|
|
70
|
+
// Aggressive OEMs: Often don't call onTaskRemoved
|
|
71
|
+
if (hasAggressiveTaskKilling()) return false
|
|
72
|
+
|
|
73
|
+
// Unknown: Assume it might work
|
|
74
|
+
return true
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Detect the OEM based on manufacturer and brand.
|
|
79
|
+
*/
|
|
80
|
+
private fun detectOEM(): OEM {
|
|
81
|
+
val manufacturer = Build.MANUFACTURER.lowercase()
|
|
82
|
+
val brand = Build.BRAND.lowercase()
|
|
83
|
+
val model = Build.MODEL.lowercase()
|
|
84
|
+
|
|
85
|
+
return when {
|
|
86
|
+
// Samsung
|
|
87
|
+
manufacturer.contains("samsung") || brand.contains("samsung") -> {
|
|
88
|
+
Logger.debug("OEM detected: Samsung")
|
|
89
|
+
OEM.SAMSUNG
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Xiaomi (includes Redmi, POCO)
|
|
93
|
+
manufacturer.contains("xiaomi") || brand.contains("xiaomi") ||
|
|
94
|
+
brand.contains("redmi") || brand.contains("poco") -> {
|
|
95
|
+
Logger.debug("OEM detected: Xiaomi")
|
|
96
|
+
OEM.XIAOMI
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Huawei (includes Honor)
|
|
100
|
+
manufacturer.contains("huawei") || brand.contains("huawei") ||
|
|
101
|
+
brand.contains("honor") -> {
|
|
102
|
+
Logger.debug("OEM detected: Huawei")
|
|
103
|
+
OEM.HUAWEI
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// OnePlus
|
|
107
|
+
manufacturer.contains("oneplus") || brand.contains("oneplus") -> {
|
|
108
|
+
Logger.debug("OEM detected: OnePlus")
|
|
109
|
+
OEM.ONEPLUS
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// OPPO
|
|
113
|
+
manufacturer.contains("oppo") || brand.contains("oppo") -> {
|
|
114
|
+
Logger.debug("OEM detected: OPPO")
|
|
115
|
+
OEM.OPPO
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Vivo
|
|
119
|
+
manufacturer.contains("vivo") || brand.contains("vivo") -> {
|
|
120
|
+
Logger.debug("OEM detected: Vivo")
|
|
121
|
+
OEM.VIVO
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Google Pixel
|
|
125
|
+
manufacturer.contains("google") && (model.contains("pixel") || brand.contains("google")) -> {
|
|
126
|
+
Logger.debug("OEM detected: Pixel")
|
|
127
|
+
OEM.PIXEL
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Stock Android (Google devices that aren't Pixel)
|
|
131
|
+
manufacturer.contains("google") -> {
|
|
132
|
+
Logger.debug("OEM detected: Stock Android")
|
|
133
|
+
OEM.STOCK_ANDROID
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
else -> {
|
|
137
|
+
Logger.debug("OEM detected: Unknown (manufacturer=$manufacturer, brand=$brand)")
|
|
138
|
+
OEM.UNKNOWN
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Get OEM-specific recommendations for app termination detection.
|
|
145
|
+
*/
|
|
146
|
+
fun getRecommendations(): String {
|
|
147
|
+
return when (oem) {
|
|
148
|
+
OEM.SAMSUNG -> "Samsung devices may have onTaskRemoved() fire incorrectly on app launch. Using validation to filter false positives."
|
|
149
|
+
OEM.XIAOMI, OEM.HUAWEI, OEM.OPPO, OEM.VIVO -> "This OEM has aggressive task killing. onTaskRemoved() may not fire. Relying on ApplicationExitInfo and persistent state checks."
|
|
150
|
+
OEM.PIXEL, OEM.STOCK_ANDROID -> "Stock Android - onTaskRemoved() should work reliably."
|
|
151
|
+
else -> "Unknown OEM - using standard detection methods."
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance timing utility aligned with iOS RJPerfTiming.
|
|
3
|
+
*/
|
|
4
|
+
package com.rejourney.utils
|
|
5
|
+
|
|
6
|
+
import android.os.Looper
|
|
7
|
+
import android.os.SystemClock
|
|
8
|
+
import com.rejourney.core.Logger
|
|
9
|
+
import java.util.concurrent.locks.ReentrantLock
|
|
10
|
+
import kotlin.concurrent.withLock
|
|
11
|
+
import kotlin.math.max
|
|
12
|
+
|
|
13
|
+
enum class PerfMetric {
|
|
14
|
+
FRAME,
|
|
15
|
+
SCREENSHOT,
|
|
16
|
+
RENDER,
|
|
17
|
+
PRIVACY_MASK,
|
|
18
|
+
VIEW_SCAN,
|
|
19
|
+
VIEW_SERIALIZE,
|
|
20
|
+
ENCODE,
|
|
21
|
+
PIXEL_BUFFER,
|
|
22
|
+
DOWNSCALE,
|
|
23
|
+
BUFFER_ALLOC,
|
|
24
|
+
ENCODE_APPEND,
|
|
25
|
+
UPLOAD
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
object PerfTiming {
|
|
29
|
+
private const val PERF_ENABLED = true
|
|
30
|
+
private const val DUMP_INTERVAL_MS = 5000.0
|
|
31
|
+
private const val MIN_SAMPLES = 5L
|
|
32
|
+
|
|
33
|
+
private val names = arrayOf(
|
|
34
|
+
"frame_total",
|
|
35
|
+
"screenshot_ui",
|
|
36
|
+
"render_draw",
|
|
37
|
+
"privacy_mask",
|
|
38
|
+
"view_scan",
|
|
39
|
+
"view_serialize",
|
|
40
|
+
"encode_h264",
|
|
41
|
+
"pixel_buffer",
|
|
42
|
+
"downscale",
|
|
43
|
+
"buffer_alloc",
|
|
44
|
+
"encode_append",
|
|
45
|
+
"upload_net"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
private val totals = DoubleArray(PerfMetric.values().size)
|
|
49
|
+
private val maxes = DoubleArray(PerfMetric.values().size)
|
|
50
|
+
private val counts = LongArray(PerfMetric.values().size)
|
|
51
|
+
private var lastDumpTimeNs = 0L
|
|
52
|
+
|
|
53
|
+
private val lock = ReentrantLock()
|
|
54
|
+
|
|
55
|
+
fun isEnabled(): Boolean = PERF_ENABLED
|
|
56
|
+
|
|
57
|
+
fun now(): Long = SystemClock.elapsedRealtimeNanos()
|
|
58
|
+
|
|
59
|
+
fun record(metric: PerfMetric, startNs: Long, endNs: Long) {
|
|
60
|
+
if (!PERF_ENABLED) {
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
val durationMs = (endNs - startNs).toDouble() / 1_000_000.0
|
|
65
|
+
val isMain = Looper.getMainLooper().thread == Thread.currentThread()
|
|
66
|
+
val threadName = if (isMain) "MAIN" else "BG"
|
|
67
|
+
val name = names[metric.ordinal]
|
|
68
|
+
|
|
69
|
+
if (isMain && durationMs > 4.0) {
|
|
70
|
+
Logger.warning("[RJ-PERF] ⚠️ [$threadName] $name: ${"%.2f".format(durationMs)}ms")
|
|
71
|
+
} else {
|
|
72
|
+
Logger.debug("[RJ-PERF] [$threadName] $name: ${"%.2f".format(durationMs)}ms")
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
lock.withLock {
|
|
76
|
+
counts[metric.ordinal]++
|
|
77
|
+
totals[metric.ordinal] += durationMs
|
|
78
|
+
maxes[metric.ordinal] = max(maxes[metric.ordinal], durationMs)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
fun dumpIfNeeded() {
|
|
83
|
+
if (!PERF_ENABLED) {
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
val now = now()
|
|
88
|
+
if (lastDumpTimeNs != 0L && msBetween(lastDumpTimeNs, now) < DUMP_INTERVAL_MS) {
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
lock.withLock {
|
|
93
|
+
if (lastDumpTimeNs != 0L && msBetween(lastDumpTimeNs, now) < DUMP_INTERVAL_MS) {
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
var totalSamples = 0L
|
|
98
|
+
counts.forEach { totalSamples += it }
|
|
99
|
+
if (totalSamples < MIN_SAMPLES) {
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
lastDumpTimeNs = now
|
|
104
|
+
|
|
105
|
+
val log = StringBuilder("[Rejourney PERF SUMMARY]")
|
|
106
|
+
for (i in counts.indices) {
|
|
107
|
+
if (counts[i] > 0) {
|
|
108
|
+
val avg = totals[i] / counts[i].toDouble()
|
|
109
|
+
log.append(" ")
|
|
110
|
+
log.append(names[i])
|
|
111
|
+
log.append("=")
|
|
112
|
+
log.append(counts[i])
|
|
113
|
+
log.append("/")
|
|
114
|
+
log.append(String.format("%.1f", avg))
|
|
115
|
+
log.append("/")
|
|
116
|
+
log.append(String.format("%.1f", maxes[i]))
|
|
117
|
+
log.append("ms")
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
Logger.debug(log.toString())
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
fun dump() {
|
|
125
|
+
if (!PERF_ENABLED) {
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
lock.withLock {
|
|
130
|
+
var totalSamples = 0L
|
|
131
|
+
counts.forEach { totalSamples += it }
|
|
132
|
+
if (totalSamples == 0L) {
|
|
133
|
+
Logger.debug("[Rejourney PERF] No samples collected")
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
val log = StringBuilder()
|
|
138
|
+
log.append("\n")
|
|
139
|
+
log.append("╔══════════════════════════════════════════════════════════════╗\n")
|
|
140
|
+
log.append("║ REJOURNEY SDK PERFORMANCE METRICS (ms) ║\n")
|
|
141
|
+
log.append("╠══════════════════════════════════════════════════════════════╣\n")
|
|
142
|
+
log.append("║ METRIC │ COUNT │ AVG (ms) │ MAX (ms) ║\n")
|
|
143
|
+
log.append("╠══════════════════════════════════════════════════════════════╣\n")
|
|
144
|
+
|
|
145
|
+
for (i in counts.indices) {
|
|
146
|
+
if (counts[i] > 0) {
|
|
147
|
+
val avg = totals[i] / counts[i].toDouble()
|
|
148
|
+
log.append(
|
|
149
|
+
String.format(
|
|
150
|
+
"║ %-16s │ %8d │ %10.2f │ %10.2f ║\n",
|
|
151
|
+
names[i],
|
|
152
|
+
counts[i],
|
|
153
|
+
avg,
|
|
154
|
+
maxes[i]
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
log.append("╚══════════════════════════════════════════════════════════════╝")
|
|
161
|
+
lastDumpTimeNs = now()
|
|
162
|
+
Logger.debug(log.toString())
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
fun reset() {
|
|
167
|
+
if (!PERF_ENABLED) {
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
lock.withLock {
|
|
172
|
+
for (i in counts.indices) {
|
|
173
|
+
totals[i] = 0.0
|
|
174
|
+
maxes[i] = 0.0
|
|
175
|
+
counts[i] = 0
|
|
176
|
+
}
|
|
177
|
+
lastDumpTimeNs = 0L
|
|
178
|
+
}
|
|
179
|
+
Logger.debug("[Rejourney PERF] Metrics reset")
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
fun snapshot(): Map<String, Map<String, Number>> {
|
|
183
|
+
if (!PERF_ENABLED) {
|
|
184
|
+
return emptyMap()
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return lock.withLock {
|
|
188
|
+
val result = mutableMapOf<String, Map<String, Number>>()
|
|
189
|
+
for (i in counts.indices) {
|
|
190
|
+
if (counts[i] > 0) {
|
|
191
|
+
val avg = totals[i] / counts[i].toDouble()
|
|
192
|
+
result[names[i]] = mapOf(
|
|
193
|
+
"count" to counts[i],
|
|
194
|
+
"avg_us" to avg,
|
|
195
|
+
"max_us" to maxes[i],
|
|
196
|
+
"total_us" to totals[i]
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
result
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
fun nameForMetric(metric: PerfMetric): String {
|
|
205
|
+
return names.getOrElse(metric.ordinal) { "unknown" }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
fun <T> time(metric: PerfMetric, block: () -> T): T {
|
|
209
|
+
if (!PERF_ENABLED) {
|
|
210
|
+
return block()
|
|
211
|
+
}
|
|
212
|
+
val start = now()
|
|
213
|
+
return try {
|
|
214
|
+
block()
|
|
215
|
+
} finally {
|
|
216
|
+
record(metric, start, now())
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
fun <T> measure(label: String, block: () -> T): T {
|
|
221
|
+
val start = now()
|
|
222
|
+
return try {
|
|
223
|
+
block()
|
|
224
|
+
} finally {
|
|
225
|
+
val end = now()
|
|
226
|
+
val durationUs = (end - start) / 1000
|
|
227
|
+
val durationMs = durationUs / 1000.0
|
|
228
|
+
Logger.debug("[PerfTiming] $label: ${durationUs}us (${String.format("%.3f", durationMs)}ms)")
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private fun msBetween(startNs: Long, endNs: Long): Double {
|
|
233
|
+
return (endNs - startNs).toDouble() / 1_000_000.0
|
|
234
|
+
}
|
|
235
|
+
}
|