@qusaieilouti99/call-manager 0.1.113 → 0.1.115

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.
@@ -42,8 +42,7 @@ 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
+ import android.view.KeyEvent
47
46
 
48
47
  /**
49
48
  * Core call‐management engine. Manages self-managed telecom calls,
@@ -55,16 +54,7 @@ object CallEngine {
55
54
  private const val NOTIF_CHANNEL_ID = "incoming_call_channel"
56
55
  private const val NOTIF_ID = 2001
57
56
 
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
-
63
57
  // --- NEW: Call-end listener API ---
64
- /**
65
- * Implement this in your UI (CallActivity). Will be called
66
- * on the main thread whenever a call is ended.
67
- */
68
58
  interface CallEndListener {
69
59
  fun onCallEnded(callId: String)
70
60
  }
@@ -82,30 +72,28 @@ object CallEngine {
82
72
  fun unregisterCallEndListener(l: CallEndListener) {
83
73
  callEndListeners.remove(l)
84
74
  }
85
- // --- end listener API ---
86
75
 
87
76
  // Core context - initialized once and maintained
88
77
  @Volatile private var appContext: Context? = null
89
78
  private val isInitialized = AtomicBoolean(false)
90
79
  private val initializationLock = Any()
91
80
 
92
- // Audio & Media Management
81
+ // Simplified Audio & Media Management
93
82
  private var ringtone: android.media.Ringtone? = null
94
83
  private var ringbackPlayer: MediaPlayer? = null
95
84
 
96
- // Volume monitoring
85
+ // FIXED: New volume key handling approach
97
86
  private var vibrator: Vibrator? = null
98
- private var volumeContentObserver: ContentObserver? = null
99
- private var lastRingVolumeLevel = -1
100
- private var isMonitoringVolume = false
87
+ private var volumeKeyReceiver: BroadcastReceiver? = null
88
+ private var isRingtonePlaying = false
101
89
 
102
90
  private var audioManager: AudioManager? = null
103
91
  private var wakeLock: PowerManager.WakeLock? = null
104
92
 
105
- // Audio routing state
106
- private var pendingAudioRoute: String? = null
107
- private var audioRouteRetryCount = 0
108
- private val audioRouteHandler = Handler(Looper.getMainLooper())
93
+ // FIXED: New audio routing approach
94
+ private var desiredAudioRoute: String? = null
95
+ private var isWaitingForTelecomAudioState = false
96
+ private val audioRoutingHandler = Handler(Looper.getMainLooper())
109
97
 
110
98
  // Call State Management
111
99
  private val activeCalls = ConcurrentHashMap<String, CallInfo>()
@@ -136,7 +124,7 @@ object CallEngine {
136
124
  if (isInitialized.compareAndSet(false, true)) {
137
125
  appContext = context.applicationContext
138
126
  audioManager = appContext?.getSystemService(Context.AUDIO_SERVICE) as? AudioManager
139
- setupVolumeMonitoring()
127
+ setupVolumeKeyDetection()
140
128
  Log.d(TAG, "CallEngine initialized successfully")
141
129
 
142
130
  if (isCallActive()) {
@@ -154,61 +142,69 @@ object CallEngine {
154
142
  )
155
143
  }
156
144
 
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()
145
+ // --- FIXED: Volume Key Detection for Ringtone Silencing ---
146
+ private fun setupVolumeKeyDetection() {
147
+ volumeKeyReceiver = object : BroadcastReceiver() {
148
+ override fun onReceive(context: Context, intent: Intent) {
149
+ when (intent.action) {
150
+ Intent.ACTION_MEDIA_BUTTON -> {
151
+ val keyEvent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
152
+ intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT, KeyEvent::class.java)
153
+ } else {
154
+ @Suppress("DEPRECATION")
155
+ intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT) as? KeyEvent
156
+ }
157
+
158
+ if (keyEvent?.action == KeyEvent.ACTION_DOWN) {
159
+ when (keyEvent.keyCode) {
160
+ KeyEvent.KEYCODE_VOLUME_UP,
161
+ KeyEvent.KEYCODE_VOLUME_DOWN -> {
162
+ if (isRingtonePlaying) {
163
+ Log.d(TAG, "Volume key pressed - silencing ringtone")
164
+ stopRingtone()
165
+ }
166
+ }
167
+ }
168
+ }
169
+ }
170
+ "android.media.VOLUME_CHANGED_ACTION" -> {
171
+ // Secondary fallback for some devices
172
+ if (isRingtonePlaying) {
173
+ Log.d(TAG, "Volume changed while ringing - silencing ringtone")
174
+ stopRingtone()
175
+ }
176
+ }
177
+ }
169
178
  }
170
179
  }
171
180
  }
172
181
 
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)
182
+ private fun startVolumeKeyMonitoring() {
183
+ val context = requireContext()
184
+ if (volumeKeyReceiver != null) {
185
+ val filter = IntentFilter().apply {
186
+ addAction(Intent.ACTION_MEDIA_BUTTON)
187
+ addAction("android.media.VOLUME_CHANGED_ACTION")
188
+ priority = IntentFilter.SYSTEM_HIGH_PRIORITY
189
+ }
190
+ try {
191
+ context.registerReceiver(volumeKeyReceiver, filter)
192
+ Log.d(TAG, "Volume key monitoring started")
193
+ } catch (e: Exception) {
194
+ Log.w(TAG, "Failed to register volume key receiver: ${e.message}")
195
195
  }
196
- Log.d(TAG, "Stopped volume monitoring")
197
196
  }
198
197
  }
199
198
 
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()
199
+ private fun stopVolumeKeyMonitoring() {
200
+ val context = requireContext()
201
+ volumeKeyReceiver?.let {
202
+ try {
203
+ context.unregisterReceiver(it)
204
+ Log.d(TAG, "Volume key monitoring stopped")
205
+ } catch (e: Exception) {
206
+ Log.w(TAG, "Failed to unregister volume key receiver: ${e.message}")
207
+ }
212
208
  }
213
209
  }
214
210
 
@@ -276,6 +272,61 @@ object CallEngine {
276
272
 
277
273
  fun getTelecomConnection(callId: String): Connection? = telecomConnections[callId]
278
274
 
275
+ // --- FIXED: Audio Route Management via Telecom Callback ---
276
+ /**
277
+ * Called by MyConnection when telecom audio state changes.
278
+ * This is the key to solving the audio routing issue.
279
+ */
280
+ fun onTelecomAudioStateChanged(callId: String, audioState: CallAudioState) {
281
+ Log.d(TAG, "Telecom audio state changed for $callId: route=${audioState.route}, muted=${audioState.isMuted}")
282
+
283
+ // If we have a desired route that differs from current, apply it now
284
+ desiredAudioRoute?.let { desired ->
285
+ val currentTelecomRoute = telecomRouteToString(audioState.route)
286
+ if (currentTelecomRoute != desired) {
287
+ Log.d(TAG, "Applying desired route $desired (current: $currentTelecomRoute)")
288
+ applyAudioRouteThroughTelecom(callId, desired)
289
+ } else {
290
+ Log.d(TAG, "Desired route $desired matches current route")
291
+ desiredAudioRoute = null // Clear since we're in sync
292
+ }
293
+ }
294
+
295
+ // Always emit the current state
296
+ emitAudioRouteChanged()
297
+ }
298
+
299
+ private fun telecomRouteToString(route: Int): String {
300
+ return when (route) {
301
+ CallAudioState.ROUTE_BLUETOOTH -> "Bluetooth"
302
+ CallAudioState.ROUTE_SPEAKER -> "Speaker"
303
+ CallAudioState.ROUTE_WIRED_HEADSET -> "Headset"
304
+ CallAudioState.ROUTE_EARPIECE -> "Earpiece"
305
+ else -> "Unknown"
306
+ }
307
+ }
308
+
309
+ private fun stringToTelecomRoute(route: String): Int {
310
+ return when (route) {
311
+ "Bluetooth" -> CallAudioState.ROUTE_BLUETOOTH
312
+ "Speaker" -> CallAudioState.ROUTE_SPEAKER
313
+ "Headset" -> CallAudioState.ROUTE_WIRED_HEADSET
314
+ "Earpiece" -> CallAudioState.ROUTE_EARPIECE
315
+ else -> CallAudioState.ROUTE_EARPIECE
316
+ }
317
+ }
318
+
319
+ private fun applyAudioRouteThroughTelecom(callId: String, route: String) {
320
+ val connection = telecomConnections[callId]
321
+ if (connection != null) {
322
+ val telecomRoute = stringToTelecomRoute(route)
323
+ connection.setAudioRoute(telecomRoute)
324
+ Log.d(TAG, "Set audio route via telecom connection: $route")
325
+ } else {
326
+ Log.w(TAG, "No telecom connection found for $callId, cannot set audio route")
327
+ }
328
+ }
329
+
279
330
  // --- Public API ---
280
331
  fun setCanMakeMultipleCalls(allow: Boolean) {
281
332
  canMakeMultipleCalls = allow
@@ -467,10 +518,8 @@ object CallEngine {
467
518
  startForegroundService()
468
519
  keepScreenAwake(true)
469
520
 
470
- // Delay initial audio route to avoid conflicts
471
- mainHandler.postDelayed({
472
- setInitialAudioRoute(callType)
473
- }, AUDIO_ROUTE_DELAY_AFTER_ANSWER)
521
+ // Set desired initial route, will be applied when telecom state changes
522
+ setInitialAudioRoute(callType)
474
523
 
475
524
  updateLockScreenBypass()
476
525
 
@@ -517,10 +566,8 @@ object CallEngine {
517
566
  keepScreenAwake(true)
518
567
  updateLockScreenBypass()
519
568
 
520
- // FIXED: Delay audio routing to avoid conflicts with system
521
- mainHandler.postDelayed({
522
- setInitialAudioRoute(callInfo.callType)
523
- }, AUDIO_ROUTE_DELAY_AFTER_ANSWER)
569
+ // FIXED: Set initial route via telecom system
570
+ setInitialAudioRoute(callInfo.callType)
524
571
 
525
572
  if (isLocalAnswer) {
526
573
  emitCallAnsweredWithMetadata(callId)
@@ -807,110 +854,20 @@ object CallEngine {
807
854
  return AudioRoutesInfo(devices.toTypedArray(), current)
808
855
  }
809
856
 
810
- // FIXED: Improved audio route handling with retries and proper timing
857
+ // FIXED: New audio route approach - work with telecom system instead of against it
811
858
  fun setAudioRoute(route: String) {
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) {
826
- val ctx = requireContext()
827
- if (audioManager == null) {
828
- audioManager = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager
829
- }
830
- val am = audioManager!!
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
-
839
- when (route) {
840
- "Speaker" -> {
841
- // Disable Bluetooth SCO first if active
842
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && am.isBluetoothScoOn) {
843
- am.stopBluetoothSco()
844
- am.isBluetoothScoOn = false
845
- }
846
- am.isSpeakerphoneOn = true
847
- Log.d(TAG, "Audio routed to SPEAKER")
848
- }
849
-
850
- "Earpiece" -> {
851
- // Disable Bluetooth SCO if active
852
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && am.isBluetoothScoOn) {
853
- am.stopBluetoothSco()
854
- am.isBluetoothScoOn = false
855
- }
856
- am.isSpeakerphoneOn = false
857
- Log.d(TAG, "Audio routed to EARPIECE")
858
- }
859
-
860
- "Bluetooth" -> {
861
- am.isSpeakerphoneOn = false
862
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
863
- am.startBluetoothSco()
864
- am.isBluetoothScoOn = true
865
- Log.d(TAG, "Audio routed to BLUETOOTH (SCO started)")
866
- } else {
867
- Log.w(TAG, "Bluetooth SCO not supported on this OS")
868
- }
869
- }
870
-
871
- "Headset" -> {
872
- am.isSpeakerphoneOn = false
873
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && am.isBluetoothScoOn) {
874
- am.stopBluetoothSco()
875
- am.isBluetoothScoOn = false
876
- }
877
- Log.d(TAG, "Audio routed to HEADSET (wired)")
878
- }
879
-
880
- else -> {
881
- Log.w(TAG, "Unknown audio route: $route")
882
- return
883
- }
884
- }
859
+ Log.d(TAG, "setAudioRoute requested: $route")
860
+ desiredAudioRoute = route
885
861
 
886
- // Verify the route was applied and retry if necessary
887
- audioRouteHandler.postDelayed({
888
- verifyAudioRoute(route)
889
- }, AUDIO_ROUTE_RETRY_DELAY)
862
+ // Try to apply immediately if we have an active call
863
+ val activeCallId = activeCalls.values.find {
864
+ it.state == CallState.ACTIVE
865
+ }?.callId
890
866
 
891
- emitAudioRouteChanged()
892
- }
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)
867
+ if (activeCallId != null) {
868
+ applyAudioRouteThroughTelecom(activeCallId, route)
905
869
  } 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
870
+ Log.d(TAG, "No active call found, route will be applied when call becomes active")
914
871
  }
915
872
  }
916
873
 
@@ -1343,21 +1300,23 @@ object CallEngine {
1343
1300
  }
1344
1301
 
1345
1302
  // --- Media Management ---
1346
- // FIXED: Improved ringtone management with proper volume monitoring
1303
+ // FIXED: Improved ringtone with volume key detection
1347
1304
  private fun playRingtone() {
1348
1305
  val context = requireContext()
1349
1306
 
1350
1307
  audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
1351
1308
 
1309
+ // Set ringtone playing flag
1310
+ isRingtonePlaying = true
1311
+
1352
1312
  // Set up proper audio mode for ringtone
1353
1313
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
1354
1314
  audioManager?.mode = AudioManager.MODE_RINGTONE
1355
1315
  }
1356
1316
  audioManager?.isSpeakerphoneOn = true
1357
1317
 
1358
- // Start volume monitoring for silencing
1359
- startVolumeMonitoring()
1360
- lastRingVolumeLevel = audioManager?.getStreamVolume(AudioManager.STREAM_RING) ?: 0
1318
+ // Start volume key monitoring
1319
+ startVolumeKeyMonitoring()
1361
1320
 
1362
1321
  // Start repeating vibration
1363
1322
  vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
@@ -1383,6 +1342,8 @@ object CallEngine {
1383
1342
  }
1384
1343
 
1385
1344
  fun stopRingtone() {
1345
+ isRingtonePlaying = false
1346
+
1386
1347
  try {
1387
1348
  ringtone?.stop()
1388
1349
  Log.d(TAG, "Ringtone stopped")
@@ -1395,8 +1356,8 @@ object CallEngine {
1395
1356
  vibrator?.cancel()
1396
1357
  vibrator = null
1397
1358
 
1398
- // Stop volume monitoring
1399
- stopVolumeMonitoring()
1359
+ // Stop volume key monitoring
1360
+ stopVolumeKeyMonitoring()
1400
1361
  }
1401
1362
 
1402
1363
  private fun startRingback() {
@@ -1433,10 +1394,9 @@ object CallEngine {
1433
1394
  stopForegroundService()
1434
1395
  keepScreenAwake(false)
1435
1396
  resetAudioMode()
1436
- stopVolumeMonitoring()
1437
- audioRouteHandler.removeCallbacksAndMessages(null)
1438
- pendingAudioRoute = null
1439
- audioRouteRetryCount = 0
1397
+ stopVolumeKeyMonitoring()
1398
+ desiredAudioRoute = null
1399
+ isWaitingForTelecomAudioState = false
1440
1400
  }
1441
1401
 
1442
1402
  // --- Lifecycle Management ---
@@ -130,4 +130,22 @@ class MyConnection(
130
130
  }
131
131
  }
132
132
  }
133
+
134
+ override fun onCallAudioStateChanged(state: CallAudioState) {
135
+ super.onCallAudioStateChanged(state)
136
+ Log.d(TAG, "Audio state changed for callId: $callId. muted=${state.isMuted}, route=${state.route}")
137
+
138
+ if (lastAudioState == null || lastAudioState!!.isMuted != state.isMuted) {
139
+ if (state.isMuted) {
140
+ CallEngine.muteCall(callId)
141
+ } else {
142
+ CallEngine.unmuteCall(callId)
143
+ }
144
+ }
145
+
146
+ // FIXED: Notify CallEngine of telecom audio state changes
147
+ CallEngine.onTelecomAudioStateChanged(callId, state)
148
+
149
+ lastAudioState = state
150
+ }
133
151
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qusaieilouti99/call-manager",
3
- "version": "0.1.113",
3
+ "version": "0.1.115",
4
4
  "description": "Call manager",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",