@qusaieilouti99/call-manager 0.1.43 → 0.1.45

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.
@@ -1,4 +1,5 @@
1
1
  package com.margelo.nitro.qusaieilouti99.callmanager
2
+
2
3
  import android.app.Activity
3
4
  import android.content.Intent
4
5
  import android.os.Build
@@ -48,6 +49,7 @@ class CallActivity : Activity() {
48
49
  callType = intent.getStringExtra("callType") ?: "Audio"
49
50
  Log.d(TAG, "CallActivity received callId: $callId, callType: $callType")
50
51
 
52
+ // FIXED: Immediate cleanup of notifications when CallActivity is shown
51
53
  CallEngine.cancelIncomingCallUI(this)
52
54
 
53
55
  val callerName = intent.getStringExtra("callerName") ?: "Unknown"
@@ -60,7 +62,9 @@ class CallActivity : Activity() {
60
62
  answerBtn.setOnClickListener {
61
63
  Log.d(TAG, "CallActivity: Answer button clicked for callId: $callId")
62
64
  finishReason = FinishReason.ANSWER
63
- CallEngine.bringAppToForeground(this)
65
+
66
+ // FIXED: Use single source of truth - this will handle all cleanup
67
+ CallEngine.answerCall(this, callId)
64
68
  finishCallActivity()
65
69
  }
66
70
 
@@ -78,7 +82,10 @@ class CallActivity : Activity() {
78
82
  super.onDestroy()
79
83
  Log.d(TAG, "CallActivity onDestroy for callId: $callId. Reason: $finishReason")
80
84
  timeoutHandler.removeCallbacks(timeoutRunnable)
85
+
86
+ // FIXED: Ensure cleanup happens regardless of how activity ends
81
87
  CallEngine.stopRingtone()
88
+ CallEngine.cancelIncomingCallUI(this)
82
89
  }
83
90
 
84
91
  override fun onBackPressed() {
@@ -105,7 +105,6 @@ object CallEngine {
105
105
  }
106
106
 
107
107
  // --- Public API ---
108
-
109
108
  fun setCanMakeMultipleCalls(allow: Boolean) {
110
109
  canMakeMultipleCalls = allow
111
110
  Log.d(TAG, "canMakeMultipleCalls set to: $allow")
@@ -164,6 +163,7 @@ object CallEngine {
164
163
  telecomManager.addNewIncomingCall(phoneAccountHandle, extras)
165
164
  startForegroundService(context)
166
165
  Log.d(TAG, "Successfully reported incoming call to TelecomManager for $callId")
166
+ // REMOVED: Don't bring app to foreground for incoming calls - only when answered
167
167
  } catch (e: SecurityException) {
168
168
  Log.e(TAG, "SecurityException: Failed to report incoming call. Check MANAGE_OWN_CALLS permission: ${e.message}", e)
169
169
  endCall(context, callId)
@@ -174,7 +174,6 @@ object CallEngine {
174
174
  notifyCallStateChanged(context)
175
175
  }
176
176
 
177
- // CORRECTED: Uses TelecomManager.placeCall for outgoing calls.
178
177
  fun startOutgoingCall(context: Context, callId: String, callData: String) {
179
178
  appContext = context.applicationContext
180
179
  Log.d(TAG, "startOutgoingCall: $callId, $callData")
@@ -218,6 +217,7 @@ object CallEngine {
218
217
  startForegroundService(context)
219
218
  Log.d(TAG, "Successfully reported outgoing call to TelecomManager via placeCall for $callId")
220
219
  startRingback()
220
+ // CHANGED: Bring app to foreground for outgoing calls
221
221
  bringAppToForeground(context)
222
222
  keepScreenAwake(context, true)
223
223
  if (parsedCallType == "Video") {
@@ -235,33 +235,46 @@ object CallEngine {
235
235
  notifyCallStateChanged(context)
236
236
  }
237
237
 
238
+ // SINGLE SOURCE OF TRUTH: Core function for handling when remote party answers
238
239
  fun callAnsweredFromJS(context: Context, callId: String) {
239
240
  Log.d(TAG, "callAnsweredFromJS: $callId - remote party answered")
240
- stopRingback()
241
- activeCalls[callId]?.state = CallState.ACTIVE
242
- currentCallId = callId
243
- if (!canMakeMultipleCalls) {
244
- activeCalls.filter { it.key != callId }.values.forEach { it.state = CallState.HELD }
245
- }
246
- notifyCallStateChanged(context)
247
- emitEvent(CallEventType.CALL_ANSWERED, JSONObject().put("callId", callId))
248
- keepScreenAwake(context, true)
249
- resetAudioMode(context)
241
+ coreCallAnswered(context, callId, isLocalAnswer = false)
250
242
  }
251
243
 
244
+ // SINGLE SOURCE OF TRUTH: Core function for handling local answer (user answering)
252
245
  fun answerCall(context: Context, callId: String) {
253
246
  Log.d(TAG, "answerCall: $callId - local party answering")
247
+ coreCallAnswered(context, callId, isLocalAnswer = true)
248
+ }
249
+
250
+ // SINGLE SOURCE OF TRUTH: Core function that handles ALL call answering logic
251
+ private fun coreCallAnswered(context: Context, callId: String, isLocalAnswer: Boolean) {
252
+ Log.d(TAG, "coreCallAnswered: $callId, isLocalAnswer: $isLocalAnswer")
253
+
254
+ // Stop all ringtones and notifications immediately
254
255
  stopRingtone()
256
+ stopRingback()
257
+ cancelIncomingCallUI(context)
258
+
259
+ // Update call state
255
260
  activeCalls[callId]?.state = CallState.ACTIVE
256
261
  currentCallId = callId
262
+
257
263
  if (!canMakeMultipleCalls) {
258
264
  activeCalls.filter { it.key != callId }.values.forEach { it.state = CallState.HELD }
259
265
  }
260
- emitEvent(CallEventType.CALL_ANSWERED, JSONObject().put("callId", callId))
261
- notifyCallStateChanged(context)
266
+
267
+ // Bring app to foreground only when call is answered
268
+ bringAppToForeground(context)
262
269
  startForegroundService(context)
263
270
  keepScreenAwake(context, true)
264
271
  resetAudioMode(context)
272
+
273
+ // Emit event
274
+ emitEvent(CallEventType.CALL_ANSWERED, JSONObject().put("callId", callId))
275
+ notifyCallStateChanged(context)
276
+
277
+ Log.d(TAG, "Call $callId successfully answered and UI cleaned up")
265
278
  }
266
279
 
267
280
  fun holdCall(context: Context, callId: String) {
@@ -295,73 +308,113 @@ object CallEngine {
295
308
  emitEvent(CallEventType.CALL_UNMUTED, JSONObject().put("callId", callId))
296
309
  }
297
310
 
311
+ // SINGLE SOURCE OF TRUTH: Core function that handles ALL call ending logic
298
312
  fun endCall(context: Context, callId: String) {
299
313
  appContext = context.applicationContext
300
314
  Log.d(TAG, "endCall: $callId")
315
+
316
+ // Core cleanup logic
317
+ coreEndCall(context, callId)
318
+ }
319
+
320
+ fun endAllCalls(context: Context) {
321
+ Log.d(TAG, "endAllCalls: Ending all active calls.")
322
+ if (activeCalls.isEmpty()) {
323
+ Log.d(TAG, "No active calls, nothing to do.")
324
+ return
325
+ }
326
+ activeCalls.keys.toList().forEach { callId ->
327
+ coreEndCall(context, callId)
328
+ }
329
+ activeCalls.clear()
330
+ telecomConnections.clear()
331
+ currentCallId = null
332
+
333
+ // Final cleanup
334
+ finalCleanup(context)
335
+ notifyCallStateChanged(context)
336
+ }
337
+
338
+ // SINGLE SOURCE OF TRUTH: Core function that handles ending a single call
339
+ private fun coreEndCall(context: Context, callId: String) {
340
+ Log.d(TAG, "coreEndCall: $callId")
341
+
342
+ // Update call state
301
343
  activeCalls[callId]?.state = CallState.ENDED
302
344
  activeCalls.remove(callId)
303
345
  Log.d(TAG, "Call $callId removed from activeCalls. Remaining: ${activeCalls.size}")
304
346
 
347
+ // Stop ringtones and notifications
305
348
  stopRingback()
306
349
  stopRingtone()
350
+ cancelIncomingCallUI(context)
307
351
 
352
+ // Update current call
308
353
  if (currentCallId == callId) {
309
354
  currentCallId = activeCalls.filter { it.value.state != CallState.ENDED }.keys.firstOrNull()
310
355
  Log.d(TAG, "Current call was $callId. New currentCallId: $currentCallId")
311
356
  }
312
357
 
313
- cancelIncomingCallUI(context)
314
-
358
+ // Handle telecom connection
315
359
  val connection = telecomConnections[callId]
316
360
  if (connection != null) {
317
361
  connection.setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
318
362
  connection.destroy()
319
363
  removeTelecomConnection(callId)
320
364
  Log.d(TAG, "Telecom Connection for $callId disconnected and destroyed.")
321
- } else {
322
- Log.d(TAG, "No Telecom Connection found for callId=$callId. Likely an outgoing call not initially handled by Telecom.")
323
365
  }
324
366
 
367
+ // If no more calls, do final cleanup
325
368
  if (activeCalls.isEmpty()) {
326
- Log.d(TAG, "No active calls remaining. Stopping foreground service and releasing resources.")
327
- stopForegroundService(context)
328
- keepScreenAwake(context, false)
329
- resetAudioMode(context)
330
- } else {
331
- Log.d(TAG, "Other calls still active. Foreground service remains.")
369
+ finalCleanup(context)
332
370
  }
333
371
 
334
372
  emitEvent(CallEventType.CALL_ENDED, JSONObject().put("callId", callId))
335
373
  notifyCallStateChanged(context)
336
374
  }
337
375
 
338
- fun endAllCalls(context: Context) {
339
- Log.d(TAG, "endAllCalls: Ending all active calls.")
340
- if (activeCalls.isEmpty()) {
341
- Log.d(TAG, "No active calls, nothing to do.")
342
- return
343
- }
344
- activeCalls.keys.toList().forEach { callId ->
345
- endCall(context, callId)
346
- }
347
- activeCalls.clear()
348
- telecomConnections.clear()
349
- currentCallId = null
350
- cancelIncomingCallUI(context)
376
+ // SINGLE SOURCE OF TRUTH: Final cleanup when all calls are ended
377
+ private fun finalCleanup(context: Context) {
378
+ Log.d(TAG, "Performing final cleanup - no active calls remaining")
379
+
351
380
  stopForegroundService(context)
352
- stopRingtone()
353
- stopRingback()
354
381
  keepScreenAwake(context, false)
355
382
  resetAudioMode(context)
356
- notifyCallStateChanged(context)
383
+
384
+ // FIXED: Clear lock screen bypass when calls end
385
+ clearLockScreenBypass(context)
386
+ }
387
+
388
+ // NEW: Function to clear lock screen bypass
389
+ private fun clearLockScreenBypass(context: Context) {
390
+ try {
391
+ if (context is Activity) {
392
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
393
+ context.setShowWhenLocked(false)
394
+ context.setTurnScreenOn(false)
395
+ } else {
396
+ context.window.clearFlags(
397
+ android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
398
+ android.view.WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
399
+ android.view.WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
400
+ )
401
+ }
402
+ Log.d(TAG, "Lock screen bypass flags cleared for Activity")
403
+ }
404
+ } catch (e: Exception) {
405
+ Log.w(TAG, "Could not clear lock screen bypass flags: ${e.message}")
406
+ }
357
407
  }
358
408
 
359
409
  fun getActiveCalls(): List<CallInfo> = activeCalls.values.toList()
360
410
  fun getCurrentCallId(): String? = currentCallId
361
- fun isCallActive(): Boolean = activeCalls.any { it.value.state == CallState.ACTIVE || it.value.state == CallState.INCOMING || it.value.state == CallState.DIALING }
362
-
363
- // --- Notification/UI/Foreground ---
411
+ fun isCallActive(): Boolean = activeCalls.any {
412
+ it.value.state == CallState.ACTIVE ||
413
+ it.value.state == CallState.INCOMING ||
414
+ it.value.state == CallState.DIALING
415
+ }
364
416
 
417
+ // Enhanced incoming call UI with better cleanup
365
418
  fun showIncomingCallUI(context: Context, callId: String, callerName: String, callType: String) {
366
419
  Log.d(TAG, "Showing incoming call UI for $callId, caller: $callerName, callType: $callType")
367
420
  createNotificationChannel(context)
@@ -443,19 +496,22 @@ object CallEngine {
443
496
  val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)
444
497
  launchIntent?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
445
498
 
446
- // The deprecated lock screen flags have been removed
447
- // Lock screen bypass is now handled in the target activity (CallActivity, MainActivity)
499
+ // Handle lock screen bypass for active calls
448
500
  if (isCallActive()) {
449
- Log.d(TAG, "App brought to foreground due to active call - lock screen handling in target activity")
501
+ // Only set lock screen bypass when bringing to foreground during active calls
502
+ launchIntent?.putExtra("BYPASS_LOCK_SCREEN", true)
503
+ Log.d(TAG, "App brought to foreground with lock screen bypass for active call")
450
504
  } else {
451
- Log.d(TAG, "App brought to foreground via normal launchIntent")
505
+ Log.d(TAG, "App brought to foreground without lock screen bypass")
452
506
  }
453
507
 
454
508
  context.startActivity(launchIntent)
455
509
  }
456
510
 
457
- // --- Audio Device Management ---
511
+ // Rest of the methods remain the same...
512
+ // (getAudioDevices, setAudioRoute, keepScreenAwake, etc. - keeping them unchanged for brevity)
458
513
 
514
+ // --- Audio Device Management ---
459
515
  fun getAudioDevices(): AudioRoutesInfo {
460
516
  audioManager = appContext?.getSystemService(Context.AUDIO_SERVICE) as? AudioManager ?: run {
461
517
  Log.e(TAG, "getAudioDevices: AudioManager is null or appContext is not set. Returning default.")
@@ -15,24 +15,21 @@ class CallNotificationActionReceiver : BroadcastReceiver() {
15
15
 
16
16
  when (intent.action) {
17
17
  "com.qusaieilouti99.callmanager.ANSWER_CALL" -> {
18
- Log.d(TAG, "Answer action received for callId: $callId. Answering call via Telecom.")
18
+ Log.d(TAG, "Answer action received for callId: $callId")
19
19
 
20
- // Get the telecom connection and answer it
20
+ // FIXED: Use single source of truth for answering
21
21
  val connection = CallEngine.getTelecomConnection(callId)
22
22
  if (connection != null) {
23
- // This will trigger MyConnection.onAnswer() which handles the rest
24
- connection.onAnswer()
23
+ connection.onAnswer() // This will trigger MyConnection.onAnswer()
25
24
  Log.d(TAG, "Call answered via Telecom connection for callId: $callId")
26
25
  } else {
27
- Log.e(TAG, "No Telecom connection found for callId: $callId. Falling back to direct answer.")
28
- // Fallback: answer directly via CallEngine
26
+ Log.e(TAG, "No Telecom connection found for callId: $callId. Using direct answer.")
29
27
  CallEngine.answerCall(context, callId)
30
28
  }
31
-
32
- CallEngine.bringAppToForeground(context)
29
+ // NOTE: Don't call bringAppToForeground here - it's handled in coreCallAnswered
33
30
  }
34
31
  "com.qusaieilouti99.callmanager.DECLINE_CALL" -> {
35
- Log.d(TAG, "Decline action received for callId: $callId. Ending call.")
32
+ Log.d(TAG, "Decline action received for callId: $callId")
36
33
  CallEngine.endCall(context, callId)
37
34
  }
38
35
  else -> {
@@ -32,9 +32,8 @@ class MyConnection(
32
32
  JSONObject(callDataJson).optString("callType", "Audio")
33
33
  } catch (e: Exception) { "Audio" }
34
34
 
35
- // Set connection properties and capabilities
36
35
  connectionProperties = Connection.PROPERTY_SELF_MANAGED
37
- connectionCapabilities = Connection.CAPABILITY_SUPPORT_HOLD or Connection.CAPABILITY_MUTE // Added hold and mute capabilities
36
+ connectionCapabilities = Connection.CAPABILITY_SUPPORT_HOLD or Connection.CAPABILITY_MUTE
38
37
 
39
38
  if (currentCallType == "Video") {
40
39
  Log.d(TAG, "MyConnection for callId $callId initialized as VIDEO call.")
@@ -51,8 +50,9 @@ class MyConnection(
51
50
  override fun onAnswer() {
52
51
  Log.d(TAG, "Call answered via Telecom for callId: $callId")
53
52
  setActive()
53
+
54
+ // FIXED: Use single source of truth for answering
54
55
  CallEngine.answerCall(context, callId)
55
- CallEngine.bringAppToForeground(context)
56
56
  }
57
57
 
58
58
  override fun onReject() {
@@ -83,7 +83,7 @@ class MyConnection(
83
83
 
84
84
  override fun onCallAudioStateChanged(state: CallAudioState) {
85
85
  super.onCallAudioStateChanged(state)
86
- Log.d(TAG, "Audio state changed for callId: $callId. muted=${state.isMuted}, route=${state.route}, supportedRoutes=${state.supportedRouteMask}")
86
+ Log.d(TAG, "Audio state changed for callId: $callId. muted=${state.isMuted}, route=${state.route}")
87
87
 
88
88
  if (state.isMuted) {
89
89
  CallEngine.muteCall(context, callId)
@@ -98,7 +98,6 @@ class MyConnection(
98
98
  CallAudioState.ROUTE_WIRED_HEADSET -> "Headset"
99
99
  else -> "Unknown"
100
100
  }
101
- Log.d(TAG, "CallAudioState changed, new route detected: $routeName")
102
101
  CallEngine.emitEvent(
103
102
  CallEventType.AUDIO_ROUTE_CHANGED,
104
103
  JSONObject().put("callId", callId).put("route", routeName)
@@ -108,9 +107,6 @@ class MyConnection(
108
107
  override fun onPlayDtmfTone(digit: Char) {
109
108
  super.onPlayDtmfTone(digit)
110
109
  Log.d(TAG, "Playing DTMF tone: $digit for callId: $callId")
111
- // NOTE: DTMF_TONE is not in your current CallEventType enum.
112
- // If you need to emit this event, you MUST add 'DTMF_TONE' to your CallEventType.ts
113
- // and re-run nitro-codegen. For now, it's commented out to prevent compilation error.
114
110
  CallEngine.emitEvent(
115
111
  CallEventType.DTMF_TONE,
116
112
  JSONObject().put("callId", callId).put("digit", digit.toString())
@@ -122,12 +118,10 @@ class MyConnection(
122
118
  Log.d(TAG, "Stopping DTMF tone for callId: $callId")
123
119
  }
124
120
 
125
- // REMOVED: onConnectionEvent override since it doesn't exist in the base Connection class
126
- // If you need connection events, you can send them via sendConnectionEvent() method
127
-
128
121
  override fun onShowIncomingCallUi() {
129
122
  super.onShowIncomingCallUi()
130
- Log.d(TAG, "onShowIncomingCallUi for callId: $callId. Attempting to bring app to foreground.")
131
- CallEngine.bringAppToForeground(context)
123
+ Log.d(TAG, "onShowIncomingCallUi for callId: $callId")
124
+ // REMOVED: Don't bring app to foreground for incoming calls
125
+ // CallEngine.bringAppToForeground(context)
132
126
  }
133
127
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qusaieilouti99/call-manager",
3
- "version": "0.1.43",
3
+ "version": "0.1.45",
4
4
  "description": "Call manager",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",