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