@qusaieilouti99/call-manager 0.1.16 → 0.1.18
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.
- package/android/src/main/java/com/qusaieilouti99/callmanager/CallActivity.kt +18 -14
- package/android/src/main/java/com/qusaieilouti99/callmanager/CallEngine.kt +196 -0
- package/android/src/main/java/com/qusaieilouti99/callmanager/CallForegroundService.kt +7 -0
- package/android/src/main/java/com/qusaieilouti99/callmanager/CallManagerModule.kt +8 -40
- package/android/src/main/java/com/qusaieilouti99/callmanager/CallNotificationActionReceiver.kt +6 -23
- package/android/src/main/java/com/qusaieilouti99/callmanager/MyConnection.kt +10 -0
- package/android/src/main/java/com/qusaieilouti99/callmanager/MyConnectionService.kt +0 -1
- package/package.json +1 -1
- package/android/src/main/java/com/qusaieilouti99/callmanager/CallNativeHelper.kt +0 -61
- package/android/src/main/java/com/qusaieilouti99/callmanager/CallNotificationHelper.kt +0 -147
|
@@ -2,9 +2,8 @@ package com.qusaieilouti99.callmanager
|
|
|
2
2
|
|
|
3
3
|
import android.app.Activity
|
|
4
4
|
import android.content.Intent
|
|
5
|
-
import android.graphics.Color
|
|
6
5
|
import android.os.Bundle
|
|
7
|
-
import android.view.
|
|
6
|
+
import android.view.WindowManager
|
|
8
7
|
import android.widget.Button
|
|
9
8
|
import android.widget.ImageView
|
|
10
9
|
import android.widget.TextView
|
|
@@ -13,34 +12,39 @@ class CallActivity : Activity() {
|
|
|
13
12
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
14
13
|
super.onCreate(savedInstanceState)
|
|
15
14
|
window.addFlags(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
|
|
16
|
+
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
|
|
17
|
+
WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
|
|
19
18
|
)
|
|
20
19
|
setContentView(R.layout.activity_call)
|
|
21
20
|
|
|
21
|
+
// Cancel the notification as soon as CallActivity is shown
|
|
22
|
+
CallEngine.cancelIncomingCallUI(this)
|
|
23
|
+
|
|
22
24
|
val callerName = intent.getStringExtra("callerName") ?: "Unknown"
|
|
23
|
-
val avatarView = findViewById<ImageView>(R.id.avatar)
|
|
24
25
|
val nameView = findViewById<TextView>(R.id.caller_name)
|
|
25
26
|
val answerBtn = findViewById<Button>(R.id.answer_btn)
|
|
26
27
|
val declineBtn = findViewById<Button>(R.id.decline_btn)
|
|
27
28
|
|
|
28
29
|
nameView.text = callerName
|
|
29
|
-
// Optionally set avatarView image
|
|
30
30
|
|
|
31
31
|
answerBtn.setOnClickListener {
|
|
32
|
-
|
|
33
|
-
val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
|
|
34
|
-
launchIntent?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
|
35
|
-
launchIntent?.putExtra("incomingCall", true)
|
|
36
|
-
startActivity(launchIntent)
|
|
32
|
+
CallEngine.bringAppToForeground(this)
|
|
37
33
|
finish()
|
|
38
34
|
}
|
|
39
35
|
|
|
40
36
|
declineBtn.setOnClickListener {
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
CallEngine.cancelIncomingCallUI(this)
|
|
38
|
+
CallEngine.stopForegroundService(this)
|
|
39
|
+
CallEngine.disconnectTelecomCall(this, intent.getStringExtra("callId") ?: "")
|
|
43
40
|
finish()
|
|
44
41
|
}
|
|
45
42
|
}
|
|
43
|
+
|
|
44
|
+
override fun onDestroy() {
|
|
45
|
+
super.onDestroy()
|
|
46
|
+
CallEngine.cancelIncomingCallUI(this)
|
|
47
|
+
CallEngine.stopForegroundService(this)
|
|
48
|
+
CallEngine.disconnectTelecomCall(this, intent.getStringExtra("callId") ?: "")
|
|
49
|
+
}
|
|
46
50
|
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
package com.qusaieilouti99.callmanager
|
|
2
|
+
|
|
3
|
+
import android.app.*
|
|
4
|
+
import android.content.ComponentName
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import android.content.Intent
|
|
7
|
+
import android.media.AudioAttributes
|
|
8
|
+
import android.media.RingtoneManager
|
|
9
|
+
import android.os.Build
|
|
10
|
+
import android.os.Bundle
|
|
11
|
+
import android.telecom.PhoneAccount
|
|
12
|
+
import android.telecom.PhoneAccountHandle
|
|
13
|
+
import android.telecom.TelecomManager
|
|
14
|
+
import android.util.Log
|
|
15
|
+
import android.graphics.Color
|
|
16
|
+
import android.app.Person
|
|
17
|
+
|
|
18
|
+
object CallEngine {
|
|
19
|
+
private const val TAG = "CallEngine"
|
|
20
|
+
private const val PHONE_ACCOUNT_ID = "com.qusaieilouti99.callmanager.SELF_MANAGED"
|
|
21
|
+
private const val NOTIF_CHANNEL_ID = "incoming_call_channel"
|
|
22
|
+
private const val NOTIF_ID = 2001
|
|
23
|
+
private const val FOREGROUND_CHANNEL_ID = "call_foreground_channel"
|
|
24
|
+
private const val FOREGROUND_NOTIF_ID = 1001
|
|
25
|
+
|
|
26
|
+
private var ringtone: android.media.Ringtone? = null
|
|
27
|
+
|
|
28
|
+
// --- Public API ---
|
|
29
|
+
|
|
30
|
+
fun reportIncomingCall(context: Context, callId: String, callData: String) {
|
|
31
|
+
Log.d(TAG, "reportIncomingCall: $callId, $callData")
|
|
32
|
+
val callerName = try {
|
|
33
|
+
val json = org.json.JSONObject(callData)
|
|
34
|
+
json.optString("name", "Unknown")
|
|
35
|
+
} catch (e: Exception) { "Unknown" }
|
|
36
|
+
|
|
37
|
+
showIncomingCallUI(context, callId, callerName)
|
|
38
|
+
registerPhoneAccount(context)
|
|
39
|
+
val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
|
40
|
+
val phoneAccountHandle = getPhoneAccountHandle(context)
|
|
41
|
+
val extras = Bundle().apply { putString(MyConnectionService.EXTRA_CALL_DATA, callData) }
|
|
42
|
+
try {
|
|
43
|
+
telecomManager.addNewIncomingCall(phoneAccountHandle, extras)
|
|
44
|
+
startForegroundService(context)
|
|
45
|
+
} catch (e: Exception) {
|
|
46
|
+
Log.e(TAG, "Failed to report incoming call: ${e.message}")
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fun showIncomingCallUI(context: Context, callId: String, callerName: String) {
|
|
51
|
+
createNotificationChannel(context)
|
|
52
|
+
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
53
|
+
|
|
54
|
+
// PendingIntents
|
|
55
|
+
val answerIntent = Intent(context, CallNotificationActionReceiver::class.java).apply {
|
|
56
|
+
action = "com.qusaieilouti99.callmanager.ANSWER_CALL"
|
|
57
|
+
putExtra("callId", callId)
|
|
58
|
+
}
|
|
59
|
+
val answerPendingIntent = PendingIntent.getBroadcast(context, 0, answerIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
|
60
|
+
|
|
61
|
+
val declineIntent = Intent(context, CallNotificationActionReceiver::class.java).apply {
|
|
62
|
+
action = "com.qusaieilouti99.callmanager.DECLINE_CALL"
|
|
63
|
+
putExtra("callId", callId)
|
|
64
|
+
}
|
|
65
|
+
val declinePendingIntent = PendingIntent.getBroadcast(context, 1, declineIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
|
66
|
+
|
|
67
|
+
val fullScreenIntent = Intent(context, CallActivity::class.java).apply {
|
|
68
|
+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
|
69
|
+
putExtra("callId", callId)
|
|
70
|
+
putExtra("callerName", callerName)
|
|
71
|
+
}
|
|
72
|
+
val fullScreenPendingIntent = PendingIntent.getActivity(context, 2, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
|
73
|
+
|
|
74
|
+
val notification: Notification = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
75
|
+
val person = Person.Builder().setName(callerName).setImportant(true).build()
|
|
76
|
+
Notification.Builder(context, NOTIF_CHANNEL_ID)
|
|
77
|
+
.setSmallIcon(android.R.drawable.sym_call_incoming)
|
|
78
|
+
.setStyle(Notification.CallStyle.forIncomingCall(person, declinePendingIntent, answerPendingIntent))
|
|
79
|
+
.setFullScreenIntent(fullScreenPendingIntent, true)
|
|
80
|
+
.setOngoing(true)
|
|
81
|
+
.setAutoCancel(false)
|
|
82
|
+
.build()
|
|
83
|
+
} else {
|
|
84
|
+
Notification.Builder(context, NOTIF_CHANNEL_ID)
|
|
85
|
+
.setSmallIcon(android.R.drawable.sym_call_incoming)
|
|
86
|
+
.setContentTitle("Incoming Call")
|
|
87
|
+
.setContentText(callerName)
|
|
88
|
+
.setPriority(Notification.PRIORITY_HIGH)
|
|
89
|
+
.setCategory(Notification.CATEGORY_CALL)
|
|
90
|
+
.setFullScreenIntent(fullScreenPendingIntent, true)
|
|
91
|
+
.addAction(android.R.drawable.sym_action_call, "Answer", answerPendingIntent)
|
|
92
|
+
.addAction(android.R.drawable.ic_menu_close_clear_cancel, "Decline", declinePendingIntent)
|
|
93
|
+
.setOngoing(true)
|
|
94
|
+
.setAutoCancel(false)
|
|
95
|
+
.build()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
notificationManager.notify(NOTIF_ID, notification)
|
|
99
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) playRingtone(context)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fun cancelIncomingCallUI(context: Context) {
|
|
103
|
+
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
104
|
+
notificationManager.cancel(NOTIF_ID)
|
|
105
|
+
stopRingtone()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
fun startForegroundService(context: Context) {
|
|
109
|
+
val intent = Intent(context, CallForegroundService::class.java)
|
|
110
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) context.startForegroundService(intent)
|
|
111
|
+
else context.startService(intent)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
fun stopForegroundService(context: Context) {
|
|
115
|
+
val intent = Intent(context, CallForegroundService::class.java)
|
|
116
|
+
context.stopService(intent)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
fun disconnectTelecomCall(context: Context, callId: String) {
|
|
120
|
+
// You can extend this to track and disconnect specific calls if needed.
|
|
121
|
+
Log.d(TAG, "disconnectTelecomCall called for callId=$callId")
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
fun bringAppToForeground(context: Context) {
|
|
125
|
+
val packageName = context.packageName
|
|
126
|
+
val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)
|
|
127
|
+
launchIntent?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
|
128
|
+
context.startActivity(launchIntent)
|
|
129
|
+
Log.d(TAG, "App brought to foreground via launchIntent")
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// --- Private helpers ---
|
|
133
|
+
|
|
134
|
+
private fun playRingtone(context: Context) {
|
|
135
|
+
try {
|
|
136
|
+
Log.d(TAG, "Playing ringtone")
|
|
137
|
+
val uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
|
|
138
|
+
ringtone = RingtoneManager.getRingtone(context, uri)
|
|
139
|
+
ringtone?.audioAttributes = AudioAttributes.Builder()
|
|
140
|
+
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
|
|
141
|
+
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
|
142
|
+
.build()
|
|
143
|
+
ringtone?.play()
|
|
144
|
+
} catch (e: Exception) {
|
|
145
|
+
Log.e(TAG, "Failed to play ringtone: ${e.message}")
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private fun stopRingtone() {
|
|
150
|
+
try { ringtone?.stop() } catch (_: Exception) {}
|
|
151
|
+
ringtone = null
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private fun createNotificationChannel(context: Context) {
|
|
155
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
156
|
+
val channel = NotificationChannel(
|
|
157
|
+
NOTIF_CHANNEL_ID,
|
|
158
|
+
"Incoming Call Channel",
|
|
159
|
+
NotificationManager.IMPORTANCE_HIGH
|
|
160
|
+
)
|
|
161
|
+
channel.description = "Notifications for incoming calls"
|
|
162
|
+
channel.enableLights(true)
|
|
163
|
+
channel.lightColor = Color.GREEN
|
|
164
|
+
channel.enableVibration(true)
|
|
165
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
|
166
|
+
channel.setSound(
|
|
167
|
+
RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE),
|
|
168
|
+
AudioAttributes.Builder()
|
|
169
|
+
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
|
|
170
|
+
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
|
171
|
+
.build()
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
val manager = context.getSystemService(NotificationManager::class.java)
|
|
175
|
+
manager.createNotificationChannel(channel)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private fun registerPhoneAccount(context: Context) {
|
|
180
|
+
val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
|
181
|
+
val phoneAccountHandle = getPhoneAccountHandle(context)
|
|
182
|
+
if (telecomManager.getPhoneAccount(phoneAccountHandle) == null) {
|
|
183
|
+
val phoneAccount = PhoneAccount.builder(phoneAccountHandle, "PingMe Call")
|
|
184
|
+
.setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)
|
|
185
|
+
.build()
|
|
186
|
+
telecomManager.registerPhoneAccount(phoneAccount)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private fun getPhoneAccountHandle(context: Context): PhoneAccountHandle {
|
|
191
|
+
return PhoneAccountHandle(
|
|
192
|
+
ComponentName(context, MyConnectionService::class.java),
|
|
193
|
+
PHONE_ACCOUNT_ID
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -53,4 +53,11 @@ class CallForegroundService : Service() {
|
|
|
53
53
|
manager.createNotificationChannel(channel)
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
|
+
|
|
57
|
+
override fun onDestroy() {
|
|
58
|
+
super.onDestroy()
|
|
59
|
+
// Clean up everything when the foreground service is stopped
|
|
60
|
+
CallEngine.cancelIncomingCallUI(this)
|
|
61
|
+
CallEngine.disconnectTelecomCall(this, "")
|
|
62
|
+
}
|
|
56
63
|
}
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
package com.qusaieilouti99.callmanager
|
|
2
2
|
|
|
3
3
|
import android.util.Log
|
|
4
|
-
import android.app.ActivityManager
|
|
5
|
-
import android.content.Context
|
|
6
|
-
import android.content.Intent
|
|
7
|
-
import android.os.Build
|
|
8
4
|
import com.facebook.react.bridge.*
|
|
9
5
|
import com.facebook.react.turbomodule.core.interfaces.TurboModule
|
|
10
6
|
import com.facebook.react.module.annotations.ReactModule
|
|
@@ -18,73 +14,45 @@ class CallManagerModule(reactContext: ReactApplicationContext) :
|
|
|
18
14
|
const val TAG = "CallManagerModule"
|
|
19
15
|
}
|
|
20
16
|
|
|
21
|
-
private var currentCallId: String? = null
|
|
22
17
|
private var eventHandler: Callback? = null
|
|
23
18
|
|
|
24
19
|
override fun getName(): String = NAME
|
|
25
20
|
|
|
26
|
-
// Register JS event handler
|
|
27
21
|
override fun setEventHandler(callback: Callback) {
|
|
28
22
|
Log.d(TAG, "setEventHandler called, registering JS callback")
|
|
29
23
|
eventHandler = callback
|
|
30
24
|
}
|
|
31
25
|
|
|
32
|
-
// Helper to emit events to JS
|
|
33
26
|
fun emitEvent(event: String, callData: String) {
|
|
34
27
|
Log.d(TAG, "emitEvent: event=$event, callData=$callData")
|
|
35
28
|
eventHandler?.invoke(event, callData)
|
|
36
29
|
}
|
|
37
30
|
|
|
38
|
-
// 1. Report incoming call (for JS, not used by NotificationService anymore)
|
|
39
31
|
override fun reportIncomingCall(callId: String, callData: String) {
|
|
40
32
|
Log.d(TAG, "reportIncomingCall called from JS with callId=$callId, callData=$callData")
|
|
41
|
-
|
|
42
|
-
CallNativeHelper.reportIncomingCall(reactApplicationContext, callId, callData)
|
|
33
|
+
CallEngine.reportIncomingCall(reactApplicationContext, callId, callData)
|
|
43
34
|
}
|
|
44
35
|
|
|
45
|
-
// 2. End call
|
|
46
36
|
override fun endCall(callId: String) {
|
|
47
37
|
Log.d(TAG, "endCall called with callId=$callId")
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
Log.d(TAG, "CallForegroundService stopped: $stopped")
|
|
38
|
+
CallEngine.cancelIncomingCallUI(reactApplicationContext)
|
|
39
|
+
CallEngine.stopForegroundService(reactApplicationContext)
|
|
40
|
+
CallEngine.disconnectTelecomCall(reactApplicationContext, callId)
|
|
52
41
|
}
|
|
53
42
|
|
|
54
|
-
// 3. Answer call
|
|
55
43
|
override fun answerCall(callId: String) {
|
|
56
44
|
Log.d(TAG, "answerCall called with callId=$callId")
|
|
57
|
-
bringAppToForeground()
|
|
45
|
+
CallEngine.bringAppToForeground(reactApplicationContext)
|
|
58
46
|
}
|
|
59
47
|
|
|
60
|
-
// 4. Silence ringtone
|
|
61
48
|
override fun silenceRingtone() {
|
|
62
49
|
Log.d(TAG, "silenceRingtone called")
|
|
63
|
-
|
|
50
|
+
CallEngine.cancelIncomingCallUI(reactApplicationContext)
|
|
64
51
|
}
|
|
65
52
|
|
|
66
|
-
// 5. Reject current and answer new
|
|
67
53
|
override fun rejectCurrentAndAnswerNew(callId: String, callData: String) {
|
|
68
54
|
Log.d(TAG, "rejectCurrentAndAnswerNew called with callId=$callId, callData=$callData")
|
|
69
|
-
endCall(
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// --- Bring App to Foreground ---
|
|
74
|
-
|
|
75
|
-
private fun bringAppToForeground() {
|
|
76
|
-
val context = reactApplicationContext
|
|
77
|
-
val packageName = context.packageName
|
|
78
|
-
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
|
79
|
-
val tasks = activityManager.appTasks
|
|
80
|
-
Log.d(TAG, "bringAppToForeground called. tasks.isNullOrEmpty()=${tasks.isNullOrEmpty()}")
|
|
81
|
-
if (tasks.isNullOrEmpty()) {
|
|
82
|
-
val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)
|
|
83
|
-
launchIntent?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
84
|
-
context.startActivity(launchIntent)
|
|
85
|
-
Log.d(TAG, "App brought to foreground via launchIntent")
|
|
86
|
-
} else {
|
|
87
|
-
Log.d(TAG, "App already in foreground or has tasks")
|
|
88
|
-
}
|
|
55
|
+
endCall(callId)
|
|
56
|
+
CallEngine.reportIncomingCall(reactApplicationContext, callId, callData)
|
|
89
57
|
}
|
|
90
58
|
}
|
package/android/src/main/java/com/qusaieilouti99/callmanager/CallNotificationActionReceiver.kt
CHANGED
|
@@ -7,37 +7,20 @@ import android.util.Log
|
|
|
7
7
|
|
|
8
8
|
class CallNotificationActionReceiver : BroadcastReceiver() {
|
|
9
9
|
override fun onReceive(context: Context, intent: Intent) {
|
|
10
|
-
|
|
11
10
|
val callId = intent.getStringExtra("callId") ?: return
|
|
12
11
|
Log.d("CallNotificationActionReceiver", "onReceive called with action=${intent.action}, callId=$callId")
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
val app = context.applicationContext as? com.facebook.react.ReactApplication
|
|
16
|
-
val reactInstanceManager = app?.reactNativeHost?.reactInstanceManager
|
|
17
|
-
val reactContext = reactInstanceManager?.currentReactContext
|
|
18
|
-
val module = reactContext?.getNativeModule(CallManagerModule::class.java)
|
|
19
|
-
|
|
20
|
-
when (action) {
|
|
12
|
+
when (intent.action) {
|
|
21
13
|
"com.qusaieilouti99.callmanager.ANSWER_CALL" -> {
|
|
22
14
|
Log.d("CallNotificationActionReceiver", "Answer action received")
|
|
23
|
-
bringAppToForeground(context)
|
|
15
|
+
CallEngine.bringAppToForeground(context)
|
|
24
16
|
}
|
|
25
17
|
"com.qusaieilouti99.callmanager.DECLINE_CALL" -> {
|
|
26
18
|
Log.d("CallNotificationActionReceiver", "Decline action received")
|
|
27
|
-
|
|
19
|
+
CallEngine.cancelIncomingCallUI(context)
|
|
20
|
+
CallEngine.stopForegroundService(context)
|
|
21
|
+
CallEngine.disconnectTelecomCall(context, callId)
|
|
28
22
|
}
|
|
29
23
|
}
|
|
30
|
-
|
|
31
|
-
// Cancel notification and stop ringtone
|
|
32
|
-
CallNotificationHelper(context).cancelIncomingCallNotification()
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
private fun bringAppToForeground(context: Context) {
|
|
36
|
-
val packageName = context.packageName
|
|
37
|
-
val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)
|
|
38
|
-
launchIntent?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
|
39
|
-
launchIntent?.putExtra("incomingCall", true)
|
|
40
|
-
context.startActivity(launchIntent)
|
|
41
|
-
Log.d("CallNotificationActionReceiver", "Bringing app to foreground for incoming call")
|
|
24
|
+
CallEngine.cancelIncomingCallUI(context)
|
|
42
25
|
}
|
|
43
26
|
}
|
|
@@ -25,6 +25,8 @@ class MyConnection(
|
|
|
25
25
|
?.getNativeModule(CallManagerModule::class.java)
|
|
26
26
|
module?.emitEvent("onCallAnswered", callData)
|
|
27
27
|
}
|
|
28
|
+
// Bring app to foreground
|
|
29
|
+
CallEngine.bringAppToForeground(context)
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
override fun onReject() {
|
|
@@ -37,6 +39,10 @@ class MyConnection(
|
|
|
37
39
|
?.getNativeModule(CallManagerModule::class.java)
|
|
38
40
|
module?.emitEvent("onCallRejected", callData)
|
|
39
41
|
}
|
|
42
|
+
// Clean up
|
|
43
|
+
CallEngine.cancelIncomingCallUI(context)
|
|
44
|
+
CallEngine.stopForegroundService(context)
|
|
45
|
+
CallEngine.disconnectTelecomCall(context, "")
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
override fun onDisconnect() {
|
|
@@ -49,5 +55,9 @@ class MyConnection(
|
|
|
49
55
|
?.getNativeModule(CallManagerModule::class.java)
|
|
50
56
|
module?.emitEvent("onCallEnded", callData)
|
|
51
57
|
}
|
|
58
|
+
// Clean up
|
|
59
|
+
CallEngine.cancelIncomingCallUI(context)
|
|
60
|
+
CallEngine.stopForegroundService(context)
|
|
61
|
+
CallEngine.disconnectTelecomCall(context, "")
|
|
52
62
|
}
|
|
53
63
|
}
|
|
@@ -18,7 +18,6 @@ class MyConnectionService : ConnectionService() {
|
|
|
18
18
|
request: ConnectionRequest
|
|
19
19
|
): Connection {
|
|
20
20
|
Log.d(TAG, "onCreateIncomingConnection: ${request.extras}")
|
|
21
|
-
|
|
22
21
|
val callData = request.extras?.getString(EXTRA_CALL_DATA) ?: ""
|
|
23
22
|
val connection = MyConnection(applicationContext, callData)
|
|
24
23
|
connection.setRinging()
|
package/package.json
CHANGED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
package com.qusaieilouti99.callmanager
|
|
2
|
-
|
|
3
|
-
import android.content.ComponentName
|
|
4
|
-
import android.content.Context
|
|
5
|
-
import android.os.Build
|
|
6
|
-
import android.os.Bundle
|
|
7
|
-
import android.telecom.PhoneAccount
|
|
8
|
-
import android.telecom.PhoneAccountHandle
|
|
9
|
-
import android.telecom.TelecomManager
|
|
10
|
-
import android.util.Log
|
|
11
|
-
|
|
12
|
-
object CallNativeHelper {
|
|
13
|
-
private const val TAG = "CallNativeHelper"
|
|
14
|
-
private const val PHONE_ACCOUNT_ID = "com.qusaieilouti99.callmanager.SELF_MANAGED"
|
|
15
|
-
|
|
16
|
-
fun reportIncomingCall(context: Context, callId: String, callData: String) {
|
|
17
|
-
Log.d(TAG, "reportIncomingCall called with callId=$callId, callData=$callData")
|
|
18
|
-
// Parse caller name from callData (or use a default)
|
|
19
|
-
val callerName = try {
|
|
20
|
-
val json = org.json.JSONObject(callData)
|
|
21
|
-
json.optString("name", "Unknown")
|
|
22
|
-
} catch (e: Exception) {
|
|
23
|
-
"Unknown"
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Show incoming call notification and play ringtone
|
|
27
|
-
CallNotificationHelper(context).showIncomingCallNotification(callId, callerName)
|
|
28
|
-
|
|
29
|
-
val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
|
30
|
-
|
|
31
|
-
// Register self-managed PhoneAccount if not already registered
|
|
32
|
-
val phoneAccountHandle = PhoneAccountHandle(
|
|
33
|
-
ComponentName(context, MyConnectionService::class.java),
|
|
34
|
-
PHONE_ACCOUNT_ID
|
|
35
|
-
)
|
|
36
|
-
if (telecomManager.getPhoneAccount(phoneAccountHandle) == null) {
|
|
37
|
-
val phoneAccount = PhoneAccount.builder(phoneAccountHandle, "PingMe Call")
|
|
38
|
-
.setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)
|
|
39
|
-
.build()
|
|
40
|
-
telecomManager.registerPhoneAccount(phoneAccount)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Prepare extras
|
|
44
|
-
val extras = Bundle()
|
|
45
|
-
extras.putString(MyConnectionService.EXTRA_CALL_DATA, callData)
|
|
46
|
-
|
|
47
|
-
// Report the call
|
|
48
|
-
try {
|
|
49
|
-
telecomManager.addNewIncomingCall(phoneAccountHandle, extras)
|
|
50
|
-
// Optionally, start foreground service for ongoing call
|
|
51
|
-
val intent = android.content.Intent(context, CallForegroundService::class.java)
|
|
52
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
53
|
-
context.startForegroundService(intent)
|
|
54
|
-
} else {
|
|
55
|
-
context.startService(intent)
|
|
56
|
-
}
|
|
57
|
-
} catch (e: Exception) {
|
|
58
|
-
Log.e(TAG, "Failed to report incoming call: ${e.message}")
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
package com.qusaieilouti99.callmanager
|
|
2
|
-
|
|
3
|
-
import android.app.Notification
|
|
4
|
-
import android.app.NotificationChannel
|
|
5
|
-
import android.app.NotificationManager
|
|
6
|
-
import android.app.PendingIntent
|
|
7
|
-
import android.app.Person
|
|
8
|
-
import android.content.Context
|
|
9
|
-
import android.content.Intent
|
|
10
|
-
import android.graphics.Color
|
|
11
|
-
import android.media.AudioAttributes
|
|
12
|
-
import android.media.RingtoneManager
|
|
13
|
-
import android.os.Build
|
|
14
|
-
import android.util.Log
|
|
15
|
-
|
|
16
|
-
class CallNotificationHelper(private val context: Context) {
|
|
17
|
-
|
|
18
|
-
companion object {
|
|
19
|
-
const val CHANNEL_ID = "incoming_call_channel"
|
|
20
|
-
const val NOTIFICATION_ID = 2001
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
private var ringtone: android.media.Ringtone? = null
|
|
24
|
-
|
|
25
|
-
fun showIncomingCallNotification(callId: String, callerName: String) {
|
|
26
|
-
Log.d("CallNotificationHelper", "Showing incoming call notification for $callerName, callId=$callId")
|
|
27
|
-
createNotificationChannel()
|
|
28
|
-
|
|
29
|
-
// Intent for answer action
|
|
30
|
-
val answerIntent = Intent(context, CallNotificationActionReceiver::class.java).apply {
|
|
31
|
-
action = "com.qusaieilouti99.callmanager.ANSWER_CALL"
|
|
32
|
-
putExtra("callId", callId)
|
|
33
|
-
}
|
|
34
|
-
val answerPendingIntent = PendingIntent.getBroadcast(
|
|
35
|
-
context, 0, answerIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
// Intent for decline action
|
|
39
|
-
val declineIntent = Intent(context, CallNotificationActionReceiver::class.java).apply {
|
|
40
|
-
action = "com.qusaieilouti99.callmanager.DECLINE_CALL"
|
|
41
|
-
putExtra("callId", callId)
|
|
42
|
-
}
|
|
43
|
-
val declinePendingIntent = PendingIntent.getBroadcast(
|
|
44
|
-
context, 1, declineIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
// Full-screen intent to pop up the native call UI
|
|
48
|
-
val fullScreenIntent = Intent(context, CallActivity::class.java).apply {
|
|
49
|
-
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
|
50
|
-
putExtra("callId", callId)
|
|
51
|
-
putExtra("callerName", callerName)
|
|
52
|
-
}
|
|
53
|
-
val fullScreenPendingIntent = PendingIntent.getActivity(
|
|
54
|
-
context, 2, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
val notification: Notification = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
58
|
-
val person = Person.Builder()
|
|
59
|
-
.setName(callerName)
|
|
60
|
-
.setImportant(true)
|
|
61
|
-
.build()
|
|
62
|
-
|
|
63
|
-
Notification.Builder(context, CHANNEL_ID)
|
|
64
|
-
.setSmallIcon(android.R.drawable.sym_call_incoming) // <-- REQUIRED!
|
|
65
|
-
.setStyle(
|
|
66
|
-
Notification.CallStyle.forIncomingCall(
|
|
67
|
-
person,
|
|
68
|
-
declinePendingIntent, // <-- Decline goes first!
|
|
69
|
-
answerPendingIntent // <-- Answer goes second!
|
|
70
|
-
)
|
|
71
|
-
)
|
|
72
|
-
.setFullScreenIntent(fullScreenPendingIntent, true)
|
|
73
|
-
.setOngoing(true)
|
|
74
|
-
.setAutoCancel(false)
|
|
75
|
-
.build()
|
|
76
|
-
} else {
|
|
77
|
-
Notification.Builder(context, CHANNEL_ID)
|
|
78
|
-
.setSmallIcon(android.R.drawable.sym_call_incoming) // <-- REQUIRED!
|
|
79
|
-
.setContentTitle("Incoming Call")
|
|
80
|
-
.setContentText(callerName)
|
|
81
|
-
.setPriority(Notification.PRIORITY_HIGH)
|
|
82
|
-
.setCategory(Notification.CATEGORY_CALL)
|
|
83
|
-
.setFullScreenIntent(fullScreenPendingIntent, true)
|
|
84
|
-
.addAction(android.R.drawable.sym_action_call, "Answer", answerPendingIntent)
|
|
85
|
-
.addAction(android.R.drawable.ic_menu_close_clear_cancel, "Decline", declinePendingIntent)
|
|
86
|
-
.setOngoing(true)
|
|
87
|
-
.setAutoCancel(false)
|
|
88
|
-
.build()
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
92
|
-
notificationManager.notify(NOTIFICATION_ID, notification)
|
|
93
|
-
|
|
94
|
-
playRingtone()
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
fun cancelIncomingCallNotification() {
|
|
98
|
-
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
99
|
-
notificationManager.cancel(NOTIFICATION_ID)
|
|
100
|
-
stopRingtone()
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
private fun createNotificationChannel() {
|
|
104
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
105
|
-
val channel = NotificationChannel(
|
|
106
|
-
CHANNEL_ID,
|
|
107
|
-
"Incoming Call Channel",
|
|
108
|
-
NotificationManager.IMPORTANCE_HIGH
|
|
109
|
-
)
|
|
110
|
-
channel.description = "Notifications for incoming calls"
|
|
111
|
-
channel.enableLights(true)
|
|
112
|
-
channel.lightColor = Color.GREEN
|
|
113
|
-
channel.enableVibration(true)
|
|
114
|
-
channel.setSound(
|
|
115
|
-
RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE),
|
|
116
|
-
AudioAttributes.Builder()
|
|
117
|
-
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
|
|
118
|
-
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
|
119
|
-
.build()
|
|
120
|
-
)
|
|
121
|
-
val manager = context.getSystemService(NotificationManager::class.java)
|
|
122
|
-
manager.createNotificationChannel(channel)
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
private fun playRingtone() {
|
|
127
|
-
try {
|
|
128
|
-
Log.d("CallNotificationHelper", "Playing ringtone")
|
|
129
|
-
val uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
|
|
130
|
-
ringtone = RingtoneManager.getRingtone(context, uri)
|
|
131
|
-
ringtone?.audioAttributes = AudioAttributes.Builder()
|
|
132
|
-
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
|
|
133
|
-
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
|
134
|
-
.build()
|
|
135
|
-
ringtone?.play()
|
|
136
|
-
} catch (e: Exception) {
|
|
137
|
-
Log.e("CallNotificationHelper", "Failed to play ringtone: ${e.message}")
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
fun stopRingtone() {
|
|
142
|
-
try {
|
|
143
|
-
ringtone?.stop()
|
|
144
|
-
} catch (_: Exception) {}
|
|
145
|
-
ringtone = null
|
|
146
|
-
}
|
|
147
|
-
}
|