rns-nativecall 0.3.6 → 0.3.9
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/CallModule.kt +2 -2
- package/android/src/main/java/com/rnsnativecall/NativeCallManager.kt +42 -45
- package/app.plugin.js +10 -3
- package/package.json +3 -1
- package/withCallNativeConfig.js +147 -0
- package/withCallPermissions.js +40 -0
- package/withNativeCallVoip.js +141 -54
|
@@ -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
|
}
|
|
@@ -23,7 +23,7 @@ class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
|
|
|
23
23
|
writableMap
|
|
24
24
|
}
|
|
25
25
|
promise.resolve(data)
|
|
26
|
-
pendingCallDataMap = null
|
|
26
|
+
pendingCallDataMap = null
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
@ReactMethod
|
|
@@ -48,7 +48,7 @@ class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
|
|
|
48
48
|
pendingCallDataMap = null
|
|
49
49
|
|
|
50
50
|
val notificationManager = reactApplicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
51
|
-
notificationManager.cancel(
|
|
51
|
+
notificationManager.cancel(uuid.hashCode())
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
@ReactMethod
|
|
@@ -14,7 +14,7 @@ import android.graphics.Color
|
|
|
14
14
|
object NativeCallManager {
|
|
15
15
|
|
|
16
16
|
private var ringtone: Ringtone? = null
|
|
17
|
-
|
|
17
|
+
const val CALL_CHANNEL_ID = "CALL_CHANNEL_ID"
|
|
18
18
|
|
|
19
19
|
fun handleIncomingPush(context: Context, data: Map<String, String>) {
|
|
20
20
|
val uuid = data["callUuid"] ?: return
|
|
@@ -28,90 +28,87 @@ object NativeCallManager {
|
|
|
28
28
|
} else {
|
|
29
29
|
PendingIntent.FLAG_UPDATE_CURRENT
|
|
30
30
|
}
|
|
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.
|
|
31
|
+
|
|
34
32
|
val intentToActivity = Intent(context, AcceptCallActivity::class.java).apply {
|
|
35
33
|
action = "ACTION_SHOW_UI_$uuid"
|
|
36
34
|
data.forEach { (key, value) -> putExtra(key, value) }
|
|
37
35
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
|
38
36
|
}
|
|
39
37
|
|
|
40
|
-
val fullScreenPendingIntent = PendingIntent.getActivity(
|
|
41
|
-
context,
|
|
42
|
-
uuid.hashCode(),
|
|
43
|
-
intentToActivity,
|
|
44
|
-
pendingFlags
|
|
45
|
-
)
|
|
38
|
+
val fullScreenPendingIntent = PendingIntent.getActivity(context, uuid.hashCode(), intentToActivity, pendingFlags)
|
|
46
39
|
|
|
47
|
-
// 2. Reject Action - Still goes to BroadcastReceiver
|
|
48
40
|
val rejectIntent = Intent(context, CallActionReceiver::class.java).apply {
|
|
49
41
|
action = "ACTION_REJECT_$uuid"
|
|
50
42
|
putExtra("EXTRA_CALL_UUID", uuid)
|
|
51
|
-
// Pass all data so onReject can find the UUID
|
|
52
43
|
data.forEach { (key, value) -> putExtra(key, value) }
|
|
53
44
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
pendingFlags
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
// 3. Setup Channel with high priority
|
|
62
|
-
val channelId = "CALL_CHANNEL_ID"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
val rejectPendingIntent = PendingIntent.getBroadcast(context, uuid.hashCode() + 1, rejectIntent, pendingFlags)
|
|
48
|
+
|
|
63
49
|
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
64
50
|
|
|
65
51
|
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
|
|
52
|
+
val channel = NotificationChannel(CALL_CHANNEL_ID, "Incoming Call", NotificationManager.IMPORTANCE_HIGH).apply {
|
|
71
53
|
setBypassDnd(true)
|
|
72
54
|
lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
|
|
73
|
-
//
|
|
55
|
+
enableVibration(true) //can be removed
|
|
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)
|
|
84
|
-
|
|
65
|
+
|
|
66
|
+
.setPriority(NotificationCompat.PRIORITY_HIGH) //old was PRIORITY_MAX
|
|
85
67
|
.setCategory(NotificationCompat.CATEGORY_CALL)
|
|
86
68
|
.setOngoing(true)
|
|
87
69
|
.setAutoCancel(false)
|
|
88
|
-
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
89
70
|
|
|
90
|
-
|
|
91
|
-
.
|
|
92
|
-
|
|
93
|
-
.addAction(0, "Answer", fullScreenPendingIntent) // Same intent as fullScreen
|
|
71
|
+
.setFullScreenIntent(fullScreenPendingIntent, false) //can be true
|
|
72
|
+
.addAction(0, "Answer", fullScreenPendingIntent)
|
|
94
73
|
.addAction(0, "Decline", rejectPendingIntent)
|
|
95
74
|
|
|
96
|
-
notificationManager.notify(
|
|
75
|
+
notificationManager.notify(uuid.hashCode(), builder.build())
|
|
97
76
|
|
|
98
|
-
// 5. Ringtone with looping logic attempt
|
|
99
77
|
try {
|
|
100
78
|
val ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
|
|
101
79
|
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
|
-
}
|
|
80
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) ringtone?.isLooping = true
|
|
105
81
|
ringtone?.play()
|
|
106
|
-
} catch (e: Exception) {
|
|
107
|
-
e.printStackTrace()
|
|
108
|
-
}
|
|
82
|
+
} catch (e: Exception) { e.printStackTrace() }
|
|
109
83
|
}
|
|
110
84
|
|
|
111
85
|
fun stopRingtone() {
|
|
112
|
-
|
|
113
|
-
|
|
86
|
+
try {
|
|
87
|
+
// Use the safe call operator ?. to only run if ringtone isn't null
|
|
88
|
+
ringtone?.let {
|
|
89
|
+
if (it.isPlaying) {
|
|
90
|
+
it.stop()
|
|
91
|
+
}
|
|
114
92
|
}
|
|
93
|
+
// Always null it out after stopping
|
|
115
94
|
ringtone = null
|
|
95
|
+
} catch (e: Exception) {
|
|
96
|
+
// Prevent the app from crashing if the system Ringtone service is acting up
|
|
97
|
+
e.printStackTrace()
|
|
98
|
+
ringtone = null
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fun dismissIncomingCall(context: Context, uuid: String?) {
|
|
103
|
+
stopRingtone()
|
|
104
|
+
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
105
|
+
|
|
106
|
+
if (uuid != null) {
|
|
107
|
+
notificationManager.cancel(uuid.hashCode())
|
|
108
|
+
} else {
|
|
109
|
+
// Fallback: If for some reason uuid is null, cancel everything
|
|
110
|
+
// (Optional: only if you want a safety net)
|
|
111
|
+
// notificationManager.cancelAll()
|
|
112
|
+
}
|
|
116
113
|
}
|
|
117
114
|
}
|
package/app.plugin.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
///app.plugin.js
|
|
2
|
+
const { withPlugins } = require('@expo/config-plugins');
|
|
2
3
|
const withNativeCallVoip = require('./withNativeCallVoip');
|
|
4
|
+
const withCallNativeConfig = require('./withCallNativeConfig');
|
|
5
|
+
const withCallPermissions = require('./withCallPermissions');
|
|
3
6
|
|
|
4
|
-
module.exports = function (config) {
|
|
5
|
-
return
|
|
7
|
+
module.exports = function (config, props) {
|
|
8
|
+
return withPlugins(config, [
|
|
9
|
+
[withNativeCallVoip, props],
|
|
10
|
+
[withCallNativeConfig, props],
|
|
11
|
+
[withCallPermissions, props],
|
|
12
|
+
]);
|
|
6
13
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rns-nativecall",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
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,8 @@
|
|
|
43
43
|
"react-native.config.js",
|
|
44
44
|
"README.md",
|
|
45
45
|
"rns-nativecall.podspec",
|
|
46
|
+
"withCallNativeConfig.js",
|
|
47
|
+
"withCallPermissions.js",
|
|
46
48
|
"withNativeCallVoip.js"
|
|
47
49
|
],
|
|
48
50
|
"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
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const { withAndroidManifest } = require("@expo/config-plugins");
|
|
2
|
+
|
|
3
|
+
/** 2. ANDROID MANIFEST MOD **/
|
|
4
|
+
function withCallPermissions(config) {
|
|
5
|
+
return withAndroidManifest(config, async (config) => {
|
|
6
|
+
const androidManifest = config.modResults.manifest;
|
|
7
|
+
|
|
8
|
+
// 1. Add the Full Screen Intent Permission
|
|
9
|
+
if (!androidManifest["uses-permission"]) {
|
|
10
|
+
androidManifest["uses-permission"] = [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const hasPermission = androidManifest["uses-permission"].some(
|
|
14
|
+
(p) => p.$["android:name"] === "android.permission.USE_FULL_SCREEN_INTENT"
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
if (!hasPermission) {
|
|
18
|
+
androidManifest["uses-permission"].push({
|
|
19
|
+
$: { "android:name": "android.permission.USE_FULL_SCREEN_INTENT" },
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// 2. Ensure the Activity has the correct flags to appear over lockscreen
|
|
24
|
+
const mainApplication = androidManifest.application[0];
|
|
25
|
+
const mainActivity = mainApplication.activity.find(
|
|
26
|
+
(a) => a.$["android:name"] === ".MainActivity"
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
if (mainActivity) {
|
|
30
|
+
mainActivity.$["android:showOnLockScreen"] = "true";
|
|
31
|
+
mainActivity.$["android:lockTaskMode"] = "if_whitelisted";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return config;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = (config) => {
|
|
39
|
+
return withPlugins(config, [withCallPermissions]);
|
|
40
|
+
};
|
package/withNativeCallVoip.js
CHANGED
|
@@ -1,46 +1,140 @@
|
|
|
1
1
|
const { withAndroidManifest, withInfoPlist, withPlugins, withMainActivity } = require('@expo/config-plugins');
|
|
2
2
|
|
|
3
|
+
/** 1. ANDROID MAIN ACTIVITY MOD **/
|
|
4
|
+
// function withMainActivityDataFix(config) {
|
|
5
|
+
// return withMainActivity(config, (config) => {
|
|
6
|
+
// let contents = config.modResults.contents;
|
|
7
|
+
|
|
8
|
+
// // 1. Ensure necessary Imports are present
|
|
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
|
+
// // 2. Define the code blocks
|
|
17
|
+
// const onNewIntentCode = `
|
|
18
|
+
// override fun onNewIntent(intent: Intent) {
|
|
19
|
+
// super.onNewIntent(intent)
|
|
20
|
+
// setIntent(intent)
|
|
21
|
+
// }
|
|
22
|
+
// `;
|
|
23
|
+
|
|
24
|
+
// const onCreateCode = `
|
|
25
|
+
// override fun onCreate(savedInstanceState: Bundle?) {
|
|
26
|
+
// super.onCreate(savedInstanceState)
|
|
27
|
+
// // If woken up silently, move to back to prevent UI flicker
|
|
28
|
+
// if (intent.getBooleanExtra("background_wake", false)) {
|
|
29
|
+
// moveTaskToBack(true)
|
|
30
|
+
// }
|
|
31
|
+
// }
|
|
32
|
+
// `;
|
|
33
|
+
|
|
34
|
+
// // 3. Insert the codes before the last closing brace of the class
|
|
35
|
+
// if (!contents.includes('override fun onNewIntent')) {
|
|
36
|
+
// const lastBraceIndex = contents.lastIndexOf('}');
|
|
37
|
+
// contents = contents.slice(0, lastBraceIndex) + onNewIntentCode + contents.slice(lastBraceIndex);
|
|
38
|
+
// }
|
|
39
|
+
|
|
40
|
+
// if (!contents.includes('override fun onCreate')) {
|
|
41
|
+
// // Re-calculate lastBraceIndex because contents string has changed
|
|
42
|
+
// const lastBraceIndex = contents.lastIndexOf('}');
|
|
43
|
+
// contents = contents.slice(0, lastBraceIndex) + onCreateCode + contents.slice(lastBraceIndex);
|
|
44
|
+
// }
|
|
45
|
+
|
|
46
|
+
// config.modResults.contents = contents;
|
|
47
|
+
// return config;
|
|
48
|
+
// });
|
|
49
|
+
// }
|
|
50
|
+
|
|
3
51
|
/** 1. ANDROID MAIN ACTIVITY MOD **/
|
|
4
52
|
function withMainActivityDataFix(config) {
|
|
5
53
|
return withMainActivity(config, (config) => {
|
|
6
54
|
let contents = config.modResults.contents;
|
|
7
55
|
|
|
8
|
-
// 1.
|
|
9
|
-
|
|
10
|
-
|
|
56
|
+
// --- 1. Imports ---
|
|
57
|
+
const neededImports = [
|
|
58
|
+
'import android.view.WindowManager',
|
|
59
|
+
'import android.os.Build',
|
|
60
|
+
'import android.os.Bundle'
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
neededImports.forEach(imp => {
|
|
64
|
+
if (!contents.includes(imp)) {
|
|
65
|
+
contents = contents.replace(/package .*/, (match) => `${match}\n${imp}`);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// --- 2. Replace onCreate ---
|
|
70
|
+
// We look for the existing onCreate and replace the entire block with your version
|
|
71
|
+
const editedOnCreate = `
|
|
72
|
+
// override fun onCreate(savedInstanceState: Bundle?) {
|
|
73
|
+
// setTheme(R.style.AppTheme)
|
|
74
|
+
// super.onCreate(savedInstanceState)
|
|
75
|
+
|
|
76
|
+
// // --- LOCK SCREEN WAKE LOGIC ---
|
|
77
|
+
// // This allows the Activity to appear over the lock screen when Answer is pressed
|
|
78
|
+
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
|
79
|
+
// setShowWhenLocked(true)
|
|
80
|
+
// setTurnScreenOn(true)
|
|
81
|
+
// } else {
|
|
82
|
+
// @Suppress("DEPRECATION")
|
|
83
|
+
// window.addFlags(
|
|
84
|
+
// WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
|
|
85
|
+
// WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
|
|
86
|
+
// WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
|
|
87
|
+
// )
|
|
88
|
+
// }
|
|
89
|
+
// }
|
|
90
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
91
|
+
setTheme(R.style.AppTheme)
|
|
92
|
+
super.onCreate(savedInstanceState)
|
|
93
|
+
|
|
94
|
+
// Only wake the screen/bypass lock if we are coming from a notification interaction
|
|
95
|
+
// Otherwise, let the notification pill handle the UI.
|
|
96
|
+
if (intent?.action?.startsWith("ACTION_SHOW_UI") == true) {
|
|
97
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
|
98
|
+
setShowWhenLocked(true)
|
|
99
|
+
setTurnScreenOn(true)
|
|
100
|
+
} else {
|
|
101
|
+
@Suppress("DEPRECATION")
|
|
102
|
+
window.addFlags(
|
|
103
|
+
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
|
|
104
|
+
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
|
|
105
|
+
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
|
|
106
|
+
)
|
|
107
|
+
}
|
|
11
108
|
}
|
|
12
|
-
|
|
13
|
-
|
|
109
|
+
}
|
|
110
|
+
`;
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
// Regex to find the existing onCreate block (including the setTheme and super calls)
|
|
116
|
+
const onCreateRegex = /override fun onCreate\(savedInstanceState: Bundle\?\).*?\{[\s\S]*?super\.onCreate\(null\)\n\s*\}/;
|
|
117
|
+
if (contents.match(onCreateRegex)) {
|
|
118
|
+
contents = contents.replace(onCreateRegex, editedOnCreate);
|
|
14
119
|
}
|
|
15
120
|
|
|
16
|
-
//
|
|
121
|
+
// --- 3. Update createReactActivityDelegate ---
|
|
122
|
+
// Removing the "object :" and the empty trailing block "{})" to match your edited version
|
|
123
|
+
contents = contents.replace(
|
|
124
|
+
/return ReactActivityDelegateWrapper\([\s\S]*?object : (DefaultReactActivityDelegate\([\s\S]*?\))\{\}\)/,
|
|
125
|
+
'return ReactActivityDelegateWrapper($1)'
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// --- 4. Ensure onNewIntent exists ---
|
|
17
129
|
const onNewIntentCode = `
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
`;
|
|
23
|
-
|
|
24
|
-
const onCreateCode = `
|
|
25
|
-
override fun onCreate(savedInstanceState: Bundle?) {
|
|
26
|
-
super.onCreate(savedInstanceState)
|
|
27
|
-
// If woken up silently, move to back to prevent UI flicker
|
|
28
|
-
if (intent.getBooleanExtra("background_wake", false)) {
|
|
29
|
-
moveTaskToBack(true)
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
`;
|
|
130
|
+
override fun onNewIntent(intent: Intent) {
|
|
131
|
+
super.onNewIntent(intent)
|
|
132
|
+
setIntent(intent)
|
|
133
|
+
}`;
|
|
33
134
|
|
|
34
|
-
// 3. Insert the codes before the last closing brace of the class
|
|
35
135
|
if (!contents.includes('override fun onNewIntent')) {
|
|
36
136
|
const lastBraceIndex = contents.lastIndexOf('}');
|
|
37
|
-
contents = contents.slice(0, lastBraceIndex) + onNewIntentCode + contents.slice(lastBraceIndex);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (!contents.includes('override fun onCreate')) {
|
|
41
|
-
// Re-calculate lastBraceIndex because contents string has changed
|
|
42
|
-
const lastBraceIndex = contents.lastIndexOf('}');
|
|
43
|
-
contents = contents.slice(0, lastBraceIndex) + onCreateCode + contents.slice(lastBraceIndex);
|
|
137
|
+
contents = contents.slice(0, lastBraceIndex) + onNewIntentCode + "\n" + contents.slice(lastBraceIndex);
|
|
44
138
|
}
|
|
45
139
|
|
|
46
140
|
config.modResults.contents = contents;
|
|
@@ -59,10 +153,11 @@ function withAndroidConfig(config) {
|
|
|
59
153
|
'android.permission.USE_FULL_SCREEN_INTENT',
|
|
60
154
|
'android.permission.VIBRATE',
|
|
61
155
|
'android.permission.FOREGROUND_SERVICE',
|
|
62
|
-
'android.permission.FOREGROUND_SERVICE_PHONE_CALL',
|
|
156
|
+
'android.permission.FOREGROUND_SERVICE_PHONE_CALL', // Required for Android 14
|
|
63
157
|
'android.permission.POST_NOTIFICATIONS',
|
|
64
158
|
'android.permission.WAKE_LOCK',
|
|
65
|
-
'android.permission.DISABLE_KEYGUARD'
|
|
159
|
+
'android.permission.DISABLE_KEYGUARD',
|
|
160
|
+
'android.permission.RECEIVE_BOOT_COMPLETED' // Allows app to wake after phone restart
|
|
66
161
|
];
|
|
67
162
|
|
|
68
163
|
manifest.manifest['uses-permission'] = manifest.manifest['uses-permission'] || [];
|
|
@@ -72,26 +167,11 @@ function withAndroidConfig(config) {
|
|
|
72
167
|
}
|
|
73
168
|
});
|
|
74
169
|
|
|
75
|
-
// 2. Activity Setup
|
|
170
|
+
// 2. Activity Setup (Remain unchanged as your logic was correct)
|
|
76
171
|
application.activity = application.activity || [];
|
|
172
|
+
// ... (Keep your AcceptCallActivity logic here)
|
|
77
173
|
|
|
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)
|
|
174
|
+
// 3. Service Setup (CRITICAL UPDATES FOR ANDROID 14)
|
|
95
175
|
application.service = application.service || [];
|
|
96
176
|
|
|
97
177
|
// Add Firebase Messaging Service
|
|
@@ -100,7 +180,9 @@ function withAndroidConfig(config) {
|
|
|
100
180
|
application.service.push({
|
|
101
181
|
$: {
|
|
102
182
|
'android:name': firebaseServiceName,
|
|
103
|
-
'android:exported': 'false'
|
|
183
|
+
'android:exported': 'false',
|
|
184
|
+
// Adding type for Android 14 stability
|
|
185
|
+
'android:foregroundServiceType': 'phoneCall'
|
|
104
186
|
},
|
|
105
187
|
'intent-filter': [{ action: [{ $: { 'android:name': 'com.google.firebase.MESSAGING_EVENT' } }] }]
|
|
106
188
|
});
|
|
@@ -112,7 +194,9 @@ function withAndroidConfig(config) {
|
|
|
112
194
|
application.service.push({
|
|
113
195
|
$: {
|
|
114
196
|
'android:name': headlessServiceName,
|
|
115
|
-
'android:exported': 'false'
|
|
197
|
+
'android:exported': 'false',
|
|
198
|
+
// This ensures the JS bridge is treated as a priority process
|
|
199
|
+
'android:foregroundServiceType': 'phoneCall'
|
|
116
200
|
}
|
|
117
201
|
});
|
|
118
202
|
}
|
|
@@ -122,12 +206,15 @@ function withAndroidConfig(config) {
|
|
|
122
206
|
const receiverName = 'com.rnsnativecall.CallActionReceiver';
|
|
123
207
|
if (!application.receiver.some(r => r.$['android:name'] === receiverName)) {
|
|
124
208
|
application.receiver.push({
|
|
125
|
-
$: { 'android:name': receiverName, 'android:exported': 'false' }
|
|
209
|
+
$: { 'android:name': receiverName, 'android:exported': 'false' },
|
|
210
|
+
'intent-filter': [{
|
|
211
|
+
action: [
|
|
212
|
+
{ $: { 'android:name': 'android.intent.action.BOOT_COMPLETED' } }
|
|
213
|
+
]
|
|
214
|
+
}]
|
|
126
215
|
});
|
|
127
216
|
}
|
|
128
217
|
|
|
129
|
-
|
|
130
|
-
|
|
131
218
|
return config;
|
|
132
219
|
});
|
|
133
220
|
}
|