@qusaieilouti99/call-manager 0.1.112 → 0.1.113

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.
@@ -28,7 +28,6 @@ import android.widget.LinearLayout
28
28
  import android.widget.TextView
29
29
  import java.net.HttpURLConnection
30
30
  import java.net.URL
31
- import android.view.KeyEvent
32
31
 
33
32
  class CallActivity : Activity(), CallEngine.CallEndListener {
34
33
 
@@ -49,17 +48,6 @@ class CallActivity : Activity(), CallEngine.CallEndListener {
49
48
  finishCallActivity()
50
49
  }
51
50
 
52
- override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
53
- if (keyCode == KeyEvent.KEYCODE_VOLUME_UP ||
54
- keyCode == KeyEvent.KEYCODE_VOLUME_DOWN ||
55
- keyCode == KeyEvent.KEYCODE_VOLUME_MUTE) {
56
- // silence the ring immediately
57
- CallEngine.stopRingtone()
58
- return true
59
- }
60
- return super.onKeyDown(keyCode, event)
61
- }
62
-
63
51
  override fun onCreate(savedInstanceState: Bundle?) {
64
52
  super.onCreate(savedInstanceState)
65
53
  val isSamsungBypass = intent.getBooleanExtra(
@@ -42,6 +42,8 @@ import android.os.Vibrator
42
42
  import android.os.VibrationEffect
43
43
  import android.content.BroadcastReceiver
44
44
  import android.content.IntentFilter
45
+ import android.database.ContentObserver
46
+ import android.provider.Settings
45
47
 
46
48
  /**
47
49
  * Core call‐management engine. Manages self-managed telecom calls,
@@ -53,6 +55,11 @@ object CallEngine {
53
55
  private const val NOTIF_CHANNEL_ID = "incoming_call_channel"
54
56
  private const val NOTIF_ID = 2001
55
57
 
58
+ // Audio routing timing constants
59
+ private const val AUDIO_ROUTE_DELAY_AFTER_ANSWER = 500L
60
+ private const val AUDIO_ROUTE_RETRY_DELAY = 200L
61
+ private const val MAX_AUDIO_ROUTE_RETRIES = 3
62
+
56
63
  // --- NEW: Call-end listener API ---
57
64
  /**
58
65
  * Implement this in your UI (CallActivity). Will be called
@@ -82,17 +89,24 @@ object CallEngine {
82
89
  private val isInitialized = AtomicBoolean(false)
83
90
  private val initializationLock = Any()
84
91
 
85
- // Simplified Audio & Media Management (NO MANUAL AUDIO FOCUS)
92
+ // Audio & Media Management
86
93
  private var ringtone: android.media.Ringtone? = null
87
94
  private var ringbackPlayer: MediaPlayer? = null
88
95
 
89
- // Vibration + volume‐change receiver
96
+ // Volume monitoring
90
97
  private var vibrator: Vibrator? = null
91
- private var volumeReceiver: BroadcastReceiver? = null
98
+ private var volumeContentObserver: ContentObserver? = null
99
+ private var lastRingVolumeLevel = -1
100
+ private var isMonitoringVolume = false
92
101
 
93
102
  private var audioManager: AudioManager? = null
94
103
  private var wakeLock: PowerManager.WakeLock? = null
95
104
 
105
+ // Audio routing state
106
+ private var pendingAudioRoute: String? = null
107
+ private var audioRouteRetryCount = 0
108
+ private val audioRouteHandler = Handler(Looper.getMainLooper())
109
+
96
110
  // Call State Management
97
111
  private val activeCalls = ConcurrentHashMap<String, CallInfo>()
98
112
  private val telecomConnections = ConcurrentHashMap<String, Connection>()
@@ -122,6 +136,7 @@ object CallEngine {
122
136
  if (isInitialized.compareAndSet(false, true)) {
123
137
  appContext = context.applicationContext
124
138
  audioManager = appContext?.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
139
+ setupVolumeMonitoring()
125
140
  Log.d(TAG, "CallEngine initialized successfully")
126
141
 
127
142
  if (isCallActive()) {
@@ -139,6 +154,64 @@ object CallEngine {
139
154
  )
140
155
  }
141
156
 
157
+ // --- VOLUME MONITORING FOR RINGTONE SILENCING ---
158
+ private fun setupVolumeMonitoring() {
159
+ val context = requireContext()
160
+ audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
161
+
162
+ // Get initial ring volume
163
+ lastRingVolumeLevel = audioManager?.getStreamVolume(AudioManager.STREAM_RING) ?: 0
164
+
165
+ // Create content observer for ring volume changes
166
+ volumeContentObserver = object : ContentObserver(mainHandler) {
167
+ override fun onChange(selfChange: Boolean) {
168
+ handleVolumeChange()
169
+ }
170
+ }
171
+ }
172
+
173
+ private fun startVolumeMonitoring() {
174
+ if (!isMonitoringVolume) {
175
+ isMonitoringVolume = true
176
+ val context = requireContext()
177
+
178
+ // Monitor ring volume changes
179
+ context.contentResolver.registerContentObserver(
180
+ Settings.System.getUriFor(Settings.System.VOLUME_RING),
181
+ false,
182
+ volumeContentObserver!!
183
+ )
184
+
185
+ Log.d(TAG, "Started volume monitoring for ringtone silencing")
186
+ }
187
+ }
188
+
189
+ private fun stopVolumeMonitoring() {
190
+ if (isMonitoringVolume) {
191
+ isMonitoringVolume = false
192
+ val context = requireContext()
193
+ volumeContentObserver?.let {
194
+ context.contentResolver.unregisterContentObserver(it)
195
+ }
196
+ Log.d(TAG, "Stopped volume monitoring")
197
+ }
198
+ }
199
+
200
+ private fun handleVolumeChange() {
201
+ if (!isMonitoringVolume) return
202
+
203
+ val currentRingVolume = audioManager?.getStreamVolume(AudioManager.STREAM_RING) ?: 0
204
+
205
+ // Check if ring volume changed
206
+ if (currentRingVolume != lastRingVolumeLevel) {
207
+ Log.d(TAG, "Ring volume changed from $lastRingVolumeLevel to $currentRingVolume - silencing ringtone")
208
+ lastRingVolumeLevel = currentRingVolume
209
+
210
+ // Stop ringtone when volume is changed
211
+ stopRingtone()
212
+ }
213
+ }
214
+
142
215
  // --- Event System ---
143
216
  fun setEventHandler(handler: ((CallEventType, String) -> Unit)?) {
144
217
  Log.d(TAG, "setEventHandler called. Handler present: ${handler != null}")
@@ -393,7 +466,12 @@ object CallEngine {
393
466
  bringAppToForeground()
394
467
  startForegroundService()
395
468
  keepScreenAwake(true)
396
- setInitialAudioRoute(callType)
469
+
470
+ // Delay initial audio route to avoid conflicts
471
+ mainHandler.postDelayed({
472
+ setInitialAudioRoute(callType)
473
+ }, AUDIO_ROUTE_DELAY_AFTER_ANSWER)
474
+
397
475
  updateLockScreenBypass()
398
476
 
399
477
  emitOutgoingCallAnsweredWithMetadata(callId)
@@ -418,10 +496,9 @@ object CallEngine {
418
496
  return
419
497
  }
420
498
 
421
-
422
499
  activeCalls[callId] = callInfo.copy(state = CallState.ACTIVE)
423
500
  currentCallId = callId
424
- Log.d(TAG, "Call $callId set to ACTIVE state (system manages audio focus)")
501
+ Log.d(TAG, "Call $callId set to ACTIVE state")
425
502
 
426
503
  stopRingtone()
427
504
  stopRingback()
@@ -440,8 +517,10 @@ object CallEngine {
440
517
  keepScreenAwake(true)
441
518
  updateLockScreenBypass()
442
519
 
443
- // then pick your route:
444
- setInitialAudioRoute(callInfo.callType)
520
+ // FIXED: Delay audio routing to avoid conflicts with system
521
+ mainHandler.postDelayed({
522
+ setInitialAudioRoute(callInfo.callType)
523
+ }, AUDIO_ROUTE_DELAY_AFTER_ANSWER)
445
524
 
446
525
  if (isLocalAnswer) {
447
526
  emitCallAnsweredWithMetadata(callId)
@@ -728,43 +807,59 @@ object CallEngine {
728
807
  return AudioRoutesInfo(devices.toTypedArray(), current)
729
808
  }
730
809
 
810
+ // FIXED: Improved audio route handling with retries and proper timing
731
811
  fun setAudioRoute(route: String) {
732
- // ensure we have a non‐null AudioManager
812
+ Log.d(TAG, "setAudioRoute called: $route")
813
+
814
+ // Clear any pending route changes
815
+ audioRouteHandler.removeCallbacksAndMessages(null)
816
+ audioRouteRetryCount = 0
817
+
818
+ // Store the pending route for retry logic
819
+ pendingAudioRoute = route
820
+
821
+ // Execute immediately, then set up retry mechanism
822
+ performAudioRouteChange(route)
823
+ }
824
+
825
+ private fun performAudioRouteChange(route: String) {
733
826
  val ctx = requireContext()
734
827
  if (audioManager == null) {
735
828
  audioManager = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
736
829
  }
737
830
  val am = audioManager!!
738
- setAudioMode()
831
+
832
+ // Ensure proper audio mode first
833
+ if (am.mode != AudioManager.MODE_IN_COMMUNICATION) {
834
+ setAudioMode()
835
+ }
836
+
837
+ Log.d(TAG, "Performing audio route change to: $route")
739
838
 
740
839
  when (route) {
741
840
  "Speaker" -> {
742
- // if Bluetooth SCO is active, turn it off
841
+ // Disable Bluetooth SCO first if active
743
842
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && am.isBluetoothScoOn) {
744
843
  am.stopBluetoothSco()
745
844
  am.isBluetoothScoOn = false
746
845
  }
747
- // route to speaker
748
846
  am.isSpeakerphoneOn = true
749
847
  Log.d(TAG, "Audio routed to SPEAKER")
750
848
  }
751
849
 
752
850
  "Earpiece" -> {
753
- // disable SCO if active
851
+ // Disable Bluetooth SCO if active
754
852
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && am.isBluetoothScoOn) {
755
853
  am.stopBluetoothSco()
756
854
  am.isBluetoothScoOn = false
757
855
  }
758
- // route to earpiece
759
856
  am.isSpeakerphoneOn = false
760
857
  Log.d(TAG, "Audio routed to EARPIECE")
761
858
  }
762
859
 
763
860
  "Bluetooth" -> {
764
- // disable speaker
765
861
  am.isSpeakerphoneOn = false
766
862
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
767
- // start SCO for Bluetooth
768
863
  am.startBluetoothSco()
769
864
  am.isBluetoothScoOn = true
770
865
  Log.d(TAG, "Audio routed to BLUETOOTH (SCO started)")
@@ -774,7 +869,6 @@ object CallEngine {
774
869
  }
775
870
 
776
871
  "Headset" -> {
777
- // wired headset is automatic when plugged in, just turn off speaker/SCO
778
872
  am.isSpeakerphoneOn = false
779
873
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && am.isBluetoothScoOn) {
780
874
  am.stopBluetoothSco()
@@ -785,13 +879,41 @@ object CallEngine {
785
879
 
786
880
  else -> {
787
881
  Log.w(TAG, "Unknown audio route: $route")
882
+ return
788
883
  }
789
884
  }
790
885
 
791
- // broadcast route‐change event
886
+ // Verify the route was applied and retry if necessary
887
+ audioRouteHandler.postDelayed({
888
+ verifyAudioRoute(route)
889
+ }, AUDIO_ROUTE_RETRY_DELAY)
890
+
792
891
  emitAudioRouteChanged()
793
892
  }
794
893
 
894
+ private fun verifyAudioRoute(expectedRoute: String) {
895
+ val currentRoute = getCurrentAudioRoute()
896
+
897
+ if (currentRoute != expectedRoute && audioRouteRetryCount < MAX_AUDIO_ROUTE_RETRIES) {
898
+ audioRouteRetryCount++
899
+ Log.d(TAG, "Audio route verification failed. Expected: $expectedRoute, Current: $currentRoute. Retry: $audioRouteRetryCount")
900
+
901
+ // Retry the audio route change
902
+ audioRouteHandler.postDelayed({
903
+ performAudioRouteChange(expectedRoute)
904
+ }, AUDIO_ROUTE_RETRY_DELAY)
905
+ } else {
906
+ if (currentRoute == expectedRoute) {
907
+ Log.d(TAG, "Audio route successfully verified: $currentRoute")
908
+ } else {
909
+ Log.w(TAG, "Audio route change failed after $MAX_AUDIO_ROUTE_RETRIES retries. Expected: $expectedRoute, Current: $currentRoute")
910
+ }
911
+ // Clear pending route
912
+ pendingAudioRoute = null
913
+ audioRouteRetryCount = 0
914
+ }
915
+ }
916
+
795
917
  private fun getCurrentAudioRoute(): String {
796
918
  return when {
797
919
  audioManager?.isBluetoothScoOn == true -> "Bluetooth"
@@ -920,7 +1042,6 @@ object CallEngine {
920
1042
  }
921
1043
 
922
1044
  // --- Notification Management ---
923
- // **SAMSUNG FIX**: Enhanced notification channel creation
924
1045
  private fun createNotificationChannel() {
925
1046
  val context = requireContext()
926
1047
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -934,7 +1055,6 @@ object CallEngine {
934
1055
  channel.lightColor = Color.GREEN
935
1056
  channel.enableVibration(true)
936
1057
 
937
- // **SAMSUNG FIX**: Set bypass DND and lock screen visibility
938
1058
  channel.setBypassDnd(true)
939
1059
  channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
940
1060
 
@@ -960,7 +1080,6 @@ object CallEngine {
960
1080
  val context = requireContext()
961
1081
  Log.d(TAG, "Showing incoming call UI for $callId")
962
1082
 
963
- // **SIMPLE LOGIC**: If locked -> overlay, if unlocked -> notification
964
1083
  if (isDeviceLocked(context)) {
965
1084
  Log.d(TAG, "Device is locked - using overlay approach")
966
1085
  showCallActivityOverlay(context, callId, callerName, callType, callerPicUrl)
@@ -972,9 +1091,6 @@ object CallEngine {
972
1091
  playRingtone()
973
1092
  }
974
1093
 
975
- /**
976
- * Simple check if device is locked
977
- */
978
1094
  private fun isDeviceLocked(context: Context): Boolean {
979
1095
  val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
980
1096
  val isLocked = keyguardManager.isKeyguardLocked
@@ -982,9 +1098,6 @@ object CallEngine {
982
1098
  return isLocked
983
1099
  }
984
1100
 
985
- /**
986
- * Shows the call activity directly as overlay (when device is locked)
987
- */
988
1101
  private fun showCallActivityOverlay(context: Context, callId: String, callerName: String, callType: String, callerPicUrl: String?) {
989
1102
  val overlayIntent = Intent(context, CallActivity::class.java).apply {
990
1103
  addFlags(
@@ -1000,11 +1113,10 @@ object CallEngine {
1000
1113
  putExtra("callerAvatar", callerPicUrl)
1001
1114
  }
1002
1115
 
1003
- putExtra("LOCK_SCREEN_MODE", true) // Flag to identify lock screen mode
1116
+ putExtra("LOCK_SCREEN_MODE", true)
1004
1117
  }
1005
1118
 
1006
1119
  try {
1007
- // Wake the screen when device is locked
1008
1120
  val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
1009
1121
  val wakeLock = powerManager.newWakeLock(
1010
1122
  PowerManager.SCREEN_BRIGHT_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP,
@@ -1012,22 +1124,15 @@ object CallEngine {
1012
1124
  )
1013
1125
  wakeLock.acquire(5000)
1014
1126
 
1015
- // Launch the activity overlay
1016
1127
  context.startActivity(overlayIntent)
1017
1128
  Log.d(TAG, "Successfully launched CallActivity overlay for locked device")
1018
1129
 
1019
- // **NO NOTIFICATION** when using overlay
1020
-
1021
1130
  } catch (e: Exception) {
1022
1131
  Log.e(TAG, "Overlay failed, falling back to notification: ${e.message}")
1023
- // If overlay fails, fall back to notification
1024
1132
  showStandardNotification(context, callId, callerName, callType, callerPicUrl)
1025
1133
  }
1026
1134
  }
1027
1135
 
1028
- /**
1029
- * Shows standard notification with full-screen intent (when device is unlocked)
1030
- */
1031
1136
  private fun showStandardNotification(context: Context, callId: String, callerName: String, callType: String, callerPicUrl: String?) {
1032
1137
  createNotificationChannel()
1033
1138
  val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@@ -1040,7 +1145,6 @@ object CallEngine {
1040
1145
  if (callerPicUrl != null) {
1041
1146
  putExtra("callerAvatar", callerPicUrl)
1042
1147
  }
1043
- // No lock screen flag - standard notification mode
1044
1148
  }
1045
1149
 
1046
1150
  val fullScreenPendingIntent = PendingIntent.getActivity(
@@ -1239,17 +1343,23 @@ object CallEngine {
1239
1343
  }
1240
1344
 
1241
1345
  // --- Media Management ---
1346
+ // FIXED: Improved ringtone management with proper volume monitoring
1242
1347
  private fun playRingtone() {
1243
1348
  val context = requireContext()
1244
1349
 
1245
1350
  audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
1246
- // 0) ensure the ring plays out loud on the speaker
1351
+
1352
+ // Set up proper audio mode for ringtone
1247
1353
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
1248
1354
  audioManager?.mode = AudioManager.MODE_RINGTONE
1249
1355
  }
1250
1356
  audioManager?.isSpeakerphoneOn = true
1251
1357
 
1252
- // 1) start repeating vibration
1358
+ // Start volume monitoring for silencing
1359
+ startVolumeMonitoring()
1360
+ lastRingVolumeLevel = audioManager?.getStreamVolume(AudioManager.STREAM_RING) ?: 0
1361
+
1362
+ // Start repeating vibration
1253
1363
  vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
1254
1364
  vibrator?.let { v ->
1255
1365
  val pattern = longArrayOf(0L, 500L, 500L)
@@ -1261,33 +1371,15 @@ object CallEngine {
1261
1371
  }
1262
1372
  }
1263
1373
 
1264
- // 2) play the ringtone
1374
+ // Play the ringtone
1265
1375
  try {
1266
1376
  val uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
1267
1377
  ringtone = RingtoneManager.getRingtone(context, uri)
1268
1378
  ringtone?.play()
1379
+ Log.d(TAG, "Ringtone started playing")
1269
1380
  } catch (e: Exception) {
1270
1381
  Log.e(TAG, "Failed to play ringtone", e)
1271
1382
  }
1272
-
1273
- // 3) register a receiver to catch volume‐key presses on the RING stream
1274
- if (volumeReceiver == null) {
1275
- val filter = IntentFilter("android.media.VOLUME_CHANGED_ACTION")
1276
- volumeReceiver = object : BroadcastReceiver() {
1277
- override fun onReceive(ctx: Context, intent: Intent) {
1278
- if (intent.action == "android.media.VOLUME_CHANGED_ACTION") {
1279
- val streamType = intent.getIntExtra(
1280
- "android.media.EXTRA_VOLUME_STREAM_TYPE", -1
1281
- )
1282
- if (streamType == AudioManager.STREAM_RING) {
1283
- Log.d(TAG, "Volume key pressed on RING → silencing ringtone/vibrate")
1284
- stopRingtone()
1285
- }
1286
- }
1287
- }
1288
- }
1289
- context.registerReceiver(volumeReceiver, filter)
1290
- }
1291
1383
  }
1292
1384
 
1293
1385
  fun stopRingtone() {
@@ -1299,15 +1391,12 @@ object CallEngine {
1299
1391
  }
1300
1392
  ringtone = null
1301
1393
 
1302
- // cancel vibration
1394
+ // Cancel vibration
1303
1395
  vibrator?.cancel()
1304
1396
  vibrator = null
1305
1397
 
1306
- // unregister our volume‐change receiver
1307
- volumeReceiver?.let {
1308
- requireContext().unregisterReceiver(it)
1309
- volumeReceiver = null
1310
- }
1398
+ // Stop volume monitoring
1399
+ stopVolumeMonitoring()
1311
1400
  }
1312
1401
 
1313
1402
  private fun startRingback() {
@@ -1344,6 +1433,10 @@ object CallEngine {
1344
1433
  stopForegroundService()
1345
1434
  keepScreenAwake(false)
1346
1435
  resetAudioMode()
1436
+ stopVolumeMonitoring()
1437
+ audioRouteHandler.removeCallbacksAndMessages(null)
1438
+ pendingAudioRoute = null
1439
+ audioRouteRetryCount = 0
1347
1440
  }
1348
1441
 
1349
1442
  // --- Lifecycle Management ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qusaieilouti99/call-manager",
3
- "version": "0.1.112",
3
+ "version": "0.1.113",
4
4
  "description": "Call manager",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",