rns-nativecall 0.1.6 → 0.1.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 +38 -0
- package/android/src/main/java/com/rnsnativecall/CallActionReceiver.kt +41 -0
- package/android/src/main/java/com/rnsnativecall/NativeCallManager.kt +77 -43
- package/android/src/main/res/values/styles.xml +12 -3
- package/index.js +4 -4
- package/package.json +1 -1
- package/withNativeCallVoip.js +39 -53
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
package com.rnsnativecall
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.app.NotificationManager
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import android.content.Intent
|
|
7
|
+
import android.os.Bundle
|
|
8
|
+
|
|
9
|
+
class AcceptCallActivity : Activity() {
|
|
10
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
11
|
+
super.onCreate(savedInstanceState)
|
|
12
|
+
|
|
13
|
+
// 1. CLEAR THE PILL IMMEDIATELY
|
|
14
|
+
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
15
|
+
notificationManager.cancel(101)
|
|
16
|
+
|
|
17
|
+
// 2. PREPARE DATA FOR JS
|
|
18
|
+
val dataMap = mutableMapOf<String, String>()
|
|
19
|
+
intent.extras?.keySet()?.forEach { key ->
|
|
20
|
+
intent.getStringExtra(key)?.let { dataMap[key] = it }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// 3. SEND EVENT TO REACT NATIVE
|
|
24
|
+
CallModule.sendEventToJS("onCallAccepted", dataMap)
|
|
25
|
+
|
|
26
|
+
// 4. OPEN THE MAIN APP
|
|
27
|
+
val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
|
|
28
|
+
launchIntent?.apply {
|
|
29
|
+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
|
30
|
+
putExtras(intent.extras ?: Bundle())
|
|
31
|
+
putExtra("navigatingToCall", true)
|
|
32
|
+
}
|
|
33
|
+
startActivity(launchIntent)
|
|
34
|
+
|
|
35
|
+
// 5. KILL THIS INVISIBLE WINDOW
|
|
36
|
+
finish()
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
package com.rnsnativecall
|
|
2
|
+
|
|
3
|
+
import android.content.BroadcastReceiver
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.Intent
|
|
6
|
+
import android.app.NotificationManager
|
|
7
|
+
import android.os.Bundle
|
|
8
|
+
|
|
9
|
+
class CallActionReceiver : BroadcastReceiver() {
|
|
10
|
+
override fun onReceive(context: Context, intent: Intent) {
|
|
11
|
+
val uuid = intent.getStringExtra("EXTRA_CALL_UUID")
|
|
12
|
+
|
|
13
|
+
// 1. Clear the notification
|
|
14
|
+
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
15
|
+
notificationManager.cancel(101)
|
|
16
|
+
|
|
17
|
+
if (intent.action == "ACTION_ACCEPT") {
|
|
18
|
+
// 2. Prepare the data for React Native
|
|
19
|
+
val dataMap = mutableMapOf<String, String>()
|
|
20
|
+
intent.extras?.keySet()?.forEach { key ->
|
|
21
|
+
intent.getStringExtra(key)?.let { dataMap[key] = it }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 3. Send the event to JS if the app is already running in background
|
|
25
|
+
CallModule.sendEventToJS("onCallAccepted", dataMap)
|
|
26
|
+
|
|
27
|
+
// 4. Bring the app to foreground
|
|
28
|
+
val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
|
|
29
|
+
launchIntent?.apply {
|
|
30
|
+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
|
31
|
+
// Forward the extras so the App can read them on startup/resume
|
|
32
|
+
putExtras(intent.extras ?: Bundle())
|
|
33
|
+
putExtra("navigatingToCall", true)
|
|
34
|
+
}
|
|
35
|
+
context.startActivity(launchIntent)
|
|
36
|
+
} else {
|
|
37
|
+
// Logic for Reject
|
|
38
|
+
CallModule.sendEventToJS("onCallRejected", mapOf("callUUID" to uuid))
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -1,55 +1,89 @@
|
|
|
1
1
|
package com.rnsnativecall
|
|
2
2
|
|
|
3
|
-
import android.
|
|
3
|
+
import android.app.NotificationChannel
|
|
4
|
+
import android.app.NotificationManager
|
|
5
|
+
import android.app.PendingIntent
|
|
4
6
|
import android.content.Context
|
|
5
|
-
import android.
|
|
6
|
-
import android.os.
|
|
7
|
-
import
|
|
8
|
-
import android.telecom.TelecomManager
|
|
9
|
-
import android.telecom.VideoProfile
|
|
7
|
+
import android.content.Intent
|
|
8
|
+
import android.os.Build
|
|
9
|
+
import androidx.core.app.NotificationCompat
|
|
10
10
|
|
|
11
11
|
object NativeCallManager {
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
// Create the Intent for the FullScreen Activity
|
|
19
|
-
val fullScreenIntent = Intent(context, IncomingCallActivity::class.java).apply {
|
|
20
|
-
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_USER_ACTION
|
|
21
|
-
putExtra("EXTRA_CALL_UUID", uuid)
|
|
22
|
-
putExtra("EXTRA_CALLER_NAME", name)
|
|
23
|
-
}
|
|
13
|
+
fun handleIncomingPush(context: Context, data: Map<String, String>) {
|
|
14
|
+
val uuid = data["callId"] ?: return
|
|
15
|
+
val name = data["name"] ?: "Incoming Call"
|
|
16
|
+
val callType = data["callType"] ?: "audio"
|
|
24
17
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
.setSmallIcon(context.applicationInfo.icon)
|
|
31
|
-
.setContentTitle("Incoming Call")
|
|
32
|
-
.setContentText(name)
|
|
33
|
-
.setPriority(NotificationCompat.PRIORITY_MAX)
|
|
34
|
-
.setCategory(NotificationCompat.CATEGORY_CALL)
|
|
35
|
-
.setFullScreenIntent(fullScreenPendingIntent, true) // High Priority HUN
|
|
36
|
-
.setOngoing(true)
|
|
37
|
-
.setAutoCancel(false)
|
|
38
|
-
|
|
39
|
-
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
40
|
-
|
|
41
|
-
// Create Channel for Android O+
|
|
42
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
43
|
-
val channel = NotificationChannel("CALL_CHANNEL_ID", "Incoming Calls", NotificationManager.IMPORTANCE_HIGH)
|
|
44
|
-
notificationManager.createNotificationChannel(channel)
|
|
45
|
-
}
|
|
18
|
+
val pendingFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
19
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
20
|
+
} else {
|
|
21
|
+
PendingIntent.FLAG_UPDATE_CURRENT
|
|
22
|
+
}
|
|
46
23
|
|
|
47
|
-
|
|
24
|
+
// 1. DUMMY INTENT: This is the key to making the Pill "Sticky".
|
|
25
|
+
// It prevents the notification from disappearing but doesn't open a screen.
|
|
26
|
+
val dummyIntent = PendingIntent.getActivity(
|
|
27
|
+
context,
|
|
28
|
+
0,
|
|
29
|
+
Intent(),
|
|
30
|
+
pendingFlags
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
// 2. Accept Action
|
|
34
|
+
val acceptIntent = Intent(context, AcceptCallActivity::class.java).apply {
|
|
35
|
+
action = "ACTION_ACCEPT"
|
|
36
|
+
putExtra("EXTRA_CALL_UUID", uuid)
|
|
37
|
+
data.forEach { (key, value) -> putExtra(key, value) }
|
|
38
|
+
// Crucial for launching from a notification
|
|
39
|
+
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
|
48
40
|
}
|
|
49
|
-
|
|
50
41
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
42
|
+
// Keep this as getActivity
|
|
43
|
+
val acceptPendingIntent = PendingIntent.getActivity(
|
|
44
|
+
context,
|
|
45
|
+
1001,
|
|
46
|
+
acceptIntent,
|
|
47
|
+
pendingFlags
|
|
48
|
+
)
|
|
49
|
+
// 3. Reject Action
|
|
50
|
+
val rejectIntent = Intent(context, CallActionReceiver::class.java).apply {
|
|
51
|
+
action = "ACTION_REJECT"
|
|
52
|
+
putExtra("EXTRA_CALL_UUID", uuid)
|
|
53
|
+
}
|
|
54
|
+
val rejectPendingIntent = PendingIntent.getBroadcast(context, 1002, rejectIntent, pendingFlags)
|
|
55
|
+
|
|
56
|
+
// 4. Setup Channel (Ensure Importance is HIGH)
|
|
57
|
+
val channelId = "CALL_CHANNEL_ID"
|
|
58
|
+
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
59
|
+
|
|
60
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
61
|
+
val channel = NotificationChannel(channelId, "Incoming Calls", NotificationManager.IMPORTANCE_HIGH).apply {
|
|
62
|
+
description = "Shows incoming call notifications"
|
|
63
|
+
enableVibration(true)
|
|
64
|
+
// Critical for bypassing "Do Not Disturb"
|
|
65
|
+
setBypassDnd(true)
|
|
66
|
+
lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
|
|
67
|
+
}
|
|
68
|
+
notificationManager.createNotificationChannel(channel)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 5. Build the Notification
|
|
72
|
+
val builder = NotificationCompat.Builder(context, channelId)
|
|
73
|
+
.setSmallIcon(context.applicationInfo.icon)
|
|
74
|
+
.setContentTitle("Incoming $callType call")
|
|
75
|
+
.setContentText(name)
|
|
76
|
+
.setPriority(NotificationCompat.PRIORITY_MAX)
|
|
77
|
+
.setCategory(NotificationCompat.CATEGORY_CALL)
|
|
78
|
+
.setOngoing(true) // Prevents user from swiping it away
|
|
79
|
+
.setAutoCancel(false)
|
|
80
|
+
|
|
81
|
+
// This is what keeps the Heads-Up "Pill" visible indefinitely:
|
|
82
|
+
.setFullScreenIntent(dummyIntent, true)
|
|
83
|
+
|
|
84
|
+
.addAction(0, "Answer", acceptPendingIntent)
|
|
85
|
+
.addAction(0, "Decline", rejectPendingIntent)
|
|
86
|
+
|
|
87
|
+
notificationManager.notify(101, builder.build())
|
|
54
88
|
}
|
|
55
89
|
}
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<resources>
|
|
3
|
+
<style name="CircleImage" parent="">
|
|
4
|
+
<item name="cornerSize">50%</item>
|
|
5
|
+
</style>
|
|
6
|
+
|
|
7
|
+
<style name="Theme.IncomingCall" parent="Theme.AppCompat.Light.NoActionBar">
|
|
8
|
+
<item name="android:windowBackground">@android:color/transparent</item>
|
|
9
|
+
<item name="android:windowIsTranslucent">true</item>
|
|
10
|
+
<item name="android:windowAnimationStyle">@android:style/Animation.Translucent</item>
|
|
11
|
+
</style>
|
|
12
|
+
</resources>
|
package/index.js
CHANGED
|
@@ -38,10 +38,10 @@ export const CallHandler = {
|
|
|
38
38
|
displayCall: async (uuid, number, name, hasVideo = false, shouldRing = true) => {
|
|
39
39
|
if (!CallModule) return false;
|
|
40
40
|
|
|
41
|
-
if (Platform.OS === 'android') {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
41
|
+
// if (Platform.OS === 'android') {
|
|
42
|
+
// const hasPerms = await ensureAndroidPermissions();
|
|
43
|
+
// if (!hasPerms) return false;
|
|
44
|
+
// }
|
|
45
45
|
|
|
46
46
|
try {
|
|
47
47
|
return await CallModule.displayIncomingCall(
|
package/package.json
CHANGED
package/withNativeCallVoip.js
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
const { withAndroidManifest, withInfoPlist, withPlugins } = require('@expo/config-plugins');
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* ANDROID CONFIGURATION
|
|
5
|
-
*/
|
|
6
3
|
function withAndroidConfig(config) {
|
|
7
4
|
return withAndroidManifest(config, (config) => {
|
|
8
5
|
const manifest = config.modResults;
|
|
9
6
|
const application = manifest.manifest.application[0];
|
|
10
7
|
|
|
11
|
-
// 1.
|
|
8
|
+
// 1. Permissions
|
|
12
9
|
const permissions = [
|
|
13
10
|
'android.permission.READ_PHONE_NUMBERS',
|
|
14
11
|
'android.permission.CALL_PHONE',
|
|
@@ -26,84 +23,73 @@ function withAndroidConfig(config) {
|
|
|
26
23
|
}
|
|
27
24
|
});
|
|
28
25
|
|
|
29
|
-
// 2.
|
|
26
|
+
// 2. Activities (UI Components)
|
|
30
27
|
application.activity = application.activity || [];
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
|
|
29
|
+
// IncomingCallActivity (Optional lock screen UI)
|
|
30
|
+
if (!application.activity.some(a => a.$['android:name'] === 'com.rnsnativecall.IncomingCallActivity')) {
|
|
33
31
|
application.activity.push({
|
|
34
32
|
$: {
|
|
35
|
-
'android:name':
|
|
33
|
+
'android:name': 'com.rnsnativecall.IncomingCallActivity',
|
|
36
34
|
'android:showOnLockScreen': 'true',
|
|
37
35
|
'android:launchMode': 'singleInstance',
|
|
38
|
-
'android:excludeFromRecents': 'true',
|
|
39
|
-
'android:screenOrientation': 'portrait',
|
|
40
36
|
'android:theme': '@style/Theme.AppCompat.Light.NoActionBar'
|
|
41
37
|
}
|
|
42
38
|
});
|
|
43
39
|
}
|
|
44
40
|
|
|
45
|
-
//
|
|
46
|
-
|
|
41
|
+
// AcceptCallActivity (The "Trampoline" that fixes the Answer button)
|
|
42
|
+
if (!application.activity.some(a => a.$['android:name'] === 'com.rnsnativecall.AcceptCallActivity')) {
|
|
43
|
+
application.activity.push({
|
|
44
|
+
$: {
|
|
45
|
+
'android:name': 'com.rnsnativecall.AcceptCallActivity',
|
|
46
|
+
'android:theme': '@android:style/Theme.Translucent.NoTitleBar',
|
|
47
|
+
'android:excludeFromRecents': 'true',
|
|
48
|
+
'android:noHistory': 'true',
|
|
49
|
+
'android:exported': 'false',
|
|
50
|
+
'android:launchMode': 'singleInstance'
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
47
54
|
|
|
55
|
+
// 3. Services (FCM and Telecom)
|
|
56
|
+
application.service = application.service || [];
|
|
48
57
|
const services = [
|
|
49
|
-
{
|
|
50
|
-
|
|
51
|
-
permission: 'android.permission.BIND_CONNECTION_SERVICE',
|
|
52
|
-
action: 'android.telecom.ConnectionService',
|
|
53
|
-
exported: 'true'
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
name: 'com.rnsnativecall.CallMessagingService',
|
|
57
|
-
action: 'com.google.firebase.MESSAGING_EVENT',
|
|
58
|
-
exported: 'false'
|
|
59
|
-
}
|
|
58
|
+
{ name: 'com.rnsnativecall.MyConnectionService', permission: 'android.permission.BIND_CONNECTION_SERVICE', action: 'android.telecom.ConnectionService' },
|
|
59
|
+
{ name: 'com.rnsnativecall.CallMessagingService', action: 'com.google.firebase.MESSAGING_EVENT' }
|
|
60
60
|
];
|
|
61
61
|
|
|
62
62
|
services.forEach(svc => {
|
|
63
63
|
if (!application.service.some(s => s.$['android:name'] === svc.name)) {
|
|
64
|
-
|
|
65
|
-
$: { 'android:name': svc.name, 'android:exported': svc.
|
|
64
|
+
application.service.push({
|
|
65
|
+
$: { 'android:name': svc.name, 'android:exported': svc.permission ? 'true' : 'false', 'android:permission': svc.permission },
|
|
66
66
|
'intent-filter': [{ action: [{ $: { 'android:name': svc.action } }] }]
|
|
67
|
-
};
|
|
68
|
-
if (svc.permission) serviceObj.$['android:permission'] = svc.permission;
|
|
69
|
-
application.service.push(serviceObj);
|
|
67
|
+
});
|
|
70
68
|
}
|
|
71
69
|
});
|
|
72
70
|
|
|
71
|
+
// 4. Receivers (The Decline Button)
|
|
72
|
+
application.receiver = application.receiver || [];
|
|
73
|
+
if (!application.receiver.some(r => r.$['android:name'] === 'com.rnsnativecall.CallActionReceiver')) {
|
|
74
|
+
application.receiver.push({
|
|
75
|
+
$: { 'android:name': 'com.rnsnativecall.CallActionReceiver', 'android:exported': 'false' }
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
73
79
|
return config;
|
|
74
80
|
});
|
|
75
81
|
}
|
|
76
82
|
|
|
77
|
-
/**
|
|
78
|
-
* IOS CONFIGURATION
|
|
79
|
-
*/
|
|
83
|
+
/** IOS Config remains the same **/
|
|
80
84
|
function withIosConfig(config) {
|
|
81
85
|
return withInfoPlist(config, (config) => {
|
|
82
86
|
const infoPlist = config.modResults;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (!infoPlist.UIBackgroundModes) {
|
|
86
|
-
infoPlist.UIBackgroundModes = [];
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
['voip', 'audio'].forEach((mode) => {
|
|
90
|
-
if (!infoPlist.UIBackgroundModes.includes(mode)) {
|
|
91
|
-
infoPlist.UIBackgroundModes.push(mode);
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
// 2. Dynamic Microphone Description
|
|
96
|
-
const appName = config.name || 'this app';
|
|
97
|
-
infoPlist.NSMicrophoneUsageDescription =
|
|
98
|
-
infoPlist.NSMicrophoneUsageDescription || `Allow ${appName} to access your microphone for calls.`;
|
|
99
|
-
|
|
87
|
+
if (!infoPlist.UIBackgroundModes) infoPlist.UIBackgroundModes = [];
|
|
88
|
+
['voip', 'audio'].forEach(mode => { if (!infoPlist.UIBackgroundModes.includes(mode)) infoPlist.UIBackgroundModes.push(mode); });
|
|
100
89
|
return config;
|
|
101
90
|
});
|
|
102
91
|
}
|
|
103
92
|
|
|
104
93
|
module.exports = (config) => {
|
|
105
|
-
return withPlugins(config, [
|
|
106
|
-
|
|
107
|
-
withIosConfig,
|
|
108
|
-
]);
|
|
109
|
-
};
|
|
94
|
+
return withPlugins(config, [withAndroidConfig, withIosConfig]);
|
|
95
|
+
};
|