rns-nativecall 0.6.5 → 0.6.7
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 -25
- package/android/src/main/java/com/rnsnativecall/CallForegroundService.kt +70 -0
- package/android/src/main/java/com/rnsnativecall/CallMessagingService.kt +22 -9
- package/android/src/main/java/com/rnsnativecall/CallModule.kt +13 -0
- package/index.d.ts +2 -0
- package/index.js +5 -0
- package/package.json +1 -1
- package/withNativeCallVoip.js +29 -48
|
@@ -13,6 +13,7 @@ class AcceptCallActivity : Activity() {
|
|
|
13
13
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
14
14
|
super.onCreate(savedInstanceState)
|
|
15
15
|
|
|
16
|
+
// Ensure we show over the lockscreen
|
|
16
17
|
window.addFlags(
|
|
17
18
|
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
|
|
18
19
|
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
|
|
@@ -34,36 +35,30 @@ class AcceptCallActivity : Activity() {
|
|
|
34
35
|
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
35
36
|
uuid?.let { notificationManager.cancel(it.hashCode()) }
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
val dataMap = mutableMapOf<String, String>()
|
|
39
|
+
extras?.keySet()?.forEach { key ->
|
|
40
|
+
extras.get(key)?.let { dataMap[key] = it.toString() }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 1. Set the data for JS (Cold start support)
|
|
44
|
+
CallModule.setPendingCallData("onCallAccepted_pending", dataMap)
|
|
45
|
+
|
|
46
|
+
// 2. Fire event immediately if JS is alive
|
|
47
|
+
if (CallModule.isReady()) {
|
|
48
|
+
CallModule.sendEventToJS("onCallAccepted", dataMap)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 3. Bring the Main App to the front
|
|
39
52
|
openMainApp(extras)
|
|
40
53
|
finish()
|
|
41
54
|
}
|
|
42
55
|
|
|
43
56
|
private fun openMainApp(extras: Bundle?) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
setClassName(packageName, mainActivityClassName)
|
|
50
|
-
action = "com.rnsnativecall.ACTION_ANSWER"
|
|
51
|
-
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
|
52
|
-
|
|
53
|
-
// Ensure extras are carried over
|
|
54
|
-
extras?.let { putExtras(it) }
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
startActivity(intent)
|
|
58
|
-
} catch (e: Exception) {
|
|
59
|
-
// Fallback: If explicit mapping fails, try the launch intent but force the action
|
|
60
|
-
val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
|
|
61
|
-
launchIntent?.apply {
|
|
62
|
-
action = "com.rnsnativecall.ACTION_ANSWER"
|
|
63
|
-
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
|
64
|
-
extras?.let { putExtras(it) }
|
|
65
|
-
startActivity(this)
|
|
66
|
-
}
|
|
57
|
+
val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
|
|
58
|
+
launchIntent?.apply {
|
|
59
|
+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
|
60
|
+
putExtras(extras ?: Bundle())
|
|
61
|
+
startActivity(this)
|
|
67
62
|
}
|
|
68
63
|
}
|
|
69
64
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
package com.rnsnativecall
|
|
2
|
+
|
|
3
|
+
import android.app.*
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.Intent
|
|
6
|
+
import android.os.Build
|
|
7
|
+
import android.os.IBinder
|
|
8
|
+
import androidx.core.app.NotificationCompat
|
|
9
|
+
import com.facebook.react.HeadlessJsTaskService
|
|
10
|
+
|
|
11
|
+
class CallForegroundService : Service() {
|
|
12
|
+
|
|
13
|
+
companion object {
|
|
14
|
+
private const val NOTIFICATION_ID = 101
|
|
15
|
+
private const val CHANNEL_ID = "incoming_call_service"
|
|
16
|
+
|
|
17
|
+
fun stop(context: Context) {
|
|
18
|
+
val intent = Intent(context, CallForegroundService::class.java)
|
|
19
|
+
context.stopService(intent)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
24
|
+
val data = intent?.extras
|
|
25
|
+
val name = data?.getString("name") ?: "Someone"
|
|
26
|
+
|
|
27
|
+
createNotificationChannel()
|
|
28
|
+
|
|
29
|
+
// Create a notification that shows "Connecting..."
|
|
30
|
+
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
|
|
31
|
+
.setContentTitle(name)
|
|
32
|
+
.setContentText("Connecting...")
|
|
33
|
+
.setSmallIcon(applicationInfo.icon)
|
|
34
|
+
.setCategory(NotificationCompat.CATEGORY_CALL)
|
|
35
|
+
.setPriority(NotificationCompat.PRIORITY_LOW) // Keep it subtle until UI pops
|
|
36
|
+
.setOngoing(true)
|
|
37
|
+
.build()
|
|
38
|
+
|
|
39
|
+
// Start Foreground immediately to satisfy OS requirements
|
|
40
|
+
startForeground(NOTIFICATION_ID, notification)
|
|
41
|
+
|
|
42
|
+
// Launch the Headless Task while under the protection of this service
|
|
43
|
+
val headlessIntent = Intent(this, CallHeadlessTask::class.java).apply {
|
|
44
|
+
data?.let { putExtras(it) }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
this.startService(headlessIntent)
|
|
49
|
+
HeadlessJsTaskService.acquireWakeLockNow(this)
|
|
50
|
+
} catch (e: Exception) {
|
|
51
|
+
e.printStackTrace()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return START_NOT_STICKY
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private fun createNotificationChannel() {
|
|
58
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
59
|
+
val channel = NotificationChannel(
|
|
60
|
+
CHANNEL_ID,
|
|
61
|
+
"Call Service",
|
|
62
|
+
NotificationManager.IMPORTANCE_LOW
|
|
63
|
+
)
|
|
64
|
+
val manager = getSystemService(NotificationManager::class.java)
|
|
65
|
+
manager.createNotificationChannel(channel)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
override fun onBind(intent: Intent?): IBinder? = null
|
|
70
|
+
}
|
|
@@ -61,17 +61,30 @@ if (!CallState.setCurrent(uuid)) {
|
|
|
61
61
|
// }
|
|
62
62
|
// ContextCompat.startForegroundService(context, serviceIntent)
|
|
63
63
|
|
|
64
|
-
val headlessIntent = Intent(context, CallHeadlessTask::class.java).apply {
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
try {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
} catch (e: Exception) {
|
|
71
|
-
|
|
72
|
-
}
|
|
64
|
+
// val headlessIntent = Intent(context, CallHeadlessTask::class.java).apply {
|
|
65
|
+
// putExtras(Bundle().apply { data.forEach { (k, v) -> putString(k, v) } })
|
|
66
|
+
// }
|
|
67
|
+
// try {
|
|
68
|
+
// context.startService(headlessIntent)
|
|
69
|
+
// HeadlessJsTaskService.acquireWakeLockNow(context)
|
|
70
|
+
// } catch (e: Exception) {
|
|
71
|
+
// e.printStackTrace()
|
|
72
|
+
// }
|
|
73
73
|
|
|
74
74
|
|
|
75
|
+
// Background → start foreground service (which in turn starts headless)
|
|
76
|
+
val serviceIntent = Intent(context, CallForegroundService::class.java).apply {
|
|
77
|
+
putExtras(Bundle().apply {
|
|
78
|
+
data.forEach { (k, v) -> putString(k, v) }
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
83
|
+
context.startForegroundService(serviceIntent)
|
|
84
|
+
} else {
|
|
85
|
+
context.startService(serviceIntent)
|
|
86
|
+
}
|
|
87
|
+
|
|
75
88
|
}
|
|
76
89
|
}
|
|
77
90
|
|
|
@@ -4,6 +4,7 @@ import android.app.NotificationManager
|
|
|
4
4
|
import android.content.Context
|
|
5
5
|
import com.facebook.react.bridge.*
|
|
6
6
|
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
7
|
+
import android.content.Intent
|
|
7
8
|
|
|
8
9
|
class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
|
|
9
10
|
|
|
@@ -66,6 +67,18 @@ class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
|
|
|
66
67
|
promise.resolve(map)
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
@ReactMethod
|
|
71
|
+
fun stopForegroundService(promise: Promise) {
|
|
72
|
+
try {
|
|
73
|
+
// Using the current react context to stop the service
|
|
74
|
+
val intent = Intent(reactApplicationContext, CallForegroundService::class.java)
|
|
75
|
+
reactApplicationContext.stopService(intent)
|
|
76
|
+
promise.resolve(true)
|
|
77
|
+
} catch (e: Exception) {
|
|
78
|
+
promise.reject("SERVICE_STOP_ERROR", e.message)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
69
82
|
/**
|
|
70
83
|
* General Status Check:
|
|
71
84
|
* Useful for checking if the UI is still relevant.
|
package/index.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -37,6 +37,7 @@ export const CallHandler = {
|
|
|
37
37
|
if (onAction) {
|
|
38
38
|
await onAction(data, 'INCOMING_CALL');
|
|
39
39
|
}
|
|
40
|
+
|
|
40
41
|
} catch (error) {
|
|
41
42
|
console.error('[RNSNativeCall] Headless Task Error:', error);
|
|
42
43
|
}
|
|
@@ -48,6 +49,10 @@ export const CallHandler = {
|
|
|
48
49
|
return await CallModule.displayIncomingCall(uuid.toLowerCase().trim(), name, callType);
|
|
49
50
|
},
|
|
50
51
|
|
|
52
|
+
stopForegroundService: async () => {
|
|
53
|
+
await CallModule.stopForegroundService(true)
|
|
54
|
+
},
|
|
55
|
+
|
|
51
56
|
destroyNativeCallUI: (uuid) => {
|
|
52
57
|
if (CallModule?.endNativeCall) {
|
|
53
58
|
CallModule.endNativeCall(uuid.toLowerCase().trim());
|
package/package.json
CHANGED
package/withNativeCallVoip.js
CHANGED
|
@@ -5,32 +5,15 @@ function withMainActivityDataFix(config) {
|
|
|
5
5
|
return withMainActivity(config, (config) => {
|
|
6
6
|
let contents = config.modResults.contents;
|
|
7
7
|
|
|
8
|
-
// Ensure
|
|
8
|
+
// Ensure Intent import for Kotlin
|
|
9
9
|
if (!contents.includes('import android.content.Intent')) {
|
|
10
10
|
contents = contents.replace(/package .*/, (match) => `${match}\n\nimport android.content.Intent`);
|
|
11
11
|
}
|
|
12
|
-
if (!contents.includes('import android.os.Bundle')) {
|
|
13
|
-
contents = contents.replace(/package .*/, (match) => `${match}\n\nimport android.os.Bundle`);
|
|
14
|
-
}
|
|
15
12
|
|
|
16
13
|
const onNewIntentCode = `
|
|
17
14
|
override fun onNewIntent(intent: Intent) {
|
|
18
15
|
super.onNewIntent(intent)
|
|
19
16
|
setIntent(intent)
|
|
20
|
-
|
|
21
|
-
// Check for the specific Answer Action
|
|
22
|
-
val isAnswerAction = intent.action == "com.rnsnativecall.ACTION_ANSWER"
|
|
23
|
-
|
|
24
|
-
val dataMap = mutableMapOf<String, String>()
|
|
25
|
-
intent.extras?.keySet()?.forEach { key ->
|
|
26
|
-
dataMap[key] = intent.extras?.get(key)?.toString() ?: ""
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// FIRE if it's the answer action, even if extras are slim
|
|
30
|
-
if (isAnswerAction) {
|
|
31
|
-
com.rnsnativecall.CallModule.setPendingCallData("onCallAccepted_pending", dataMap)
|
|
32
|
-
com.rnsnativecall.CallModule.sendEventToJS("onCallAccepted", dataMap)
|
|
33
|
-
}
|
|
34
17
|
}
|
|
35
18
|
`;
|
|
36
19
|
|
|
@@ -38,25 +21,12 @@ function withMainActivityDataFix(config) {
|
|
|
38
21
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
39
22
|
super.onCreate(savedInstanceState)
|
|
40
23
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
// Logic for Cold Start (App was dead, user answered)
|
|
44
|
-
if (isAnswerAction) {
|
|
45
|
-
val dataMap = mutableMapOf<String, String>()
|
|
46
|
-
intent.extras?.keySet()?.forEach { key ->
|
|
47
|
-
dataMap[key] = intent.extras?.get(key)?.toString() ?: ""
|
|
48
|
-
}
|
|
49
|
-
com.rnsnativecall.CallModule.setPendingCallData(dataMap)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Move to back if it's a background wake (FCM) and NOT an answer click
|
|
53
|
-
if (intent.getBooleanExtra("background_wake", false) && !isAnswerAction) {
|
|
24
|
+
// If background wake from FCM, move to back to let LockScreen UI show
|
|
25
|
+
if (intent.getBooleanExtra("background_wake", false)) {
|
|
54
26
|
moveTaskToBack(true)
|
|
55
27
|
}
|
|
56
28
|
}
|
|
57
29
|
`;
|
|
58
|
-
|
|
59
|
-
// Inject codes
|
|
60
30
|
if (!contents.includes('override fun onNewIntent')) {
|
|
61
31
|
const lastBraceIndex = contents.lastIndexOf('}');
|
|
62
32
|
contents = contents.slice(0, lastBraceIndex) + onNewIntentCode + contents.slice(lastBraceIndex);
|
|
@@ -70,11 +40,14 @@ function withMainActivityDataFix(config) {
|
|
|
70
40
|
return config;
|
|
71
41
|
});
|
|
72
42
|
}
|
|
43
|
+
|
|
73
44
|
/** 2. ANDROID MANIFEST CONFIG **/
|
|
74
45
|
function withAndroidConfig(config) {
|
|
75
46
|
return withAndroidManifest(config, (config) => {
|
|
76
47
|
const manifest = config.modResults;
|
|
77
48
|
const application = manifest.manifest.application[0];
|
|
49
|
+
|
|
50
|
+
// Comprehensive list for VoIP & Foreground Services
|
|
78
51
|
const permissions = [
|
|
79
52
|
'android.permission.USE_FULL_SCREEN_INTENT',
|
|
80
53
|
'android.permission.VIBRATE',
|
|
@@ -83,15 +56,19 @@ function withAndroidConfig(config) {
|
|
|
83
56
|
'android.permission.POST_NOTIFICATIONS',
|
|
84
57
|
'android.permission.SYSTEM_ALERT_WINDOW',
|
|
85
58
|
'android.permission.WAKE_LOCK',
|
|
86
|
-
'android.permission.DISABLE_KEYGUARD'
|
|
59
|
+
'android.permission.DISABLE_KEYGUARD',
|
|
60
|
+
'android.permission.MANAGE_OWN_CALLS'
|
|
87
61
|
];
|
|
62
|
+
|
|
88
63
|
manifest.manifest['uses-permission'] = manifest.manifest['uses-permission'] || [];
|
|
89
64
|
permissions.forEach((perm) => {
|
|
90
65
|
if (!manifest.manifest['uses-permission'].some((p) => p.$['android:name'] === perm)) {
|
|
91
66
|
manifest.manifest['uses-permission'].push({ $: { 'android:name': perm } });
|
|
92
67
|
}
|
|
93
68
|
});
|
|
69
|
+
|
|
94
70
|
application.activity = application.activity || [];
|
|
71
|
+
// AcceptCallActivity registration
|
|
95
72
|
if (!application.activity.some(a => a.$['android:name'] === 'com.rnsnativecall.AcceptCallActivity')) {
|
|
96
73
|
application.activity.push({
|
|
97
74
|
$: {
|
|
@@ -106,21 +83,10 @@ function withAndroidConfig(config) {
|
|
|
106
83
|
}
|
|
107
84
|
});
|
|
108
85
|
}
|
|
109
|
-
|
|
110
|
-
application.activity.push({
|
|
111
|
-
$: {
|
|
112
|
-
'android:name': 'com.rnsnativecall.UnlockPromptActivity',
|
|
113
|
-
'android:theme': '@android:style/Theme.Translucent.NoTitleBar',
|
|
114
|
-
'android:excludeFromRecents': 'true',
|
|
115
|
-
'android:noHistory': 'true',
|
|
116
|
-
'android:exported': 'false',
|
|
117
|
-
'android:launchMode': 'singleInstance',
|
|
118
|
-
'android:showWhenLocked': 'true',
|
|
119
|
-
'android:turnScreenOn': 'true'
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
}
|
|
86
|
+
|
|
123
87
|
application.service = application.service || [];
|
|
88
|
+
|
|
89
|
+
// 1. Firebase Messaging Service
|
|
124
90
|
const firebaseServiceName = 'com.rnsnativecall.CallMessagingService';
|
|
125
91
|
if (!application.service.some(s => s.$['android:name'] === firebaseServiceName)) {
|
|
126
92
|
application.service.push({
|
|
@@ -128,12 +94,27 @@ function withAndroidConfig(config) {
|
|
|
128
94
|
'intent-filter': [{ action: [{ $: { 'android:name': 'com.google.firebase.MESSAGING_EVENT' } }] }]
|
|
129
95
|
});
|
|
130
96
|
}
|
|
97
|
+
|
|
98
|
+
// 2. Headless Task Service
|
|
131
99
|
const headlessServiceName = 'com.rnsnativecall.CallHeadlessTask';
|
|
132
100
|
if (!application.service.some(s => s.$['android:name'] === headlessServiceName)) {
|
|
133
101
|
application.service.push({
|
|
134
102
|
$: { 'android:name': headlessServiceName, 'android:exported': 'false' }
|
|
135
103
|
});
|
|
136
104
|
}
|
|
105
|
+
|
|
106
|
+
// 3. Foreground Service (The "Connecting..." spinner)
|
|
107
|
+
const foregroundServiceName = 'com.rnsnativecall.CallForegroundService';
|
|
108
|
+
if (!application.service.some(s => s.$['android:name'] === foregroundServiceName)) {
|
|
109
|
+
application.service.push({
|
|
110
|
+
$: {
|
|
111
|
+
'android:name': foregroundServiceName,
|
|
112
|
+
'android:foregroundServiceType': 'phoneCall',
|
|
113
|
+
'android:exported': 'false'
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
137
118
|
application.receiver = application.receiver || [];
|
|
138
119
|
const receiverName = 'com.rnsnativecall.CallActionReceiver';
|
|
139
120
|
if (!application.receiver.some(r => r.$['android:name'] === receiverName)) {
|