@qusaieilouti99/call-manager 0.1.21 → 0.1.22

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.
@@ -2,13 +2,25 @@ package com.qusaieilouti99.callmanager
2
2
 
3
3
  import android.app.Activity
4
4
  import android.content.Intent
5
+ import android.os.Build
5
6
  import android.os.Bundle
7
+ import android.os.Handler
8
+ import android.os.Looper
6
9
  import android.view.WindowManager
7
10
  import android.widget.Button
8
- import android.widget.ImageView
9
11
  import android.widget.TextView
10
12
 
11
13
  class CallActivity : Activity() {
14
+
15
+ private enum class FinishReason { ANSWER, DECLINE, TIMEOUT, MANUAL }
16
+ private var finishReason: FinishReason? = null
17
+
18
+ private val timeoutHandler = Handler(Looper.getMainLooper())
19
+ private val timeoutRunnable = Runnable {
20
+ finishReason = FinishReason.TIMEOUT
21
+ finishCallActivity()
22
+ }
23
+
12
24
  override fun onCreate(savedInstanceState: Bundle?) {
13
25
  super.onCreate(savedInstanceState)
14
26
  window.addFlags(
@@ -18,7 +30,6 @@ class CallActivity : Activity() {
18
30
  )
19
31
  setContentView(R.layout.activity_call)
20
32
 
21
- // Cancel the notification as soon as CallActivity is shown
22
33
  CallEngine.cancelIncomingCallUI(this)
23
34
  CallEngine.playRingtone(this)
24
35
 
@@ -31,22 +42,41 @@ class CallActivity : Activity() {
31
42
 
32
43
  answerBtn.setOnClickListener {
33
44
  CallEngine.bringAppToForeground(this)
34
- finish()
45
+ finishReason = FinishReason.ANSWER
46
+ finishCallActivity()
35
47
  }
36
48
 
37
49
  declineBtn.setOnClickListener {
38
50
  CallEngine.cancelIncomingCallUI(this)
39
- CallEngine.stopForegroundService(this)
40
- CallEngine.disconnectTelecomCall(this, intent.getStringExtra("callId") ?: "")
41
- finish()
51
+ finishReason = FinishReason.DECLINE
52
+ finishCallActivity()
42
53
  }
54
+
55
+ timeoutHandler.postDelayed(timeoutRunnable, 60_000)
43
56
  }
44
57
 
45
58
  override fun onDestroy() {
46
59
  super.onDestroy()
60
+ timeoutHandler.removeCallbacks(timeoutRunnable)
47
61
  CallEngine.stopRingtone()
48
62
  CallEngine.cancelIncomingCallUI(this)
49
- CallEngine.stopForegroundService(this)
50
- CallEngine.disconnectTelecomCall(this, intent.getStringExtra("callId") ?: "")
63
+ // Only clean up call if not answered
64
+ if (finishReason == FinishReason.DECLINE || finishReason == FinishReason.TIMEOUT || finishReason == FinishReason.MANUAL) {
65
+ CallEngine.stopForegroundService(this)
66
+ CallEngine.disconnectTelecomCall(this, intent.getStringExtra("callId") ?: "")
67
+ }
68
+ }
69
+
70
+ override fun onBackPressed() {
71
+ finishReason = FinishReason.MANUAL
72
+ finishCallActivity()
73
+ }
74
+
75
+ private fun finishCallActivity() {
76
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
77
+ finishAndRemoveTask()
78
+ } else {
79
+ finish()
80
+ }
51
81
  }
52
82
  }
@@ -8,12 +8,15 @@ import android.media.AudioAttributes
8
8
  import android.media.RingtoneManager
9
9
  import android.os.Build
10
10
  import android.os.Bundle
11
- import android.telecom.PhoneAccount
12
- import android.telecom.PhoneAccountHandle
13
- import android.telecom.TelecomManager
11
+ import android.telecom.*
14
12
  import android.util.Log
15
13
  import android.graphics.Color
16
14
  import android.app.Person
15
+ import java.util.concurrent.ConcurrentHashMap
16
+ import android.media.AudioDeviceInfo
17
+ import android.media.AudioManager
18
+ import android.view.Window
19
+ import android.view.WindowManager
17
20
 
18
21
  object CallEngine {
19
22
  private const val TAG = "CallEngine"
@@ -24,9 +27,29 @@ object CallEngine {
24
27
  private const val FOREGROUND_NOTIF_ID = 1001
25
28
 
26
29
  private var ringtone: android.media.Ringtone? = null
30
+ private var audioManager: AudioManager? = null
31
+
32
+ // --- Multi-call state ---
33
+ private val activeCalls = ConcurrentHashMap<String, CallInfo>() // callId -> CallInfo
34
+ private var currentCallId: String? = null // The call currently in foreground (active/ringing)
35
+ private var canMakeMultipleCalls: Boolean = true
36
+
37
+ data class CallInfo(
38
+ val callId: String,
39
+ val callData: String,
40
+ var state: CallState
41
+ )
42
+
43
+ enum class CallState {
44
+ INCOMING, ACTIVE, HELD, ENDED
45
+ }
27
46
 
28
47
  // --- Public API ---
29
48
 
49
+ fun setCanMakeMultipleCalls(allow: Boolean) {
50
+ canMakeMultipleCalls = allow
51
+ }
52
+
30
53
  fun reportIncomingCall(context: Context, callId: String, callData: String) {
31
54
  Log.d(TAG, "reportIncomingCall: $callId, $callData")
32
55
  val callerName = try {
@@ -34,6 +57,18 @@ object CallEngine {
34
57
  json.optString("name", "Unknown")
35
58
  } catch (e: Exception) { "Unknown" }
36
59
 
60
+ // If not allowed, auto-hold or reject current call
61
+ if (!canMakeMultipleCalls && activeCalls.isNotEmpty()) {
62
+ // Option 1: Auto-hold current call
63
+ activeCalls.values.forEach { it.state = CallState.HELD }
64
+ // Option 2: Reject new call (uncomment to use)
65
+ // return
66
+ }
67
+
68
+ // Add to active calls
69
+ activeCalls[callId] = CallInfo(callId, callData, CallState.INCOMING)
70
+ currentCallId = callId
71
+
37
72
  showIncomingCallUI(context, callId, callerName)
38
73
  registerPhoneAccount(context)
39
74
  val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
@@ -45,13 +80,56 @@ object CallEngine {
45
80
  } catch (e: Exception) {
46
81
  Log.e(TAG, "Failed to report incoming call: ${e.message}")
47
82
  }
83
+ notifyCallStateChanged(context)
84
+ }
85
+
86
+ fun answerCall(context: Context, callId: String) {
87
+ Log.d(TAG, "answerCall: $callId")
88
+ activeCalls[callId]?.state = CallState.ACTIVE
89
+ currentCallId = callId
90
+ // Optionally, auto-hold other calls
91
+ if (!canMakeMultipleCalls) {
92
+ activeCalls.filter { it.key != callId }.values.forEach { it.state = CallState.HELD }
93
+ }
94
+ notifyCallStateChanged(context)
95
+ }
96
+
97
+ fun holdCall(context: Context, callId: String) {
98
+ Log.d(TAG, "holdCall: $callId")
99
+ activeCalls[callId]?.state = CallState.HELD
100
+ notifyCallStateChanged(context)
101
+ }
102
+
103
+ fun endCall(context: Context, callId: String) {
104
+ Log.d(TAG, "endCall: $callId")
105
+ activeCalls[callId]?.state = CallState.ENDED
106
+ activeCalls.remove(callId)
107
+ if (currentCallId == callId) {
108
+ currentCallId = activeCalls.keys.firstOrNull()
109
+ }
110
+ cancelIncomingCallUI(context)
111
+ stopForegroundService(context)
112
+ disconnectTelecomCall(context, callId)
113
+ notifyCallStateChanged(context)
114
+ }
115
+
116
+ fun endAllCalls(context: Context) {
117
+ Log.d(TAG, "endAllCalls")
118
+ activeCalls.keys.toList().forEach { endCall(context, it) }
119
+ currentCallId = null
120
+ notifyCallStateChanged(context)
48
121
  }
49
122
 
123
+ fun getActiveCalls(): List<CallInfo> = activeCalls.values.toList()
124
+ fun getCurrentCallId(): String? = currentCallId
125
+ fun isCallActive(): Boolean = activeCalls.any { it.value.state == CallState.ACTIVE || it.value.state == CallState.INCOMING }
126
+
127
+ // --- Notification/UI/Foreground ---
128
+
50
129
  fun showIncomingCallUI(context: Context, callId: String, callerName: String) {
51
130
  createNotificationChannel(context)
52
131
  val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
53
132
 
54
- // PendingIntents
55
133
  val answerIntent = Intent(context, CallNotificationActionReceiver::class.java).apply {
56
134
  action = "com.qusaieilouti99.callmanager.ANSWER_CALL"
57
135
  putExtra("callId", callId)
@@ -65,7 +143,7 @@ object CallEngine {
65
143
  val declinePendingIntent = PendingIntent.getBroadcast(context, 1, declineIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
66
144
 
67
145
  val fullScreenIntent = Intent(context, CallActivity::class.java).apply {
68
- addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
146
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
69
147
  putExtra("callId", callId)
70
148
  putExtra("callerName", callerName)
71
149
  }
@@ -96,6 +174,7 @@ object CallEngine {
96
174
  }
97
175
 
98
176
  notificationManager.notify(NOTIF_ID, notification)
177
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) playRingtone(context)
99
178
  }
100
179
 
101
180
  fun cancelIncomingCallUI(context: Context) {
@@ -115,7 +194,7 @@ object CallEngine {
115
194
  context.stopService(intent)
116
195
  }
117
196
 
118
- fun disconnectTelecomCall(context: Context, callId: String) {
197
+ fun disconnectTelecomCall(context: Context, callId: String?) {
119
198
  // You can extend this to track and disconnect specific calls if needed.
120
199
  Log.d(TAG, "disconnectTelecomCall called for callId=$callId")
121
200
  }
@@ -130,7 +209,7 @@ object CallEngine {
130
209
 
131
210
  // --- Private helpers ---
132
211
 
133
- fun playRingtone(context: Context) {
212
+ private fun playRingtone(context: Context) {
134
213
  try {
135
214
  Log.d(TAG, "Playing ringtone")
136
215
  val uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
@@ -145,7 +224,7 @@ object CallEngine {
145
224
  }
146
225
  }
147
226
 
148
- fun stopRingtone() {
227
+ private fun stopRingtone() {
149
228
  try { ringtone?.stop() } catch (_: Exception) {}
150
229
  ringtone = null
151
230
  }
@@ -192,4 +271,68 @@ object CallEngine {
192
271
  PHONE_ACCOUNT_ID
193
272
  )
194
273
  }
274
+
275
+ // --- JS/React Native Integration ---
276
+
277
+ private var callStateListener: ((List<CallInfo>) -> Unit)? = null
278
+
279
+ fun setCallStateListener(listener: ((List<CallInfo>) -> Unit)?) {
280
+ callStateListener = listener
281
+ }
282
+
283
+ private fun notifyCallStateChanged(context: Context) {
284
+ callStateListener?.invoke(getActiveCalls())
285
+ // Optionally, send a local broadcast or use DeviceEventEmitter for JS
286
+ }
287
+
288
+ fun getAudioDevices(context: Context): List<String> {
289
+ audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
290
+ val devices = mutableListOf<String>()
291
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
292
+ audioManager?.getDevices(AudioManager.GET_DEVICES_OUTPUTS)?.forEach { device ->
293
+ when (device.type) {
294
+ AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> devices.add("Bluetooth")
295
+ AudioDeviceInfo.TYPE_WIRED_HEADPHONES, AudioDeviceInfo.TYPE_WIRED_HEADSET -> devices.add("Headset")
296
+ AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> devices.add("Speaker")
297
+ AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> devices.add("Earpiece")
298
+ }
299
+ }
300
+ } else {
301
+ // Fallback for older devices
302
+ devices.add("Speaker")
303
+ devices.add("Earpiece")
304
+ }
305
+ return devices.distinct()
306
+ }
307
+
308
+ fun setAudioRoute(context: Context, route: String) {
309
+ audioManager = audioManager ?: context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
310
+ when (route) {
311
+ "Speaker" -> audioManager?.isSpeakerphoneOn = true
312
+ "Earpiece" -> audioManager?.isSpeakerphoneOn = false
313
+ "Bluetooth" -> audioManager?.startBluetoothSco()
314
+ "Headset" -> { /* usually auto-selected */ }
315
+ }
316
+ }
317
+
318
+ // --- Screen Awake Management ---
319
+ private var wakeLock: android.os.PowerManager.WakeLock? = null
320
+
321
+ fun keepScreenAwake(context: Context, keepAwake: Boolean) {
322
+ val powerManager = context.getSystemService(Context.POWER_SERVICE) as android.os.PowerManager
323
+ if (keepAwake) {
324
+ if (wakeLock == null || !wakeLock!!.isHeld) {
325
+ wakeLock = powerManager.newWakeLock(
326
+ android.os.PowerManager.SCREEN_DIM_WAKE_LOCK or android.os.PowerManager.ACQUIRE_CAUSES_WAKEUP,
327
+ "CallEngine:WakeLock"
328
+ )
329
+ wakeLock?.acquire()
330
+ }
331
+ } else {
332
+ wakeLock?.let {
333
+ if (it.isHeld) it.release()
334
+ }
335
+ wakeLock = null
336
+ }
337
+ }
195
338
  }
@@ -14,20 +14,26 @@ class CallManagerModule(reactContext: ReactApplicationContext) :
14
14
  const val TAG = "CallManagerModule"
15
15
  }
16
16
 
17
+ // JSI event handler (set from JS)
17
18
  private var eventHandler: Callback? = null
18
19
 
19
20
  override fun getName(): String = NAME
20
21
 
22
+ // JSI event handler registration
21
23
  override fun setEventHandler(callback: Callback) {
22
24
  Log.d(TAG, "setEventHandler called, registering JS callback")
23
25
  eventHandler = callback
26
+ // Optionally, you can call CallEngine.setCallStateListener here if you want to push call state to JS
24
27
  }
25
28
 
29
+ // JSI event emission (call from CallEngine or anywhere)
26
30
  fun emitEvent(event: String, callData: String) {
27
31
  Log.d(TAG, "emitEvent: event=$event, callData=$callData")
28
32
  eventHandler?.invoke(event, callData)
29
33
  }
30
34
 
35
+ // --- Call control (all JSI, no bridge) ---
36
+
31
37
  override fun reportIncomingCall(callId: String, callData: String) {
32
38
  Log.d(TAG, "reportIncomingCall called from JS with callId=$callId, callData=$callData")
33
39
  CallEngine.reportIncomingCall(reactApplicationContext, callId, callData)
@@ -35,13 +41,12 @@ class CallManagerModule(reactContext: ReactApplicationContext) :
35
41
 
36
42
  override fun endCall(callId: String) {
37
43
  Log.d(TAG, "endCall called with callId=$callId")
38
- CallEngine.cancelIncomingCallUI(reactApplicationContext)
39
- CallEngine.stopForegroundService(reactApplicationContext)
40
- CallEngine.disconnectTelecomCall(reactApplicationContext, callId)
44
+ CallEngine.endCall(reactApplicationContext, callId)
41
45
  }
42
46
 
43
47
  override fun answerCall(callId: String) {
44
48
  Log.d(TAG, "answerCall called with callId=$callId")
49
+ CallEngine.answerCall(reactApplicationContext, callId)
45
50
  CallEngine.bringAppToForeground(reactApplicationContext)
46
51
  }
47
52
 
@@ -52,7 +57,24 @@ class CallManagerModule(reactContext: ReactApplicationContext) :
52
57
 
53
58
  override fun rejectCurrentAndAnswerNew(callId: String, callData: String) {
54
59
  Log.d(TAG, "rejectCurrentAndAnswerNew called with callId=$callId, callData=$callData")
55
- endCall(callId)
60
+ CallEngine.endCall(reactApplicationContext, CallEngine.getCurrentCallId() ?: "")
56
61
  CallEngine.reportIncomingCall(reactApplicationContext, callId, callData)
57
62
  }
63
+
64
+ // --- JSI-native audio/device/screen APIs ---
65
+
66
+ override fun getAudioDevices(): ReadableArray {
67
+ val devices = CallEngine.getAudioDevices(reactApplicationContext)
68
+ val array = Arguments.createArray()
69
+ devices.forEach { array.pushString(it) }
70
+ return array
71
+ }
72
+
73
+ override fun setAudioRoute(route: String) {
74
+ CallEngine.setAudioRoute(reactApplicationContext, route)
75
+ }
76
+
77
+ override fun keepScreenAwake(keepAwake: Boolean) {
78
+ CallEngine.keepScreenAwake(reactApplicationContext, keepAwake)
79
+ }
58
80
  }
@@ -1 +1 @@
1
- {"version":3,"names":["TurboModuleRegistry","getEnforcing"],"sourceRoot":"..\\..\\src","sources":["NativeCallManager.ts"],"mappings":";;AACA,SAASA,mBAAmB,QAAQ,cAAc;AAWlD,eAAeA,mBAAmB,CAACC,YAAY,CAAO,aAAa,CAAC","ignoreList":[]}
1
+ {"version":3,"names":["TurboModuleRegistry","getEnforcing"],"sourceRoot":"..\\..\\src","sources":["NativeCallManager.ts"],"mappings":";;AACA,SAASA,mBAAmB,QAAQ,cAAc;AAgBlD,eAAeA,mBAAmB,CAACC,YAAY,CAAO,aAAa,CAAC","ignoreList":[]}
@@ -6,6 +6,9 @@ export interface Spec extends TurboModule {
6
6
  silenceRingtone(): void;
7
7
  rejectCurrentAndAnswerNew(callId: string, callData: string): void;
8
8
  setEventHandler(callback: (event: string, callData: string) => void): void;
9
+ getAudioDevices(): string[];
10
+ setAudioRoute(route: string): void;
11
+ keepScreenAwake(keepAwake: boolean): void;
9
12
  }
10
13
  declare const _default: Spec;
11
14
  export default _default;
@@ -1 +1 @@
1
- {"version":3,"file":"NativeCallManager.d.ts","sourceRoot":"","sources":["../../../src/NativeCallManager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAGhD,MAAM,WAAW,IAAK,SAAQ,WAAW;IACvC,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3D,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,eAAe,IAAI,IAAI,CAAC;IACxB,yBAAyB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAClE,eAAe,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC;CAC5E;;AAED,wBAAqE"}
1
+ {"version":3,"file":"NativeCallManager.d.ts","sourceRoot":"","sources":["../../../src/NativeCallManager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAGhD,MAAM,WAAW,IAAK,SAAQ,WAAW;IACvC,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3D,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,eAAe,IAAI,IAAI,CAAC;IACxB,yBAAyB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAClE,eAAe,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC;IAG3E,eAAe,IAAI,MAAM,EAAE,CAAC;IAC5B,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,eAAe,CAAC,SAAS,EAAE,OAAO,GAAG,IAAI,CAAC;CAC3C;;AAED,wBAAqE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qusaieilouti99/call-manager",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "description": "call manager",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -8,6 +8,11 @@ export interface Spec extends TurboModule {
8
8
  silenceRingtone(): void;
9
9
  rejectCurrentAndAnswerNew(callId: string, callData: string): void;
10
10
  setEventHandler(callback: (event: string, callData: string) => void): void;
11
+
12
+ // JSI-native audio/device/screen APIs:
13
+ getAudioDevices(): string[];
14
+ setAudioRoute(route: string): void;
15
+ keepScreenAwake(keepAwake: boolean): void;
11
16
  }
12
17
 
13
18
  export default TurboModuleRegistry.getEnforcing<Spec>('CallManager');