rns-nativecall 0.6.6 → 0.6.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/CallForegroundService.kt +104 -0
- package/android/src/main/java/com/rnsnativecall/CallMessagingService.kt +25 -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 +31 -15
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
package com.rnsnativecall
|
|
2
|
+
|
|
3
|
+
import android.app.*
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.Intent
|
|
6
|
+
import android.content.pm.ServiceInfo
|
|
7
|
+
import android.os.Build
|
|
8
|
+
import android.os.Bundle
|
|
9
|
+
import android.os.IBinder
|
|
10
|
+
import androidx.core.app.NotificationCompat
|
|
11
|
+
import com.facebook.react.HeadlessJsTaskService
|
|
12
|
+
|
|
13
|
+
import android.os.Handler // Added
|
|
14
|
+
import android.os.Looper // Added
|
|
15
|
+
|
|
16
|
+
class CallForegroundService : Service() {
|
|
17
|
+
|
|
18
|
+
companion object {
|
|
19
|
+
private const val NOTIFICATION_ID = 101
|
|
20
|
+
private const val CHANNEL_ID = "incoming_call_service"
|
|
21
|
+
|
|
22
|
+
fun stop(context: Context) {
|
|
23
|
+
val intent = Intent(context, CallForegroundService::class.java)
|
|
24
|
+
context.stopService(intent)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
29
|
+
val data = intent?.extras
|
|
30
|
+
val name = data?.getString("name") ?: "Someone"
|
|
31
|
+
|
|
32
|
+
createNotificationChannel()
|
|
33
|
+
|
|
34
|
+
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
|
|
35
|
+
.setContentTitle(name)
|
|
36
|
+
.setContentText("Connecting...")
|
|
37
|
+
.setSmallIcon(applicationInfo.icon)
|
|
38
|
+
.setCategory(NotificationCompat.CATEGORY_CALL)
|
|
39
|
+
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
|
40
|
+
.setOngoing(true)
|
|
41
|
+
.build()
|
|
42
|
+
|
|
43
|
+
// --- ANDROID 14 FIX START ---
|
|
44
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
45
|
+
startForeground(
|
|
46
|
+
NOTIFICATION_ID,
|
|
47
|
+
notification,
|
|
48
|
+
ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL
|
|
49
|
+
)
|
|
50
|
+
} else {
|
|
51
|
+
startForeground(NOTIFICATION_ID, notification)
|
|
52
|
+
}
|
|
53
|
+
// --- ANDROID 14 FIX END ---
|
|
54
|
+
|
|
55
|
+
// Launch the Headless Task
|
|
56
|
+
val headlessIntent = Intent(this, CallHeadlessTask::class.java).apply {
|
|
57
|
+
// Safe bundle copy
|
|
58
|
+
val bundle = Bundle()
|
|
59
|
+
data?.let { b ->
|
|
60
|
+
for (key in b.keySet()) {
|
|
61
|
+
bundle.putString(key, b.get(key)?.toString())
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
putExtras(bundle)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
this.startService(headlessIntent)
|
|
69
|
+
HeadlessJsTaskService.acquireWakeLockNow(this)
|
|
70
|
+
} catch (e: Exception) {
|
|
71
|
+
e.printStackTrace()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
// Automatically stop this service if JS doesn't stop it within 30 seconds
|
|
76
|
+
Handler(Looper.getMainLooper()).postDelayed({
|
|
77
|
+
try {
|
|
78
|
+
stopSelf()
|
|
79
|
+
} catch (e: Exception) {
|
|
80
|
+
// Service might already be stopped
|
|
81
|
+
}
|
|
82
|
+
}, 30000)
|
|
83
|
+
return START_NOT_STICKY
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private fun createNotificationChannel() {
|
|
87
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
88
|
+
val channel = NotificationChannel(
|
|
89
|
+
CHANNEL_ID,
|
|
90
|
+
"Call Service",
|
|
91
|
+
NotificationManager.IMPORTANCE_HIGH
|
|
92
|
+
).apply {
|
|
93
|
+
description = "Handles incoming call connection state"
|
|
94
|
+
// Optional: makes the "Connecting" notification silent
|
|
95
|
+
// so it doesn't double-beep with the actual ringtone
|
|
96
|
+
setSound(null, null)
|
|
97
|
+
}
|
|
98
|
+
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
99
|
+
manager.createNotificationChannel(channel)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
override fun onBind(intent: Intent?): IBinder? = null
|
|
104
|
+
}
|
|
@@ -49,6 +49,9 @@ if (!CallState.setCurrent(uuid)) {
|
|
|
49
49
|
return
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
52
55
|
if (isAppInForeground(context)) {
|
|
53
56
|
// Foreground → send event directly
|
|
54
57
|
CallModule.sendEventToJS("onCallReceived", data)
|
|
@@ -61,16 +64,29 @@ if (!CallState.setCurrent(uuid)) {
|
|
|
61
64
|
// }
|
|
62
65
|
// ContextCompat.startForegroundService(context, serviceIntent)
|
|
63
66
|
|
|
64
|
-
val headlessIntent = Intent(context, CallHeadlessTask::class.java).apply {
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
try {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
} catch (e: Exception) {
|
|
71
|
-
|
|
72
|
-
}
|
|
67
|
+
// val headlessIntent = Intent(context, CallHeadlessTask::class.java).apply {
|
|
68
|
+
// putExtras(Bundle().apply { data.forEach { (k, v) -> putString(k, v) } })
|
|
69
|
+
// }
|
|
70
|
+
// try {
|
|
71
|
+
// context.startService(headlessIntent)
|
|
72
|
+
// HeadlessJsTaskService.acquireWakeLockNow(context)
|
|
73
|
+
// } catch (e: Exception) {
|
|
74
|
+
// e.printStackTrace()
|
|
75
|
+
// }
|
|
76
|
+
|
|
73
77
|
|
|
78
|
+
// Background → start foreground service (which in turn starts headless)
|
|
79
|
+
val serviceIntent = Intent(context, CallForegroundService::class.java).apply {
|
|
80
|
+
putExtras(Bundle().apply {
|
|
81
|
+
data.forEach { (k, v) -> putString(k, v) }
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
86
|
+
context.startForegroundService(serviceIntent)
|
|
87
|
+
} else {
|
|
88
|
+
context.startService(serviceIntent)
|
|
89
|
+
}
|
|
74
90
|
|
|
75
91
|
}
|
|
76
92
|
}
|
|
@@ -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,6 +5,11 @@ function withMainActivityDataFix(config) {
|
|
|
5
5
|
return withMainActivity(config, (config) => {
|
|
6
6
|
let contents = config.modResults.contents;
|
|
7
7
|
|
|
8
|
+
// Ensure Intent import for Kotlin
|
|
9
|
+
if (!contents.includes('import android.content.Intent')) {
|
|
10
|
+
contents = contents.replace(/package .*/, (match) => `${match}\n\nimport android.content.Intent`);
|
|
11
|
+
}
|
|
12
|
+
|
|
8
13
|
const onNewIntentCode = `
|
|
9
14
|
override fun onNewIntent(intent: Intent) {
|
|
10
15
|
super.onNewIntent(intent)
|
|
@@ -35,11 +40,14 @@ function withMainActivityDataFix(config) {
|
|
|
35
40
|
return config;
|
|
36
41
|
});
|
|
37
42
|
}
|
|
43
|
+
|
|
38
44
|
/** 2. ANDROID MANIFEST CONFIG **/
|
|
39
45
|
function withAndroidConfig(config) {
|
|
40
46
|
return withAndroidManifest(config, (config) => {
|
|
41
47
|
const manifest = config.modResults;
|
|
42
48
|
const application = manifest.manifest.application[0];
|
|
49
|
+
|
|
50
|
+
// Comprehensive list for VoIP & Foreground Services
|
|
43
51
|
const permissions = [
|
|
44
52
|
'android.permission.USE_FULL_SCREEN_INTENT',
|
|
45
53
|
'android.permission.VIBRATE',
|
|
@@ -48,15 +56,19 @@ function withAndroidConfig(config) {
|
|
|
48
56
|
'android.permission.POST_NOTIFICATIONS',
|
|
49
57
|
'android.permission.SYSTEM_ALERT_WINDOW',
|
|
50
58
|
'android.permission.WAKE_LOCK',
|
|
51
|
-
'android.permission.DISABLE_KEYGUARD'
|
|
59
|
+
'android.permission.DISABLE_KEYGUARD',
|
|
60
|
+
'android.permission.MANAGE_OWN_CALLS'
|
|
52
61
|
];
|
|
62
|
+
|
|
53
63
|
manifest.manifest['uses-permission'] = manifest.manifest['uses-permission'] || [];
|
|
54
64
|
permissions.forEach((perm) => {
|
|
55
65
|
if (!manifest.manifest['uses-permission'].some((p) => p.$['android:name'] === perm)) {
|
|
56
66
|
manifest.manifest['uses-permission'].push({ $: { 'android:name': perm } });
|
|
57
67
|
}
|
|
58
68
|
});
|
|
69
|
+
|
|
59
70
|
application.activity = application.activity || [];
|
|
71
|
+
// AcceptCallActivity registration
|
|
60
72
|
if (!application.activity.some(a => a.$['android:name'] === 'com.rnsnativecall.AcceptCallActivity')) {
|
|
61
73
|
application.activity.push({
|
|
62
74
|
$: {
|
|
@@ -71,21 +83,10 @@ function withAndroidConfig(config) {
|
|
|
71
83
|
}
|
|
72
84
|
});
|
|
73
85
|
}
|
|
74
|
-
|
|
75
|
-
application.activity.push({
|
|
76
|
-
$: {
|
|
77
|
-
'android:name': 'com.rnsnativecall.UnlockPromptActivity',
|
|
78
|
-
'android:theme': '@android:style/Theme.Translucent.NoTitleBar',
|
|
79
|
-
'android:excludeFromRecents': 'true',
|
|
80
|
-
'android:noHistory': 'true',
|
|
81
|
-
'android:exported': 'false',
|
|
82
|
-
'android:launchMode': 'singleInstance',
|
|
83
|
-
'android:showWhenLocked': 'true',
|
|
84
|
-
'android:turnScreenOn': 'true'
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
}
|
|
86
|
+
|
|
88
87
|
application.service = application.service || [];
|
|
88
|
+
|
|
89
|
+
// 1. Firebase Messaging Service
|
|
89
90
|
const firebaseServiceName = 'com.rnsnativecall.CallMessagingService';
|
|
90
91
|
if (!application.service.some(s => s.$['android:name'] === firebaseServiceName)) {
|
|
91
92
|
application.service.push({
|
|
@@ -93,12 +94,27 @@ function withAndroidConfig(config) {
|
|
|
93
94
|
'intent-filter': [{ action: [{ $: { 'android:name': 'com.google.firebase.MESSAGING_EVENT' } }] }]
|
|
94
95
|
});
|
|
95
96
|
}
|
|
97
|
+
|
|
98
|
+
// 2. Headless Task Service
|
|
96
99
|
const headlessServiceName = 'com.rnsnativecall.CallHeadlessTask';
|
|
97
100
|
if (!application.service.some(s => s.$['android:name'] === headlessServiceName)) {
|
|
98
101
|
application.service.push({
|
|
99
102
|
$: { 'android:name': headlessServiceName, 'android:exported': 'false' }
|
|
100
103
|
});
|
|
101
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
|
+
|
|
102
118
|
application.receiver = application.receiver || [];
|
|
103
119
|
const receiverName = 'com.rnsnativecall.CallActionReceiver';
|
|
104
120
|
if (!application.receiver.some(r => r.$['android:name'] === receiverName)) {
|