rns-nativecall 0.6.2 → 0.6.4
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/README.md +77 -157
- package/android/build.gradle +0 -3
- package/android/src/main/java/com/rnsnativecall/AcceptCallActivity.kt +38 -38
- package/android/src/main/java/com/rnsnativecall/CallActionReceiver.kt +32 -27
- package/android/src/main/java/com/rnsnativecall/CallHeadlessTask.kt +21 -49
- package/android/src/main/java/com/rnsnativecall/CallMessagingService.kt +87 -83
- package/android/src/main/java/com/rnsnativecall/CallModule.kt +90 -21
- package/android/src/main/java/com/rnsnativecall/CallState.kt +54 -0
- package/android/src/main/java/com/rnsnativecall/NativeCallManager.kt +93 -53
- package/android/src/main/java/com/rnsnativecall/UnlockPromptActivity.kt +55 -0
- package/app.plugin.js +1 -1
- package/index.d.ts +13 -26
- package/index.js +36 -10
- package/package.json +12 -24
- package/rns-nativecall.podspec +1 -1
- package/withNativeCallVoip.js +94 -80
|
@@ -1,135 +1,139 @@
|
|
|
1
1
|
package com.rnsnativecall
|
|
2
2
|
|
|
3
3
|
import android.app.ActivityManager
|
|
4
|
-
import android.content.Context
|
|
5
|
-
import android.content.Intent
|
|
6
|
-
import android.os.Bundle
|
|
7
|
-
import android.os.Handler
|
|
8
|
-
import android.os.Looper
|
|
9
4
|
import android.app.NotificationManager
|
|
10
5
|
import android.app.NotificationChannel
|
|
11
6
|
import android.app.PendingIntent
|
|
7
|
+
import android.content.Context
|
|
8
|
+
import android.content.Intent
|
|
12
9
|
import android.os.Build
|
|
13
|
-
import android.
|
|
10
|
+
import android.os.Bundle
|
|
14
11
|
import androidx.core.app.NotificationCompat
|
|
15
12
|
import androidx.core.content.ContextCompat
|
|
16
13
|
import com.google.firebase.messaging.FirebaseMessagingService
|
|
17
14
|
import com.google.firebase.messaging.RemoteMessage
|
|
18
15
|
import com.facebook.react.HeadlessJsTaskService
|
|
19
16
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
companion object {
|
|
23
|
-
private val handler = Handler(Looper.getMainLooper())
|
|
24
|
-
private val pendingNotifications = mutableMapOf<String, Runnable>()
|
|
17
|
+
import com.rnsnativecall.CallState
|
|
25
18
|
|
|
26
|
-
|
|
27
|
-
fun stopBackupTimer(uuid: String) {
|
|
28
|
-
pendingNotifications[uuid]?.let {
|
|
29
|
-
Log.d("CallMessagingService", "Stopping backup timer for UUID: $uuid")
|
|
30
|
-
handler.removeCallbacks(it)
|
|
31
|
-
pendingNotifications.remove(uuid)
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
private fun isAppInForeground(context: Context): Boolean {
|
|
37
|
-
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
|
38
|
-
val appProcesses = activityManager.runningAppProcesses ?: return false
|
|
39
|
-
return appProcesses.any {
|
|
40
|
-
it.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND && it.processName == context.packageName
|
|
41
|
-
}
|
|
42
|
-
}
|
|
19
|
+
class CallMessagingService : FirebaseMessagingService() {
|
|
43
20
|
|
|
44
21
|
override fun onMessageReceived(remoteMessage: RemoteMessage) {
|
|
45
22
|
val data = remoteMessage.data
|
|
46
23
|
val context = applicationContext
|
|
24
|
+
|
|
47
25
|
val uuid = data["callUuid"] ?: return
|
|
48
26
|
val type = data["type"] ?: ""
|
|
49
|
-
|
|
27
|
+
|
|
50
28
|
if (type == "CANCEL") {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
29
|
+
NativeCallManager.stopRingtone()
|
|
30
|
+
CallState.markCanceled(uuid)
|
|
31
|
+
// Gate future headless runs for this UUID
|
|
32
|
+
if (CallState.getCurrent() == uuid) {
|
|
33
|
+
CallState.clear(uuid)
|
|
34
|
+
}
|
|
57
35
|
showMissedCallNotification(context, data, uuid)
|
|
58
36
|
return
|
|
59
37
|
}
|
|
60
|
-
|
|
38
|
+
|
|
39
|
+
// Inside onMessageReceived
|
|
40
|
+
if (!CallState.setCurrent(uuid)) {
|
|
41
|
+
// We are busy! Start a SILENT headless task to send the WebSocket busy msg
|
|
42
|
+
val busyIntent = Intent(context, CallHeadlessTask::class.java).apply {
|
|
43
|
+
putExtras(Bundle().apply {
|
|
44
|
+
data.forEach { (k, v) -> putString(k, v) }
|
|
45
|
+
putBoolean("isBusySignal", true)
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
context.startService(busyIntent)
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
61
52
|
if (isAppInForeground(context)) {
|
|
53
|
+
// Foreground → send event directly
|
|
62
54
|
CallModule.sendEventToJS("onCallReceived", data)
|
|
63
55
|
} else {
|
|
64
|
-
|
|
65
|
-
//
|
|
56
|
+
// Show incoming call notification instantly
|
|
57
|
+
//NativeCallManager.handleIncomingPush(context, data) /// i will use headless instead
|
|
58
|
+
// Background → start foreground service + headless task
|
|
59
|
+
// val serviceIntent = Intent(context, CallForegroundService::class.java).apply {
|
|
60
|
+
// putExtras(Bundle().apply { data.forEach { (k, v) -> putString(k, v) } })
|
|
61
|
+
// }
|
|
62
|
+
// ContextCompat.startForegroundService(context, serviceIntent)
|
|
63
|
+
|
|
64
|
+
val headlessIntent = Intent(context, CallHeadlessTask::class.java).apply {
|
|
65
|
+
putExtras(Bundle().apply { data.forEach { (k, v) -> putString(k, v) } })
|
|
66
|
+
}
|
|
66
67
|
try {
|
|
67
|
-
|
|
68
|
-
val bundle = Bundle()
|
|
69
|
-
data.forEach { (k, v) -> bundle.putString(k, v) }
|
|
70
|
-
putExtras(bundle)
|
|
71
|
-
}
|
|
72
|
-
ContextCompat.startForegroundService(context, headlessIntent)
|
|
68
|
+
context.startService(headlessIntent)
|
|
73
69
|
HeadlessJsTaskService.acquireWakeLockNow(context)
|
|
74
|
-
} catch (e: Exception) {
|
|
75
|
-
e.printStackTrace()
|
|
70
|
+
} catch (e: Exception) {
|
|
71
|
+
e.printStackTrace()
|
|
76
72
|
}
|
|
77
73
|
|
|
78
|
-
// 3. Set Backup Timer (The Ghost Prevention Logic)
|
|
79
|
-
// Remove any old timer for this UUID before setting a new one
|
|
80
|
-
stopBackupTimer(uuid)
|
|
81
74
|
|
|
82
|
-
val showNotificationRunnable = Runnable {
|
|
83
|
-
if (!isAppInForeground(context)) {
|
|
84
|
-
Log.d("CallMessagingService", "Backup timer triggered for: $uuid")
|
|
85
|
-
NativeCallManager.handleIncomingPush(context, data)
|
|
86
|
-
// 1. Trigger Notification UI immediately
|
|
87
|
-
// NativeCallManager.handleIncomingPush(context, data)
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
pendingNotifications.remove(uuid)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
pendingNotifications[uuid] = showNotificationRunnable
|
|
94
|
-
handler.postDelayed(showNotificationRunnable, 18000)
|
|
95
75
|
}
|
|
96
76
|
}
|
|
97
77
|
|
|
98
|
-
private fun showMissedCallNotification(
|
|
78
|
+
private fun showMissedCallNotification(
|
|
79
|
+
context: Context,
|
|
80
|
+
data: Map<String, String>,
|
|
81
|
+
uuid: String
|
|
82
|
+
) {
|
|
99
83
|
val name = data["name"] ?: "Unknown"
|
|
100
84
|
val callType = data["callType"] ?: "video"
|
|
101
|
-
val channelId = "missed_calls"
|
|
102
|
-
val article = if (callType.startsWith("a", ignoreCase = true)) "an" else "a"
|
|
103
|
-
val appName = context.applicationInfo.loadLabel(context.packageManager).toString()
|
|
104
|
-
val capitalizedAppName = appName.replaceFirstChar { it.uppercase() }
|
|
85
|
+
val channelId = "missed_calls"
|
|
105
86
|
|
|
106
|
-
val notificationManager =
|
|
87
|
+
val notificationManager =
|
|
88
|
+
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
107
89
|
|
|
108
90
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
109
|
-
val channel = NotificationChannel(
|
|
91
|
+
val channel = NotificationChannel(
|
|
92
|
+
channelId,
|
|
93
|
+
"Missed Calls",
|
|
94
|
+
NotificationManager.IMPORTANCE_DEFAULT
|
|
95
|
+
).apply { description = "Missed call notifications" }
|
|
110
96
|
notificationManager.createNotificationChannel(channel)
|
|
111
97
|
}
|
|
112
98
|
|
|
113
|
-
val
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
PendingIntent.FLAG_UPDATE_CURRENT
|
|
118
|
-
}
|
|
119
|
-
val contentIntent = PendingIntent.getActivity(context, uuid.hashCode(), launchIntent, pendingFlags)
|
|
99
|
+
val iconResId =
|
|
100
|
+
context.resources.getIdentifier("ic_missed_call", "drawable", context.packageName)
|
|
101
|
+
.takeIf { it != 0 } ?: android.R.drawable.sym_call_missed
|
|
102
|
+
val appName = context.applicationInfo.loadLabel(context.packageManager).toString()
|
|
120
103
|
|
|
121
|
-
|
|
122
|
-
|
|
104
|
+
val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
|
|
105
|
+
launchIntent?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
|
106
|
+
val pendingIntent = PendingIntent.getActivity(
|
|
107
|
+
context,
|
|
108
|
+
uuid.hashCode(),
|
|
109
|
+
launchIntent,
|
|
110
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
|
111
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
112
|
+
else
|
|
113
|
+
PendingIntent.FLAG_UPDATE_CURRENT
|
|
114
|
+
)
|
|
123
115
|
|
|
124
116
|
val builder = NotificationCompat.Builder(context, channelId)
|
|
125
117
|
.setSmallIcon(iconResId)
|
|
126
|
-
.setContentTitle("$
|
|
127
|
-
.setContentText("You missed
|
|
118
|
+
.setContentTitle("$appName • Missed $callType call")
|
|
119
|
+
.setContentText("You missed a call from $name")
|
|
128
120
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
|
129
121
|
.setAutoCancel(true)
|
|
130
|
-
.
|
|
131
|
-
.
|
|
122
|
+
.setCategory(NotificationCompat.CATEGORY_MISSED_CALL)
|
|
123
|
+
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
124
|
+
.setContentIntent(pendingIntent)
|
|
132
125
|
|
|
133
126
|
notificationManager.notify(uuid.hashCode(), builder.build())
|
|
134
127
|
}
|
|
135
|
-
|
|
128
|
+
|
|
129
|
+
private fun isAppInForeground(context: Context): Boolean {
|
|
130
|
+
val activityManager =
|
|
131
|
+
context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
|
132
|
+
val appProcesses = activityManager.runningAppProcesses ?: return false
|
|
133
|
+
val packageName = context.packageName
|
|
134
|
+
return appProcesses.any {
|
|
135
|
+
it.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND &&
|
|
136
|
+
it.processName == packageName
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -7,23 +7,32 @@ import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
|
7
7
|
|
|
8
8
|
class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
|
|
9
9
|
|
|
10
|
-
init {
|
|
10
|
+
init {
|
|
11
11
|
instance = this
|
|
12
|
+
notifyReady()
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
override fun getName() = "CallModule"
|
|
15
16
|
|
|
16
17
|
@ReactMethod
|
|
17
18
|
fun getInitialCallData(promise: Promise) {
|
|
18
|
-
val
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
writableMap
|
|
19
|
+
val writableMap = Arguments.createMap()
|
|
20
|
+
|
|
21
|
+
pendingCallDataMap?.let { map ->
|
|
22
|
+
val eventMap = Arguments.createMap()
|
|
23
|
+
map.forEach { (key, value) -> eventMap.putString(key, value) }
|
|
24
|
+
writableMap.putMap("default", eventMap)
|
|
25
|
+
pendingCallDataMap = null
|
|
24
26
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
|
|
28
|
+
pendingEvents.forEach { (eventName, data) ->
|
|
29
|
+
val eventMap = Arguments.createMap()
|
|
30
|
+
data.forEach { (key, value) -> eventMap.putString(key, value) }
|
|
31
|
+
writableMap.putMap(eventName, eventMap)
|
|
32
|
+
}
|
|
33
|
+
pendingEvents.clear()
|
|
34
|
+
|
|
35
|
+
promise.resolve(writableMap)
|
|
27
36
|
}
|
|
28
37
|
|
|
29
38
|
@ReactMethod
|
|
@@ -34,7 +43,6 @@ class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
|
|
|
34
43
|
"name" to name,
|
|
35
44
|
"callType" to callType
|
|
36
45
|
)
|
|
37
|
-
// Use reactApplicationContext safely here
|
|
38
46
|
NativeCallManager.handleIncomingPush(reactApplicationContext, data)
|
|
39
47
|
promise.resolve(true)
|
|
40
48
|
} catch (e: Exception) {
|
|
@@ -42,13 +50,47 @@ class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
|
|
|
42
50
|
}
|
|
43
51
|
}
|
|
44
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Combined Validity Check:
|
|
55
|
+
* Used by Headless Task to see if it should proceed with the UI.
|
|
56
|
+
*/
|
|
57
|
+
@ReactMethod
|
|
58
|
+
fun checkCallValidity(uuid: String, promise: Promise) {
|
|
59
|
+
val isValid = CallState.shouldProceed(uuid)
|
|
60
|
+
val isCanceled = CallState.isCanceled(uuid)
|
|
61
|
+
|
|
62
|
+
val map = Arguments.createMap().apply {
|
|
63
|
+
putBoolean("isValid", isValid)
|
|
64
|
+
putBoolean("isCanceled", isCanceled)
|
|
65
|
+
}
|
|
66
|
+
promise.resolve(map)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* General Status Check:
|
|
71
|
+
* Useful for checking if the UI is still relevant.
|
|
72
|
+
*/
|
|
73
|
+
@ReactMethod
|
|
74
|
+
fun checkCallStatus(uuid: String, promise: Promise) {
|
|
75
|
+
val isCanceled = CallState.isCanceled(uuid)
|
|
76
|
+
val isCurrent = CallState.getCurrent() == uuid
|
|
77
|
+
|
|
78
|
+
val map = Arguments.createMap().apply {
|
|
79
|
+
putBoolean("isCanceled", isCanceled)
|
|
80
|
+
putBoolean("isActive", isCurrent)
|
|
81
|
+
putBoolean("shouldDisplay", isCurrent && !isCanceled)
|
|
82
|
+
}
|
|
83
|
+
promise.resolve(map)
|
|
84
|
+
}
|
|
85
|
+
|
|
45
86
|
@ReactMethod
|
|
46
87
|
fun endNativeCall(uuid: String) {
|
|
47
88
|
NativeCallManager.stopRingtone()
|
|
89
|
+
CallState.clear(uuid)
|
|
48
90
|
pendingCallDataMap = null
|
|
49
91
|
|
|
50
92
|
val notificationManager = reactApplicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
51
|
-
notificationManager.cancel(
|
|
93
|
+
notificationManager.cancel(101)
|
|
52
94
|
}
|
|
53
95
|
|
|
54
96
|
@ReactMethod
|
|
@@ -62,17 +104,47 @@ class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
|
|
|
62
104
|
companion object {
|
|
63
105
|
private var instance: CallModule? = null
|
|
64
106
|
private var pendingCallDataMap: Map<String, String>? = null
|
|
107
|
+
private var pendingEvents: MutableMap<String, Map<String, String>> = mutableMapOf()
|
|
108
|
+
private val onReadyCallbacks = mutableListOf<() -> Unit>()
|
|
65
109
|
|
|
66
|
-
// This is the static gate accessible from AcceptCallActivity
|
|
67
110
|
@JvmStatic
|
|
111
|
+
fun isReady(): Boolean {
|
|
112
|
+
val reactContext = instance?.reactApplicationContext
|
|
113
|
+
return reactContext != null && reactContext.hasActiveCatalystInstance()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@JvmStatic
|
|
68
117
|
fun setPendingCallData(data: Map<String, String>) {
|
|
69
|
-
|
|
118
|
+
pendingCallDataMap = data
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@JvmStatic
|
|
122
|
+
fun setPendingCallData(eventName: String, data: Map<String, String>) {
|
|
123
|
+
pendingEvents[eventName] = data
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@JvmStatic
|
|
127
|
+
fun registerOnReadyCallback(callback: () -> Unit) {
|
|
128
|
+
if (isReady()) {
|
|
129
|
+
callback()
|
|
130
|
+
} else {
|
|
131
|
+
onReadyCallbacks.add(callback)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
fun notifyReady() {
|
|
136
|
+
onReadyCallbacks.forEach { it() }
|
|
137
|
+
onReadyCallbacks.clear()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@JvmStatic
|
|
141
|
+
fun setPendingEvent(eventName: String, data: Map<String, String>) {
|
|
142
|
+
pendingEvents[eventName] = data
|
|
70
143
|
}
|
|
71
144
|
|
|
72
145
|
@JvmStatic
|
|
73
146
|
fun sendEventToJS(eventName: String, params: Any?) {
|
|
74
147
|
val reactContext = instance?.reactApplicationContext
|
|
75
|
-
|
|
76
148
|
val bridgeData = when (params) {
|
|
77
149
|
is Map<*, *> -> {
|
|
78
150
|
val map = Arguments.createMap()
|
|
@@ -84,16 +156,13 @@ class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
|
|
|
84
156
|
else -> null
|
|
85
157
|
}
|
|
86
158
|
|
|
87
|
-
|
|
88
|
-
if (reactContext != null && reactContext.hasActiveCatalystInstance()) {
|
|
159
|
+
if (isReady()) {
|
|
89
160
|
reactContext
|
|
90
|
-
|
|
161
|
+
?.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
91
162
|
?.emit(eventName, bridgeData)
|
|
92
|
-
}
|
|
93
|
-
// If app is in killed state, save for polling
|
|
94
|
-
else if (eventName == "onCallAccepted" && params is Map<*, *>) {
|
|
163
|
+
} else if (params is Map<*, *>) {
|
|
95
164
|
@Suppress("UNCHECKED_CAST")
|
|
96
|
-
|
|
165
|
+
setPendingEvent(eventName, params as Map<String, String>)
|
|
97
166
|
}
|
|
98
167
|
}
|
|
99
168
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
package com.rnsnativecall
|
|
2
|
+
|
|
3
|
+
object CallState {
|
|
4
|
+
@Volatile private var currentUuid: String? = null
|
|
5
|
+
@Volatile private var canceledUuids = mutableSetOf<String>()
|
|
6
|
+
|
|
7
|
+
@Synchronized
|
|
8
|
+
fun isBusy(): Boolean = currentUuid != null
|
|
9
|
+
|
|
10
|
+
@Synchronized
|
|
11
|
+
fun setCurrent(uuid: String): Boolean {
|
|
12
|
+
if (currentUuid == null) {
|
|
13
|
+
currentUuid = uuid
|
|
14
|
+
return true
|
|
15
|
+
}
|
|
16
|
+
return currentUuid == uuid
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
fun getCurrent(): String? = currentUuid
|
|
20
|
+
|
|
21
|
+
@Synchronized
|
|
22
|
+
fun markCanceled(uuid: String) {
|
|
23
|
+
canceledUuids.add(uuid)
|
|
24
|
+
if (currentUuid == uuid) {
|
|
25
|
+
currentUuid = null
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ✅ Fixes the "Unresolved reference 'isCanceled'" error
|
|
30
|
+
@Synchronized
|
|
31
|
+
fun isCanceled(uuid: String?): Boolean {
|
|
32
|
+
if (uuid == null) return false
|
|
33
|
+
return canceledUuids.contains(uuid)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@Synchronized
|
|
37
|
+
fun clear(uuid: String?) {
|
|
38
|
+
if (uuid == null) return
|
|
39
|
+
if (currentUuid == uuid) currentUuid = null
|
|
40
|
+
canceledUuids.remove(uuid)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@Synchronized
|
|
44
|
+
fun shouldProceed(uuid: String?): Boolean {
|
|
45
|
+
if (uuid == null) return false
|
|
46
|
+
return (currentUuid == uuid) && !canceledUuids.contains(uuid)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@Synchronized
|
|
50
|
+
fun clearAll() {
|
|
51
|
+
currentUuid = null
|
|
52
|
+
canceledUuids.clear()
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -6,25 +6,23 @@ import android.app.PendingIntent
|
|
|
6
6
|
import android.content.Context
|
|
7
7
|
import android.content.Intent
|
|
8
8
|
import android.os.Build
|
|
9
|
-
import android.os.Bundle
|
|
10
9
|
import androidx.core.app.NotificationCompat
|
|
11
10
|
import android.media.Ringtone
|
|
12
11
|
import android.media.RingtoneManager
|
|
13
12
|
import android.graphics.Color
|
|
14
|
-
import androidx.core.app.Person
|
|
15
|
-
import androidx.core.graphics.drawable.IconCompat
|
|
16
13
|
|
|
17
14
|
object NativeCallManager {
|
|
18
15
|
|
|
19
16
|
private var ringtone: Ringtone? = null
|
|
20
|
-
const val
|
|
17
|
+
const val channelId = "CALL_CHANNEL_ID"
|
|
21
18
|
|
|
22
19
|
fun handleIncomingPush(context: Context, data: Map<String, String>) {
|
|
23
20
|
val uuid = data["callUuid"] ?: return
|
|
24
21
|
stopRingtone()
|
|
25
|
-
|
|
26
|
-
val name = data["name"] ?: "
|
|
22
|
+
|
|
23
|
+
val name = data["name"] ?: "Incoming Call"
|
|
27
24
|
val callType = data["callType"] ?: "audio"
|
|
25
|
+
val notificationId = uuid.hashCode()
|
|
28
26
|
|
|
29
27
|
val pendingFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
30
28
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
|
|
@@ -32,94 +30,136 @@ object NativeCallManager {
|
|
|
32
30
|
PendingIntent.FLAG_UPDATE_CURRENT
|
|
33
31
|
}
|
|
34
32
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
val noOpIntent = PendingIntent.getActivity(
|
|
34
|
+
context,
|
|
35
|
+
notificationId + 1,
|
|
36
|
+
Intent(),
|
|
37
|
+
pendingFlags
|
|
38
|
+
)
|
|
40
39
|
|
|
41
|
-
// 1. UI Intent (When tapping the notification body)
|
|
42
40
|
val intentToActivity = Intent(context, AcceptCallActivity::class.java).apply {
|
|
43
|
-
action = "ACTION_SHOW_UI_$uuid"
|
|
44
|
-
|
|
45
|
-
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
|
41
|
+
this.action = "ACTION_SHOW_UI_$uuid"
|
|
42
|
+
data.forEach { (key, value) -> this.putExtra(key, value) }
|
|
43
|
+
this.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
|
46
44
|
}
|
|
45
|
+
|
|
47
46
|
val fullScreenPendingIntent = PendingIntent.getActivity(
|
|
48
|
-
context,
|
|
47
|
+
context,
|
|
48
|
+
notificationId,
|
|
49
|
+
intentToActivity,
|
|
50
|
+
pendingFlags
|
|
49
51
|
)
|
|
50
52
|
|
|
51
|
-
// 2. Reject Intent
|
|
52
53
|
val rejectIntent = Intent(context, CallActionReceiver::class.java).apply {
|
|
53
|
-
action = "ACTION_REJECT_$uuid"
|
|
54
|
-
|
|
54
|
+
this.action = "ACTION_REJECT_$uuid"
|
|
55
|
+
this.putExtra("EXTRA_CALL_UUID", uuid)
|
|
56
|
+
data.forEach { (key, value) -> this.putExtra(key, value) }
|
|
55
57
|
}
|
|
56
|
-
val rejectPendingIntent = PendingIntent.getBroadcast(
|
|
57
|
-
context, uuid.hashCode() + 1, rejectIntent, pendingFlags
|
|
58
|
-
)
|
|
59
58
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
val answerPendingIntent = PendingIntent.getBroadcast(
|
|
66
|
-
context, uuid.hashCode() + 2, answerIntent, pendingFlags
|
|
59
|
+
val rejectPendingIntent = PendingIntent.getBroadcast(
|
|
60
|
+
context,
|
|
61
|
+
notificationId,
|
|
62
|
+
rejectIntent,
|
|
63
|
+
pendingFlags
|
|
67
64
|
)
|
|
68
65
|
|
|
69
66
|
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
70
67
|
|
|
71
68
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
72
|
-
val channel = NotificationChannel(
|
|
69
|
+
val channel = NotificationChannel(
|
|
70
|
+
channelId,
|
|
71
|
+
"Incoming Calls",
|
|
72
|
+
NotificationManager.IMPORTANCE_HIGH // NotificationManager.IMPORTANCE_HIGH
|
|
73
|
+
).apply {
|
|
74
|
+
enableVibration(true)
|
|
75
|
+
vibrationPattern = longArrayOf(0, 500, 500, 500)
|
|
76
|
+
lightColor = Color.GREEN
|
|
73
77
|
setBypassDnd(true)
|
|
74
78
|
lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
|
|
75
|
-
|
|
76
|
-
setSound(null, null)
|
|
79
|
+
setSound(null, null)
|
|
77
80
|
}
|
|
78
81
|
notificationManager.createNotificationChannel(channel)
|
|
79
82
|
}
|
|
80
83
|
|
|
81
|
-
val builder = NotificationCompat.Builder(context,
|
|
84
|
+
val builder = NotificationCompat.Builder(context, channelId)
|
|
82
85
|
.setSmallIcon(context.applicationInfo.icon)
|
|
83
86
|
.setContentTitle("Incoming $callType call")
|
|
84
87
|
.setContentText(name)
|
|
85
|
-
.setPriority(NotificationCompat.PRIORITY_MAX)
|
|
86
|
-
.setCategory(NotificationCompat.CATEGORY_CALL)
|
|
87
|
-
.setOngoing(true)
|
|
88
|
+
.setPriority(NotificationCompat.PRIORITY_MAX) // PRIORITY_HIGH
|
|
89
|
+
.setCategory(NotificationCompat.CATEGORY_CALL) // CATEGORY_CALL
|
|
90
|
+
.setOngoing(true)
|
|
88
91
|
.setAutoCancel(false)
|
|
89
|
-
.
|
|
90
|
-
.
|
|
91
|
-
.
|
|
92
|
-
|
|
93
|
-
// First Action added = Leftmost button
|
|
94
|
-
.addAction(0, "Answer", answerPendingIntent)
|
|
92
|
+
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
93
|
+
.setFullScreenIntent(fullScreenPendingIntent, true)
|
|
94
|
+
.setContentIntent(noOpIntent)
|
|
95
|
+
.addAction(0, "Answer", fullScreenPendingIntent)
|
|
95
96
|
.addAction(0, "Decline", rejectPendingIntent)
|
|
96
97
|
|
|
98
|
+
notificationManager.notify(notificationId, builder.build())
|
|
97
99
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
notificationManager.notify(uuid.hashCode(), builder.build())
|
|
101
|
-
|
|
102
|
-
// Start Ringtone
|
|
103
100
|
try {
|
|
104
101
|
val ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
|
|
105
102
|
ringtone = RingtoneManager.getRingtone(context, ringtoneUri)
|
|
106
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
|
|
103
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
104
|
+
ringtone?.isLooping = true
|
|
105
|
+
}
|
|
107
106
|
ringtone?.play()
|
|
108
|
-
} catch (e: Exception) {
|
|
107
|
+
} catch (e: Exception) {
|
|
108
|
+
e.printStackTrace()
|
|
109
|
+
}
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
fun stopRingtone() {
|
|
112
113
|
try {
|
|
113
114
|
ringtone?.let { if (it.isPlaying) it.stop() }
|
|
114
115
|
ringtone = null
|
|
115
|
-
} catch (e: Exception) {
|
|
116
|
+
} catch (e: Exception) {
|
|
117
|
+
ringtone = null
|
|
118
|
+
}
|
|
116
119
|
}
|
|
117
120
|
|
|
121
|
+
fun connecting(context: Context, uuid: String, name: String, callType: String) {
|
|
122
|
+
val notificationId = uuid.hashCode()
|
|
123
|
+
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
124
|
+
|
|
125
|
+
val builder = NotificationCompat.Builder(context, channelId)
|
|
126
|
+
.setSmallIcon(context.applicationInfo.icon)
|
|
127
|
+
.setContentTitle("Incoming $callType call")
|
|
128
|
+
.setContentText("Connecting…") // ✅ show connecting text
|
|
129
|
+
.setSubText("Connecting…") // status line
|
|
130
|
+
.setPriority(NotificationCompat.PRIORITY_MAX)
|
|
131
|
+
.setCategory(NotificationCompat.CATEGORY_CALL)
|
|
132
|
+
.setOngoing(true)
|
|
133
|
+
.setAutoCancel(false)
|
|
134
|
+
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
135
|
+
.setProgress(0, 0, true) // ✅ system activity indicator (indeterminate progress bar)
|
|
136
|
+
|
|
137
|
+
notificationManager.notify(notificationId, builder.build())
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
fun aborting(context: Context, uuid: String, name: String, callType: String) {
|
|
141
|
+
val notificationId = uuid.hashCode()
|
|
142
|
+
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
143
|
+
|
|
144
|
+
val builder = NotificationCompat.Builder(context, channelId)
|
|
145
|
+
.setSmallIcon(context.applicationInfo.icon)
|
|
146
|
+
.setContentTitle("Incoming $callType call")
|
|
147
|
+
.setContentText("Aborting…") // ✅ show aborting text
|
|
148
|
+
.setSubText("Aborting…") // status line
|
|
149
|
+
.setPriority(NotificationCompat.PRIORITY_MAX)
|
|
150
|
+
.setCategory(NotificationCompat.CATEGORY_CALL)
|
|
151
|
+
.setOngoing(true)
|
|
152
|
+
.setAutoCancel(false)
|
|
153
|
+
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
154
|
+
.setProgress(0, 0, true) // ✅ indeterminate progress indicator
|
|
155
|
+
|
|
156
|
+
notificationManager.notify(notificationId, builder.build())
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
118
160
|
fun dismissIncomingCall(context: Context, uuid: String?) {
|
|
119
161
|
stopRingtone()
|
|
120
162
|
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
121
|
-
if (uuid != null)
|
|
122
|
-
notificationManager.cancel(uuid.hashCode())
|
|
123
|
-
}
|
|
163
|
+
if (uuid != null) notificationManager.cancel(uuid.hashCode())
|
|
124
164
|
}
|
|
125
165
|
}
|