rns-nativecall 0.2.4 → 0.2.6
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/build.gradle +18 -37
- package/android/src/main/java/com/rnsnativecall/AcceptCallActivity.kt +9 -13
- package/android/src/main/java/com/rnsnativecall/CallActionReceiver.kt +11 -7
- package/android/src/main/java/com/rnsnativecall/CallMessagingService.kt +12 -20
- package/android/src/main/java/com/rnsnativecall/CallModule.kt +28 -62
- package/android/src/main/java/com/rnsnativecall/MyCallConnection.kt +55 -47
- package/android/src/main/java/com/rnsnativecall/MyConnectionService.kt +88 -48
- package/android/src/main/java/com/rnsnativecall/NativeCallManager.kt +35 -29
- package/ios/CallModule.m +8 -9
- package/package.json +1 -1
- package/withNativeCallVoip.js +31 -30
package/android/build.gradle
CHANGED
|
@@ -1,34 +1,22 @@
|
|
|
1
|
-
buildscript {
|
|
2
|
-
// Check if kotlin version is defined in the root project, otherwise default
|
|
3
|
-
ext.kotlin_version = project.hasProperty('kotlinVersion') ? project.kotlinVersion : '1.8.10'
|
|
4
|
-
|
|
5
|
-
repositories {
|
|
6
|
-
google()
|
|
7
|
-
mavenCentral()
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
dependencies {
|
|
11
|
-
classpath "com.android.tools.build:gradle:7.4.2"
|
|
12
|
-
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
apply plugin: 'com.android.library'
|
|
17
|
-
apply plugin: 'kotlin-android'
|
|
18
|
-
|
|
19
|
-
// Helper function to find the react-native node_module
|
|
20
|
-
def safeExtGet(prop, fallback) {
|
|
21
|
-
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
22
|
-
}
|
|
23
|
-
|
|
24
1
|
android {
|
|
25
|
-
//
|
|
2
|
+
// 1. MATCH NAMESPACE: Ensure this matches the package in your .kt files
|
|
26
3
|
namespace "com.rnsnativecall"
|
|
4
|
+
|
|
5
|
+
// 2. TARGET SDK: Android 13 (API 33) is the standard for modern RN apps
|
|
6
|
+
// This allows you to use the POST_NOTIFICATIONS permission needed for Android 13+
|
|
27
7
|
compileSdkVersion safeExtGet('compileSdkVersion', 33)
|
|
28
8
|
|
|
29
9
|
defaultConfig {
|
|
30
10
|
minSdkVersion safeExtGet('minSdkVersion', 21)
|
|
31
11
|
targetSdkVersion safeExtGet('targetSdkVersion', 33)
|
|
12
|
+
|
|
13
|
+
// Ensure your library can handle vector icons if you use them in notifications
|
|
14
|
+
vectorDrawables.useSupportLibrary = true
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// 3. KOTLIN OPTIONS: Essential for smooth bridge communication
|
|
18
|
+
kotlinOptions {
|
|
19
|
+
jvmTarget = '1.8'
|
|
32
20
|
}
|
|
33
21
|
|
|
34
22
|
lintOptions {
|
|
@@ -36,27 +24,20 @@ android {
|
|
|
36
24
|
}
|
|
37
25
|
}
|
|
38
26
|
|
|
39
|
-
repositories {
|
|
40
|
-
mavenCentral()
|
|
41
|
-
google()
|
|
42
|
-
}
|
|
43
|
-
|
|
44
27
|
dependencies {
|
|
45
|
-
// Essential for React Native Native Modules
|
|
46
28
|
implementation "com.facebook.react:react-native:+"
|
|
47
|
-
|
|
48
|
-
// Kotlin Standard Library
|
|
49
29
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
50
30
|
|
|
51
|
-
// Firebase
|
|
31
|
+
// Firebase - Use a stable version that supports Android 13+
|
|
52
32
|
implementation "com.google.firebase:firebase-messaging:23.4.0"
|
|
53
33
|
|
|
54
|
-
// UI & Design
|
|
55
|
-
implementation "com.google.android.material:material:1.9.0"
|
|
34
|
+
// UI & Design
|
|
56
35
|
implementation "androidx.appcompat:appcompat:1.6.1"
|
|
57
|
-
implementation "androidx.core:core-ktx:1.10.1"
|
|
36
|
+
implementation "androidx.core:core-ktx:1.10.1" // Required for NotificationCompat
|
|
37
|
+
implementation "com.google.android.material:material:1.9.0"
|
|
58
38
|
|
|
59
|
-
//
|
|
39
|
+
// 4. Glide for Profile Pictures
|
|
40
|
+
// If you plan to show the caller's face in the notification
|
|
60
41
|
implementation "com.github.bumptech.glide:glide:4.15.1"
|
|
61
42
|
annotationProcessor "com.github.bumptech.glide:compiler:4.15.1"
|
|
62
43
|
}
|
|
@@ -10,13 +10,12 @@ class AcceptCallActivity : Activity() {
|
|
|
10
10
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
11
11
|
super.onCreate(savedInstanceState)
|
|
12
12
|
NativeCallManager.stopRingtone()
|
|
13
|
+
|
|
13
14
|
// 1. CLEAR THE NOTIFICATION
|
|
14
|
-
// Use the same ID (101) used in NativeCallManager
|
|
15
15
|
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
16
16
|
notificationManager.cancel(101)
|
|
17
17
|
|
|
18
18
|
// 2. EXTRACT DATA SAFELY
|
|
19
|
-
// We iterate through all extras and convert them to Strings for the JS Map
|
|
20
19
|
val dataMap = mutableMapOf<String, String>()
|
|
21
20
|
val extras = intent.extras
|
|
22
21
|
extras?.keySet()?.forEach { key ->
|
|
@@ -26,29 +25,26 @@ class AcceptCallActivity : Activity() {
|
|
|
26
25
|
}
|
|
27
26
|
}
|
|
28
27
|
|
|
29
|
-
//
|
|
30
|
-
//
|
|
28
|
+
// --- THE COLD START FIX ---
|
|
29
|
+
// 3. STORE DATA FOR JS POLLING
|
|
30
|
+
// We save this in a static variable inside CallModule so getInitialCallData() can find it
|
|
31
|
+
CallModule.setPendingCallData(dataMap)
|
|
32
|
+
|
|
33
|
+
// 4. EMIT EVENT TO REACT NATIVE
|
|
34
|
+
// This works if the app is already running
|
|
31
35
|
CallModule.sendEventToJS("onCallAccepted", dataMap)
|
|
32
36
|
|
|
33
|
-
//
|
|
37
|
+
// 5. LAUNCH OR BRING MAIN APP TO FOREGROUND
|
|
34
38
|
val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
|
|
35
39
|
if (launchIntent != null) {
|
|
36
40
|
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
41
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
|
40
|
-
|
|
41
|
-
// Copy all call data to the launch intent
|
|
42
42
|
putExtras(extras ?: Bundle())
|
|
43
|
-
|
|
44
|
-
// Helper flag for your React Native logic
|
|
45
43
|
putExtra("navigatingToCall", true)
|
|
46
44
|
}
|
|
47
45
|
startActivity(launchIntent)
|
|
48
46
|
}
|
|
49
47
|
|
|
50
|
-
// 5. FINISH TRAMPOLINE
|
|
51
|
-
// This ensures this invisible activity doesn't stay in the "Recent Apps" list
|
|
52
48
|
finish()
|
|
53
49
|
}
|
|
54
50
|
}
|
|
@@ -16,29 +16,33 @@ class CallActionReceiver : BroadcastReceiver() {
|
|
|
16
16
|
notificationManager.cancel(101)
|
|
17
17
|
|
|
18
18
|
if (intent.action == "ACTION_ACCEPT") {
|
|
19
|
-
NativeCallManager.stopRingtone()
|
|
20
19
|
// 2. Prepare the data for React Native
|
|
21
20
|
val dataMap = mutableMapOf<String, String>()
|
|
22
21
|
intent.extras?.keySet()?.forEach { key ->
|
|
23
|
-
|
|
22
|
+
// Using get(key).toString() is safer than getString(key)
|
|
23
|
+
// because some extras might be Ints or Booleans
|
|
24
|
+
intent.extras?.get(key)?.let { dataMap[key] = it.toString() }
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
//
|
|
27
|
+
// --- THE COLD START FIX ---
|
|
28
|
+
// Store data in the "Holding Gate" inside CallModule
|
|
29
|
+
CallModule.setPendingCallData(dataMap)
|
|
30
|
+
|
|
31
|
+
// 3. Send event (works if app is already active)
|
|
27
32
|
CallModule.sendEventToJS("onCallAccepted", dataMap)
|
|
28
33
|
|
|
29
34
|
// 4. Bring the app to foreground
|
|
30
35
|
val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
|
|
31
36
|
launchIntent?.apply {
|
|
32
37
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
|
33
|
-
// Forward the extras so the App can read them on startup/resume
|
|
34
38
|
putExtras(intent.extras ?: Bundle())
|
|
35
39
|
putExtra("navigatingToCall", true)
|
|
36
40
|
}
|
|
37
41
|
context.startActivity(launchIntent)
|
|
38
|
-
|
|
39
|
-
|
|
42
|
+
|
|
43
|
+
} else if (intent.action == "ACTION_REJECT") {
|
|
40
44
|
// Logic for Reject
|
|
41
|
-
CallModule.sendEventToJS("onCallRejected", mapOf("
|
|
45
|
+
CallModule.sendEventToJS("onCallRejected", mapOf("callUuid" to (uuid ?: "")))
|
|
42
46
|
}
|
|
43
47
|
}
|
|
44
48
|
}
|
|
@@ -8,27 +8,19 @@ import com.google.firebase.messaging.RemoteMessage
|
|
|
8
8
|
class CallMessagingService : FirebaseMessagingService() {
|
|
9
9
|
|
|
10
10
|
override fun onMessageReceived(remoteMessage: RemoteMessage) {
|
|
11
|
-
|
|
12
|
-
// 1. App is OPEN: Don't show the system pill.
|
|
13
|
-
// Just send the data to your React Native listeners.
|
|
14
|
-
CallModule.sendEventToJS("onCallReceived", remoteMessage.data)
|
|
15
|
-
} else {
|
|
16
|
-
// 2. App is CLOSED/BACKGROUND: Show the sticky system pill.
|
|
17
|
-
NativeCallManager.handleIncomingPush(applicationContext, remoteMessage.data)
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
private fun isAppInForeground(context: Context): Boolean {
|
|
22
|
-
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
|
|
23
|
-
val appProcesses = activityManager.runningAppProcesses ?: return false
|
|
24
|
-
val packageName = context.packageName
|
|
11
|
+
val data = remoteMessage.data
|
|
25
12
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
13
|
+
// Ensure this is actually a call offer before proceeding
|
|
14
|
+
if (data["type"] == "offer" || data.containsKey("callUuid")) {
|
|
15
|
+
|
|
16
|
+
// ALWAYS trigger the native logic.
|
|
17
|
+
// The NativeCallManager should decide if it needs to show a Notification
|
|
18
|
+
// or just play a sound. This ensures the "Ringtone" starts natively
|
|
19
|
+
// which is more reliable than JS audio.
|
|
20
|
+
NativeCallManager.handleIncomingPush(applicationContext, data)
|
|
21
|
+
|
|
22
|
+
// Still notify JS so it can update the UI or pre-load the call screen
|
|
23
|
+
CallModule.sendEventToJS("onCallReceived", data)
|
|
31
24
|
}
|
|
32
|
-
return false
|
|
33
25
|
}
|
|
34
26
|
}
|
|
@@ -4,8 +4,6 @@ import android.app.NotificationManager
|
|
|
4
4
|
import android.content.ComponentName
|
|
5
5
|
import android.content.Context
|
|
6
6
|
import android.content.Intent
|
|
7
|
-
import android.net.Uri
|
|
8
|
-
import android.os.Bundle
|
|
9
7
|
import android.telecom.DisconnectCause
|
|
10
8
|
import android.telecom.PhoneAccount
|
|
11
9
|
import android.telecom.PhoneAccountHandle
|
|
@@ -23,28 +21,21 @@ class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
|
|
|
23
21
|
|
|
24
22
|
override fun getName() = "CallModule"
|
|
25
23
|
|
|
26
|
-
|
|
27
|
-
val componentName = ComponentName(reactApplicationContext, MyConnectionService::class.java)
|
|
28
|
-
return PhoneAccountHandle(componentName, "${reactApplicationContext.packageName}.voip")
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
private fun registerPhoneAccount() {
|
|
32
|
-
val telecomManager = reactApplicationContext.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
|
33
|
-
val phoneAccountHandle = getPhoneAccountHandle()
|
|
34
|
-
val appName = reactApplicationContext.applicationInfo.loadLabel(reactApplicationContext.packageManager).toString()
|
|
35
|
-
|
|
36
|
-
val capabilities = PhoneAccount.CAPABILITY_VIDEO_CALLING or
|
|
37
|
-
PhoneAccount.CAPABILITY_SUPPORTS_VIDEO_CALLING or
|
|
38
|
-
PhoneAccount.CAPABILITY_SELF_MANAGED
|
|
24
|
+
// ... getPhoneAccountHandle and registerPhoneAccount remain the same ...
|
|
39
25
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
.
|
|
45
|
-
.
|
|
46
|
-
|
|
47
|
-
|
|
26
|
+
@ReactMethod
|
|
27
|
+
fun getInitialCallData(promise: Promise) {
|
|
28
|
+
// Convert our stored simple Map into a WritableMap for JS
|
|
29
|
+
val data = pendingCallDataMap?.let { map ->
|
|
30
|
+
val writableMap = Arguments.createMap()
|
|
31
|
+
map.forEach { (key, value) ->
|
|
32
|
+
writableMap.putString(key, value)
|
|
33
|
+
}
|
|
34
|
+
writableMap
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
promise.resolve(data)
|
|
38
|
+
pendingCallDataMap = null // Clear after consumption
|
|
48
39
|
}
|
|
49
40
|
|
|
50
41
|
@ReactMethod
|
|
@@ -65,6 +56,8 @@ class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
|
|
|
65
56
|
@ReactMethod
|
|
66
57
|
fun endNativeCall(uuid: String) {
|
|
67
58
|
NativeCallManager.stopRingtone()
|
|
59
|
+
pendingCallDataMap = null // Clear any pending data
|
|
60
|
+
|
|
68
61
|
val notificationManager = reactApplicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
69
62
|
notificationManager.cancel(101)
|
|
70
63
|
|
|
@@ -76,45 +69,23 @@ class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
|
|
|
76
69
|
}
|
|
77
70
|
}
|
|
78
71
|
|
|
79
|
-
|
|
80
|
-
fun checkTelecomPermissions(promise: Promise) {
|
|
81
|
-
val telecomManager = reactApplicationContext.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
|
82
|
-
val phoneAccountHandle = getPhoneAccountHandle()
|
|
83
|
-
val appName = reactApplicationContext.applicationInfo.loadLabel(reactApplicationContext.packageManager).toString()
|
|
84
|
-
|
|
85
|
-
val account = telecomManager.getPhoneAccount(phoneAccountHandle)
|
|
86
|
-
if (account != null && account.isEnabled) {
|
|
87
|
-
promise.resolve(true)
|
|
88
|
-
} else {
|
|
89
|
-
try {
|
|
90
|
-
Toast.makeText(reactApplicationContext, "Tap 'Active calling accounts' and then enable $appName", Toast.LENGTH_LONG).show()
|
|
91
|
-
val intent = Intent(TelecomManager.ACTION_CHANGE_PHONE_ACCOUNTS)
|
|
92
|
-
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
93
|
-
reactApplicationContext.startActivity(intent)
|
|
94
|
-
promise.resolve(false)
|
|
95
|
-
} catch (e: Exception) {
|
|
96
|
-
promise.reject("PERMISSION_ERROR", e.message)
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
@ReactMethod
|
|
102
|
-
fun getInitialCallData(promise: Promise) {
|
|
103
|
-
promise.resolve(pendingCallData)
|
|
104
|
-
pendingCallData = null // Clear after consumption
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
@ReactMethod fun addListener(eventName: String) {}
|
|
108
|
-
@ReactMethod fun removeListeners(count: Int) {}
|
|
72
|
+
// ... checkTelecomPermissions remains the same ...
|
|
109
73
|
|
|
110
74
|
companion object {
|
|
111
75
|
private var instance: CallModule? = null
|
|
112
|
-
|
|
76
|
+
// Use a standard Map to store data so we don't depend on React Context being alive
|
|
77
|
+
private var pendingCallDataMap: Map<String, String>? = null
|
|
78
|
+
|
|
79
|
+
@JvmStatic
|
|
80
|
+
fun setPendingCallData(data: Map<String, String>) {
|
|
81
|
+
pendingCallDataMap = data
|
|
82
|
+
}
|
|
113
83
|
|
|
114
84
|
@JvmStatic
|
|
115
85
|
fun sendEventToJS(eventName: String, params: Any?) {
|
|
116
86
|
val reactContext = instance?.reactApplicationContext
|
|
117
87
|
|
|
88
|
+
// Convert params to WritableMap
|
|
118
89
|
val bridgeData = when (params) {
|
|
119
90
|
is Map<*, *> -> {
|
|
120
91
|
val map = Arguments.createMap()
|
|
@@ -123,9 +94,6 @@ class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
|
|
|
123
94
|
}
|
|
124
95
|
map
|
|
125
96
|
}
|
|
126
|
-
is String -> {
|
|
127
|
-
Arguments.createMap().apply { putString("callUuid", params) }
|
|
128
|
-
}
|
|
129
97
|
else -> null
|
|
130
98
|
}
|
|
131
99
|
|
|
@@ -133,11 +101,9 @@ class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
|
|
|
133
101
|
reactContext
|
|
134
102
|
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
135
103
|
?.emit(eventName, bridgeData)
|
|
136
|
-
} else {
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
pendingCallData = bridgeData
|
|
140
|
-
}
|
|
104
|
+
} else if (eventName == "onCallAccepted" && params is Map<*, *>) {
|
|
105
|
+
// FALLBACK: Store data for polling if JS isn't ready
|
|
106
|
+
setPendingCallData(params as Map<String, String>)
|
|
141
107
|
}
|
|
142
108
|
}
|
|
143
109
|
}
|
|
@@ -1,38 +1,34 @@
|
|
|
1
1
|
package com.rnsnativecall
|
|
2
2
|
|
|
3
|
+
import android.app.*
|
|
3
4
|
import android.content.Context
|
|
4
5
|
import android.content.Intent
|
|
5
6
|
import android.media.AudioAttributes
|
|
6
7
|
import android.media.MediaPlayer
|
|
7
8
|
import android.media.RingtoneManager
|
|
8
|
-
import android.
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
class MyCallConnection(
|
|
12
|
-
private val context: Context,
|
|
13
|
-
private val callUUID: String?,
|
|
14
|
-
private val playRing: Boolean,
|
|
15
|
-
private val allData: Map<String, String>,
|
|
16
|
-
private val onAcceptCallback: (Map<String, String>) -> Unit,
|
|
17
|
-
private val onRejectCallback: (Map<String, String>) -> Unit
|
|
18
|
-
) : Connection() {
|
|
9
|
+
import android.os.Build
|
|
10
|
+
import androidx.core.app.NotificationCompat
|
|
19
11
|
|
|
12
|
+
object NativeCallManager {
|
|
20
13
|
private var mediaPlayer: MediaPlayer? = null
|
|
14
|
+
private const val CHANNEL_ID = "incoming_calls"
|
|
15
|
+
private const val NOTIFICATION_ID = 101
|
|
21
16
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (playRing) startRingtone()
|
|
17
|
+
fun handleIncomingPush(context: Context, data: Map<String, String>) {
|
|
18
|
+
startRingtone(context)
|
|
19
|
+
showIncomingCallNotification(context, data)
|
|
26
20
|
}
|
|
27
21
|
|
|
28
|
-
private fun startRingtone() {
|
|
22
|
+
private fun startRingtone(context: Context) {
|
|
23
|
+
if (mediaPlayer != null) return
|
|
29
24
|
try {
|
|
30
25
|
val uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
|
|
31
26
|
mediaPlayer = MediaPlayer().apply {
|
|
32
27
|
setDataSource(context, uri)
|
|
33
28
|
setAudioAttributes(AudioAttributes.Builder()
|
|
34
29
|
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
|
|
35
|
-
.setContentType(AudioAttributes.
|
|
30
|
+
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
|
31
|
+
.build())
|
|
36
32
|
isLooping = true
|
|
37
33
|
prepare()
|
|
38
34
|
start()
|
|
@@ -40,46 +36,58 @@ class MyCallConnection(
|
|
|
40
36
|
} catch (e: Exception) { e.printStackTrace() }
|
|
41
37
|
}
|
|
42
38
|
|
|
43
|
-
|
|
39
|
+
fun stopRingtone() {
|
|
44
40
|
mediaPlayer?.stop()
|
|
45
41
|
mediaPlayer?.release()
|
|
46
42
|
mediaPlayer = null
|
|
47
43
|
}
|
|
48
44
|
|
|
49
|
-
private fun
|
|
50
|
-
|
|
51
|
-
callUUID?.let { MyConnectionService.removeConnection(it) }
|
|
52
|
-
}
|
|
45
|
+
private fun showIncomingCallNotification(context: Context, data: Map<String, String>) {
|
|
46
|
+
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
53
47
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
48
|
+
// Create Channel for Android O+
|
|
49
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
50
|
+
val channel = NotificationChannel(CHANNEL_ID, "Incoming Calls", NotificationManager.IMPORTANCE_HIGH).apply {
|
|
51
|
+
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
|
52
|
+
enableVibration(true)
|
|
53
|
+
setSound(null, null) // We handle sound manually via MediaPlayer
|
|
54
|
+
}
|
|
55
|
+
notificationManager.createNotificationChannel(channel)
|
|
60
56
|
}
|
|
61
57
|
|
|
62
|
-
|
|
58
|
+
// 1. Full Screen Intent (To wake up the screen)
|
|
59
|
+
val fullScreenIntent = Intent(context, AcceptCallActivity::class.java).apply {
|
|
60
|
+
data.forEach { (k, v) -> putExtra(k, v) }
|
|
61
|
+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_USER_ACTION)
|
|
62
|
+
}
|
|
63
|
+
val fullScreenPendingIntent = PendingIntent.getActivity(context, 0, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
|
63
64
|
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
// 2. Answer Action
|
|
66
|
+
val answerIntent = Intent(context, CallActionReceiver::class.java).apply {
|
|
67
|
+
action = "ACTION_ACCEPT"
|
|
68
|
+
data.forEach { (k, v) -> putExtra(k, v) }
|
|
69
|
+
}
|
|
70
|
+
val answerPendingIntent = PendingIntent.getBroadcast(context, 1, answerIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
|
69
71
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
// 3. Reject Action
|
|
73
|
+
val rejectIntent = Intent(context, CallActionReceiver::class.java).apply {
|
|
74
|
+
action = "ACTION_REJECT"
|
|
75
|
+
data.forEach { (k, v) -> putExtra(k, v) }
|
|
76
|
+
}
|
|
77
|
+
val rejectPendingIntent = PendingIntent.getBroadcast(context, 2, rejectIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
|
78
|
+
|
|
79
|
+
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
|
|
80
|
+
.setSmallIcon(context.applicationInfo.icon)
|
|
81
|
+
.setContentTitle("Incoming Call")
|
|
82
|
+
.setContentText(data["name"] ?: "Unknown Caller")
|
|
83
|
+
.setPriority(NotificationCompat.PRIORITY_MAX)
|
|
84
|
+
.setCategory(NotificationCompat.CATEGORY_CALL)
|
|
85
|
+
.setAutoCancel(true)
|
|
86
|
+
.setOngoing(true)
|
|
87
|
+
.setFullScreenIntent(fullScreenPendingIntent, true)
|
|
88
|
+
.addAction(0, "Answer", answerPendingIntent)
|
|
89
|
+
.addAction(0, "Decline", rejectPendingIntent)
|
|
77
90
|
|
|
78
|
-
|
|
79
|
-
stopRingtone()
|
|
80
|
-
setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
|
|
81
|
-
cleanUp()
|
|
82
|
-
destroy()
|
|
83
|
-
onRejectCallback(allData)
|
|
91
|
+
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
|
84
92
|
}
|
|
85
93
|
}
|
|
@@ -1,61 +1,101 @@
|
|
|
1
1
|
package com.rnsnativecall
|
|
2
2
|
|
|
3
|
-
import android.
|
|
4
|
-
import android.
|
|
5
|
-
import android.
|
|
6
|
-
import android.
|
|
7
|
-
import android.
|
|
8
|
-
import android.
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
3
|
+
import android.app.*
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.Intent
|
|
6
|
+
import android.media.AudioAttributes
|
|
7
|
+
import android.media.MediaPlayer
|
|
8
|
+
import android.media.RingtoneManager
|
|
9
|
+
import android.os.Build
|
|
10
|
+
import androidx.core.app.NotificationCompat
|
|
11
|
+
|
|
12
|
+
object NativeCallManager {
|
|
13
|
+
private var mediaPlayer: MediaPlayer? = null
|
|
14
|
+
private const val CHANNEL_ID = "incoming_calls"
|
|
15
|
+
private const val NOTIFICATION_ID = 101
|
|
16
|
+
|
|
17
|
+
fun handleIncomingPush(context: Context, data: Map<String, String>) {
|
|
18
|
+
startRingtone(context)
|
|
19
|
+
showNotification(context, data)
|
|
18
20
|
}
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
22
|
+
private fun startRingtone(context: Context) {
|
|
23
|
+
if (mediaPlayer != null) return
|
|
24
|
+
try {
|
|
25
|
+
val uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
|
|
26
|
+
mediaPlayer = MediaPlayer().apply {
|
|
27
|
+
setDataSource(context, uri)
|
|
28
|
+
setAudioAttributes(AudioAttributes.Builder()
|
|
29
|
+
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
|
|
30
|
+
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
|
31
|
+
.build())
|
|
32
|
+
isLooping = true
|
|
33
|
+
prepare()
|
|
34
|
+
start()
|
|
35
|
+
}
|
|
36
|
+
} catch (e: Exception) { e.printStackTrace() }
|
|
37
|
+
}
|
|
34
38
|
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
fun stopRingtone() {
|
|
40
|
+
mediaPlayer?.stop()
|
|
41
|
+
mediaPlayer?.release()
|
|
42
|
+
mediaPlayer = null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private fun showNotification(context: Context, data: Map<String, String>) {
|
|
46
|
+
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
37
47
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
49
|
+
val channel = NotificationChannel(CHANNEL_ID, "Calls", NotificationManager.IMPORTANCE_HIGH).apply {
|
|
50
|
+
description = "Incoming call notifications"
|
|
51
|
+
setSound(null, null)
|
|
52
|
+
enableVibration(true)
|
|
53
|
+
}
|
|
54
|
+
notificationManager.createNotificationChannel(channel)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 1. Full Screen Intent (Wakes up screen & goes to AcceptCallActivity)
|
|
58
|
+
val fullScreenIntent = Intent(context, AcceptCallActivity::class.java).apply {
|
|
59
|
+
data.forEach { (k, v) -> putExtra(k, v) }
|
|
60
|
+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
61
|
+
}
|
|
62
|
+
val fullScreenPendingIntent = PendingIntent.getActivity(
|
|
63
|
+
context, 0, fullScreenIntent,
|
|
64
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
45
65
|
)
|
|
46
66
|
|
|
47
|
-
|
|
48
|
-
val
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
67
|
+
// 2. Answer Button
|
|
68
|
+
val answerIntent = Intent(context, CallActionReceiver::class.java).apply {
|
|
69
|
+
action = "ACTION_ACCEPT"
|
|
70
|
+
data.forEach { (k, v) -> putExtra(k, v) }
|
|
71
|
+
}
|
|
72
|
+
val answerPendingIntent = PendingIntent.getBroadcast(
|
|
73
|
+
context, 1, answerIntent,
|
|
74
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
75
|
+
)
|
|
53
76
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
77
|
+
// 3. Reject Button
|
|
78
|
+
val rejectIntent = Intent(context, CallActionReceiver::class.java).apply {
|
|
79
|
+
action = "ACTION_REJECT"
|
|
80
|
+
data.forEach { (k, v) -> putExtra(k, v) }
|
|
57
81
|
}
|
|
82
|
+
val rejectPendingIntent = PendingIntent.getBroadcast(
|
|
83
|
+
context, 2, rejectIntent,
|
|
84
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
|
|
88
|
+
.setSmallIcon(android.R.drawable.ic_menu_call) // Replace with your app icon
|
|
89
|
+
.setContentTitle("Incoming Call")
|
|
90
|
+
.setContentText(data["name"] ?: "Raiidr User")
|
|
91
|
+
.setPriority(NotificationCompat.PRIORITY_MAX)
|
|
92
|
+
.setCategory(NotificationCompat.CATEGORY_CALL)
|
|
93
|
+
.setFullScreenIntent(fullScreenPendingIntent, true)
|
|
94
|
+
.setOngoing(true)
|
|
95
|
+
.setAutoCancel(true)
|
|
96
|
+
.addAction(0, "Answer", answerPendingIntent)
|
|
97
|
+
.addAction(0, "Decline", rejectPendingIntent)
|
|
58
98
|
|
|
59
|
-
|
|
99
|
+
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
|
60
100
|
}
|
|
61
101
|
}
|
|
@@ -9,62 +9,57 @@ import android.os.Build
|
|
|
9
9
|
import androidx.core.app.NotificationCompat
|
|
10
10
|
import android.media.Ringtone
|
|
11
11
|
import android.media.RingtoneManager
|
|
12
|
+
import android.graphics.Color
|
|
12
13
|
|
|
13
14
|
object NativeCallManager {
|
|
14
15
|
|
|
15
|
-
private var ringtone: Ringtone? = null
|
|
16
|
+
private var ringtone: Ringtone? = null
|
|
17
|
+
private const val NOTIFICATION_ID = 101
|
|
16
18
|
|
|
17
19
|
fun handleIncomingPush(context: Context, data: Map<String, String>) {
|
|
18
20
|
val uuid = data["callUuid"] ?: return
|
|
19
21
|
stopRingtone()
|
|
22
|
+
|
|
20
23
|
val name = data["name"] ?: "Incoming Call"
|
|
21
24
|
val callType = data["callType"] ?: "audio"
|
|
22
25
|
|
|
23
|
-
// Use MUTABLE flag to ensure extras are properly passed on Android 12+
|
|
24
26
|
val pendingFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
25
27
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
|
|
26
28
|
} else {
|
|
27
29
|
PendingIntent.FLAG_UPDATE_CURRENT
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
// 1.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
// 2. Accept Action - Use unique action and requestCode
|
|
39
|
-
val acceptIntent = Intent(context, AcceptCallActivity::class.java).apply {
|
|
40
|
-
// Unique action string prevents intent caching issues
|
|
41
|
-
action = "ACTION_ACCEPT_$uuid"
|
|
42
|
-
putExtra("EXTRA_CALL_UUID", uuid)
|
|
43
|
-
// Flatten the map into the intent extras
|
|
32
|
+
// 1. IMPROVED INTENT: Direct to AcceptCallActivity
|
|
33
|
+
// This is better than a dummy intent because it allows the OS to
|
|
34
|
+
// launch your "Accept" logic immediately if the phone is locked.
|
|
35
|
+
val intentToActivity = Intent(context, AcceptCallActivity::class.java).apply {
|
|
36
|
+
action = "ACTION_SHOW_UI_$uuid"
|
|
44
37
|
data.forEach { (key, value) -> putExtra(key, value) }
|
|
45
|
-
|
|
38
|
+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
|
46
39
|
}
|
|
47
40
|
|
|
48
|
-
val
|
|
41
|
+
val fullScreenPendingIntent = PendingIntent.getActivity(
|
|
49
42
|
context,
|
|
50
|
-
uuid.hashCode(),
|
|
51
|
-
|
|
43
|
+
uuid.hashCode(),
|
|
44
|
+
intentToActivity,
|
|
52
45
|
pendingFlags
|
|
53
46
|
)
|
|
54
47
|
|
|
55
|
-
//
|
|
48
|
+
// 2. Reject Action - Still goes to BroadcastReceiver
|
|
56
49
|
val rejectIntent = Intent(context, CallActionReceiver::class.java).apply {
|
|
57
50
|
action = "ACTION_REJECT_$uuid"
|
|
58
51
|
putExtra("EXTRA_CALL_UUID", uuid)
|
|
52
|
+
// Pass all data so onReject can find the UUID
|
|
53
|
+
data.forEach { (key, value) -> putExtra(key, value) }
|
|
59
54
|
}
|
|
60
55
|
val rejectPendingIntent = PendingIntent.getBroadcast(
|
|
61
56
|
context,
|
|
62
|
-
uuid.hashCode() + 1,
|
|
57
|
+
uuid.hashCode() + 1,
|
|
63
58
|
rejectIntent,
|
|
64
59
|
pendingFlags
|
|
65
60
|
)
|
|
66
61
|
|
|
67
|
-
//
|
|
62
|
+
// 3. Setup Channel with high priority
|
|
68
63
|
val channelId = "CALL_CHANNEL_ID"
|
|
69
64
|
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
70
65
|
|
|
@@ -72,13 +67,17 @@ private var ringtone: Ringtone? = null
|
|
|
72
67
|
val channel = NotificationChannel(channelId, "Incoming Calls", NotificationManager.IMPORTANCE_HIGH).apply {
|
|
73
68
|
description = "Shows incoming call notifications"
|
|
74
69
|
enableVibration(true)
|
|
70
|
+
vibrationPattern = longArrayOf(0, 500, 500, 500)
|
|
71
|
+
lightColor = Color.GREEN
|
|
75
72
|
setBypassDnd(true)
|
|
76
73
|
lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
|
|
74
|
+
// On Android O+, sound should be set on the channel
|
|
75
|
+
setSound(null, null)
|
|
77
76
|
}
|
|
78
77
|
notificationManager.createNotificationChannel(channel)
|
|
79
78
|
}
|
|
80
79
|
|
|
81
|
-
//
|
|
80
|
+
// 4. Build the Notification
|
|
82
81
|
val builder = NotificationCompat.Builder(context, channelId)
|
|
83
82
|
.setSmallIcon(context.applicationInfo.icon)
|
|
84
83
|
.setContentTitle("Incoming $callType call")
|
|
@@ -89,24 +88,31 @@ private var ringtone: Ringtone? = null
|
|
|
89
88
|
.setAutoCancel(false)
|
|
90
89
|
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
91
90
|
|
|
92
|
-
// This
|
|
93
|
-
.setFullScreenIntent(
|
|
91
|
+
// This is the key for the "Pill" / Heads-up display
|
|
92
|
+
.setFullScreenIntent(fullScreenPendingIntent, true)
|
|
94
93
|
|
|
95
|
-
.addAction(0, "Answer",
|
|
94
|
+
.addAction(0, "Answer", fullScreenPendingIntent) // Same intent as fullScreen
|
|
96
95
|
.addAction(0, "Decline", rejectPendingIntent)
|
|
97
96
|
|
|
98
|
-
notificationManager.notify(
|
|
97
|
+
notificationManager.notify(NOTIFICATION_ID, builder.build())
|
|
99
98
|
|
|
99
|
+
// 5. Ringtone with looping logic attempt
|
|
100
100
|
try {
|
|
101
101
|
val ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
|
|
102
102
|
ringtone = RingtoneManager.getRingtone(context, ringtoneUri)
|
|
103
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
104
|
+
ringtone?.isLooping = true // Android 9+ supports looping natively
|
|
105
|
+
}
|
|
103
106
|
ringtone?.play()
|
|
104
107
|
} catch (e: Exception) {
|
|
105
108
|
e.printStackTrace()
|
|
106
109
|
}
|
|
107
110
|
}
|
|
111
|
+
|
|
108
112
|
fun stopRingtone() {
|
|
109
|
-
ringtone?.
|
|
113
|
+
if (ringtone?.isPlaying == true) {
|
|
114
|
+
ringtone?.stop()
|
|
115
|
+
}
|
|
110
116
|
ringtone = null
|
|
111
117
|
}
|
|
112
118
|
}
|
package/ios/CallModule.m
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
#import "CallModule.h"
|
|
2
|
+
#import <AVFoundation/AVFoundation.h>
|
|
2
3
|
|
|
3
|
-
// Private interface to hold the pending UUID for cold starts
|
|
4
4
|
@interface CallModule ()
|
|
5
5
|
@property (nonatomic, strong) NSString *pendingCallUuid;
|
|
6
|
-
@property (nonatomic, strong) NSUUID *currentCallUUID;
|
|
7
6
|
@end
|
|
8
7
|
|
|
9
8
|
@implementation CallModule
|
|
@@ -50,7 +49,6 @@ RCT_EXPORT_METHOD(displayIncomingCall:(NSString *)uuidString
|
|
|
50
49
|
|
|
51
50
|
self.currentCallUUID = uuid;
|
|
52
51
|
|
|
53
|
-
// 1. CONFIGURE AUDIO SESSION (Pre-warm)
|
|
54
52
|
AVAudioSession *session = [AVAudioSession sharedInstance];
|
|
55
53
|
[session setCategory:AVAudioSessionCategoryPlayAndRecord
|
|
56
54
|
mode:AVAudioSessionModeVoiceChat
|
|
@@ -82,11 +80,10 @@ RCT_EXPORT_METHOD(endNativeCall:(NSString *)uuidString)
|
|
|
82
80
|
self.pendingCallUuid = nil;
|
|
83
81
|
}
|
|
84
82
|
|
|
85
|
-
// Correct implementation for cold-start check
|
|
86
83
|
RCT_EXPORT_METHOD(getInitialCallData:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
|
|
87
84
|
if (self.pendingCallUuid) {
|
|
88
85
|
resolve(@{@"callUuid": self.pendingCallUuid});
|
|
89
|
-
self.pendingCallUuid = nil;
|
|
86
|
+
self.pendingCallUuid = nil;
|
|
90
87
|
} else {
|
|
91
88
|
resolve([NSNull null]);
|
|
92
89
|
}
|
|
@@ -99,7 +96,6 @@ RCT_EXPORT_METHOD(checkTelecomPermissions:(RCTPromiseResolveBlock)resolve reject
|
|
|
99
96
|
// MARK: - CXProviderDelegate
|
|
100
97
|
|
|
101
98
|
- (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action {
|
|
102
|
-
// 1. Force audio session active
|
|
103
99
|
AVAudioSession *session = [AVAudioSession sharedInstance];
|
|
104
100
|
[session setCategory:AVAudioSessionCategoryPlayAndRecord
|
|
105
101
|
mode:AVAudioSessionModeVoiceChat
|
|
@@ -107,15 +103,18 @@ RCT_EXPORT_METHOD(checkTelecomPermissions:(RCTPromiseResolveBlock)resolve reject
|
|
|
107
103
|
error:nil];
|
|
108
104
|
[session setActive:YES error:nil];
|
|
109
105
|
|
|
110
|
-
// 2. Fulfill to tell system we answered
|
|
111
106
|
[action fulfill];
|
|
112
107
|
|
|
113
|
-
// 3. Save the UUID for JS to poll if the app was killed
|
|
114
108
|
NSString *uuidStr = [action.callUUID.UUIDString lowercaseString];
|
|
115
109
|
self.pendingCallUuid = uuidStr;
|
|
116
110
|
|
|
117
|
-
// 4. Dispatch event for when app is backgrounded (not killed)
|
|
118
111
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
|
112
|
+
// NUDGE: Force the window to become visible.
|
|
113
|
+
// This helps transition from "Ghost Mode" to "Active UI" once the user unlocks.
|
|
114
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
115
|
+
[[[UIApplication sharedApplication] keyWindow] makeKeyAndVisible];
|
|
116
|
+
});
|
|
117
|
+
|
|
119
118
|
[self sendEventWithName:@"onCallAccepted" body:@{@"callUuid": uuidStr}];
|
|
120
119
|
});
|
|
121
120
|
}
|
package/package.json
CHANGED
package/withNativeCallVoip.js
CHANGED
|
@@ -34,15 +34,15 @@ function withAndroidConfig(config) {
|
|
|
34
34
|
const manifest = config.modResults;
|
|
35
35
|
const application = manifest.manifest.application[0];
|
|
36
36
|
|
|
37
|
-
// Permissions
|
|
37
|
+
// 1. Updated Permissions for Custom Notifications & Android 13/14
|
|
38
38
|
const permissions = [
|
|
39
|
-
'android.permission.READ_PHONE_NUMBERS',
|
|
40
|
-
'android.permission.CALL_PHONE',
|
|
41
|
-
'android.permission.MANAGE_OWN_CALLS',
|
|
42
39
|
'android.permission.USE_FULL_SCREEN_INTENT',
|
|
43
40
|
'android.permission.VIBRATE',
|
|
44
41
|
'android.permission.FOREGROUND_SERVICE',
|
|
45
|
-
'android.permission.FOREGROUND_SERVICE_PHONE_CALL'
|
|
42
|
+
'android.permission.FOREGROUND_SERVICE_PHONE_CALL', // Required for Android 14
|
|
43
|
+
'android.permission.POST_NOTIFICATIONS', // Required for Android 13+
|
|
44
|
+
'android.permission.WAKE_LOCK', // Helps wake the screen
|
|
45
|
+
'android.permission.DISABLE_KEYGUARD' // Allows showing over lockscreen
|
|
46
46
|
];
|
|
47
47
|
|
|
48
48
|
manifest.manifest['uses-permission'] = manifest.manifest['uses-permission'] || [];
|
|
@@ -52,46 +52,47 @@ function withAndroidConfig(config) {
|
|
|
52
52
|
}
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
-
//
|
|
55
|
+
// 2. AcceptCallActivity - The "Pill" UI handler
|
|
56
|
+
// Added showWhenLocked and turnScreenOn for the "Gate" logic
|
|
56
57
|
application.activity = application.activity || [];
|
|
58
|
+
const acceptActivityName = 'com.rnsnativecall.AcceptCallActivity';
|
|
59
|
+
|
|
60
|
+
// Remove old entry if exists to avoid duplicates
|
|
61
|
+
application.activity = application.activity.filter(a => a.$['android:name'] !== acceptActivityName);
|
|
62
|
+
|
|
63
|
+
application.activity.push({
|
|
64
|
+
$: {
|
|
65
|
+
'android:name': acceptActivityName,
|
|
66
|
+
'android:theme': '@android:style/Theme.Translucent.NoTitleBar',
|
|
67
|
+
'android:excludeFromRecents': 'true',
|
|
68
|
+
'android:noHistory': 'true',
|
|
69
|
+
'android:exported': 'false',
|
|
70
|
+
'android:launchMode': 'singleInstance',
|
|
71
|
+
'android:showWhenLocked': 'true', // CRUCIAL: Show over lockscreen
|
|
72
|
+
'android:turnScreenOn': 'true' // CRUCIAL: Wake device
|
|
73
|
+
}
|
|
74
|
+
});
|
|
57
75
|
|
|
58
|
-
//
|
|
59
|
-
const mainActivity = application.activity.find(a => a.$['android:name'] === '.MainActivity');
|
|
60
|
-
if (mainActivity) {
|
|
61
|
-
mainActivity.$['android:launchMode'] = 'singleTask';
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// AcceptCallActivity (The Trampoline - Must remain in Manifest)
|
|
65
|
-
if (!application.activity.some(a => a.$['android:name'] === 'com.rnsnativecall.AcceptCallActivity')) {
|
|
66
|
-
application.activity.push({
|
|
67
|
-
$: {
|
|
68
|
-
'android:name': 'com.rnsnativecall.AcceptCallActivity',
|
|
69
|
-
'android:theme': '@android:style/Theme.Translucent.NoTitleBar',
|
|
70
|
-
'android:excludeFromRecents': 'true',
|
|
71
|
-
'android:noHistory': 'true',
|
|
72
|
-
'android:exported': 'false',
|
|
73
|
-
'android:launchMode': 'singleInstance'
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Services
|
|
76
|
+
// 3. Updated Services
|
|
79
77
|
application.service = application.service || [];
|
|
78
|
+
// Removed MyConnectionService as you are no longer using Telecom
|
|
80
79
|
const services = [
|
|
81
|
-
{ name: 'com.rnsnativecall.MyConnectionService', permission: 'android.permission.BIND_CONNECTION_SERVICE', action: 'android.telecom.ConnectionService' },
|
|
82
80
|
{ name: 'com.rnsnativecall.CallMessagingService', action: 'com.google.firebase.MESSAGING_EVENT' }
|
|
83
81
|
];
|
|
84
82
|
|
|
85
83
|
services.forEach(svc => {
|
|
86
84
|
if (!application.service.some(s => s.$['android:name'] === svc.name)) {
|
|
87
85
|
application.service.push({
|
|
88
|
-
$: {
|
|
86
|
+
$: {
|
|
87
|
+
'android:name': svc.name,
|
|
88
|
+
'android:exported': 'false' // Firebase services should usually be false unless needed
|
|
89
|
+
},
|
|
89
90
|
'intent-filter': [{ action: [{ $: { 'android:name': svc.action } }] }]
|
|
90
91
|
});
|
|
91
92
|
}
|
|
92
93
|
});
|
|
93
94
|
|
|
94
|
-
// Receivers
|
|
95
|
+
// 4. Receivers (Answer/Reject Buttons)
|
|
95
96
|
application.receiver = application.receiver || [];
|
|
96
97
|
if (!application.receiver.some(r => r.$['android:name'] === 'com.rnsnativecall.CallActionReceiver')) {
|
|
97
98
|
application.receiver.push({
|