rns-nativecall 0.3.5 → 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/CallHeadlessTask.kt +4 -4
- package/android/src/main/java/com/rnsnativecall/CallMessagingService.kt +58 -53
- 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
|
}
|
|
@@ -6,15 +6,15 @@ import com.facebook.react.bridge.Arguments
|
|
|
6
6
|
import com.facebook.react.jstasks.HeadlessJsTaskConfig
|
|
7
7
|
|
|
8
8
|
class CallHeadlessTask : HeadlessJsTaskService() {
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
// Note: The '?' after Intent and the return type are specific in Kotlin
|
|
10
|
+
override fun getTaskConfig(intent: Intent?): HeadlessJsTaskConfig? {
|
|
11
|
+
val extras = intent?.extras
|
|
11
12
|
return if (extras != null) {
|
|
12
|
-
// INCREASE timeout to 30000 (30s) so it covers your 18s delay
|
|
13
13
|
HeadlessJsTaskConfig(
|
|
14
14
|
"ColdStartCallTask",
|
|
15
15
|
Arguments.fromBundle(extras),
|
|
16
16
|
30000,
|
|
17
|
-
true
|
|
17
|
+
true
|
|
18
18
|
)
|
|
19
19
|
} else {
|
|
20
20
|
null
|
|
@@ -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
|
|
@@ -14,7 +17,6 @@ import com.facebook.react.HeadlessJsTaskService
|
|
|
14
17
|
|
|
15
18
|
class CallMessagingService : FirebaseMessagingService() {
|
|
16
19
|
|
|
17
|
-
// This companion object allows the Cancel logic to find the active timer
|
|
18
20
|
companion object {
|
|
19
21
|
private val handler = Handler(Looper.getMainLooper())
|
|
20
22
|
private val pendingNotifications = mutableMapOf<String, Runnable>()
|
|
@@ -23,96 +25,99 @@ class CallMessagingService : FirebaseMessagingService() {
|
|
|
23
25
|
override fun onMessageReceived(remoteMessage: RemoteMessage) {
|
|
24
26
|
val data = remoteMessage.data
|
|
25
27
|
val context = applicationContext
|
|
26
|
-
|
|
27
|
-
// Extract variables for logic
|
|
28
28
|
val uuid = data["callUuid"] ?: return
|
|
29
29
|
val type = data["type"] ?: ""
|
|
30
30
|
|
|
31
|
-
// CASE 1: CALLER HUNG UP / CANCELLED
|
|
32
31
|
if (type == "CANCEL") {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
showMissedCallNotification(context, data, uuid)
|
|
45
|
-
return
|
|
46
|
-
}
|
|
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
|
+
}
|
|
47
43
|
|
|
48
|
-
// CASE 2: NEW INCOMING CALL
|
|
49
44
|
if (isAppInForeground(context)) {
|
|
50
|
-
// App is visible: Send event to active JS bridge immediately
|
|
51
45
|
CallModule.sendEventToJS("onCallReceived", data)
|
|
52
46
|
} else {
|
|
53
|
-
// 1.
|
|
54
|
-
val
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
putExtra("background_wake", true)
|
|
59
|
-
}
|
|
60
|
-
context.startActivity(launchIntent)
|
|
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)
|
|
61
52
|
|
|
62
|
-
//
|
|
53
|
+
// 2. Start the Service
|
|
54
|
+
context.startService(headlessIntent)
|
|
63
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
|
+
}
|
|
62
|
+
|
|
63
|
+
// 4. Start the 18s backup timer
|
|
64
64
|
val showNotificationRunnable = Runnable {
|
|
65
65
|
if (!isAppInForeground(context)) {
|
|
66
66
|
NativeCallManager.handleIncomingPush(context, data)
|
|
67
67
|
}
|
|
68
|
+
pendingNotifications.remove(uuid)
|
|
68
69
|
}
|
|
69
|
-
|
|
70
|
+
pendingNotifications[uuid] = showNotificationRunnable
|
|
71
|
+
handler.postDelayed(showNotificationRunnable, 18000)
|
|
70
72
|
}
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
private fun startHeadlessTask(context: Context, data: Map<String, String>) {
|
|
74
|
-
val serviceIntent = Intent(context, CallHeadlessTask::class.java)
|
|
75
|
-
val bundle = Bundle()
|
|
76
|
-
data.forEach { (key, value) -> bundle.putString(key, value) }
|
|
77
|
-
serviceIntent.putExtras(bundle)
|
|
78
|
-
|
|
79
|
-
context.startService(serviceIntent)
|
|
80
|
-
HeadlessJsTaskService.acquireWakeLockNow(context)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
75
|
private fun showMissedCallNotification(context: Context, data: Map<String, String>, uuid: String) {
|
|
84
76
|
val name = data["name"] ?: "Unknown"
|
|
85
77
|
val callType = data["callType"] ?: "video"
|
|
86
|
-
val channelId = "missed_calls"
|
|
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() }
|
|
87
83
|
|
|
88
84
|
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
89
85
|
|
|
90
|
-
|
|
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
|
+
|
|
91
100
|
var iconResId = context.resources.getIdentifier("ic_missed_call", "drawable", context.packageName)
|
|
92
101
|
if (iconResId == 0) iconResId = android.R.drawable.sym_call_missed
|
|
93
102
|
|
|
94
103
|
val builder = NotificationCompat.Builder(context, channelId)
|
|
95
104
|
.setSmallIcon(iconResId)
|
|
96
|
-
.setContentTitle("
|
|
97
|
-
.setContentText("You missed
|
|
105
|
+
.setContentTitle("$capitalizedAppName missed call")
|
|
106
|
+
.setContentText("You missed $article $callType call from $name")
|
|
98
107
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
|
99
108
|
.setAutoCancel(true)
|
|
100
|
-
.
|
|
109
|
+
.setContentIntent(contentIntent) // Opens app on click
|
|
110
|
+
.setCategory(NotificationCompat.CATEGORY_MISSED_CALL)
|
|
101
111
|
|
|
112
|
+
// 3. Use unique hashCode so multiple missed calls are stacked separately
|
|
102
113
|
notificationManager.notify(uuid.hashCode(), builder.build())
|
|
103
114
|
}
|
|
104
115
|
|
|
105
116
|
private fun isAppInForeground(context: Context): Boolean {
|
|
106
117
|
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
|
107
118
|
val appProcesses = activityManager.runningAppProcesses ?: return false
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
for (appProcess in appProcesses) {
|
|
111
|
-
if (appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND &&
|
|
112
|
-
appProcess.processName == packageName) {
|
|
113
|
-
return true
|
|
114
|
-
}
|
|
119
|
+
return appProcesses.any {
|
|
120
|
+
it.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND && it.processName == context.packageName
|
|
115
121
|
}
|
|
116
|
-
return false
|
|
117
122
|
}
|
|
118
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
|
}
|