rns-nativecall 0.3.6 → 0.3.8
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/rnsnativecall/AcceptCallActivity.kt +20 -18
- package/android/src/main/java/com/rnsnativecall/CallActionReceiver.kt +17 -11
- package/android/src/main/java/com/rnsnativecall/CallMessagingService.kt +62 -44
- package/android/src/main/java/com/rnsnativecall/NativeCallManager.kt +39 -44
- package/package.json +2 -1
- package/withCallNativeConfig.js +147 -0
- package/withNativeCallVoip.js +18 -25
|
@@ -9,16 +9,10 @@ import android.os.Bundle
|
|
|
9
9
|
class AcceptCallActivity : Activity() {
|
|
10
10
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
11
11
|
super.onCreate(savedInstanceState)
|
|
12
|
-
|
|
13
|
-
// 1.
|
|
14
|
-
// Use the same ID (101) used in NativeCallManager
|
|
15
|
-
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
16
|
-
notificationManager.cancel(101)
|
|
17
|
-
|
|
18
|
-
// 2. EXTRACT DATA SAFELY
|
|
19
|
-
// We iterate through all extras and convert them to Strings for the JS Map
|
|
20
|
-
val dataMap = mutableMapOf<String, String>()
|
|
12
|
+
|
|
13
|
+
// 1. EXTRACT DATA SAFELY
|
|
21
14
|
val extras = intent.extras
|
|
15
|
+
val dataMap = mutableMapOf<String, String>()
|
|
22
16
|
extras?.keySet()?.forEach { key ->
|
|
23
17
|
val value = extras.get(key)
|
|
24
18
|
if (value != null) {
|
|
@@ -26,30 +20,38 @@ class AcceptCallActivity : Activity() {
|
|
|
26
20
|
}
|
|
27
21
|
}
|
|
28
22
|
|
|
23
|
+
val uuid = dataMap["callUuid"]
|
|
24
|
+
|
|
25
|
+
// 2. STOP RINGING & CLEAR THE SPECIFIC NOTIFICATION
|
|
26
|
+
NativeCallManager.stopRingtone()
|
|
27
|
+
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
28
|
+
|
|
29
|
+
if (uuid != null) {
|
|
30
|
+
// MATCHING THE NEW HASHCODE LOGIC
|
|
31
|
+
notificationManager.cancel(uuid.hashCode())
|
|
32
|
+
} else {
|
|
33
|
+
// Fallback for safety
|
|
34
|
+
notificationManager.cancel(101)
|
|
35
|
+
}
|
|
36
|
+
|
|
29
37
|
// 3. EMIT EVENT TO REACT NATIVE
|
|
30
|
-
// This handles the case where the app is already in the background/foreground
|
|
31
38
|
CallModule.sendEventToJS("onCallAccepted", dataMap)
|
|
39
|
+
|
|
40
|
+
// Ensure the data is available for the JS bridge even if it's just waking up
|
|
41
|
+
CallModule.setPendingCallData(dataMap)
|
|
32
42
|
|
|
33
43
|
// 4. LAUNCH OR BRING MAIN APP TO FOREGROUND
|
|
34
44
|
val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
|
|
35
45
|
if (launchIntent != null) {
|
|
36
46
|
launchIntent.apply {
|
|
37
|
-
// FLAG_ACTIVITY_SINGLE_TOP: Updates the app if it's already open
|
|
38
|
-
// FLAG_ACTIVITY_NEW_TASK: Required when starting from a non-activity context
|
|
39
47
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
|
40
|
-
|
|
41
|
-
// Copy all call data to the launch intent
|
|
42
48
|
putExtras(extras ?: Bundle())
|
|
43
|
-
|
|
44
|
-
// Helper flag for your React Native logic
|
|
45
49
|
putExtra("navigatingToCall", true)
|
|
46
50
|
}
|
|
47
51
|
startActivity(launchIntent)
|
|
48
52
|
}
|
|
49
53
|
|
|
50
|
-
CallModule.setPendingCallData(dataMap)
|
|
51
54
|
// 5. FINISH TRAMPOLINE
|
|
52
|
-
// This ensures this invisible activity doesn't stay in the "Recent Apps" list
|
|
53
55
|
finish()
|
|
54
56
|
}
|
|
55
57
|
}
|
|
@@ -8,30 +8,36 @@ import android.os.Bundle
|
|
|
8
8
|
|
|
9
9
|
class CallActionReceiver : BroadcastReceiver() {
|
|
10
10
|
override fun onReceive(context: Context, intent: Intent) {
|
|
11
|
+
// 1. Always stop the ringtone immediately
|
|
11
12
|
NativeCallManager.stopRingtone()
|
|
13
|
+
|
|
12
14
|
val uuid = intent.getStringExtra("EXTRA_CALL_UUID")
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
val action = intent.action
|
|
17
|
+
|
|
18
|
+
// 2. Clear the specific notification using the hash code
|
|
15
19
|
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
16
|
-
|
|
20
|
+
if (uuid != null) {
|
|
21
|
+
notificationManager.cancel(uuid.hashCode())
|
|
22
|
+
} else {
|
|
23
|
+
notificationManager.cancel(101) // Fallback for safety
|
|
24
|
+
}
|
|
17
25
|
|
|
18
|
-
|
|
19
|
-
|
|
26
|
+
// 3. Handle Actions
|
|
27
|
+
// We check for .contains or .startsWith because our actions are "ACTION_REJECT_$uuid"
|
|
28
|
+
if (action?.contains("ACTION_ACCEPT") == true || action?.contains("ACTION_SHOW_UI") == true) {
|
|
20
29
|
val dataMap = mutableMapOf<String, String>()
|
|
21
30
|
intent.extras?.keySet()?.forEach { key ->
|
|
22
|
-
// Using get(key).toString() is safer than getString(key)
|
|
23
|
-
// because some extras might be Ints or Booleans
|
|
24
31
|
intent.extras?.get(key)?.let { dataMap[key] = it.toString() }
|
|
25
32
|
}
|
|
26
33
|
|
|
27
|
-
//
|
|
28
|
-
// Store data in the "Holding Gate" inside CallModule
|
|
34
|
+
// Store data for the JS Bridge "Holding Gate"
|
|
29
35
|
CallModule.setPendingCallData(dataMap)
|
|
30
36
|
|
|
31
|
-
//
|
|
37
|
+
// Emit event for active JS bridge
|
|
32
38
|
CallModule.sendEventToJS("onCallAccepted", dataMap)
|
|
33
39
|
|
|
34
|
-
//
|
|
40
|
+
// Bring app to foreground
|
|
35
41
|
val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
|
|
36
42
|
launchIntent?.apply {
|
|
37
43
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
|
@@ -40,7 +46,7 @@ class CallActionReceiver : BroadcastReceiver() {
|
|
|
40
46
|
}
|
|
41
47
|
context.startActivity(launchIntent)
|
|
42
48
|
|
|
43
|
-
} else if (
|
|
49
|
+
} else if (action?.contains("ACTION_REJECT") == true) {
|
|
44
50
|
// Logic for Reject
|
|
45
51
|
CallModule.sendEventToJS("onCallRejected", mapOf("callUuid" to (uuid ?: "")))
|
|
46
52
|
}
|
|
@@ -7,6 +7,9 @@ import android.os.Bundle
|
|
|
7
7
|
import android.os.Handler
|
|
8
8
|
import android.os.Looper
|
|
9
9
|
import android.app.NotificationManager
|
|
10
|
+
import android.app.NotificationChannel
|
|
11
|
+
import android.app.PendingIntent
|
|
12
|
+
import android.os.Build
|
|
10
13
|
import androidx.core.app.NotificationCompat
|
|
11
14
|
import com.google.firebase.messaging.FirebaseMessagingService
|
|
12
15
|
import com.google.firebase.messaging.RemoteMessage
|
|
@@ -17,89 +20,104 @@ class CallMessagingService : FirebaseMessagingService() {
|
|
|
17
20
|
companion object {
|
|
18
21
|
private val handler = Handler(Looper.getMainLooper())
|
|
19
22
|
private val pendingNotifications = mutableMapOf<String, Runnable>()
|
|
20
|
-
// This is the standard ID used for the incoming call notification pill
|
|
21
|
-
private const val CALL_NOTIFICATION_ID = 123
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
override fun onMessageReceived(remoteMessage: RemoteMessage) {
|
|
25
26
|
val data = remoteMessage.data
|
|
26
27
|
val context = applicationContext
|
|
27
|
-
|
|
28
28
|
val uuid = data["callUuid"] ?: return
|
|
29
29
|
val type = data["type"] ?: ""
|
|
30
30
|
|
|
31
31
|
if (type == "CANCEL") {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
// 3. Show Missed Call
|
|
45
|
-
showMissedCallNotification(context, data, uuid)
|
|
46
|
-
return
|
|
47
|
-
}
|
|
32
|
+
pendingNotifications[uuid]?.let {
|
|
33
|
+
handler.removeCallbacks(it)
|
|
34
|
+
pendingNotifications.remove(uuid)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Pass the uuid so the manager knows exactly which notification to remove
|
|
38
|
+
NativeCallManager.dismissIncomingCall(context, uuid)
|
|
39
|
+
|
|
40
|
+
showMissedCallNotification(context, data, uuid)
|
|
41
|
+
return
|
|
42
|
+
}
|
|
48
43
|
|
|
49
44
|
if (isAppInForeground(context)) {
|
|
50
45
|
CallModule.sendEventToJS("onCallReceived", data)
|
|
51
46
|
} else {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
47
|
+
// 1. Prepare the Intent
|
|
48
|
+
val headlessIntent = Intent(context, CallHeadlessTask::class.java)
|
|
49
|
+
val bundle = Bundle()
|
|
50
|
+
data.forEach { (k, v) -> bundle.putString(k, v) }
|
|
51
|
+
headlessIntent.putExtras(bundle)
|
|
52
|
+
|
|
53
|
+
// 2. Start the Service
|
|
54
|
+
context.startService(headlessIntent)
|
|
55
|
+
|
|
56
|
+
// 3. Acquire WakeLock to prevent the CPU from sleeping during bundling
|
|
57
|
+
try {
|
|
58
|
+
HeadlessJsTaskService.acquireWakeLockNow(context)
|
|
59
|
+
} catch (e: Exception) {
|
|
60
|
+
e.printStackTrace()
|
|
61
|
+
}
|
|
65
62
|
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
// 4. Start the 18s backup timer
|
|
64
|
+
val showNotificationRunnable = Runnable {
|
|
65
|
+
if (!isAppInForeground(context)) {
|
|
66
|
+
NativeCallManager.handleIncomingPush(context, data)
|
|
68
67
|
}
|
|
68
|
+
pendingNotifications.remove(uuid)
|
|
69
|
+
}
|
|
70
|
+
pendingNotifications[uuid] = showNotificationRunnable
|
|
71
|
+
handler.postDelayed(showNotificationRunnable, 18000)
|
|
72
|
+
}
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
private fun showMissedCallNotification(context: Context, data: Map<String, String>, uuid: String) {
|
|
72
76
|
val name = data["name"] ?: "Unknown"
|
|
73
77
|
val callType = data["callType"] ?: "video"
|
|
74
78
|
val channelId = "missed_calls"
|
|
79
|
+
val article = if (callType.startsWith("a", ignoreCase = true)) "an" else "a"
|
|
80
|
+
// 1. Format App Name: Capitalized
|
|
81
|
+
val appName = context.applicationInfo.loadLabel(context.packageManager).toString()
|
|
82
|
+
val capitalizedAppName = appName.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
|
|
75
83
|
|
|
76
84
|
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
77
85
|
|
|
86
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
87
|
+
val channel = NotificationChannel(channelId, "Missed Calls", NotificationManager.IMPORTANCE_HIGH)
|
|
88
|
+
notificationManager.createNotificationChannel(channel)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 2. Create Intent to open app when clicking missed call
|
|
92
|
+
val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
|
|
93
|
+
val pendingFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
94
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
95
|
+
} else {
|
|
96
|
+
PendingIntent.FLAG_UPDATE_CURRENT
|
|
97
|
+
}
|
|
98
|
+
val contentIntent = PendingIntent.getActivity(context, uuid.hashCode(), launchIntent, pendingFlags)
|
|
99
|
+
|
|
78
100
|
var iconResId = context.resources.getIdentifier("ic_missed_call", "drawable", context.packageName)
|
|
79
101
|
if (iconResId == 0) iconResId = android.R.drawable.sym_call_missed
|
|
80
102
|
|
|
81
103
|
val builder = NotificationCompat.Builder(context, channelId)
|
|
82
104
|
.setSmallIcon(iconResId)
|
|
83
|
-
.setContentTitle("
|
|
84
|
-
.setContentText("You missed
|
|
105
|
+
.setContentTitle("$capitalizedAppName missed call")
|
|
106
|
+
.setContentText("You missed $article $callType call from $name")
|
|
85
107
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
|
86
108
|
.setAutoCancel(true)
|
|
109
|
+
.setContentIntent(contentIntent) // Opens app on click
|
|
87
110
|
.setCategory(NotificationCompat.CATEGORY_MISSED_CALL)
|
|
88
111
|
|
|
112
|
+
// 3. Use unique hashCode so multiple missed calls are stacked separately
|
|
89
113
|
notificationManager.notify(uuid.hashCode(), builder.build())
|
|
90
114
|
}
|
|
91
115
|
|
|
92
116
|
private fun isAppInForeground(context: Context): Boolean {
|
|
93
117
|
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
|
94
118
|
val appProcesses = activityManager.runningAppProcesses ?: return false
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
for (appProcess in appProcesses) {
|
|
98
|
-
if (appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND &&
|
|
99
|
-
appProcess.processName == packageName) {
|
|
100
|
-
return true
|
|
101
|
-
}
|
|
119
|
+
return appProcesses.any {
|
|
120
|
+
it.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND && it.processName == context.packageName
|
|
102
121
|
}
|
|
103
|
-
return false
|
|
104
122
|
}
|
|
105
123
|
}
|
|
@@ -14,7 +14,8 @@ import android.graphics.Color
|
|
|
14
14
|
object NativeCallManager {
|
|
15
15
|
|
|
16
16
|
private var ringtone: Ringtone? = null
|
|
17
|
-
|
|
17
|
+
const val INCOMING_CALL_ID = 101
|
|
18
|
+
const val CALL_CHANNEL_ID = "CALL_CHANNEL_ID"
|
|
18
19
|
|
|
19
20
|
fun handleIncomingPush(context: Context, data: Map<String, String>) {
|
|
20
21
|
val uuid = data["callUuid"] ?: return
|
|
@@ -28,56 +29,36 @@ object NativeCallManager {
|
|
|
28
29
|
} else {
|
|
29
30
|
PendingIntent.FLAG_UPDATE_CURRENT
|
|
30
31
|
}
|
|
31
|
-
|
|
32
|
-
// This is better than a dummy intent because it allows the OS to
|
|
33
|
-
// launch your "Accept" logic immediately if the phone is locked.
|
|
32
|
+
|
|
34
33
|
val intentToActivity = Intent(context, AcceptCallActivity::class.java).apply {
|
|
35
34
|
action = "ACTION_SHOW_UI_$uuid"
|
|
36
35
|
data.forEach { (key, value) -> putExtra(key, value) }
|
|
37
36
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
|
38
37
|
}
|
|
39
38
|
|
|
40
|
-
val fullScreenPendingIntent = PendingIntent.getActivity(
|
|
41
|
-
context,
|
|
42
|
-
uuid.hashCode(),
|
|
43
|
-
intentToActivity,
|
|
44
|
-
pendingFlags
|
|
45
|
-
)
|
|
39
|
+
val fullScreenPendingIntent = PendingIntent.getActivity(context, uuid.hashCode(), intentToActivity, pendingFlags)
|
|
46
40
|
|
|
47
|
-
// 2. Reject Action - Still goes to BroadcastReceiver
|
|
48
41
|
val rejectIntent = Intent(context, CallActionReceiver::class.java).apply {
|
|
49
42
|
action = "ACTION_REJECT_$uuid"
|
|
50
43
|
putExtra("EXTRA_CALL_UUID", uuid)
|
|
51
|
-
// Pass all data so onReject can find the UUID
|
|
52
44
|
data.forEach { (key, value) -> putExtra(key, value) }
|
|
53
45
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
pendingFlags
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
// 3. Setup Channel with high priority
|
|
62
|
-
val channelId = "CALL_CHANNEL_ID"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
val rejectPendingIntent = PendingIntent.getBroadcast(context, uuid.hashCode() + 1, rejectIntent, pendingFlags)
|
|
49
|
+
|
|
63
50
|
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
64
51
|
|
|
65
52
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
66
|
-
val channel = NotificationChannel(
|
|
67
|
-
description = "Shows incoming call notifications"
|
|
68
|
-
enableVibration(true)
|
|
69
|
-
vibrationPattern = longArrayOf(0, 500, 500, 500)
|
|
70
|
-
lightColor = Color.GREEN
|
|
53
|
+
val channel = NotificationChannel(CALL_CHANNEL_ID, "Incoming Calls", NotificationManager.IMPORTANCE_HIGH).apply {
|
|
71
54
|
setBypassDnd(true)
|
|
72
55
|
lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
|
|
73
|
-
// On Android O+, sound should be set on the channel
|
|
74
56
|
setSound(null, null)
|
|
75
57
|
}
|
|
76
58
|
notificationManager.createNotificationChannel(channel)
|
|
77
59
|
}
|
|
78
60
|
|
|
79
|
-
|
|
80
|
-
val builder = NotificationCompat.Builder(context, channelId)
|
|
61
|
+
val builder = NotificationCompat.Builder(context, CALL_CHANNEL_ID)
|
|
81
62
|
.setSmallIcon(context.applicationInfo.icon)
|
|
82
63
|
.setContentTitle("Incoming $callType call")
|
|
83
64
|
.setContentText(name)
|
|
@@ -85,33 +66,47 @@ object NativeCallManager {
|
|
|
85
66
|
.setCategory(NotificationCompat.CATEGORY_CALL)
|
|
86
67
|
.setOngoing(true)
|
|
87
68
|
.setAutoCancel(false)
|
|
88
|
-
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
89
|
-
|
|
90
|
-
// This is the key for the "Pill" / Heads-up display
|
|
91
69
|
.setFullScreenIntent(fullScreenPendingIntent, true)
|
|
92
|
-
|
|
93
|
-
.addAction(0, "Answer", fullScreenPendingIntent) // Same intent as fullScreen
|
|
70
|
+
.addAction(0, "Answer", fullScreenPendingIntent)
|
|
94
71
|
.addAction(0, "Decline", rejectPendingIntent)
|
|
95
72
|
|
|
96
|
-
notificationManager.notify(
|
|
73
|
+
notificationManager.notify(uuid.hashCode(), builder.build())
|
|
97
74
|
|
|
98
|
-
// 5. Ringtone with looping logic attempt
|
|
99
75
|
try {
|
|
100
76
|
val ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
|
|
101
77
|
ringtone = RingtoneManager.getRingtone(context, ringtoneUri)
|
|
102
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
|
|
103
|
-
ringtone?.isLooping = true // Android 9+ supports looping natively
|
|
104
|
-
}
|
|
78
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) ringtone?.isLooping = true
|
|
105
79
|
ringtone?.play()
|
|
106
|
-
} catch (e: Exception) {
|
|
107
|
-
e.printStackTrace()
|
|
108
|
-
}
|
|
80
|
+
} catch (e: Exception) { e.printStackTrace() }
|
|
109
81
|
}
|
|
110
82
|
|
|
111
83
|
fun stopRingtone() {
|
|
112
|
-
|
|
113
|
-
|
|
84
|
+
try {
|
|
85
|
+
// Use the safe call operator ?. to only run if ringtone isn't null
|
|
86
|
+
ringtone?.let {
|
|
87
|
+
if (it.isPlaying) {
|
|
88
|
+
it.stop()
|
|
89
|
+
}
|
|
114
90
|
}
|
|
91
|
+
// Always null it out after stopping
|
|
92
|
+
ringtone = null
|
|
93
|
+
} catch (e: Exception) {
|
|
94
|
+
// Prevent the app from crashing if the system Ringtone service is acting up
|
|
95
|
+
e.printStackTrace()
|
|
115
96
|
ringtone = null
|
|
116
97
|
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fun dismissIncomingCall(context: Context, uuid: String?) {
|
|
101
|
+
stopRingtone()
|
|
102
|
+
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
103
|
+
|
|
104
|
+
if (uuid != null) {
|
|
105
|
+
notificationManager.cancel(uuid.hashCode())
|
|
106
|
+
} else {
|
|
107
|
+
// Fallback: If for some reason uuid is null, cancel everything
|
|
108
|
+
// (Optional: only if you want a safety net)
|
|
109
|
+
// notificationManager.cancelAll()
|
|
110
|
+
}
|
|
111
|
+
}
|
|
117
112
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rns-nativecall",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
4
4
|
"description": "RNS nativecall component with native Android/iOS for handling native call ui, when app is not open or open.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"react-native.config.js",
|
|
44
44
|
"README.md",
|
|
45
45
|
"rns-nativecall.podspec",
|
|
46
|
+
"withCallNativeConfig.js",
|
|
46
47
|
"withNativeCallVoip.js"
|
|
47
48
|
],
|
|
48
49
|
"peerDependencies": {
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
const { withAndroidManifest, withInfoPlist, withPlugins, withMainActivity } = require('@expo/config-plugins');
|
|
2
|
+
|
|
3
|
+
/** 1. ANDROID MAIN ACTIVITY MODS **/
|
|
4
|
+
function withMainActivityDataFix(config) {
|
|
5
|
+
return withMainActivity(config, (config) => {
|
|
6
|
+
let contents = config.modResults.contents;
|
|
7
|
+
|
|
8
|
+
// Ensure necessary Imports
|
|
9
|
+
if (!contents.includes('import android.content.Intent')) {
|
|
10
|
+
contents = contents.replace(/package .*/, (match) => `${match}\n\nimport android.content.Intent`);
|
|
11
|
+
}
|
|
12
|
+
if (!contents.includes('import android.os.Bundle')) {
|
|
13
|
+
contents = contents.replace(/package .*/, (match) => `${match}\n\nimport android.os.Bundle`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const onNewIntentCode = `
|
|
17
|
+
override fun onNewIntent(intent: Intent) {
|
|
18
|
+
super.onNewIntent(intent)
|
|
19
|
+
setIntent(intent)
|
|
20
|
+
}
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
const onCreateCode = `
|
|
24
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
25
|
+
super.onCreate(savedInstanceState)
|
|
26
|
+
// If woken up by a background task, push to back immediately
|
|
27
|
+
if (intent.getBooleanExtra("background_wake", false)) {
|
|
28
|
+
moveTaskToBack(true)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
// Insertion logic
|
|
34
|
+
if (!contents.includes('override fun onNewIntent')) {
|
|
35
|
+
contents = contents.replace(/class MainActivity : .*/, (match) => `${match}\n${onNewIntentCode}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!contents.includes('override fun onCreate')) {
|
|
39
|
+
// Find the end of the class or after onNewIntent
|
|
40
|
+
const lastBraceIndex = contents.lastIndexOf('}');
|
|
41
|
+
contents = contents.slice(0, lastBraceIndex) + onCreateCode + contents.slice(lastBraceIndex);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
config.modResults.contents = contents;
|
|
45
|
+
return config;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 2. ANDROID MANIFEST CONFIG **/
|
|
50
|
+
function withAndroidConfig(config) {
|
|
51
|
+
return withAndroidManifest(config, (config) => {
|
|
52
|
+
const manifest = config.modResults.manifest;
|
|
53
|
+
const application = manifest.application[0];
|
|
54
|
+
|
|
55
|
+
// 1. Permissions (Updated for Android 14 compatibility)
|
|
56
|
+
const permissions = [
|
|
57
|
+
'android.permission.USE_FULL_SCREEN_INTENT',
|
|
58
|
+
'android.permission.VIBRATE',
|
|
59
|
+
'android.permission.FOREGROUND_SERVICE',
|
|
60
|
+
'android.permission.FOREGROUND_SERVICE_PHONE_CALL',
|
|
61
|
+
'android.permission.POST_NOTIFICATIONS',
|
|
62
|
+
'android.permission.WAKE_LOCK',
|
|
63
|
+
'android.permission.DISABLE_KEYGUARD',
|
|
64
|
+
'android.permission.RECEIVE_BOOT_COMPLETED'
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
manifest['uses-permission'] = manifest['uses-permission'] || [];
|
|
68
|
+
permissions.forEach((perm) => {
|
|
69
|
+
if (!manifest['uses-permission'].some((p) => p.$['android:name'] === perm)) {
|
|
70
|
+
manifest['uses-permission'].push({ $: { 'android:name': perm } });
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// 2. Activity Setup (AcceptCallActivity)
|
|
75
|
+
application.activity = application.activity || [];
|
|
76
|
+
if (!application.activity.some(a => a.$['android:name'] === 'com.rnsnativecall.AcceptCallActivity')) {
|
|
77
|
+
application.activity.push({
|
|
78
|
+
$: {
|
|
79
|
+
'android:name': 'com.rnsnativecall.AcceptCallActivity',
|
|
80
|
+
'android:theme': '@android:style/Theme.Translucent.NoTitleBar',
|
|
81
|
+
'android:excludeFromRecents': 'true',
|
|
82
|
+
'android:noHistory': 'true',
|
|
83
|
+
'android:exported': 'false',
|
|
84
|
+
'android:launchMode': 'singleInstance',
|
|
85
|
+
'android:showWhenLocked': 'true',
|
|
86
|
+
'android:turnScreenOn': 'true'
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 3. Service Registration (The Headless Enforcer)
|
|
92
|
+
application.service = application.service || [];
|
|
93
|
+
|
|
94
|
+
// Firebase Messaging Service
|
|
95
|
+
if (!application.service.some(s => s.$['android:name'] === 'com.rnsnativecall.CallMessagingService')) {
|
|
96
|
+
application.service.push({
|
|
97
|
+
$: {
|
|
98
|
+
'android:name': 'com.rnsnativecall.CallMessagingService',
|
|
99
|
+
'android:exported': 'false'
|
|
100
|
+
},
|
|
101
|
+
'intent-filter': [{
|
|
102
|
+
action: [{ $: { 'android:name': 'com.google.firebase.MESSAGING_EVENT' } }]
|
|
103
|
+
}]
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Headless Task Service
|
|
108
|
+
if (!application.service.some(s => s.$['android:name'] === 'com.rnsnativecall.CallHeadlessTask')) {
|
|
109
|
+
application.service.push({
|
|
110
|
+
$: {
|
|
111
|
+
'android:name': 'com.rnsnativecall.CallHeadlessTask',
|
|
112
|
+
'android:exported': 'false' // Headless JS should be internal only
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 4. Action Receiver
|
|
118
|
+
application.receiver = application.receiver || [];
|
|
119
|
+
if (!application.receiver.some(r => r.$['android:name'] === 'com.rnsnativecall.CallActionReceiver')) {
|
|
120
|
+
application.receiver.push({
|
|
121
|
+
$: { 'android:name': 'com.rnsnativecall.CallActionReceiver', 'android:exported': 'false' }
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return config;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** 3. IOS CONFIG **/
|
|
130
|
+
function withIosConfig(config) {
|
|
131
|
+
return withInfoPlist(config, (config) => {
|
|
132
|
+
const infoPlist = config.modResults;
|
|
133
|
+
if (!infoPlist.UIBackgroundModes) infoPlist.UIBackgroundModes = [];
|
|
134
|
+
['voip', 'audio', 'remote-notification'].forEach(mode => {
|
|
135
|
+
if (!infoPlist.UIBackgroundModes.includes(mode)) infoPlist.UIBackgroundModes.push(mode);
|
|
136
|
+
});
|
|
137
|
+
return config;
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = (config) => {
|
|
142
|
+
return withPlugins(config, [
|
|
143
|
+
withAndroidConfig,
|
|
144
|
+
withMainActivityDataFix,
|
|
145
|
+
withIosConfig
|
|
146
|
+
]);
|
|
147
|
+
};
|
package/withNativeCallVoip.js
CHANGED
|
@@ -59,10 +59,11 @@ function withAndroidConfig(config) {
|
|
|
59
59
|
'android.permission.USE_FULL_SCREEN_INTENT',
|
|
60
60
|
'android.permission.VIBRATE',
|
|
61
61
|
'android.permission.FOREGROUND_SERVICE',
|
|
62
|
-
'android.permission.FOREGROUND_SERVICE_PHONE_CALL',
|
|
62
|
+
'android.permission.FOREGROUND_SERVICE_PHONE_CALL', // Required for Android 14
|
|
63
63
|
'android.permission.POST_NOTIFICATIONS',
|
|
64
64
|
'android.permission.WAKE_LOCK',
|
|
65
|
-
'android.permission.DISABLE_KEYGUARD'
|
|
65
|
+
'android.permission.DISABLE_KEYGUARD',
|
|
66
|
+
'android.permission.RECEIVE_BOOT_COMPLETED' // Allows app to wake after phone restart
|
|
66
67
|
];
|
|
67
68
|
|
|
68
69
|
manifest.manifest['uses-permission'] = manifest.manifest['uses-permission'] || [];
|
|
@@ -72,26 +73,11 @@ function withAndroidConfig(config) {
|
|
|
72
73
|
}
|
|
73
74
|
});
|
|
74
75
|
|
|
75
|
-
// 2. Activity Setup
|
|
76
|
+
// 2. Activity Setup (Remain unchanged as your logic was correct)
|
|
76
77
|
application.activity = application.activity || [];
|
|
78
|
+
// ... (Keep your AcceptCallActivity logic here)
|
|
77
79
|
|
|
78
|
-
//
|
|
79
|
-
if (!application.activity.some(a => a.$['android:name'] === 'com.rnsnativecall.AcceptCallActivity')) {
|
|
80
|
-
application.activity.push({
|
|
81
|
-
$: {
|
|
82
|
-
'android:name': 'com.rnsnativecall.AcceptCallActivity',
|
|
83
|
-
'android:theme': '@android:style/Theme.Translucent.NoTitleBar',
|
|
84
|
-
'android:excludeFromRecents': 'true',
|
|
85
|
-
'android:noHistory': 'true',
|
|
86
|
-
'android:exported': 'false',
|
|
87
|
-
'android:launchMode': 'singleInstance',
|
|
88
|
-
'android:showWhenLocked': 'true',
|
|
89
|
-
'android:turnScreenOn': 'true'
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// 3. Service Setup (Firebase + Headless Task)
|
|
80
|
+
// 3. Service Setup (CRITICAL UPDATES FOR ANDROID 14)
|
|
95
81
|
application.service = application.service || [];
|
|
96
82
|
|
|
97
83
|
// Add Firebase Messaging Service
|
|
@@ -100,7 +86,9 @@ function withAndroidConfig(config) {
|
|
|
100
86
|
application.service.push({
|
|
101
87
|
$: {
|
|
102
88
|
'android:name': firebaseServiceName,
|
|
103
|
-
'android:exported': 'false'
|
|
89
|
+
'android:exported': 'false',
|
|
90
|
+
// Adding type for Android 14 stability
|
|
91
|
+
'android:foregroundServiceType': 'phoneCall'
|
|
104
92
|
},
|
|
105
93
|
'intent-filter': [{ action: [{ $: { 'android:name': 'com.google.firebase.MESSAGING_EVENT' } }] }]
|
|
106
94
|
});
|
|
@@ -112,7 +100,9 @@ function withAndroidConfig(config) {
|
|
|
112
100
|
application.service.push({
|
|
113
101
|
$: {
|
|
114
102
|
'android:name': headlessServiceName,
|
|
115
|
-
'android:exported': 'false'
|
|
103
|
+
'android:exported': 'false',
|
|
104
|
+
// This ensures the JS bridge is treated as a priority process
|
|
105
|
+
'android:foregroundServiceType': 'phoneCall'
|
|
116
106
|
}
|
|
117
107
|
});
|
|
118
108
|
}
|
|
@@ -122,12 +112,15 @@ function withAndroidConfig(config) {
|
|
|
122
112
|
const receiverName = 'com.rnsnativecall.CallActionReceiver';
|
|
123
113
|
if (!application.receiver.some(r => r.$['android:name'] === receiverName)) {
|
|
124
114
|
application.receiver.push({
|
|
125
|
-
$: { 'android:name': receiverName, 'android:exported': 'false' }
|
|
115
|
+
$: { 'android:name': receiverName, 'android:exported': 'false' },
|
|
116
|
+
'intent-filter': [{
|
|
117
|
+
action: [
|
|
118
|
+
{ $: { 'android:name': 'android.intent.action.BOOT_COMPLETED' } }
|
|
119
|
+
]
|
|
120
|
+
}]
|
|
126
121
|
});
|
|
127
122
|
}
|
|
128
123
|
|
|
129
|
-
|
|
130
|
-
|
|
131
124
|
return config;
|
|
132
125
|
});
|
|
133
126
|
}
|