@qusaieilouti99/call-manager 0.1.111 → 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,10 +517,10 @@ object CallEngine {
440
517
  keepScreenAwake(true)
441
518
  updateLockScreenBypass()
442
519
 
443
- // before you do anything else:
444
- setAudioMode()
445
- // then pick your route:
446
- setInitialAudioRoute(callType)
520
+ // FIXED: Delay audio routing to avoid conflicts with system
521
+ mainHandler.postDelayed({
522
+ setInitialAudioRoute(callInfo.callType)
523
+ }, AUDIO_ROUTE_DELAY_AFTER_ANSWER)
447
524
 
448
525
  if (isLocalAnswer) {
449
526
  emitCallAnsweredWithMetadata(callId)
@@ -730,42 +807,59 @@ object CallEngine {
730
807
  return AudioRoutesInfo(devices.toTypedArray(), current)
731
808
  }
732
809
 
810
+ // FIXED: Improved audio route handling with retries and proper timing
733
811
  fun setAudioRoute(route: String) {
734
- // 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) {
735
826
  val ctx = requireContext()
736
827
  if (audioManager == null) {
737
828
  audioManager = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
738
829
  }
739
830
  val am = audioManager!!
740
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")
838
+
741
839
  when (route) {
742
840
  "Speaker" -> {
743
- // if Bluetooth SCO is active, turn it off
841
+ // Disable Bluetooth SCO first if active
744
842
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && am.isBluetoothScoOn) {
745
843
  am.stopBluetoothSco()
746
844
  am.isBluetoothScoOn = false
747
845
  }
748
- // route to speaker
749
846
  am.isSpeakerphoneOn = true
750
847
  Log.d(TAG, "Audio routed to SPEAKER")
751
848
  }
752
849
 
753
850
  "Earpiece" -> {
754
- // disable SCO if active
851
+ // Disable Bluetooth SCO if active
755
852
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && am.isBluetoothScoOn) {
756
853
  am.stopBluetoothSco()
757
854
  am.isBluetoothScoOn = false
758
855
  }
759
- // route to earpiece
760
856
  am.isSpeakerphoneOn = false
761
857
  Log.d(TAG, "Audio routed to EARPIECE")
762
858
  }
763
859
 
764
860
  "Bluetooth" -> {
765
- // disable speaker
766
861
  am.isSpeakerphoneOn = false
767
862
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
768
- // start SCO for Bluetooth
769
863
  am.startBluetoothSco()
770
864
  am.isBluetoothScoOn = true
771
865
  Log.d(TAG, "Audio routed to BLUETOOTH (SCO started)")
@@ -775,7 +869,6 @@ object CallEngine {
775
869
  }
776
870
 
777
871
  "Headset" -> {
778
- // wired headset is automatic when plugged in, just turn off speaker/SCO
779
872
  am.isSpeakerphoneOn = false
780
873
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && am.isBluetoothScoOn) {
781
874
  am.stopBluetoothSco()
@@ -786,13 +879,41 @@ object CallEngine {
786
879
 
787
880
  else -> {
788
881
  Log.w(TAG, "Unknown audio route: $route")
882
+ return
789
883
  }
790
884
  }
791
885
 
792
- // 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
+
793
891
  emitAudioRouteChanged()
794
892
  }
795
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
+
796
917
  private fun getCurrentAudioRoute(): String {
797
918
  return when {
798
919
  audioManager?.isBluetoothScoOn == true -> "Bluetooth"
@@ -921,7 +1042,6 @@ object CallEngine {
921
1042
  }
922
1043
 
923
1044
  // --- Notification Management ---
924
- // **SAMSUNG FIX**: Enhanced notification channel creation
925
1045
  private fun createNotificationChannel() {
926
1046
  val context = requireContext()
927
1047
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -935,7 +1055,6 @@ object CallEngine {
935
1055
  channel.lightColor = Color.GREEN
936
1056
  channel.enableVibration(true)
937
1057
 
938
- // **SAMSUNG FIX**: Set bypass DND and lock screen visibility
939
1058
  channel.setBypassDnd(true)
940
1059
  channel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
941
1060
 
@@ -961,7 +1080,6 @@ object CallEngine {
961
1080
  val context = requireContext()
962
1081
  Log.d(TAG, "Showing incoming call UI for $callId")
963
1082
 
964
- // **SIMPLE LOGIC**: If locked -> overlay, if unlocked -> notification
965
1083
  if (isDeviceLocked(context)) {
966
1084
  Log.d(TAG, "Device is locked - using overlay approach")
967
1085
  showCallActivityOverlay(context, callId, callerName, callType, callerPicUrl)
@@ -973,9 +1091,6 @@ object CallEngine {
973
1091
  playRingtone()
974
1092
  }
975
1093
 
976
- /**
977
- * Simple check if device is locked
978
- */
979
1094
  private fun isDeviceLocked(context: Context): Boolean {
980
1095
  val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
981
1096
  val isLocked = keyguardManager.isKeyguardLocked
@@ -983,9 +1098,6 @@ object CallEngine {
983
1098
  return isLocked
984
1099
  }
985
1100
 
986
- /**
987
- * Shows the call activity directly as overlay (when device is locked)
988
- */
989
1101
  private fun showCallActivityOverlay(context: Context, callId: String, callerName: String, callType: String, callerPicUrl: String?) {
990
1102
  val overlayIntent = Intent(context, CallActivity::class.java).apply {
991
1103
  addFlags(
@@ -1001,11 +1113,10 @@ object CallEngine {
1001
1113
  putExtra("callerAvatar", callerPicUrl)
1002
1114
  }
1003
1115
 
1004
- putExtra("LOCK_SCREEN_MODE", true) // Flag to identify lock screen mode
1116
+ putExtra("LOCK_SCREEN_MODE", true)
1005
1117
  }
1006
1118
 
1007
1119
  try {
1008
- // Wake the screen when device is locked
1009
1120
  val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
1010
1121
  val wakeLock = powerManager.newWakeLock(
1011
1122
  PowerManager.SCREEN_BRIGHT_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP,
@@ -1013,22 +1124,15 @@ object CallEngine {
1013
1124
  )
1014
1125
  wakeLock.acquire(5000)
1015
1126
 
1016
- // Launch the activity overlay
1017
1127
  context.startActivity(overlayIntent)
1018
1128
  Log.d(TAG, "Successfully launched CallActivity overlay for locked device")
1019
1129
 
1020
- // **NO NOTIFICATION** when using overlay
1021
-
1022
1130
  } catch (e: Exception) {
1023
1131
  Log.e(TAG, "Overlay failed, falling back to notification: ${e.message}")
1024
- // If overlay fails, fall back to notification
1025
1132
  showStandardNotification(context, callId, callerName, callType, callerPicUrl)
1026
1133
  }
1027
1134
  }
1028
1135
 
1029
- /**
1030
- * Shows standard notification with full-screen intent (when device is unlocked)
1031
- */
1032
1136
  private fun showStandardNotification(context: Context, callId: String, callerName: String, callType: String, callerPicUrl: String?) {
1033
1137
  createNotificationChannel()
1034
1138
  val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@@ -1041,7 +1145,6 @@ object CallEngine {
1041
1145
  if (callerPicUrl != null) {
1042
1146
  putExtra("callerAvatar", callerPicUrl)
1043
1147
  }
1044
- // No lock screen flag - standard notification mode
1045
1148
  }
1046
1149
 
1047
1150
  val fullScreenPendingIntent = PendingIntent.getActivity(
@@ -1240,17 +1343,23 @@ object CallEngine {
1240
1343
  }
1241
1344
 
1242
1345
  // --- Media Management ---
1346
+ // FIXED: Improved ringtone management with proper volume monitoring
1243
1347
  private fun playRingtone() {
1244
1348
  val context = requireContext()
1245
1349
 
1246
1350
  audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
1247
- // 0) ensure the ring plays out loud on the speaker
1351
+
1352
+ // Set up proper audio mode for ringtone
1248
1353
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
1249
1354
  audioManager?.mode = AudioManager.MODE_RINGTONE
1250
1355
  }
1251
1356
  audioManager?.isSpeakerphoneOn = true
1252
1357
 
1253
- // 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
1254
1363
  vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
1255
1364
  vibrator?.let { v ->
1256
1365
  val pattern = longArrayOf(0L, 500L, 500L)
@@ -1262,33 +1371,15 @@ object CallEngine {
1262
1371
  }
1263
1372
  }
1264
1373
 
1265
- // 2) play the ringtone
1374
+ // Play the ringtone
1266
1375
  try {
1267
1376
  val uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
1268
1377
  ringtone = RingtoneManager.getRingtone(context, uri)
1269
1378
  ringtone?.play()
1379
+ Log.d(TAG, "Ringtone started playing")
1270
1380
  } catch (e: Exception) {
1271
1381
  Log.e(TAG, "Failed to play ringtone", e)
1272
1382
  }
1273
-
1274
- // 3) register a receiver to catch volume‐key presses on the RING stream
1275
- if (volumeReceiver == null) {
1276
- val filter = IntentFilter("android.media.VOLUME_CHANGED_ACTION")
1277
- volumeReceiver = object : BroadcastReceiver() {
1278
- override fun onReceive(ctx: Context, intent: Intent) {
1279
- if (intent.action == "android.media.VOLUME_CHANGED_ACTION") {
1280
- val streamType = intent.getIntExtra(
1281
- "android.media.EXTRA_VOLUME_STREAM_TYPE", -1
1282
- )
1283
- if (streamType == AudioManager.STREAM_RING) {
1284
- Log.d(TAG, "Volume key pressed on RING → silencing ringtone/vibrate")
1285
- stopRingtone()
1286
- }
1287
- }
1288
- }
1289
- }
1290
- context.registerReceiver(volumeReceiver, filter)
1291
- }
1292
1383
  }
1293
1384
 
1294
1385
  fun stopRingtone() {
@@ -1300,15 +1391,12 @@ object CallEngine {
1300
1391
  }
1301
1392
  ringtone = null
1302
1393
 
1303
- // cancel vibration
1394
+ // Cancel vibration
1304
1395
  vibrator?.cancel()
1305
1396
  vibrator = null
1306
1397
 
1307
- // unregister our volume‐change receiver
1308
- volumeReceiver?.let {
1309
- requireContext().unregisterReceiver(it)
1310
- volumeReceiver = null
1311
- }
1398
+ // Stop volume monitoring
1399
+ stopVolumeMonitoring()
1312
1400
  }
1313
1401
 
1314
1402
  private fun startRingback() {
@@ -1345,6 +1433,10 @@ object CallEngine {
1345
1433
  stopForegroundService()
1346
1434
  keepScreenAwake(false)
1347
1435
  resetAudioMode()
1436
+ stopVolumeMonitoring()
1437
+ audioRouteHandler.removeCallbacksAndMessages(null)
1438
+ pendingAudioRoute = null
1439
+ audioRouteRetryCount = 0
1348
1440
  }
1349
1441
 
1350
1442
  // --- Lifecycle Management ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qusaieilouti99/call-manager",
3
- "version": "0.1.111",
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",