@rejourneyco/react-native 1.0.2 → 1.0.3

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 (39) hide show
  1. package/android/src/main/java/com/rejourney/RejourneyModuleImpl.kt +38 -363
  2. package/android/src/main/java/com/rejourney/capture/CaptureEngine.kt +11 -113
  3. package/android/src/main/java/com/rejourney/capture/SegmentUploader.kt +1 -15
  4. package/android/src/main/java/com/rejourney/capture/VideoEncoder.kt +1 -61
  5. package/android/src/main/java/com/rejourney/capture/ViewHierarchyScanner.kt +3 -1
  6. package/android/src/main/java/com/rejourney/lifecycle/SessionLifecycleService.kt +1 -22
  7. package/android/src/main/java/com/rejourney/network/DeviceAuthManager.kt +3 -26
  8. package/android/src/main/java/com/rejourney/network/NetworkMonitor.kt +0 -2
  9. package/android/src/main/java/com/rejourney/network/UploadManager.kt +7 -93
  10. package/android/src/main/java/com/rejourney/network/UploadWorker.kt +5 -41
  11. package/android/src/main/java/com/rejourney/privacy/PrivacyMask.kt +2 -58
  12. package/android/src/main/java/com/rejourney/touch/TouchInterceptor.kt +4 -4
  13. package/android/src/main/java/com/rejourney/utils/EventBuffer.kt +36 -7
  14. package/ios/Capture/RJViewHierarchyScanner.m +68 -51
  15. package/ios/Core/RJLifecycleManager.m +0 -14
  16. package/ios/Core/Rejourney.mm +24 -37
  17. package/ios/Network/RJDeviceAuthManager.m +0 -2
  18. package/ios/Network/RJUploadManager.h +8 -0
  19. package/ios/Network/RJUploadManager.m +45 -0
  20. package/ios/Privacy/RJPrivacyMask.m +5 -31
  21. package/ios/Rejourney.h +0 -14
  22. package/ios/Touch/RJTouchInterceptor.m +21 -15
  23. package/ios/Utils/RJEventBuffer.m +57 -69
  24. package/ios/Utils/RJWindowUtils.m +87 -86
  25. package/lib/commonjs/index.js +42 -30
  26. package/lib/commonjs/sdk/autoTracking.js +0 -3
  27. package/lib/commonjs/sdk/networkInterceptor.js +0 -11
  28. package/lib/commonjs/sdk/utils.js +73 -14
  29. package/lib/module/index.js +42 -30
  30. package/lib/module/sdk/autoTracking.js +0 -3
  31. package/lib/module/sdk/networkInterceptor.js +0 -11
  32. package/lib/module/sdk/utils.js +73 -14
  33. package/lib/typescript/sdk/utils.d.ts +31 -1
  34. package/package.json +16 -4
  35. package/src/index.ts +40 -19
  36. package/src/sdk/autoTracking.ts +0 -2
  37. package/src/sdk/constants.ts +13 -13
  38. package/src/sdk/networkInterceptor.ts +0 -9
  39. package/src/sdk/utils.ts +76 -14
@@ -59,7 +59,6 @@ interface VideoEncoderDelegate {
59
59
 
60
60
  class VideoEncoder(private val segmentDir: File) {
61
61
 
62
- // Configuration - matching iOS RJVideoEncoder defaults for quality and efficiency
63
62
  /** Target video bitrate in bits per second. Default: 1.5 Mbps - matches iOS */
64
63
  var targetBitrate: Int = 1_500_000
65
64
 
@@ -84,7 +83,6 @@ class VideoEncoder(private val segmentDir: File) {
84
83
  /** Delegate for receiving segment completion notifications */
85
84
  var delegate: VideoEncoderDelegate? = null
86
85
 
87
- // State
88
86
  private var encoder: MediaCodec? = null
89
87
  private var muxer: MediaMuxer? = null
90
88
  private var inputSurface: Surface? = null
@@ -101,37 +99,24 @@ class VideoEncoder(private val segmentDir: File) {
101
99
  private val presentationTimesUs = ArrayDeque<Long>()
102
100
  private var lastPresentationTimeUs = 0L
103
101
 
104
- // CRITICAL FIX: Track original unscaled dimensions for segment rotation
105
- // Without this, finishSegmentAndContinue() would pass already-scaled dimensions
106
- // to startSegment(), causing double-scaling and degrading video quality
107
102
  private var originalRequestedWidth = 0
108
103
  private var originalRequestedHeight = 0
109
104
 
110
105
  private var sessionId: String? = null
111
106
 
112
- // Encoding thread
113
107
  private var encodingThread: HandlerThread? = null
114
108
  private var encodingHandler: Handler? = null
115
109
 
116
- // Buffer info for draining encoder
117
110
  private val bufferInfo = MediaCodec.BufferInfo()
118
111
 
119
- // Lock for thread-safe encoder access
120
- // Prevents concurrent calls to drainEncoder from appendFrame and finishSegment
121
112
  private val encoderLock = ReentrantLock()
122
113
 
123
- // isRecording: Allow frames once encoder and surface are ready
124
- // NOTE: The muxer starts asynchronously when the first frame is encoded and
125
- // drainEncoder receives INFO_OUTPUT_FORMAT_CHANGED. We cannot require muxerStarted
126
- // here because that would create a chicken-and-egg problem where no frames can be
127
- // appended to trigger the muxer to start.
128
114
  val isRecording: Boolean
129
115
  get() = encoder != null && inputSurface != null
130
116
 
131
117
  val currentFrameCount: Int
132
118
  get() = frameCount.get()
133
119
 
134
- // Prewarm state
135
120
  private var isPrewarmed = false
136
121
 
137
122
  init {
@@ -147,7 +132,6 @@ class VideoEncoder(private val segmentDir: File) {
147
132
  if (isPrewarmed) return
148
133
 
149
134
  try {
150
- // Pre-query available H.264 encoders to cache codec info
151
135
  val codecInfo = findEncoder(MediaFormat.MIMETYPE_VIDEO_AVC)
152
136
  if (codecInfo != null) {
153
137
  Logger.debug("[VideoEncoder] Prewarm: found encoder ${codecInfo.name}")
@@ -178,22 +162,17 @@ class VideoEncoder(private val segmentDir: File) {
178
162
  fun startSegment(width: Int, height: Int): Boolean {
179
163
  Logger.debug("[VideoEncoder] startSegment: ${width}x${height}")
180
164
 
181
- // If already recording, finish current segment first
182
165
  if (isRecording) {
183
166
  Logger.debug("[VideoEncoder] Already recording, finishing previous segment")
184
167
  finishSegment()
185
168
  }
186
169
 
187
- // CRITICAL FIX: Store original unscaled dimensions BEFORE scaling
188
- // Used by finishSegmentInternal() during segment rotation to prevent double-scaling
189
170
  originalRequestedWidth = width
190
171
  originalRequestedHeight = height
191
172
 
192
- // Apply scale factor using native pixels (matching iOS)
193
173
  var scaledWidth = (width * captureScale).toInt()
194
174
  var scaledHeight = (height * captureScale).toInt()
195
175
 
196
- // Apply max dimension cap (like iOS)
197
176
  val maxDim = maxOf(scaledWidth, scaledHeight)
198
177
  if (maxDim > maxDimension) {
199
178
  val scale = maxDimension.toFloat() / maxDim.toFloat()
@@ -202,11 +181,9 @@ class VideoEncoder(private val segmentDir: File) {
202
181
  Logger.debug("[VideoEncoder] Applied max dimension cap: ${scaledWidth}x${scaledHeight}")
203
182
  }
204
183
 
205
- // Ensure dimensions are even (required for H.264)
206
184
  scaledWidth = (scaledWidth / 2) * 2
207
185
  scaledHeight = (scaledHeight / 2) * 2
208
186
 
209
- // Minimum size check
210
187
  if (scaledWidth < 100 || scaledHeight < 100) {
211
188
  Logger.warning("[VideoEncoder] Frame size too small, using minimum 100x100")
212
189
  scaledWidth = 100
@@ -216,7 +193,6 @@ class VideoEncoder(private val segmentDir: File) {
216
193
  currentFrameWidth = scaledWidth
217
194
  currentFrameHeight = scaledHeight
218
195
 
219
- // Reset counters
220
196
  frameCount.set(0)
221
197
  lastFrameTimestamp.set(0)
222
198
  segmentFirstFrameTimestamp.set(0)
@@ -224,21 +200,17 @@ class VideoEncoder(private val segmentDir: File) {
224
200
  lastPresentationTimeUs = 0L
225
201
  segmentStartTime.set(System.currentTimeMillis())
226
202
 
227
- // Generate unique filename
228
203
  val sessionPrefix = sessionId ?: "unknown"
229
204
  val filename = "seg_${sessionPrefix}_${segmentStartTime.get()}.mp4"
230
205
  currentSegmentFile = File(segmentDir, filename)
231
206
 
232
- // Delete existing file if any
233
207
  currentSegmentFile?.delete()
234
208
 
235
209
  Logger.debug("[VideoEncoder] Creating segment: $filename (${scaledWidth}x${scaledHeight})")
236
210
 
237
211
  return try {
238
- // Start encoding thread
239
212
  startEncodingThread()
240
213
 
241
- // Configure encoder
242
214
  configureEncoder(scaledWidth, scaledHeight)
243
215
 
244
216
  true
@@ -274,20 +246,17 @@ class VideoEncoder(private val segmentDir: File) {
274
246
  }
275
247
 
276
248
  try {
277
- // Scale bitmap if needed
278
249
  val scaledBitmap = if (bitmap.width != currentFrameWidth || bitmap.height != currentFrameHeight) {
279
250
  Bitmap.createScaledBitmap(bitmap, currentFrameWidth, currentFrameHeight, true)
280
251
  } else {
281
252
  bitmap
282
253
  }
283
254
 
284
- // Track first frame timestamp for presentation time calculation
285
255
  if (frameCount.get() == 0) {
286
256
  segmentFirstFrameTimestamp.set(timestamp)
287
257
  }
288
258
 
289
259
 
290
- // Draw bitmap to encoder input surface
291
260
  val canvas = surface.lockHardwareCanvas()
292
261
  try {
293
262
  canvas.drawBitmap(scaledBitmap, 0f, 0f, null)
@@ -295,12 +264,10 @@ class VideoEncoder(private val segmentDir: File) {
295
264
  surface.unlockCanvasAndPost(canvas)
296
265
  }
297
266
 
298
- // Recycle scaled bitmap if we created a new one
299
267
  if (scaledBitmap != bitmap) {
300
268
  scaledBitmap.recycle()
301
269
  }
302
270
 
303
- // Drain encoder output (thread-safe)
304
271
  encoderLock.withLock {
305
272
  val presentationTimeUs = computePresentationTimeUs(timestamp)
306
273
  presentationTimesUs.add(presentationTimeUs)
@@ -314,7 +281,6 @@ class VideoEncoder(private val segmentDir: File) {
314
281
  Logger.debug("[VideoEncoder] Frame appended: ${frameCount.get()}/$framesPerSegment")
315
282
  }
316
283
 
317
- // Auto-rotate segment if we've reached the limit
318
284
  if (frameCount.get() >= framesPerSegment) {
319
285
  Logger.info("[VideoEncoder] Segment full (${frameCount.get()} frames), rotating")
320
286
  finishSegmentAndContinue()
@@ -364,7 +330,6 @@ class VideoEncoder(private val segmentDir: File) {
364
330
  val width = currentFrameWidth
365
331
  val height = currentFrameHeight
366
332
 
367
- // Skip if no frames were written
368
333
  if (count == 0) {
369
334
  Logger.debug("[VideoEncoder] No frames in segment, canceling")
370
335
  cancelSegment()
@@ -372,25 +337,19 @@ class VideoEncoder(private val segmentDir: File) {
372
337
  }
373
338
 
374
339
  try {
375
- // Thread-safe: Lock to prevent race with appendFrame's drainEncoder
376
340
  encoderLock.withLock {
377
- // Signal end of stream
378
341
  enc.signalEndOfInputStream()
379
342
 
380
- // Drain remaining output
381
343
  drainEncoder(true)
382
344
  }
383
345
 
384
- // Stop and release muxer
385
346
  if (muxerStarted.get()) {
386
347
  muxer?.stop()
387
348
  }
388
349
 
389
- // Get file size for logging
390
350
  val fileSize = segmentFile?.length() ?: 0
391
351
  Logger.info("[VideoEncoder] Segment complete - $count frames, ${fileSize / 1024.0} KB, ${(endTime - startTime) / 1000.0}s")
392
352
 
393
- // Notify delegate
394
353
  segmentFile?.let {
395
354
  delegate?.onSegmentFinished(it, startTime, endTime, count)
396
355
  }
@@ -401,13 +360,8 @@ class VideoEncoder(private val segmentDir: File) {
401
360
  } finally {
402
361
  cleanup()
403
362
 
404
- // Start new segment if requested
405
363
  if (shouldContinue && sessionId != null) {
406
364
  Logger.debug("[VideoEncoder] Starting new segment after rotation")
407
- // CRITICAL FIX: Use original unscaled dimensions, NOT width/height
408
- // which are the already-scaled currentFrameWidth/currentFrameHeight.
409
- // Using scaled dimensions would cause double-scaling:
410
- // 1080x2424 -> 356x800 (first segment) -> 142x320 (second segment)
411
365
  startSegment(originalRequestedWidth, originalRequestedHeight)
412
366
  }
413
367
  }
@@ -482,12 +436,9 @@ class VideoEncoder(private val segmentDir: File) {
482
436
  Logger.warning("[VideoEncoder] Emergency flush - attempting to save ${frameCount.get()} frames")
483
437
 
484
438
  return try {
485
- // Save segment metadata to disk for recovery
486
439
  saveCrashSegmentMetadata()
487
440
 
488
- // Thread-safe: Lock to prevent race with appendFrame
489
441
  encoderLock.withLock {
490
- // Try to finalize
491
442
  encoder?.signalEndOfInputStream()
492
443
  drainEncoder(true)
493
444
  }
@@ -525,32 +476,27 @@ class VideoEncoder(private val segmentDir: File) {
525
476
  }
526
477
 
527
478
  private fun configureEncoder(width: Int, height: Int) {
528
- // Find H.264 encoder
529
479
  val codecInfo = findEncoder(MediaFormat.MIMETYPE_VIDEO_AVC)
530
480
  ?: throw IllegalStateException("No H.264 encoder available")
531
481
 
532
- // Configure format - matching iOS RJVideoEncoder settings
533
482
  val format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, width, height).apply {
534
483
  setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface)
535
484
  setInteger(MediaFormat.KEY_BIT_RATE, targetBitrate)
536
485
  setInteger(MediaFormat.KEY_FRAME_RATE, fps)
537
- setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, keyframeInterval) // 10s keyframes like iOS for smaller files
486
+ setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, keyframeInterval)
538
487
 
539
- // Baseline profile to reduce encoder CPU cost (matches iOS Baseline+CAVLC change)
540
488
  if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
541
489
  setInteger(MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline)
542
490
  setInteger(MediaFormat.KEY_LEVEL, MediaCodecInfo.CodecProfileLevel.AVCLevel31)
543
491
  }
544
492
  }
545
493
 
546
- // Create encoder
547
494
  encoder = MediaCodec.createByCodecName(codecInfo.name).apply {
548
495
  configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
549
496
  inputSurface = createInputSurface()
550
497
  start()
551
498
  }
552
499
 
553
- // Create muxer
554
500
  muxer = MediaMuxer(currentSegmentFile!!.absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
555
501
 
556
502
  Logger.debug("[VideoEncoder] Encoder configured: ${codecInfo.name} ${width}x${height} @ ${targetBitrate}bps")
@@ -589,7 +535,6 @@ class VideoEncoder(private val segmentDir: File) {
589
535
  when {
590
536
  outputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER -> {
591
537
  if (!endOfStream) break
592
- // Continue draining if end of stream
593
538
  }
594
539
 
595
540
  outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
@@ -608,7 +553,6 @@ class VideoEncoder(private val segmentDir: File) {
608
553
  ?: throw RuntimeException("Encoder output buffer $outputBufferIndex was null")
609
554
 
610
555
  if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG != 0) {
611
- // Codec config data - skip
612
556
  bufferInfo.size = 0
613
557
  }
614
558
 
@@ -662,7 +606,6 @@ class VideoEncoder(private val segmentDir: File) {
662
606
  }
663
607
 
664
608
  companion object {
665
- // Class-level prewarm state (matching iOS +prewarmEncoderAsync)
666
609
  @Volatile
667
610
  private var staticPrewarmed = false
668
611
 
@@ -687,7 +630,6 @@ class VideoEncoder(private val segmentDir: File) {
687
630
  try {
688
631
  val startTime = System.nanoTime()
689
632
 
690
- // Query available H.264 encoders to cache codec info in system
691
633
  val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
692
634
  val codecInfos = codecList.codecInfos
693
635
 
@@ -698,7 +640,6 @@ class VideoEncoder(private val segmentDir: File) {
698
640
  val types = info.supportedTypes
699
641
  if (types.any { it.equals(MediaFormat.MIMETYPE_VIDEO_AVC, ignoreCase = true) }) {
700
642
  encoderName = info.name
701
- // Touch capabilities to trigger JIT/caching
702
643
  val caps = info.getCapabilitiesForType(MediaFormat.MIMETYPE_VIDEO_AVC)
703
644
  caps.videoCapabilities
704
645
  caps.encoderCapabilities
@@ -731,7 +672,6 @@ class VideoEncoder(private val segmentDir: File) {
731
672
  return try {
732
673
  val json = metaFile.readText()
733
674
  val result = mutableMapOf<String, Any>()
734
- // Simple JSON parsing (avoiding JSONObject for minimal dependencies)
735
675
  json.lines().forEach { line ->
736
676
  val trimmed = line.trim().removeSuffix(",")
737
677
  when {
@@ -627,7 +627,9 @@ class ViewHierarchyScanner(val config: ViewHierarchyScannerConfig = ViewHierarch
627
627
  "AIRMap",
628
628
  "AIRMapView",
629
629
  "RNMMapView",
630
- "GMSMapView"
630
+ "GMSMapView",
631
+ "Mapbox",
632
+ "MGLMapView"
631
633
  )
632
634
  }
633
635
  }
@@ -47,24 +47,18 @@ class SessionLifecycleService : Service() {
47
47
  companion object {
48
48
  private const val TAG = "SessionLifecycleService"
49
49
 
50
- // Static listener reference - set by RejourneyModuleImpl
51
50
  @Volatile
52
51
  var taskRemovedListener: TaskRemovedListener? = null
53
52
 
54
- // Track if service is running
55
53
  @Volatile
56
54
  var isRunning = false
57
55
  private set
58
56
 
59
- // Minimum time between service start and onTaskRemoved to be considered valid
60
- // Samsung devices sometimes fire onTaskRemoved immediately on app launch
61
- private const val MIN_TIME_BEFORE_TASK_REMOVED_MS = 2000L // 2 seconds
57
+ private const val MIN_TIME_BEFORE_TASK_REMOVED_MS = 2000L
62
58
  }
63
59
 
64
60
  private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
65
61
 
66
- // Track service start time to detect false positives (Samsung bug)
67
- // Instance variable, not static, since each service instance has its own start time
68
62
  private var serviceStartTime: Long = 0
69
63
 
70
64
  override fun onCreate() {
@@ -78,8 +72,6 @@ class SessionLifecycleService : Service() {
78
72
  override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
79
73
  serviceStartTime = SystemClock.elapsedRealtime()
80
74
  Logger.debug("[$TAG] Service started (startId=$startId, OEM: ${OEMDetector.getOEM()})")
81
- // Return START_STICKY to allow system to restart service if killed
82
- // This helps ensure the service stays alive to catch termination events
83
75
  return START_STICKY
84
76
  }
85
77
 
@@ -106,9 +98,6 @@ class SessionLifecycleService : Service() {
106
98
 
107
99
  Logger.debug("[$TAG] ⚠️ onTaskRemoved() called (OEM: $oem, timeSinceStart: ${timeSinceStart}ms)")
108
100
 
109
- // SAMSUNG BUG FIX: Samsung devices sometimes fire onTaskRemoved() immediately
110
- // after app launch (within 1-2 seconds). This is a known bug on Samsung devices.
111
- // We filter out these false positives by checking the time since service start.
112
101
  if (OEMDetector.isSamsung() && timeSinceStart < Companion.MIN_TIME_BEFORE_TASK_REMOVED_MS) {
113
102
  Logger.warning("[$TAG] ⚠️ Ignoring onTaskRemoved() - likely Samsung false positive (fired ${timeSinceStart}ms after start, expected > ${MIN_TIME_BEFORE_TASK_REMOVED_MS}ms)")
114
103
  Logger.warning("[$TAG] This is a known Samsung bug where onTaskRemoved fires on app launch")
@@ -116,20 +105,12 @@ class SessionLifecycleService : Service() {
116
105
  return
117
106
  }
118
107
 
119
- // For other OEMs with aggressive task killing, log but still process
120
108
  if (OEMDetector.hasAggressiveTaskKilling()) {
121
109
  Logger.debug("[$TAG] OEM has aggressive task killing - onTaskRemoved may be unreliable")
122
110
  }
123
111
 
124
112
  Logger.debug("[$TAG] ✅ Valid onTaskRemoved() - app is being killed/swiped away")
125
113
 
126
- // CRITICAL: Notify the listener IMMEDIATELY and SYNCHRONOUSLY
127
- // The previous implementation used a coroutine with 100ms delay which caused
128
- // the process to be killed before session end could complete.
129
- //
130
- // We now call the listener directly on the main thread. The listener
131
- // (RejourneyModuleImpl) is responsible for doing synchronous work as fast
132
- // as possible since the process may be killed immediately after this returns.
133
114
  try {
134
115
  Logger.debug("[$TAG] Calling listener synchronously...")
135
116
  taskRemovedListener?.onTaskRemoved()
@@ -138,7 +119,6 @@ class SessionLifecycleService : Service() {
138
119
  Logger.error("[$TAG] Error notifying task removed listener", e)
139
120
  }
140
121
 
141
- // Stop the service - no need for async since listener is already done
142
122
  try {
143
123
  stopSelf()
144
124
  } catch (e: Exception) {
@@ -149,7 +129,6 @@ class SessionLifecycleService : Service() {
149
129
  }
150
130
 
151
131
  override fun onBind(intent: Intent?): IBinder? {
152
- // This is a started service, not a bound service
153
132
  return null
154
133
  }
155
134
 
@@ -57,9 +57,8 @@ class DeviceAuthManager private constructor(private val context: Context) {
57
57
  private const val KEYSTORE_ALIAS = "com.rejourney.devicekey"
58
58
  private const val ANDROID_KEYSTORE = "AndroidKeyStore"
59
59
 
60
- // Rate limiting constants for auto-registration
61
- private const val AUTH_COOLDOWN_BASE_MS = 5000L // 5 second base cooldown
62
- private const val AUTH_COOLDOWN_MAX_MS = 300000L // 5 minute max cooldown
60
+ private const val AUTH_COOLDOWN_BASE_MS = 5000L
61
+ private const val AUTH_COOLDOWN_MAX_MS = 300000L
63
62
  private const val AUTH_MAX_CONSECUTIVE_FAILURES = 10
64
63
 
65
64
  fun getInstance(context: Context): DeviceAuthManager {
@@ -69,10 +68,8 @@ class DeviceAuthManager private constructor(private val context: Context) {
69
68
  }
70
69
  }
71
70
 
72
- // Listener for authentication failures (set by RejourneyModuleImpl)
73
71
  var authFailureListener: AuthFailureListener? = null
74
72
 
75
- // Use shared client (SSL pinning removed)
76
73
  private val client = HttpClientProvider.shared
77
74
 
78
75
  private val prefs: SharedPreferences by lazy {
@@ -143,7 +140,6 @@ class DeviceAuthManager private constructor(private val context: Context) {
143
140
  .putString(KEY_SDK_VERSION, sdkVersion)
144
141
  .apply()
145
142
 
146
- // Check if already registered
147
143
  val existingCredentialId = prefs.getString(KEY_CREDENTIAL_ID, null)
148
144
  if (!existingCredentialId.isNullOrEmpty()) {
149
145
  Logger.debug("Device already registered with credential: $existingCredentialId")
@@ -151,7 +147,6 @@ class DeviceAuthManager private constructor(private val context: Context) {
151
147
  return
152
148
  }
153
149
 
154
- // Generate or load ECDSA keypair
155
150
  val publicKeyPEM = try {
156
151
  getOrCreatePublicKeyPEM()
157
152
  } catch (e: Exception) {
@@ -165,7 +160,6 @@ class DeviceAuthManager private constructor(private val context: Context) {
165
160
  return
166
161
  }
167
162
 
168
- // Register with backend
169
163
  val requestBody = JSONObject().apply {
170
164
  put("projectPublicKey", projectKey)
171
165
  put("bundleId", bundleId)
@@ -241,7 +235,6 @@ class DeviceAuthManager private constructor(private val context: Context) {
241
235
  fun getUploadToken(
242
236
  callback: (success: Boolean, token: String?, expiresIn: Int, error: String?) -> Unit
243
237
  ) {
244
- // Check cached token
245
238
  val cachedToken = prefs.getString(KEY_UPLOAD_TOKEN, null)
246
239
  val tokenExpiry = prefs.getLong(KEY_TOKEN_EXPIRY, 0)
247
240
 
@@ -261,14 +254,12 @@ class DeviceAuthManager private constructor(private val context: Context) {
261
254
 
262
255
  val savedApiUrl = prefs.getString(KEY_API_URL, null)?.takeIf { it.startsWith("http") } ?: "https://api.rejourney.co"
263
256
 
264
- // Step 1: Request challenge
265
257
  requestChallenge(credentialId) { challengeSuccess, challenge, nonce, challengeError ->
266
258
  if (!challengeSuccess || challenge == null || nonce == null) {
267
259
  callback(false, null, 0, challengeError ?: "Failed to get challenge")
268
260
  return@requestChallenge
269
261
  }
270
262
 
271
- // Step 2: Sign challenge
272
263
  val signature = try {
273
264
  signChallenge(challenge)
274
265
  } catch (e: Exception) {
@@ -282,7 +273,6 @@ class DeviceAuthManager private constructor(private val context: Context) {
282
273
  return@requestChallenge
283
274
  }
284
275
 
285
- // Step 3: Start session with signed challenge
286
276
  startSession(credentialId, challenge, nonce, signature, callback)
287
277
  }
288
278
  }
@@ -434,7 +424,6 @@ class DeviceAuthManager private constructor(private val context: Context) {
434
424
  suspend fun ensureValidToken(): Boolean {
435
425
  Logger.debug("[DeviceAuthManager] ensureValidToken() called")
436
426
 
437
- // Always refresh from SharedPreferences before network operations
438
427
  loadStoredRegistrationInfo()
439
428
 
440
429
  return kotlinx.coroutines.suspendCancellableCoroutine { continuation ->
@@ -478,7 +467,6 @@ class DeviceAuthManager private constructor(private val context: Context) {
478
467
  .remove(KEY_TOKEN_EXPIRY)
479
468
  .apply()
480
469
 
481
- // Also delete the private key from Keystore
482
470
  try {
483
471
  val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE)
484
472
  keyStore.load(null)
@@ -506,7 +494,6 @@ class DeviceAuthManager private constructor(private val context: Context) {
506
494
  }
507
495
  }
508
496
 
509
- // ==================== ECDSA Keypair Management ====================
510
497
 
511
498
  /**
512
499
  * Get or create ECDSA P-256 keypair and return public key in PEM format.
@@ -515,14 +502,12 @@ class DeviceAuthManager private constructor(private val context: Context) {
515
502
  val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE)
516
503
  keyStore.load(null)
517
504
 
518
- // Try to load existing key
519
505
  if (keyStore.containsAlias(KEYSTORE_ALIAS)) {
520
506
  Logger.debug("Loaded existing ECDSA private key")
521
507
  val certificate = keyStore.getCertificate(KEYSTORE_ALIAS)
522
508
  return exportPublicKeyToPEM(certificate.publicKey)
523
509
  }
524
510
 
525
- // Generate new keypair
526
511
  Logger.debug("Generating new ECDSA P-256 keypair")
527
512
 
528
513
  val keyPairGenerator = KeyPairGenerator.getInstance(
@@ -536,7 +521,7 @@ class DeviceAuthManager private constructor(private val context: Context) {
536
521
  )
537
522
  .setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1")) // P-256
538
523
  .setDigests(KeyProperties.DIGEST_SHA256)
539
- .setUserAuthenticationRequired(false) // No biometric required
524
+ .setUserAuthenticationRequired(false)
540
525
  .build()
541
526
 
542
527
  keyPairGenerator.initialize(parameterSpec)
@@ -551,7 +536,6 @@ class DeviceAuthManager private constructor(private val context: Context) {
551
536
  */
552
537
  private fun exportPublicKeyToPEM(publicKey: PublicKey): String? {
553
538
  return try {
554
- // Get the raw public key bytes (X.509 SubjectPublicKeyInfo format)
555
539
  val encoded = publicKey.encoded
556
540
  val base64 = Base64.encodeToString(encoded, Base64.NO_WRAP)
557
541
 
@@ -574,10 +558,8 @@ class DeviceAuthManager private constructor(private val context: Context) {
574
558
  val privateKey = keyStore.getKey(KEYSTORE_ALIAS, null) as? PrivateKey
575
559
  ?: throw Exception("Private key not found")
576
560
 
577
- // Decode base64 challenge
578
561
  val challengeBytes = Base64.decode(challenge, Base64.DEFAULT)
579
562
 
580
- // Sign with ECDSA-SHA256
581
563
  val signature = Signature.getInstance("SHA256withECDSA")
582
564
  signature.initSign(privateKey)
583
565
  signature.update(challengeBytes)
@@ -591,7 +573,6 @@ class DeviceAuthManager private constructor(private val context: Context) {
591
573
  }
592
574
  }
593
575
 
594
- // ==================== Backend Communication ====================
595
576
 
596
577
  /**
597
578
  * Request challenge from backend.
@@ -638,7 +619,6 @@ class DeviceAuthManager private constructor(private val context: Context) {
638
619
 
639
620
  when (errorCode) {
640
621
  403 -> {
641
- // Access forbidden - security error
642
622
  Logger.error("SECURITY: Challenge request forbidden")
643
623
  val errorMessage = parseErrorMessage(errorBody) ?: "Access forbidden"
644
624
  clearCredentials()
@@ -646,7 +626,6 @@ class DeviceAuthManager private constructor(private val context: Context) {
646
626
  callback(false, null, null, errorMessage)
647
627
  }
648
628
  404 -> {
649
- // Device credential rejected by backend (likely DB reset or invalid)
650
629
  Logger.error("SECURITY: Device credential not found")
651
630
  val errorMessage = parseErrorMessage(errorBody) ?: "Credential not found"
652
631
  clearCredentials()
@@ -720,7 +699,6 @@ class DeviceAuthManager private constructor(private val context: Context) {
720
699
 
721
700
  when (errorCode) {
722
701
  403 -> {
723
- // Access forbidden - security error
724
702
  Logger.error("SECURITY: Start-session forbidden")
725
703
  val errorMessage = parseErrorMessage(errorBody) ?: "Access forbidden"
726
704
  clearCredentials()
@@ -728,7 +706,6 @@ class DeviceAuthManager private constructor(private val context: Context) {
728
706
  callback(false, null, 0, errorMessage)
729
707
  }
730
708
  404 -> {
731
- // Credential or project not found
732
709
  Logger.error("SECURITY: Start-session resource not found")
733
710
  val errorMessage = parseErrorMessage(errorBody) ?: "Resource not found"
734
711
  clearCredentials()
@@ -108,11 +108,9 @@ class NetworkMonitor private constructor(private val context: Context) {
108
108
  connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
109
109
  telephonyManager = context.getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager
110
110
 
111
- // Check initial state
112
111
  updateCurrentNetworkState()
113
112
  listener?.onNetworkChanged(currentQuality)
114
113
 
115
- // Register for updates
116
114
  networkCallback = object : ConnectivityManager.NetworkCallback() {
117
115
  override fun onAvailable(network: Network) {
118
116
  updateCurrentNetworkState()