omikit-plugin 3.3.28 → 4.0.1

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 (49) hide show
  1. package/README.md +949 -1223
  2. package/android/build.gradle +1 -1
  3. package/android/src/main/java/com/omikitplugin/FLLocalCameraModule.kt +1 -1
  4. package/android/src/main/java/com/omikitplugin/FLRemoteCameraModule.kt +1 -1
  5. package/android/src/main/java/com/omikitplugin/OmikitPluginModule.kt +277 -325
  6. package/android/src/main/java/com/omikitplugin/constants/constant.kt +2 -1
  7. package/ios/CallProcess/CallManager.swift +45 -35
  8. package/ios/Constant/Constant.swift +1 -0
  9. package/ios/Library/OmikitPlugin.m +75 -1
  10. package/ios/Library/OmikitPlugin.swift +199 -16
  11. package/ios/OmikitPlugin-Protocol.h +161 -0
  12. package/lib/commonjs/NativeOmikitPlugin.js +9 -0
  13. package/lib/commonjs/NativeOmikitPlugin.js.map +1 -0
  14. package/lib/commonjs/index.js +11 -0
  15. package/lib/commonjs/index.js.map +1 -1
  16. package/lib/commonjs/omi_audio_type.js +20 -0
  17. package/lib/commonjs/omi_audio_type.js.map +1 -0
  18. package/lib/commonjs/omi_local_camera.js +12 -2
  19. package/lib/commonjs/omi_local_camera.js.map +1 -1
  20. package/lib/commonjs/omi_remote_camera.js +12 -2
  21. package/lib/commonjs/omi_remote_camera.js.map +1 -1
  22. package/lib/commonjs/omi_start_call_status.js +30 -0
  23. package/lib/commonjs/omi_start_call_status.js.map +1 -1
  24. package/lib/commonjs/omikit.js +110 -16
  25. package/lib/commonjs/omikit.js.map +1 -1
  26. package/lib/module/NativeOmikitPlugin.js +3 -0
  27. package/lib/module/NativeOmikitPlugin.js.map +1 -0
  28. package/lib/module/index.js +1 -0
  29. package/lib/module/index.js.map +1 -1
  30. package/lib/module/omi_audio_type.js +14 -0
  31. package/lib/module/omi_audio_type.js.map +1 -0
  32. package/lib/module/omi_local_camera.js +13 -2
  33. package/lib/module/omi_local_camera.js.map +1 -1
  34. package/lib/module/omi_remote_camera.js +13 -2
  35. package/lib/module/omi_remote_camera.js.map +1 -1
  36. package/lib/module/omi_start_call_status.js +30 -0
  37. package/lib/module/omi_start_call_status.js.map +1 -1
  38. package/lib/module/omikit.js +104 -17
  39. package/lib/module/omikit.js.map +1 -1
  40. package/omikit-plugin.podspec +26 -24
  41. package/package.json +11 -2
  42. package/src/NativeOmikitPlugin.ts +160 -0
  43. package/src/index.tsx +2 -1
  44. package/src/omi_audio_type.tsx +9 -0
  45. package/src/omi_local_camera.tsx +12 -3
  46. package/src/omi_remote_camera.tsx +12 -3
  47. package/src/omi_start_call_status.tsx +29 -10
  48. package/src/omikit.tsx +96 -19
  49. package/src/types/index.d.ts +111 -11
@@ -120,7 +120,7 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
120
120
  private val mainScope = CoroutineScope(Dispatchers.Main)
121
121
  private var isIncoming: Boolean = false
122
122
  private var isAnswerCall: Boolean = false
123
- private var permissionPromise: Promise? = null
123
+ @Volatile private var permissionPromise: Promise? = null
124
124
 
125
125
  // Call state management to prevent concurrent calls
126
126
  private var isCallInProgress: Boolean = false
@@ -134,7 +134,22 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
134
134
  override fun getName(): String {
135
135
  return NAME
136
136
  }
137
-
137
+
138
+ override fun getConstants(): MutableMap<String, Any> {
139
+ return mutableMapOf(
140
+ "CALL_STATE_CHANGED" to CALL_STATE_CHANGED,
141
+ "MUTED" to MUTED,
142
+ "HOLD" to HOLD,
143
+ "SPEAKER" to SPEAKER,
144
+ "REMOTE_VIDEO_READY" to REMOTE_VIDEO_READY,
145
+ "CLICK_MISSED_CALL" to CLICK_MISSED_CALL,
146
+ "SWITCHBOARD_ANSWER" to SWITCHBOARD_ANSWER,
147
+ "CALL_QUALITY" to CALL_QUALITY,
148
+ "AUDIO_CHANGE" to AUDIO_CHANGE,
149
+ "REQUEST_PERMISSION" to REQUEST_PERMISSION
150
+ )
151
+ }
152
+
138
153
  /**
139
154
  * Check if we can start a new call (no concurrent calls, cooldown passed)
140
155
  */
@@ -145,12 +160,11 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
145
160
 
146
161
  // Check if call is in progress or cooldown not passed
147
162
  if (isCallInProgress) {
148
- Log.d("OMISDK", "🚫 Call blocked: Call already in progress")
163
+
149
164
  return false
150
165
  }
151
166
 
152
167
  if (timeSinceLastCall < callCooldownMs) {
153
- Log.d("OMISDK", "🚫 Call blocked: Cooldown period (${callCooldownMs - timeSinceLastCall}ms remaining)")
154
168
  return false
155
169
  }
156
170
 
@@ -165,7 +179,6 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
165
179
  synchronized(callStateLock) {
166
180
  isCallInProgress = true
167
181
  lastCallTime = System.currentTimeMillis()
168
- Log.d("OMISDK", "📞 Call started, marking in progress")
169
182
  }
170
183
  }
171
184
 
@@ -175,7 +188,6 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
175
188
  private fun markCallEnded() {
176
189
  synchronized(callStateLock) {
177
190
  isCallInProgress = false
178
- Log.d("OMISDK", "📴 Call ended, clearing in progress flag")
179
191
  }
180
192
  }
181
193
 
@@ -183,11 +195,8 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
183
195
  private val handler = Handler(Looper.getMainLooper())
184
196
 
185
197
  override fun incomingReceived(callerId: Int?, phoneNumber: String?, isVideo: Boolean?) {
186
- Log.d("OMISDK", "=>> incomingReceived CALLED - BEFORE: isIncoming: $isIncoming, isAnswerCall: $isAnswerCall")
187
198
  isIncoming = true;
188
199
  isAnswerCall = false; // Reset answer state for new incoming call
189
- Log.d("OMISDK", "=>> incomingReceived AFTER SET - isIncoming: $isIncoming, isAnswerCall: $isAnswerCall, phoneNumber: $phoneNumber")
190
-
191
200
  val typeNumber = OmiKitUtils().checkTypeNumber(phoneNumber ?: "")
192
201
 
193
202
  val map: WritableMap = WritableNativeMap().apply {
@@ -210,11 +219,8 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
210
219
  transactionId: String?,
211
220
  ) {
212
221
  isAnswerCall = true
213
- Log.d("OMISDK", "=>> ON CALL ESTABLISHED => ")
214
222
 
215
223
  Handler(Looper.getMainLooper()).postDelayed({
216
- Log.d("OmikitReactNative", "onCallEstablished")
217
-
218
224
  val typeNumber = OmiKitUtils().checkTypeNumber(phoneNumber ?: "")
219
225
 
220
226
  // ✅ Sử dụng safe WritableMap creation
@@ -233,18 +239,13 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
233
239
  }
234
240
 
235
241
  override fun onCallEnd(callInfo: MutableMap<String, Any?>, statusCode: Int) {
236
- Log.d("OMISDK RN", "=>> onCallEnd CALLED - BEFORE RESET: isIncoming: $isIncoming, isAnswerCall: $isAnswerCall")
237
- Log.d("OMISDK RN", "=>> onCallEnd callInfo => $callInfo")
238
-
239
242
  // Reset call state variables
240
243
  isIncoming = false
241
244
  isAnswerCall = false
242
245
  // Clear call progress state when remote party ends call
243
246
  markCallEnded()
244
- Log.d("OMISDK", "=>> onCallEnd AFTER RESET - isIncoming: $isIncoming, isAnswerCall: $isAnswerCall")
245
-
246
247
  // Kiểm tra kiểu dữ liệu trước khi ép kiểu để tránh lỗi
247
- val call = callInfo ?: mutableMapOf()
248
+ val call = callInfo
248
249
 
249
250
  val timeStartToAnswer = (call["time_start_to_answer"] as? Long) ?: 0L
250
251
  val timeEnd = (call["time_end"] as? Long) ?: 0L
@@ -267,18 +268,14 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
267
268
  )
268
269
 
269
270
  val map = createSafeWritableMap(eventData)
270
-
271
- Log.d("OMISDK RN", "=>> onCallEnd => ")
272
271
  sendEvent(CALL_STATE_CHANGED, map)
273
272
  }
274
273
 
275
274
  override fun onConnecting() {
276
- Log.d("OMISDK", "=>> ON CONNECTING CALL => ")
277
-
278
275
  val map: WritableMap = WritableNativeMap().apply {
279
276
  putString("callerNumber", "")
280
277
  putBoolean("isVideo", NotificationService.isVideo)
281
- putBoolean("incoming", isIncoming ?: false)
278
+ putBoolean("incoming", isIncoming)
282
279
  putString("transactionId", "")
283
280
  putString("_id", "")
284
281
  putInt("status", CallState.connecting.value)
@@ -299,18 +296,12 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
299
296
  val prePhoneNumber = OmiClient.prePhoneNumber ?: ""
300
297
  val typeNumber = OmiKitUtils().checkTypeNumber(prePhoneNumber)
301
298
 
302
- Log.d("OMISDK", "=>> onRinging CALLED - BEFORE: isIncoming: $isIncoming, isAnswerCall: $isAnswerCall, callDirection: $callDirection")
303
-
304
299
  if (callDirection == "inbound") {
305
300
  isIncoming = true;
306
- Log.d("OMISDK", "=>> onRinging SET isIncoming = true for inbound call")
307
301
  } else if (callDirection == "outbound") {
308
302
  isIncoming = false;
309
- Log.d("OMISDK", "=>> onRinging SET isIncoming = false for outbound call")
310
303
  }
311
304
 
312
- Log.d("OMISDK", "=>> onRinging AFTER: isIncoming: $isIncoming, isAnswerCall: $isAnswerCall")
313
-
314
305
  // ✅ Sử dụng safe WritableMap creation
315
306
  val eventData = mapOf(
316
307
  "callerNumber" to if (callDirection == "inbound") prePhoneNumber else "",
@@ -322,8 +313,6 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
322
313
  )
323
314
 
324
315
  val map = createSafeWritableMap(eventData)
325
-
326
- Log.d("OMISDK", if (callDirection == "inbound") "=>> ON INCOMING CALL => " else "=>> ON RINGING CALL => ")
327
316
  sendEvent(CALL_STATE_CHANGED, map)
328
317
  }
329
318
 
@@ -331,13 +320,21 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
331
320
  override fun networkHealth(stat: Map<String, *>, quality: Int) {
332
321
  val map: WritableMap = WritableNativeMap()
333
322
  map.putInt("quality", quality)
323
+ // Pass full diagnostics from stat map
324
+ val statMap: WritableMap = WritableNativeMap()
325
+ (stat["mos"] as? Number)?.let { statMap.putDouble("mos", it.toDouble()) }
326
+ (stat["jitter"] as? Number)?.let { statMap.putDouble("jitter", it.toDouble()) }
327
+ (stat["latency"] as? Number)?.let { statMap.putDouble("latency", it.toDouble()) }
328
+ (stat["ppl"] as? Number)?.let { statMap.putDouble("packetLoss", it.toDouble()) }
329
+ (stat["lcn"] as? Number)?.let { statMap.putInt("lcn", it.toInt()) }
330
+ map.putMap("stat", statMap)
334
331
  sendEvent(CALL_QUALITY, map)
335
332
  }
336
333
 
337
334
  override fun onAudioChanged(audioInfo: Map<String, Any>) {
338
335
  val audio: WritableMap = WritableNativeMap()
339
- audio.putString("name", audioInfo["name"] as String)
340
- audio.putInt("type", audioInfo["type"] as Int)
336
+ audio.putString("name", audioInfo["name"] as? String ?: "")
337
+ audio.putInt("type", audioInfo["type"] as? Int ?: 0)
341
338
  val map: WritableMap = WritableNativeMap()
342
339
  val writeList = WritableNativeArray()
343
340
  writeList.pushMap(audio)
@@ -355,13 +352,9 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
355
352
  }
356
353
 
357
354
  override fun onOutgoingStarted(callerId: Int, phoneNumber: String?, isVideo: Boolean?) {
358
- Log.d("OMISDK", "=>> onOutgoingStarted CALLED - BEFORE: isIncoming: $isIncoming, isAnswerCall: $isAnswerCall")
359
-
360
355
  // For outgoing calls, set states appropriately
361
356
  isIncoming = false;
362
357
  isAnswerCall = false;
363
- Log.d("OMISDK", "=>> onOutgoingStarted AFTER SET - isIncoming: $isIncoming, isAnswerCall: $isAnswerCall")
364
-
365
358
  val typeNumber = OmiKitUtils().checkTypeNumber(phoneNumber ?: "")
366
359
 
367
360
  val map: WritableMap = WritableNativeMap().apply {
@@ -384,8 +377,6 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
384
377
  }
385
378
 
386
379
  override fun onRegisterCompleted(statusCode: Int) {
387
- Log.d("OMISDK", "=> ON REGISTER COMPLETED => status code: $statusCode")
388
-
389
380
  if (statusCode != 200) {
390
381
  val normalizedStatusCode = if (statusCode == 403) 853 else statusCode
391
382
  val typeNumber = ""
@@ -413,7 +404,6 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
413
404
  }
414
405
  })
415
406
  }
416
- Log.d("OMISDK", "=>> onRequestPermission => $map")
417
407
  sendEvent(REQUEST_PERMISSION, map)
418
408
 
419
409
  }
@@ -456,16 +446,14 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
456
446
 
457
447
  override fun initialize() {
458
448
  super.initialize()
459
-
460
-
449
+
450
+ moduleInstance = this
461
451
  reactApplicationContext!!.addActivityEventListener(this)
462
452
  Handler(Looper.getMainLooper()).post {
463
- OmiClient.getInstance(reactApplicationContext!!).addCallStateListener(this)
464
-
465
- // ✅ Add listener cho AUTO-UNREGISTER status
466
- OmiClient.getInstance(reactApplicationContext!!).addCallStateListener(autoUnregisterListener)
467
-
468
- OmiClient.getInstance(reactApplicationContext!!).setDebug(false)
453
+ val client = OmiClient.getInstance(reactApplicationContext!!)
454
+ client.addCallStateListener(this)
455
+ client.addCallStateListener(autoUnregisterListener)
456
+ client.setDebug(false)
469
457
  }
470
458
  }
471
459
 
@@ -482,7 +470,6 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
482
470
  OmiClient.getInstance(reactApplicationContext!!).setDebug(false)
483
471
  promise.resolve(true)
484
472
  } catch (e: Exception) {
485
- Log.e("OmikitPlugin", "❌ Error in startServices: ${e.message}", e)
486
473
  promise.resolve(false)
487
474
  }
488
475
  }
@@ -493,7 +480,7 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
493
480
  // ✅ Method để check status AUTO-UNREGISTER (DEPRECATED)
494
481
  @ReactMethod
495
482
  fun getAutoUnregisterStatus(promise: Promise) {
496
- Log.w("OmikitPlugin", "⚠️ DEPRECATED: getAutoUnregisterStatus() - Use Silent Registration API instead")
483
+
497
484
  try {
498
485
  OmiClient.getInstance(reactApplicationContext!!).getAutoUnregisterStatus { isScheduled, timeUntilExecution ->
499
486
  try {
@@ -504,12 +491,10 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
504
491
  )
505
492
  promise.resolve(Arguments.makeNativeMap(status))
506
493
  } catch (e: Exception) {
507
- Log.e("OmikitPlugin", "❌ Error in getAutoUnregisterStatus callback: ${e.message}", e)
508
494
  promise.resolve(null)
509
495
  }
510
496
  }
511
497
  } catch (e: Exception) {
512
- Log.e("OmikitPlugin", "❌ Error calling getAutoUnregisterStatus: ${e.message}", e)
513
498
  promise.resolve(null)
514
499
  }
515
500
  }
@@ -517,7 +502,7 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
517
502
  // ✅ Method để manually prevent AUTO-UNREGISTER (DEPRECATED)
518
503
  @ReactMethod
519
504
  fun preventAutoUnregister(reason: String, promise: Promise) {
520
- Log.w("OmikitPlugin", "⚠️ DEPRECATED: preventAutoUnregister() - No longer supported in new SDK version")
505
+
521
506
  // Function removed - no longer supported
522
507
  promise.resolve(false)
523
508
  }
@@ -525,47 +510,37 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
525
510
  // ✅ Convenience methods cho các scenario phổ biến (DEPRECATED)
526
511
  @ReactMethod
527
512
  fun prepareForIncomingCall(promise: Promise) {
528
- Log.w("OmikitPlugin", "⚠️ DEPRECATED: prepareForIncomingCall() - Use Silent Registration API instead")
529
513
  try {
530
514
  OmiClient.getInstance(reactApplicationContext!!).prepareForIncomingCall()
531
515
  promise.resolve(true)
532
516
  } catch (e: Exception) {
533
- Log.e("OmikitPlugin", "❌ Prepare for incoming call failed: ${e.message}", e)
534
517
  promise.resolve(false)
535
518
  }
536
519
  }
537
520
 
538
521
  @ReactMethod
539
522
  fun prepareForOutgoingCall(promise: Promise) {
540
- Log.w("OmikitPlugin", "⚠️ DEPRECATED: prepareForOutgoingCall() - Use Silent Registration API instead")
541
523
  try {
542
524
  OmiClient.getInstance(reactApplicationContext!!).prepareForOutgoingCall()
543
525
  promise.resolve(true)
544
526
  } catch (e: Exception) {
545
- Log.e("OmikitPlugin", "❌ Prepare for outgoing call failed: ${e.message}", e)
546
527
  promise.resolve(false)
547
528
  }
548
529
  }
549
530
 
550
531
  private fun prepareAudioSystem() {
551
532
  try {
552
- // ✅ Check network connectivity first
553
533
  if (!isNetworkAvailable()) {
554
534
  return
555
535
  }
556
-
536
+
557
537
  // Release any existing audio focus
558
538
  val audioManager = reactApplicationContext?.getSystemService(android.content.Context.AUDIO_SERVICE) as? android.media.AudioManager
559
539
  audioManager?.let {
560
- // Reset audio mode
561
540
  it.mode = android.media.AudioManager.MODE_NORMAL
562
541
  }
563
-
564
- // Small delay để audio system ổn định
565
- Thread.sleep(200)
566
-
567
542
  } catch (e: Exception) {
568
- Log.w("OmikitPlugin", "⚠️ Audio preparation warning: ${e.message}")
543
+ Log.w("OmikitPlugin", "Audio preparation warning: ${e.message}")
569
544
  }
570
545
  }
571
546
 
@@ -573,11 +548,17 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
573
548
  private fun isNetworkAvailable(): Boolean {
574
549
  return try {
575
550
  val connectivityManager = reactApplicationContext?.getSystemService(android.content.Context.CONNECTIVITY_SERVICE) as? android.net.ConnectivityManager
576
- val activeNetwork = connectivityManager?.activeNetworkInfo
577
- val isConnected = activeNetwork?.isConnectedOrConnecting == true
578
- isConnected
551
+ ?: return true
552
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
553
+ val network = connectivityManager.activeNetwork ?: return false
554
+ val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
555
+ capabilities.hasCapability(android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET)
556
+ } else {
557
+ @Suppress("DEPRECATION")
558
+ connectivityManager.activeNetworkInfo?.isConnectedOrConnecting == true
559
+ }
579
560
  } catch (e: Exception) {
580
- Log.w("OmikitPlugin", "⚠️ Network check failed: ${e.message}")
561
+ Log.w("OmikitPlugin", "Network check failed: ${e.message}")
581
562
  true // Assume network is available if check fails
582
563
  }
583
564
  }
@@ -681,15 +662,11 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
681
662
 
682
663
  // Configure decline call behavior
683
664
  OmiClient.getInstance(context).configureDeclineCallBehavior(isUserBusy)
684
-
685
- Log.d("OmikitPlugin", "✅ Push notification configured successfully")
686
665
  promise.resolve(true)
687
666
  } catch (e: Exception) {
688
- Log.e("OmikitPlugin", "❌ Error configuring push notification: ${e.message}", e)
689
667
  promise.reject("E_CONFIG_FAILED", "Failed to configure push notification", e)
690
668
  }
691
669
  } ?: run {
692
- Log.e("OmikitPlugin", "❌ Current activity is null")
693
670
  promise.reject("E_NULL_ACTIVITY", "Current activity is null")
694
671
  }
695
672
  } catch (e: Exception) {
@@ -704,32 +681,34 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
704
681
  val userName = data.getString("userName")
705
682
  val password = data.getString("password")
706
683
  val realm = data.getString("realm")
707
- val host = data.getString("host") ?: "vh.omicrm.com"
684
+ val host = data.getString("host").let { if (it.isNullOrEmpty()) "vh.omicrm.com" else it }
708
685
  val isVideo = data.getBoolean("isVideo")
709
686
  val firebaseToken = data.getString("fcmToken")
710
687
  val projectId = data.getString("projectId") ?: ""
688
+ val isSkipDevices = if (data.hasKey("isSkipDevices")) data.getBoolean("isSkipDevices") else false
711
689
 
712
690
  // Validate required parameters
713
691
  if (!ValidationHelper.validateRequired(mapOf(
714
692
  "userName" to userName,
715
- "password" to password,
693
+ "password" to password,
716
694
  "realm" to realm,
717
695
  "fcmToken" to firebaseToken
718
696
  ), promise)) return@launch
719
697
 
720
698
  withContext(Dispatchers.Default) {
721
699
  try {
722
- // Cleanup trước khi register
700
+ // Cleanup before register using logout callback
723
701
  try {
724
- OmiClient.getInstance(reactApplicationContext!!).logout()
725
- delay(500) // Chờ cleanup hoàn tất
702
+ val logoutComplete = kotlinx.coroutines.CompletableDeferred<Unit>()
703
+ OmiClient.getInstance(reactApplicationContext!!).logout {
704
+ logoutComplete.complete(Unit)
705
+ }
706
+ // Wait for logout callback with timeout
707
+ kotlinx.coroutines.withTimeoutOrNull(3000) { logoutComplete.await() }
726
708
  } catch (e: Exception) {
727
- Log.w("OmikitPlugin", "⚠️ Cleanup warning (expected): ${e.message}")
709
+ Log.w("OmikitPlugin", "Cleanup warning (expected): ${e.message}")
728
710
  }
729
-
730
- // ✅ Sử dụng Silent Registration API mới từ OmiSDK 2.3.67
731
- Log.d("OmikitPlugin", "🔇 Using Silent Registration API for user: $userName")
732
-
711
+
733
712
  OmiClient.getInstance(reactApplicationContext!!).silentRegister(
734
713
  userName = userName ?: "",
735
714
  password = password ?: "",
@@ -737,26 +716,18 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
737
716
  isVideo = isVideo ?: true,
738
717
  firebaseToken = firebaseToken ?: "",
739
718
  host = host,
740
- projectId = projectId
719
+ projectId = projectId,
720
+ isSkipDevices = isSkipDevices
741
721
  ) { success, statusCode, message ->
742
- Log.d("OmikitPlugin", "🔇 Silent registration callback - success: $success, status: $statusCode, message: $message")
743
- if (success) {
744
- Log.d("OmikitPlugin", "✅ Silent registration successful - no notification, no auto-unregister")
745
- // ✅ Resolve promise với kết quả từ callback
746
- promise.resolve(success)
722
+ if (success || statusCode == 200) {
723
+ promise.resolve(true)
747
724
  } else {
748
- Log.e("OmikitPlugin", "❌ Silent registration failed: $message")
749
- if (statusCode == 200) {
750
- promise.resolve(true)
751
- } else {
752
- val (errorCode, errorMessage) = OmiRegistrationStatus.getError(statusCode)
753
- promise.reject(errorCode, "$errorMessage (Status: $statusCode)")
754
- }
725
+ val (errorCode, errorMessage) = OmiRegistrationStatus.getError(statusCode)
726
+ promise.reject(errorCode, "$errorMessage (Status: $statusCode)")
755
727
  }
756
728
  }
757
729
 
758
730
  } catch (e: Exception) {
759
- Log.e("OmikitPlugin", "❌ Error during silent registration: ${e.message}", e)
760
731
  promise.reject("ERROR_INITIALIZATION_EXCEPTION", "OMICALL initialization failed due to an unexpected error: ${e.message}. Please check your network connection and configuration.", e)
761
732
  }
762
733
  }
@@ -765,7 +736,6 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
765
736
 
766
737
  @ReactMethod
767
738
  fun initCallWithApiKey(data: ReadableMap, promise: Promise) {
768
- Log.d("OmikitPlugin", "🔑 initCallWithApiKey called")
769
739
  mainScope.launch {
770
740
  var loginResult = false
771
741
  val usrName = data.getString("fullName") ?: ""
@@ -776,11 +746,8 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
776
746
  val firebaseToken = data.getString("fcmToken") ?: ""
777
747
  val projectId = data.getString("projectId") ?: ""
778
748
 
779
- Log.d("OmikitPlugin", "🔑 Parameters - usrName: $usrName, usrUuid: $usrUuid, isVideo: $isVideo")
780
-
781
749
  withContext(Dispatchers.Default) {
782
750
  try {
783
- Log.d("OmikitPlugin", "🔑 Starting validation")
784
751
  // Validate required parameters
785
752
  if (!ValidationHelper.validateRequired(mapOf(
786
753
  "fullName" to usrName,
@@ -792,8 +759,6 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
792
759
  return@withContext
793
760
  }
794
761
 
795
- Log.d("OmikitPlugin", "✅ Validation passed")
796
-
797
762
  // Check RECORD_AUDIO permission for Android 14+
798
763
  val hasRecordAudio = ContextCompat.checkSelfPermission(
799
764
  reactApplicationContext,
@@ -801,28 +766,22 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
801
766
  ) == PackageManager.PERMISSION_GRANTED
802
767
 
803
768
  if (!hasRecordAudio) {
804
- Log.e("OmikitPlugin", "❌ RECORD_AUDIO permission is required for Android 14+")
805
769
  promise.resolve(false)
806
770
  return@withContext
807
771
  }
808
-
809
- Log.d("OmikitPlugin", "✅ RECORD_AUDIO permission granted")
810
-
811
- // ✅ Cleanup trước khi register với mutex
772
+ // Cleanup before register using logout callback
812
773
  try {
813
- Log.d("OmikitPlugin", "🧹 Starting cleanup")
814
774
  omiClientMutex.withLock {
815
- OmiClient.getInstance(reactApplicationContext!!).logout()
775
+ val logoutComplete = kotlinx.coroutines.CompletableDeferred<Unit>()
776
+ OmiClient.getInstance(reactApplicationContext!!).logout {
777
+ logoutComplete.complete(Unit)
778
+ }
779
+ kotlinx.coroutines.withTimeoutOrNull(3000) { logoutComplete.await() }
816
780
  }
817
- delay(1000) // Chờ cleanup hoàn tất
818
- Log.d("OmikitPlugin", "✅ Cleanup completed")
819
781
  } catch (e: Exception) {
820
- Log.w("OmikitPlugin", "⚠️ Cleanup warning (expected): ${e.message}")
782
+ Log.w("OmikitPlugin", "Cleanup warning (expected): ${e.message}")
821
783
  }
822
784
 
823
- Log.d("OmikitPlugin", "🔑 Using API key registration for user: $usrName")
824
-
825
- Log.d("OmikitPlugin", "🔑 Calling OmiClient.registerWithApiKey...")
826
785
  omiClientMutex.withLock {
827
786
  loginResult = OmiClient.registerWithApiKey(
828
787
  apiKey ?: "",
@@ -834,16 +793,7 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
834
793
  projectId
835
794
  )
836
795
  }
837
-
838
- Log.d("OmikitPlugin", "🔑 OmiClient.registerWithApiKey returned: $loginResult")
839
-
840
- if (loginResult) {
841
- Log.d("OmikitPlugin", "✅ API key registration successful")
842
- promise.resolve(true)
843
- } else {
844
- Log.e("OmikitPlugin", "❌ API key registration failed")
845
- promise.resolve(false)
846
- }
796
+ promise.resolve(loginResult)
847
797
  } catch (e: Exception) {
848
798
  Log.e("OmikitPlugin", "❌ Error during API key registration: ${e.message}", e)
849
799
  promise.resolve(false)
@@ -855,21 +805,17 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
855
805
  @ReactMethod
856
806
  fun getInitialCall(counter: Int = 1, promise: Promise) {
857
807
  val context = reactApplicationContext ?: run {
858
- Log.e("getInitialCall", "❌ React context is null")
859
808
  promise.resolve(false)
860
809
  return
861
810
  }
862
811
 
863
812
  val call = Utils.getActiveCall(context)
864
- Log.d("getInitialCall RN", "📞 Active call: $call")
865
-
866
813
  if (call == null) {
867
814
  if (counter <= 0) {
868
815
  promise.resolve(false)
869
816
  } else {
870
817
  mainScope.launch {
871
- Log.d("getInitialCall RN", "🔄 Retrying in 2s... (Attempts left: $counter)")
872
- delay(1000) // Wait 2 seconds
818
+ delay(1000) // Wait 1 second before retry
873
819
  getInitialCall(counter - 1, promise) // Retry recursively
874
820
  }
875
821
  }
@@ -893,11 +839,8 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
893
839
  statusPendingCall == 5 // 5 = User clicked pickup (CONFIRMED)
894
840
 
895
841
  if (shouldAutoAnswer) {
896
- Log.d("getInitialCall RN", "🚀 AUTO-ANSWER: User clicked pickup (statusPendingCall=$statusPendingCall), answering call immediately")
897
842
  try {
898
843
  OmiClient.getInstance(context).pickUp()
899
- Log.d("getInitialCall RN", "✅ AUTO-ANSWER: Call answered successfully")
900
-
901
844
  // Status already cleared by getStatusPendingCall()
902
845
  } catch (e: Exception) {
903
846
  Log.e("getInitialCall RN", "❌ AUTO-ANSWER: Failed to answer call: ${e.message}", e)
@@ -922,8 +865,6 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
922
865
  promise.resolve(map)
923
866
 
924
867
  if (statusPendingCall == 2 && call.state != 5) {
925
- Log.d("getInitialCall RN", "🚀 Incoming Receive Triggered ($statusPendingCall)")
926
-
927
868
  val eventMap: WritableMap = WritableNativeMap().apply {
928
869
  putBoolean("isVideo", call.isVideo ?: false)
929
870
  putBoolean("incoming", true)
@@ -946,25 +887,34 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
946
887
  )
947
888
  val map: WritableMap = WritableNativeMap()
948
889
  if (audio == PackageManager.PERMISSION_GRANTED) {
949
- currentActivity?.runOnUiThread {
950
- val phoneNumber = data.getString("phoneNumber") as String
951
- val isVideo = data.getBoolean("isVideo") ?: false;
952
-
890
+ val activity = currentActivity
891
+ if (activity == null) {
892
+ promise.reject("E_NO_ACTIVITY", "Current activity is null")
893
+ return
894
+ }
895
+ activity.runOnUiThread {
896
+ val phoneNumber = data.getString("phoneNumber")
897
+ if (phoneNumber.isNullOrEmpty()) {
898
+ promise.reject("E_INVALID_PHONE", "Phone number is required")
899
+ return@runOnUiThread
900
+ }
901
+ val isVideo = data.getBoolean("isVideo")
902
+
953
903
  val startCallResult =
954
904
  OmiClient.getInstance(reactApplicationContext!!).startCall(phoneNumber, isVideo)
955
- var statusCalltemp = startCallResult.value as Int;
905
+ var statusCalltemp = startCallResult.value as Int
956
906
  if (startCallResult.value == 200 || startCallResult.value == 407) {
957
907
  statusCalltemp = 8
958
908
  }
959
909
  map.putInt("status", statusCalltemp)
960
910
  map.putString("_id", "")
961
- map.putString("message", messageCall(startCallResult.value) as String)
911
+ map.putString("message", messageCall(startCallResult.value))
962
912
  promise.resolve(map)
963
913
  }
964
914
  } else {
965
915
  map.putInt("status", 4)
966
916
  map.putString("_id", "")
967
- map.putString("message", messageCall(406) as String)
917
+ map.putString("message", messageCall(406))
968
918
  promise.resolve(map)
969
919
  }
970
920
  }
@@ -976,28 +926,27 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
976
926
  reactApplicationContext!!,
977
927
  Manifest.permission.RECORD_AUDIO
978
928
  )
979
- Log.d("OMISDK", "📤 Start Call With UUID")
980
929
  val map: WritableMap = WritableNativeMap()
981
930
  if (audio == PackageManager.PERMISSION_GRANTED) {
982
931
  mainScope.launch {
983
932
  val uuid = data.getString("usrUuid") ?: ""
984
- val isVideo = data.getBoolean("isVideo") ?: false;
985
-
933
+ val isVideo = data.getBoolean("isVideo")
934
+
986
935
  val startCallResult =
987
936
  OmiClient.getInstance(reactApplicationContext!!).startCallWithUuid(uuid, isVideo)
988
- var statusCalltemp = startCallResult.value as Int;
937
+ var statusCalltemp = startCallResult.value as Int
989
938
  if (startCallResult.value == 200 || startCallResult.value == 407) {
990
939
  statusCalltemp = 8
991
940
  }
992
941
  map.putInt("status", statusCalltemp)
993
942
  map.putString("_id", "")
994
- map.putString("message", messageCall(startCallResult.value) as String)
943
+ map.putString("message", messageCall(startCallResult.value))
995
944
  promise.resolve(map)
996
945
  }
997
946
  } else {
998
947
  map.putInt("status", 4)
999
948
  map.putString("_id", "")
1000
- map.putString("message", messageCall(406) as String)
949
+ map.putString("message", messageCall(406))
1001
950
  promise.resolve(map)
1002
951
  }
1003
952
  }
@@ -1008,7 +957,7 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1008
957
  val appContext = reactApplicationContext.applicationContext
1009
958
  val activity = currentActivity
1010
959
 
1011
- if (appContext == null) {
960
+ if (appContext == null) {
1012
961
  promise.reject("E_NULL_CONTEXT", "Application context is null")
1013
962
  return
1014
963
  }
@@ -1044,15 +993,11 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1044
993
 
1045
994
  @ReactMethod
1046
995
  fun rejectCall(promise: Promise) {
1047
- Log.d("OMISDK", "➡️ rejectCall called - isIncoming: $isIncoming, isAnswerCall: $isAnswerCall")
1048
996
  if (isIncoming) {
1049
- Log.d("OMISDK", "📞 Incoming call")
1050
997
  ValidationHelper.safeOmiClientAccess(reactApplicationContext!!) { omiClient ->
1051
998
  if (!isAnswerCall) {
1052
- Log.d("OMISDK", "🚫 Declining call with declineWithCode(true)")
1053
999
  omiClient.declineWithCode(true) // 486 Busy Here
1054
1000
  } else {
1055
- Log.d("OMISDK", "📴 Call already answered, hanging up")
1056
1001
  omiClient.hangUp()
1057
1002
  }
1058
1003
  }
@@ -1060,7 +1005,6 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1060
1005
  markCallEnded()
1061
1006
  promise.resolve(true)
1062
1007
  } else {
1063
- Log.d("OMISDK", "📤 Not incoming call, skipping reject")
1064
1008
  promise.resolve(false)
1065
1009
  }
1066
1010
  }
@@ -1120,7 +1064,9 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1120
1064
 
1121
1065
  @ReactMethod
1122
1066
  fun toggleSpeaker(promise: Promise) {
1123
- currentActivity?.runOnUiThread {
1067
+ val activity = currentActivity
1068
+ if (activity == null) { promise.resolve(null); return }
1069
+ activity.runOnUiThread {
1124
1070
  val newStatus = OmiClient.getInstance(reactApplicationContext!!).toggleSpeaker()
1125
1071
  promise.resolve(newStatus)
1126
1072
  sendEvent(SPEAKER, newStatus)
@@ -1129,7 +1075,9 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1129
1075
 
1130
1076
  @ReactMethod
1131
1077
  fun sendDTMF(data: ReadableMap, promise: Promise) {
1132
- currentActivity?.runOnUiThread {
1078
+ val activity = currentActivity
1079
+ if (activity == null) { promise.resolve(false); return }
1080
+ activity.runOnUiThread {
1133
1081
  val character = data.getString("character")
1134
1082
  var characterCode: Int? = character?.toIntOrNull()
1135
1083
  if (character == "*") {
@@ -1147,7 +1095,9 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1147
1095
 
1148
1096
  @ReactMethod
1149
1097
  fun switchOmiCamera(promise: Promise) {
1150
- currentActivity?.runOnUiThread {
1098
+ val activity = currentActivity
1099
+ if (activity == null) { promise.resolve(false); return }
1100
+ activity.runOnUiThread {
1151
1101
  OmiClient.getInstance(reactApplicationContext!!).switchCamera()
1152
1102
  promise.resolve(true)
1153
1103
  }
@@ -1155,7 +1105,9 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1155
1105
 
1156
1106
  @ReactMethod
1157
1107
  fun toggleOmiVideo(promise: Promise) {
1158
- currentActivity?.runOnUiThread {
1108
+ val activity = currentActivity
1109
+ if (activity == null) { promise.resolve(false); return }
1110
+ activity.runOnUiThread {
1159
1111
  OmiClient.getInstance(reactApplicationContext!!).toggleCamera()
1160
1112
  promise.resolve(true)
1161
1113
  }
@@ -1261,14 +1213,111 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1261
1213
  }
1262
1214
  }
1263
1215
 
1216
+ // MARK: - Getter Functions
1217
+ @ReactMethod
1218
+ fun getProjectId(promise: Promise) {
1219
+ try {
1220
+ val info = OmiClient.registrationInfo
1221
+ if (info?.projectId != null) {
1222
+ promise.resolve(info.projectId)
1223
+ return
1224
+ }
1225
+ // Fallback: get from Firebase project ID
1226
+ val firebaseProjectId = try {
1227
+ com.google.firebase.FirebaseApp.getInstance().options.projectId
1228
+ } catch (e: Exception) { null }
1229
+ promise.resolve(firebaseProjectId)
1230
+ } catch (e: Throwable) {
1231
+ promise.resolve(null)
1232
+ }
1233
+ }
1234
+
1235
+ @ReactMethod
1236
+ fun getAppId(promise: Promise) {
1237
+ try {
1238
+ val info = OmiClient.registrationInfo
1239
+ if (info?.appId != null) {
1240
+ promise.resolve(info.appId)
1241
+ return
1242
+ }
1243
+ // Fallback: get package name of the host app
1244
+ promise.resolve(reactApplicationContext?.packageName)
1245
+ } catch (e: Throwable) {
1246
+ promise.resolve(null)
1247
+ }
1248
+ }
1249
+
1250
+ @ReactMethod
1251
+ fun getDeviceId(promise: Promise) {
1252
+ try {
1253
+ val info = OmiClient.registrationInfo
1254
+ if (info?.deviceId != null) {
1255
+ promise.resolve(info.deviceId)
1256
+ return
1257
+ }
1258
+ // Fallback: get Android ID directly
1259
+ val androidId = try {
1260
+ Settings.Secure.getString(
1261
+ reactApplicationContext?.contentResolver,
1262
+ Settings.Secure.ANDROID_ID
1263
+ )
1264
+ } catch (e: Exception) { null }
1265
+ promise.resolve(androidId)
1266
+ } catch (e: Throwable) {
1267
+ promise.resolve(null)
1268
+ }
1269
+ }
1270
+
1271
+ @ReactMethod
1272
+ fun getFcmToken(promise: Promise) {
1273
+ try {
1274
+ val info = OmiClient.registrationInfo
1275
+ if (info?.firebaseToken != null) {
1276
+ promise.resolve(info.firebaseToken)
1277
+ return
1278
+ }
1279
+ // Fallback: get FCM token directly from Firebase Messaging
1280
+ try {
1281
+ com.google.firebase.messaging.FirebaseMessaging.getInstance().token
1282
+ .addOnSuccessListener { token -> promise.resolve(token) }
1283
+ .addOnFailureListener { promise.resolve(null) }
1284
+ } catch (e: Exception) {
1285
+ promise.resolve(null)
1286
+ }
1287
+ } catch (e: Throwable) {
1288
+ promise.resolve(null)
1289
+ }
1290
+ }
1291
+
1292
+ @ReactMethod
1293
+ fun getSipInfo(promise: Promise) {
1294
+ try {
1295
+ val sipUser = OmiClient.getInstance(reactApplicationContext!!).getSipUser()
1296
+ val sipRealm = OmiClient.getInstance(reactApplicationContext!!).getSipRealm()
1297
+ if (!sipUser.isNullOrEmpty() && !sipRealm.isNullOrEmpty()) {
1298
+ promise.resolve("$sipUser@$sipRealm")
1299
+ } else {
1300
+ promise.resolve(sipUser)
1301
+ }
1302
+ } catch (e: Throwable) {
1303
+ promise.resolve(null)
1304
+ }
1305
+ }
1306
+
1307
+ @ReactMethod
1308
+ fun getVoipToken(promise: Promise) {
1309
+ // VoIP token is iOS only, Android returns null
1310
+ promise.resolve(null)
1311
+ }
1312
+
1264
1313
  @ReactMethod
1265
1314
  fun getAudio(promise: Promise) {
1266
1315
  val inputs = OmiClient.getInstance(reactApplicationContext!!).getAudioOutputs()
1267
1316
  val writeList = WritableNativeArray()
1268
1317
  inputs.forEach {
1269
1318
  val map = WritableNativeMap()
1270
- map.putString("name", it["name"] as String)
1271
- map.putInt("type", it["type"] as Int)
1319
+ map.putString("name", it["name"] as? String ?: "")
1320
+ map.putInt("type", it["type"] as? Int ?: 0)
1272
1321
  writeList.pushMap(map)
1273
1322
  }
1274
1323
  promise.resolve(writeList)
@@ -1278,8 +1327,8 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1278
1327
  fun getCurrentAudio(promise: Promise) {
1279
1328
  val currentAudio = OmiClient.getInstance(reactApplicationContext!!).getCurrentAudio()
1280
1329
  val map: WritableMap = WritableNativeMap()
1281
- map.putString("name", currentAudio["name"] as String)
1282
- map.putInt("type", currentAudio["type"] as Int)
1330
+ map.putString("name", currentAudio["name"] as? String ?: "")
1331
+ map.putInt("type", currentAudio["type"] as? Int ?: 0)
1283
1332
  val writeList = WritableNativeArray()
1284
1333
  writeList.pushMap(map)
1285
1334
  promise.resolve(writeList)
@@ -1294,14 +1343,16 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1294
1343
 
1295
1344
  @ReactMethod
1296
1345
  fun transferCall(data: ReadableMap, promise: Promise) {
1297
- currentActivity?.runOnUiThread {
1346
+ val activity = currentActivity
1347
+ if (activity == null) { promise.resolve(false); return }
1348
+ activity.runOnUiThread {
1298
1349
  val phone = data.getString("phoneNumber")
1299
- Log.d("phone", "phone transferCall ==>> ${phone} ")
1300
- if (reactApplicationContext != null) {
1301
- Log.d("phone", "phone transferCall reactApplicationContext ==>> ${phone} ")
1302
- OmiClient.getInstance(reactApplicationContext!!).forwardCallTo(phone as String)
1303
- promise.resolve(true)
1350
+ if (phone.isNullOrEmpty()) {
1351
+ promise.reject("E_INVALID_PHONE", "Phone number is required for transfer")
1352
+ return@runOnUiThread
1304
1353
  }
1354
+ OmiClient.getInstance(reactApplicationContext!!).forwardCallTo(phone)
1355
+ promise.resolve(true)
1305
1356
  }
1306
1357
  }
1307
1358
 
@@ -1310,6 +1361,9 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1310
1361
  const val REQUEST_PERMISSIONS_CODE = 1001
1311
1362
  const val REQUEST_OVERLAY_PERMISSION_CODE = 1002
1312
1363
 
1364
+ // Singleton reference for companion methods to access module instance
1365
+ @Volatile var moduleInstance: OmikitPluginModule? = null
1366
+
1313
1367
  fun onDestroy() {
1314
1368
  try {
1315
1369
  // Cleanup OmiClient resources safely
@@ -1320,7 +1374,6 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1320
1374
 
1321
1375
  fun onResume(act: ReactActivity) {
1322
1376
  act.let { context ->
1323
- Log.d("OMISDK_REACT", "=>> onResume => ")
1324
1377
  OmiClient.getInstance(context, true)
1325
1378
  OmiClient.isAppReady = true;
1326
1379
  }
@@ -1349,37 +1402,28 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1349
1402
  act: ReactActivity
1350
1403
  ) {
1351
1404
  try {
1352
- val deniedPermissions = mutableListOf<String>()
1353
1405
  val grantedPermissions = mutableListOf<String>()
1354
-
1406
+
1355
1407
  for (i in permissions.indices) {
1356
1408
  if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
1357
1409
  grantedPermissions.add(permissions[i])
1358
- } else {
1359
- deniedPermissions.add(permissions[i])
1360
1410
  }
1361
1411
  }
1362
-
1363
- Log.d("OmikitPlugin", "✅ Granted: ${grantedPermissions.joinToString()}")
1364
- if (deniedPermissions.isNotEmpty()) {
1365
- Log.w("OmikitPlugin", "❌ Denied: ${deniedPermissions.joinToString()}")
1366
- }
1367
-
1412
+
1368
1413
  // Check if we have essential permissions for VoIP
1369
1414
  val hasRecordAudio = grantedPermissions.contains(Manifest.permission.RECORD_AUDIO)
1370
1415
  val hasCallPhone = grantedPermissions.contains(Manifest.permission.CALL_PHONE)
1371
1416
  val hasModifyAudio = grantedPermissions.contains(Manifest.permission.MODIFY_AUDIO_SETTINGS)
1372
-
1417
+
1373
1418
  val canProceed = hasRecordAudio && hasCallPhone && hasModifyAudio
1374
-
1375
- if (canProceed) {
1376
- Log.d("OmikitPlugin", "🎉 Essential VoIP permissions granted!")
1377
- } else {
1378
- Log.e("OmikitPlugin", "⚠️ Missing essential VoIP permissions - app may not work properly")
1379
- }
1380
-
1419
+
1420
+ // Resolve the stored permission promise
1421
+ moduleInstance?.permissionPromise?.resolve(canProceed)
1422
+ moduleInstance?.permissionPromise = null
1381
1423
  } catch (e: Exception) {
1382
- Log.e("OmikitPlugin", "Error handling permission results: ${e.message}", e)
1424
+ Log.e("OmikitPlugin", "Error handling permission results: ${e.message}", e)
1425
+ moduleInstance?.permissionPromise?.resolve(false)
1426
+ moduleInstance?.permissionPromise = null
1383
1427
  }
1384
1428
  }
1385
1429
 
@@ -1392,7 +1436,6 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1392
1436
  try {
1393
1437
  val isIncoming = intent.getBooleanExtra(SipServiceConstants.ACTION_IS_INCOMING_CALL, false)
1394
1438
  if (!isIncoming) {
1395
- Log.d("PICKUP-FIX", "Not an incoming call intent, skipping")
1396
1439
  return
1397
1440
  }
1398
1441
 
@@ -1400,20 +1443,17 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1400
1443
  SipServiceConstants.ACTION_ACCEPT_INCOMING_CALL, false
1401
1444
  )
1402
1445
 
1403
- Log.d("PICKUP-FIX", "🚀 Early intent handler - isIncoming: $isIncoming, isAccepted: $isAcceptedCall")
1404
1446
 
1405
1447
  // Save to SharedPreferences so getInitialCall() can detect it later
1406
1448
  // setStatusPendingCall(true) → saves status=5 (CONFIRMED)
1407
1449
  // setStatusPendingCall(false) → saves status=2 (INCOMING)
1408
1450
  OmiKitUtils().setStatusPendingCall(act, isAcceptedCall)
1409
- Log.d("PICKUP-FIX", "✅ Saved pickup state to SharedPreferences (isAccepted=$isAcceptedCall)")
1410
1451
 
1411
1452
  if (isAcceptedCall) {
1412
1453
  // Try to answer immediately if possible (may fail if SDK not ready)
1413
1454
  try {
1414
1455
  OmiClient.getInstance(act, true)?.let { client ->
1415
1456
  client.pickUp()
1416
- Log.d("PICKUP-FIX", "✅ Successfully answered call immediately")
1417
1457
  } ?: Log.w("PICKUP-FIX", "⚠️ OmiClient not ready, will auto-answer in getInitialCall()")
1418
1458
  } catch (e: Exception) {
1419
1459
  Log.w("PICKUP-FIX", "⚠️ Cannot answer immediately (SDK not ready): ${e.message}. Will auto-answer in getInitialCall()")
@@ -1440,7 +1480,8 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1440
1480
  )
1441
1481
  if (isReopenCall) {
1442
1482
  val activeCall = Utils.getActiveCall(context)
1443
- OmikitPluginModule(context).onCallEstablished(
1483
+ // Use singleton module instance instead of creating a throwaway one
1484
+ moduleInstance?.onCallEstablished(
1444
1485
  activeCall?.id ?: 0,
1445
1486
  activeCall?.remoteNumber,
1446
1487
  activeCall?.isVideo,
@@ -1453,7 +1494,6 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1453
1494
  }
1454
1495
  OmiKitUtils().setStatusPendingCall(context, isAcceptedCall)
1455
1496
  }
1456
-
1457
1497
  }
1458
1498
  }
1459
1499
  }
@@ -1461,19 +1501,16 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1461
1501
  // ✅ Di chuyển sendEvent vào trong class để có thể access reactApplicationContext
1462
1502
  private fun sendEvent(eventName: String?, params: Any?) {
1463
1503
  if (eventName == null) {
1464
- Log.e("OmikitPlugin", "❌ eventName is null or empty. Không thể gửi event.")
1465
1504
  return
1466
1505
  }
1467
1506
 
1468
1507
  try {
1469
1508
  // ✅ Kiểm tra reactApplicationContext
1470
1509
  if (reactApplicationContext == null) {
1471
- Log.e("OmikitPlugin", "❌ reactApplicationContext is null")
1472
1510
  return
1473
1511
  }
1474
1512
 
1475
1513
  if (!reactApplicationContext.hasActiveReactInstance()) {
1476
- Log.w("OmikitPlugin", "⚠️ ReactApplicationContext không có active React instance")
1477
1514
  return
1478
1515
  }
1479
1516
 
@@ -1498,35 +1535,16 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1498
1535
  }
1499
1536
  }
1500
1537
 
1501
- // ✅ Thêm method để React Native biết các event được hỗ trợ
1502
- override fun getConstants(): MutableMap<String, Any> {
1503
- return hashMapOf(
1504
- "CALL_STATE_CHANGED" to CALL_STATE_CHANGED,
1505
- "MUTED" to MUTED,
1506
- "HOLD" to HOLD,
1507
- "SPEAKER" to SPEAKER,
1508
- "CALL_QUALITY" to CALL_QUALITY,
1509
- "AUDIO_CHANGE" to AUDIO_CHANGE,
1510
- "SWITCHBOARD_ANSWER" to SWITCHBOARD_ANSWER,
1511
- "REQUEST_PERMISSION" to REQUEST_PERMISSION,
1512
- "CLICK_MISSED_CALL" to CLICK_MISSED_CALL,
1513
- "AUTO_UNREGISTER_STATUS" to "AUTO_UNREGISTER_STATUS"
1514
- )
1515
- }
1516
-
1517
1538
  @ReactMethod
1518
1539
  fun checkAndRequestPermissions(isVideo: Boolean, promise: Promise) {
1519
1540
  try {
1520
1541
  val missingPermissions = getMissingPermissions(isVideo)
1521
1542
 
1522
1543
  if (missingPermissions.isEmpty()) {
1523
- Log.d("OmikitPlugin", "✅ All permissions already granted")
1524
1544
  promise.resolve(true)
1525
1545
  return
1526
1546
  }
1527
1547
 
1528
- Log.d("OmikitPlugin", "📋 Missing permissions: ${missingPermissions.joinToString()}")
1529
-
1530
1548
  // Store promise for callback
1531
1549
  permissionPromise = promise
1532
1550
 
@@ -1536,7 +1554,6 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1536
1554
  REQUEST_PERMISSIONS_CODE,
1537
1555
  )
1538
1556
  } catch (e: Exception) {
1539
- Log.e("OmikitPlugin", "❌ Error checking permissions: ${e.message}", e)
1540
1557
  promise.resolve(false)
1541
1558
  }
1542
1559
  }
@@ -1704,8 +1721,6 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1704
1721
  // Store promise for callback
1705
1722
  permissionPromise = promise
1706
1723
 
1707
- Log.d("OmikitPlugin", "🔐 Requesting permissions for codes ${permissionCodes.joinToString()}: ${permissionsToRequest.joinToString()}")
1708
-
1709
1724
  ActivityCompat.requestPermissions(
1710
1725
  reactApplicationContext.currentActivity!!,
1711
1726
  permissionsToRequest.toTypedArray(),
@@ -1713,7 +1728,6 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1713
1728
  )
1714
1729
 
1715
1730
  } catch (e: Exception) {
1716
- Log.e("OmikitPlugin", "❌ Error requesting permissions by codes: ${e.message}", e)
1717
1731
  promise.reject("ERROR_PERMISSION_REQUEST", "Failed to request permissions: ${e.message}")
1718
1732
  }
1719
1733
  }
@@ -1722,12 +1736,9 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1722
1736
  val missingPermissions = getMissingPermissions(isVideo)
1723
1737
 
1724
1738
  if (missingPermissions.isEmpty()) {
1725
- Log.d("OmikitPlugin", "✅ All permissions already granted")
1726
1739
  return
1727
1740
  }
1728
1741
 
1729
- Log.d("OmikitPlugin", "📋 Requesting missing permissions for Android ${Build.VERSION.SDK_INT}: ${missingPermissions.joinToString()}")
1730
-
1731
1742
  ActivityCompat.requestPermissions(
1732
1743
  reactApplicationContext.currentActivity!!,
1733
1744
  missingPermissions.toTypedArray(),
@@ -1735,7 +1746,16 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1735
1746
  )
1736
1747
  }
1737
1748
 
1738
- override fun onActivityResult(p0: Activity?, p1: Int, p2: Int, p3: Intent?) {
1749
+ override fun onActivityResult(p0: Activity?, requestCode: Int, resultCode: Int, p3: Intent?) {
1750
+ if (requestCode == REQUEST_OVERLAY_PERMISSION_CODE) {
1751
+ val granted = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1752
+ Settings.canDrawOverlays(reactApplicationContext)
1753
+ } else {
1754
+ true
1755
+ }
1756
+ permissionPromise?.resolve(granted)
1757
+ permissionPromise = null
1758
+ }
1739
1759
  }
1740
1760
 
1741
1761
  override fun onNewIntent(p0: Intent?) {
@@ -1760,16 +1780,10 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1760
1780
  }
1761
1781
  }
1762
1782
 
1763
- // Thêm listener cho AUTO-UNREGISTER status
1783
+ // Auto-unregister listener - only handles onAutoUnregisterStatus
1784
+ // Other callbacks are NO-OP to prevent double-firing events (Fix #1)
1764
1785
  private val autoUnregisterListener = object : OmiListener {
1765
1786
  override fun onAutoUnregisterStatus(isScheduled: Boolean, timeUntilExecution: Long) {
1766
- // ✅ Auto-unregister prevention removed - no longer supported in new SDK
1767
- if (isScheduled && timeUntilExecution > 0 && timeUntilExecution < 3000) {
1768
- Log.w("OmikitPlugin", "🚨 AUTO-UNREGISTER sắp thực hiện trong ${timeUntilExecution}ms - SDK tự xử lý")
1769
- // preventAutoUnregisterCrash deprecated - SDK handles automatically
1770
- }
1771
-
1772
- // ✅ Gửi event cho React Native
1773
1787
  try {
1774
1788
  val statusData = mapOf(
1775
1789
  "isScheduled" to isScheduled,
@@ -1779,94 +1793,48 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1779
1793
  val map = createSafeWritableMap(statusData)
1780
1794
  sendEvent("AUTO_UNREGISTER_STATUS", map)
1781
1795
  } catch (e: Exception) {
1782
- Log.e("OmikitPlugin", "Error sending AUTO_UNREGISTER_STATUS event: ${e.message}", e)
1796
+ Log.e("OmikitPlugin", "Error sending AUTO_UNREGISTER_STATUS event: ${e.message}", e)
1783
1797
  }
1784
1798
  }
1785
-
1786
- // Implement các method khác của OmiListener (delegate to main listener)
1787
- override fun incomingReceived(callerId: Int?, phoneNumber: String?, isVideo: Boolean?) {
1788
- this@OmikitPluginModule.incomingReceived(callerId, phoneNumber, isVideo)
1789
- }
1790
-
1791
- override fun onCallEstablished(callerId: Int, phoneNumber: String?, isVideo: Boolean?, startTime: Long, transactionId: String?) {
1792
- this@OmikitPluginModule.onCallEstablished(callerId, phoneNumber, isVideo, startTime, transactionId)
1793
- }
1794
-
1795
- override fun onCallEnd(callInfo: MutableMap<String, Any?>, statusCode: Int) {
1796
- this@OmikitPluginModule.onCallEnd(callInfo, statusCode)
1797
- }
1798
-
1799
- override fun onConnecting() {
1800
- this@OmikitPluginModule.onConnecting()
1801
- }
1802
-
1803
- override fun onDescriptionError() {
1804
- this@OmikitPluginModule.onDescriptionError()
1805
- }
1806
-
1807
- override fun onFcmReceived(uuid: String, userName: String, avatar: String) {
1808
- this@OmikitPluginModule.onFcmReceived(uuid, userName, avatar)
1809
- }
1810
-
1811
- override fun onRinging(callerId: Int, transactionId: String?) {
1812
- this@OmikitPluginModule.onRinging(callerId, transactionId)
1813
- }
1814
-
1815
- override fun networkHealth(stat: Map<String, *>, quality: Int) {
1816
- this@OmikitPluginModule.networkHealth(stat, quality)
1817
- }
1818
-
1819
- override fun onAudioChanged(audioInfo: Map<String, Any>) {
1820
- this@OmikitPluginModule.onAudioChanged(audioInfo)
1821
- }
1822
-
1823
- override fun onHold(isHold: Boolean) {
1824
- this@OmikitPluginModule.onHold(isHold)
1825
- }
1826
-
1827
- override fun onMuted(isMuted: Boolean) {
1828
- this@OmikitPluginModule.onMuted(isMuted)
1829
- }
1830
-
1831
- override fun onOutgoingStarted(callerId: Int, phoneNumber: String?, isVideo: Boolean?) {
1832
- this@OmikitPluginModule.onOutgoingStarted(callerId, phoneNumber, isVideo)
1833
- }
1834
-
1835
- override fun onSwitchBoardAnswer(sip: String) {
1836
- this@OmikitPluginModule.onSwitchBoardAnswer(sip)
1837
- }
1838
-
1839
- override fun onRegisterCompleted(statusCode: Int) {
1840
- this@OmikitPluginModule.onRegisterCompleted(statusCode)
1841
- }
1842
-
1843
- override fun onRequestPermission(permissions: Array<String>) {
1844
- this@OmikitPluginModule.onRequestPermission(permissions)
1845
- }
1846
-
1847
- override fun onVideoSize(width: Int, height: Int) {
1848
- this@OmikitPluginModule.onVideoSize(width, height)
1849
- }
1799
+
1800
+ // NO-OP: main module listener handles these - avoid double-firing
1801
+ override fun incomingReceived(callerId: Int?, phoneNumber: String?, isVideo: Boolean?) {}
1802
+ override fun onCallEstablished(callerId: Int, phoneNumber: String?, isVideo: Boolean?, startTime: Long, transactionId: String?) {}
1803
+ override fun onCallEnd(callInfo: MutableMap<String, Any?>, statusCode: Int) {}
1804
+ override fun onConnecting() {}
1805
+ override fun onDescriptionError() {}
1806
+ override fun onFcmReceived(uuid: String, userName: String, avatar: String) {}
1807
+ override fun onRinging(callerId: Int, transactionId: String?) {}
1808
+ override fun networkHealth(stat: Map<String, *>, quality: Int) {}
1809
+ override fun onAudioChanged(audioInfo: Map<String, Any>) {}
1810
+ override fun onHold(isHold: Boolean) {}
1811
+ override fun onMuted(isMuted: Boolean) {}
1812
+ override fun onOutgoingStarted(callerId: Int, phoneNumber: String?, isVideo: Boolean?) {}
1813
+ override fun onSwitchBoardAnswer(sip: String) {}
1814
+ override fun onRegisterCompleted(statusCode: Int) {}
1815
+ override fun onRequestPermission(permissions: Array<String>) {}
1816
+ override fun onVideoSize(width: Int, height: Int) {}
1850
1817
  }
1851
1818
 
1852
1819
  // ✅ Helper function để hide notification một cách an toàn
1853
1820
  @ReactMethod
1854
1821
  fun hideSystemNotificationSafely(promise: Promise) {
1855
1822
  try {
1856
- // Delay 2 giây để đảm bảo registration hoàn tất
1857
- Handler(Looper.getMainLooper()).postDelayed({
1823
+ val context = reactApplicationContext ?: run {
1824
+ promise.resolve(false)
1825
+ return
1826
+ }
1827
+ // Delay to ensure registration completes before hiding
1828
+ mainScope.launch {
1858
1829
  try {
1859
- // ✅ Gọi function hide notification với error handling
1860
- OmiClient.getInstance(reactApplicationContext!!).hideSystemNotificationAndUnregister("Registration check completed")
1861
- Log.d("OmikitPlugin", "✅ Successfully hidden system notification and unregistered")
1830
+ delay(2000)
1831
+ OmiClient.getInstance(context).hideSystemNotificationAndUnregister("Registration check completed")
1862
1832
  promise.resolve(true)
1863
1833
  } catch (e: Exception) {
1864
- Log.e("OmikitPlugin", "❌ Failed to hide system notification: ${e.message}", e)
1865
1834
  promise.resolve(false)
1866
1835
  }
1867
- }, 2000) // Delay 2 giây
1836
+ }
1868
1837
  } catch (e: Exception) {
1869
- Log.e("OmikitPlugin", "❌ Error in hideSystemNotificationSafely: ${e.message}", e)
1870
1838
  promise.resolve(false)
1871
1839
  }
1872
1840
  }
@@ -1876,10 +1844,8 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1876
1844
  fun hideSystemNotificationOnly(promise: Promise) {
1877
1845
  try {
1878
1846
  OmiClient.getInstance(reactApplicationContext!!).hideSystemNotification()
1879
- Log.d("OmikitPlugin", "✅ Successfully hidden system notification (keeping registration)")
1880
1847
  promise.resolve(true)
1881
1848
  } catch (e: Exception) {
1882
- Log.e("OmikitPlugin", "❌ Failed to hide system notification only: ${e.message}", e)
1883
1849
  promise.resolve(false)
1884
1850
  }
1885
1851
  }
@@ -1889,10 +1855,8 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1889
1855
  fun hideSystemNotificationAndUnregister(reason: String, promise: Promise) {
1890
1856
  try {
1891
1857
  OmiClient.getInstance(reactApplicationContext!!).hideSystemNotificationAndUnregister(reason)
1892
- Log.d("OmikitPlugin", "✅ Successfully hidden notification and unregistered: $reason")
1893
1858
  promise.resolve(true)
1894
1859
  } catch (e: Exception) {
1895
- Log.e("OmikitPlugin", "❌ Failed to hide notification and unregister: ${e.message}", e)
1896
1860
  promise.resolve(false)
1897
1861
  }
1898
1862
  }
@@ -1904,21 +1868,18 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1904
1868
  val userName = data.getString("userName")
1905
1869
  val password = data.getString("password")
1906
1870
  val realm = data.getString("realm")
1907
- val host = data.getString("host") ?: "vh.omicrm.com"
1871
+ val host = data.getString("host").let { if (it.isNullOrEmpty()) "vh.omicrm.com" else it }
1908
1872
  val firebaseToken = data.getString("fcmToken")
1909
1873
  val projectId = data.getString("projectId") ?: ""
1910
1874
 
1911
1875
  // Validate required parameters
1912
1876
  if (userName.isNullOrEmpty() || password.isNullOrEmpty() || realm.isNullOrEmpty() || firebaseToken.isNullOrEmpty()) {
1913
- Log.e("OmikitPlugin", "❌ Missing required parameters for credential check")
1914
1877
  promise.resolve(mapOf("success" to false, "message" to "Missing required parameters"))
1915
1878
  return@launch
1916
1879
  }
1917
1880
 
1918
1881
  withContext(Dispatchers.Default) {
1919
1882
  try {
1920
- Log.d("OmikitPlugin", "🔍 Checking credentials for user: $userName")
1921
-
1922
1883
  OmiClient.getInstance(reactApplicationContext!!).checkCredentials(
1923
1884
  userName = userName ?: "",
1924
1885
  password = password ?: "",
@@ -1927,8 +1888,6 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1927
1888
  host = host,
1928
1889
  projectId = projectId
1929
1890
  ) { success, statusCode, message ->
1930
- Log.d("OmikitPlugin", "🔍 Credential check callback - success: $success, status: $statusCode, message: $message")
1931
-
1932
1891
  val result = mapOf(
1933
1892
  "success" to success,
1934
1893
  "statusCode" to statusCode,
@@ -1939,7 +1898,6 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1939
1898
  }
1940
1899
 
1941
1900
  } catch (e: Exception) {
1942
- Log.e("OmikitPlugin", "❌ Error during credential check: ${e.message}", e)
1943
1901
  val errorResult = mapOf(
1944
1902
  "success" to false,
1945
1903
  "message" to e.message
@@ -1957,7 +1915,7 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1957
1915
  val userName = data.getString("userName")
1958
1916
  val password = data.getString("password")
1959
1917
  val realm = data.getString("realm")
1960
- val host = data.getString("host") ?: "vh.omicrm.com"
1918
+ val host = data.getString("host").let { if (it.isNullOrEmpty()) "vh.omicrm.com" else it }
1961
1919
  val isVideo = data.getBoolean("isVideo")
1962
1920
  val firebaseToken = data.getString("fcmToken")
1963
1921
  val projectId = data.getString("projectId") ?: ""
@@ -1966,15 +1924,12 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1966
1924
 
1967
1925
  // Validate required parameters
1968
1926
  if (userName.isNullOrEmpty() || password.isNullOrEmpty() || realm.isNullOrEmpty() || firebaseToken.isNullOrEmpty()) {
1969
- Log.e("OmikitPlugin", "❌ Missing required parameters for registration with options")
1970
1927
  promise.resolve(mapOf("success" to false, "message" to "Missing required parameters"))
1971
1928
  return@launch
1972
1929
  }
1973
1930
 
1974
1931
  withContext(Dispatchers.Default) {
1975
1932
  try {
1976
- Log.d("OmikitPlugin", "⚙️ Registering with options for user: $userName - showNotification: $showNotification, enableAutoUnregister: $enableAutoUnregister")
1977
-
1978
1933
  OmiClient.getInstance(reactApplicationContext!!).registerWithOptions(
1979
1934
  userName = userName ?: "",
1980
1935
  password = password ?: "",
@@ -1986,8 +1941,6 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1986
1941
  showNotification = showNotification,
1987
1942
  enableAutoUnregister = enableAutoUnregister
1988
1943
  ) { success, statusCode, message ->
1989
- Log.d("OmikitPlugin", "⚙️ Registration with options callback - success: $success, status: $statusCode, message: $message")
1990
-
1991
1944
  val result = mapOf(
1992
1945
  "success" to success,
1993
1946
  "statusCode" to statusCode,
@@ -1998,7 +1951,6 @@ class OmikitPluginModule(reactContext: ReactApplicationContext?) :
1998
1951
  }
1999
1952
 
2000
1953
  } catch (e: Exception) {
2001
- Log.e("OmikitPlugin", "❌ Error during registration with options: ${e.message}", e)
2002
1954
  val errorResult = mapOf(
2003
1955
  "success" to false,
2004
1956
  "message" to e.message