rns-nativecall 0.8.1 → 0.8.3
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/CallActionReceiver.kt +49 -34
- package/android/src/main/java/com/rnsnativecall/CallForegroundService.kt +76 -45
- package/android/src/main/java/com/rnsnativecall/NativeCallManager.kt +156 -140
- package/package.json +1 -1
- package/withNativeCallVoip.js +20 -31
|
@@ -1,73 +1,88 @@
|
|
|
1
1
|
package com.rnsnativecall
|
|
2
2
|
|
|
3
|
+
import android.app.NotificationManager
|
|
3
4
|
import android.content.BroadcastReceiver
|
|
4
5
|
import android.content.Context
|
|
5
6
|
import android.content.Intent
|
|
6
|
-
import android.app.NotificationManager
|
|
7
7
|
import android.os.Bundle
|
|
8
|
+
import android.util.Log
|
|
8
9
|
|
|
9
10
|
class CallActionReceiver : BroadcastReceiver() {
|
|
10
|
-
override fun onReceive(
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
override fun onReceive(
|
|
12
|
+
context: Context,
|
|
13
|
+
intent: Intent,
|
|
14
|
+
) {
|
|
14
15
|
val uuid = intent.getStringExtra("EXTRA_CALL_UUID") ?: return
|
|
15
16
|
val action = intent.action ?: ""
|
|
16
|
-
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
17
17
|
|
|
18
|
-
// 1. Reconstruct the full data map from Intent Extras
|
|
19
18
|
val fullDataMap = mutableMapOf<String, String>()
|
|
20
19
|
intent.extras?.keySet()?.forEach { key ->
|
|
21
20
|
intent.extras?.get(key)?.let { fullDataMap[key] = it.toString() }
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
val name = fullDataMap["name"] ?: "Someone"
|
|
25
|
-
val callType = fullDataMap["callType"] ?: "audio"
|
|
26
|
-
|
|
27
23
|
// --- HANDLE REJECT ---
|
|
28
24
|
if (action == "ACTION_REJECT") {
|
|
25
|
+
NativeCallManager.dismissIncomingCall(context, uuid)
|
|
26
|
+
CallForegroundService.stop(context)
|
|
27
|
+
|
|
29
28
|
if (CallModule.isReady()) {
|
|
30
29
|
CallModule.sendEventToJS("onCallRejected", fullDataMap)
|
|
31
|
-
notificationManager.cancel(uuid.hashCode())
|
|
32
|
-
CallForegroundService.stop(context)
|
|
33
30
|
} else {
|
|
34
31
|
CallModule.setPendingCallData("onCallRejected_pending", fullDataMap)
|
|
35
|
-
NativeCallManager.aborting(context, uuid, name, callType)
|
|
36
32
|
}
|
|
33
|
+
// Just close the shade
|
|
34
|
+
collapseNotificationShade(context)
|
|
37
35
|
}
|
|
38
36
|
|
|
39
37
|
// --- HANDLE ANSWER ---
|
|
40
38
|
if (action == "ACTION_ANSWER") {
|
|
41
|
-
|
|
42
|
-
notificationManager.cancel(uuid.hashCode())
|
|
39
|
+
NativeCallManager.dismissIncomingCall(context, uuid)
|
|
43
40
|
|
|
44
41
|
if (CallModule.isReady()) {
|
|
45
|
-
// Inform JS with full payload
|
|
46
42
|
CallModule.sendEventToJS("onCallAccepted", fullDataMap)
|
|
47
|
-
CallForegroundService.stop(context)
|
|
48
|
-
|
|
49
|
-
// Open the app
|
|
50
|
-
launchApp(context, intent.extras)
|
|
51
43
|
} else {
|
|
52
|
-
// Cold start: Queue full payload and show connecting
|
|
53
44
|
CallModule.setPendingCallData("onCallAccepted_pending", fullDataMap)
|
|
54
|
-
NativeCallManager.connecting(context, uuid, name, callType)
|
|
55
|
-
|
|
56
|
-
launchApp(context, intent.extras)
|
|
57
45
|
}
|
|
46
|
+
|
|
47
|
+
// This will open your app and the system usually collapses the shade automatically
|
|
48
|
+
launchApp(context, intent.extras)
|
|
49
|
+
collapseNotificationShade(context)
|
|
58
50
|
}
|
|
59
51
|
}
|
|
60
52
|
|
|
53
|
+
private fun launchApp(
|
|
54
|
+
context: Context,
|
|
55
|
+
extras: Bundle?,
|
|
56
|
+
) {
|
|
57
|
+
CallForegroundService.stop(context)
|
|
61
58
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
59
|
+
val intent =
|
|
60
|
+
Intent(context, AcceptCallActivity::class.java).apply {
|
|
61
|
+
// Use these flags to ensure the app jumps to the front
|
|
62
|
+
addFlags(
|
|
63
|
+
Intent.FLAG_ACTIVITY_NEW_TASK or
|
|
64
|
+
Intent.FLAG_ACTIVITY_SINGLE_TOP or
|
|
65
|
+
Intent.FLAG_ACTIVITY_REORDER_TO_FRONT,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if (extras != null) putExtras(extras)
|
|
69
|
+
}
|
|
70
|
+
context.startActivity(intent)
|
|
69
71
|
}
|
|
70
|
-
context.startActivity(intent)
|
|
71
|
-
}
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
private fun collapseNotificationShade(context: Context) {
|
|
74
|
+
try {
|
|
75
|
+
// Using reflection for StatusBarManager is safe because we wrap it
|
|
76
|
+
// in a try-catch. If it fails, it just won't close the shade, but won't crash.
|
|
77
|
+
val statusBarService = context.getSystemService("statusbar")
|
|
78
|
+
val statusBarManager = Class.forName("android.app.StatusBarManager")
|
|
79
|
+
val method = statusBarManager.getMethod("collapsePanels")
|
|
80
|
+
method.isAccessible = true
|
|
81
|
+
method.invoke(statusBarService)
|
|
82
|
+
} catch (e: Exception) {
|
|
83
|
+
// Log it, but don't do anything else.
|
|
84
|
+
// DO NOT send ACTION_CLOSE_SYSTEM_DIALOGS here.
|
|
85
|
+
Log.e("CallActionReceiver", "Safe collapse failed: ${e.message}")
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -8,14 +8,13 @@ import android.content.IntentFilter
|
|
|
8
8
|
import android.content.pm.ServiceInfo
|
|
9
9
|
import android.os.Build
|
|
10
10
|
import android.os.Bundle
|
|
11
|
+
import android.os.Handler
|
|
11
12
|
import android.os.IBinder
|
|
13
|
+
import android.os.Looper
|
|
12
14
|
import androidx.core.app.NotificationCompat
|
|
13
15
|
import com.facebook.react.HeadlessJsTaskService
|
|
14
|
-
import android.os.Handler
|
|
15
|
-
import android.os.Looper
|
|
16
16
|
|
|
17
17
|
class CallForegroundService : Service() {
|
|
18
|
-
|
|
19
18
|
private var unlockReceiver: UnlockReceiver? = null // Store reference for unregistering
|
|
20
19
|
|
|
21
20
|
companion object {
|
|
@@ -30,65 +29,91 @@ class CallForegroundService : Service() {
|
|
|
30
29
|
|
|
31
30
|
override fun onCreate() {
|
|
32
31
|
super.onCreate()
|
|
33
|
-
|
|
32
|
+
|
|
34
33
|
// --- DYNAMIC REGISTRATION FIX ---
|
|
35
34
|
// Registering here makes the system allow the USER_PRESENT broadcast
|
|
36
35
|
// because the app is already in the Foreground (via this service).
|
|
37
36
|
unlockReceiver = UnlockReceiver()
|
|
38
37
|
val filter = IntentFilter(Intent.ACTION_USER_PRESENT)
|
|
39
|
-
|
|
38
|
+
|
|
40
39
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
41
|
-
//
|
|
42
|
-
|
|
40
|
+
// Prefer NOT_EXPORTED for dynamically-registered receivers to avoid
|
|
41
|
+
// background delivery issues and to restrict broadcasts to our process.
|
|
42
|
+
registerReceiver(unlockReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
|
|
43
43
|
} else {
|
|
44
44
|
registerReceiver(unlockReceiver, filter)
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
override fun onStartCommand(
|
|
48
|
+
override fun onStartCommand(
|
|
49
|
+
intent: Intent?,
|
|
50
|
+
flags: Int,
|
|
51
|
+
startId: Int,
|
|
52
|
+
): Int {
|
|
49
53
|
val data = intent?.extras
|
|
50
54
|
val name = data?.getString("name") ?: "Someone"
|
|
51
|
-
|
|
55
|
+
|
|
52
56
|
createNotificationChannel()
|
|
53
57
|
|
|
54
|
-
val notification =
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
val notification =
|
|
59
|
+
NotificationCompat
|
|
60
|
+
.Builder(this, CHANNEL_ID)
|
|
61
|
+
.setContentTitle(name)
|
|
62
|
+
.setContentText("Incoming Call...")
|
|
63
|
+
.setSmallIcon(applicationInfo.icon)
|
|
64
|
+
.setCategory(NotificationCompat.CATEGORY_CALL)
|
|
65
|
+
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
|
66
|
+
.setOngoing(true)
|
|
67
|
+
.build()
|
|
62
68
|
|
|
63
69
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
64
70
|
startForeground(
|
|
65
|
-
NOTIFICATION_ID,
|
|
66
|
-
notification,
|
|
67
|
-
ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL
|
|
71
|
+
NOTIFICATION_ID,
|
|
72
|
+
notification,
|
|
73
|
+
ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL,
|
|
68
74
|
)
|
|
69
75
|
} else {
|
|
70
76
|
startForeground(NOTIFICATION_ID, notification)
|
|
71
77
|
}
|
|
72
78
|
|
|
73
79
|
// Launch the Headless Task
|
|
74
|
-
val headlessIntent =
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
80
|
+
val headlessIntent =
|
|
81
|
+
Intent(this, CallHeadlessTask::class.java).apply {
|
|
82
|
+
val bundle = Bundle()
|
|
83
|
+
data?.let { b ->
|
|
84
|
+
for (key in b.keySet()) {
|
|
85
|
+
bundle.putString(key, b.get(key)?.toString())
|
|
86
|
+
}
|
|
79
87
|
}
|
|
88
|
+
putExtras(bundle)
|
|
80
89
|
}
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
90
|
+
|
|
84
91
|
try {
|
|
85
92
|
this.startService(headlessIntent)
|
|
86
93
|
HeadlessJsTaskService.acquireWakeLockNow(this)
|
|
87
|
-
} catch (e: Exception) {
|
|
94
|
+
} catch (e: Exception) {
|
|
95
|
+
e.printStackTrace()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// // Trigger the incoming call notification UI handled by NativeCallManager
|
|
99
|
+
// try {
|
|
100
|
+
// val map = mutableMapOf<String, String>()
|
|
101
|
+
// data?.let { b ->
|
|
102
|
+
// for (key in b.keySet()) {
|
|
103
|
+
// map[key] = b.get(key)?.toString() ?: ""
|
|
104
|
+
// }
|
|
105
|
+
// }
|
|
106
|
+
// NativeCallManager.handleIncomingPush(this, map)
|
|
107
|
+
// } catch (e: Exception) {
|
|
108
|
+
// e.printStackTrace()
|
|
109
|
+
// }
|
|
88
110
|
|
|
89
111
|
// Auto-stop after 30s
|
|
90
112
|
Handler(Looper.getMainLooper()).postDelayed({
|
|
91
|
-
try {
|
|
113
|
+
try {
|
|
114
|
+
stopSelf()
|
|
115
|
+
} catch (e: Exception) {
|
|
116
|
+
}
|
|
92
117
|
}, 30000)
|
|
93
118
|
|
|
94
119
|
return START_NOT_STICKY
|
|
@@ -99,29 +124,35 @@ class CallForegroundService : Service() {
|
|
|
99
124
|
// Unregister to prevent memory leaks once the call ends or service stops
|
|
100
125
|
try {
|
|
101
126
|
unlockReceiver?.let { unregisterReceiver(it) }
|
|
102
|
-
} catch (e: Exception) {
|
|
103
|
-
|
|
104
|
-
|
|
127
|
+
} catch (e: Exception) {
|
|
128
|
+
e.printStackTrace()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
stopForeground(true)
|
|
133
|
+
} catch (_: Exception) {
|
|
134
|
+
}
|
|
105
135
|
super.onDestroy()
|
|
106
136
|
}
|
|
107
137
|
|
|
108
138
|
private fun createNotificationChannel() {
|
|
109
139
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
110
|
-
val channel =
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
140
|
+
val channel =
|
|
141
|
+
NotificationChannel(
|
|
142
|
+
CHANNEL_ID,
|
|
143
|
+
"Call Service",
|
|
144
|
+
NotificationManager.IMPORTANCE_LOW,
|
|
145
|
+
).apply {
|
|
146
|
+
description = "Incoming call connection state"
|
|
147
|
+
setSound(null, null)
|
|
148
|
+
enableVibration(false)
|
|
149
|
+
setBypassDnd(true)
|
|
150
|
+
lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
|
|
151
|
+
}
|
|
121
152
|
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
122
153
|
manager.createNotificationChannel(channel)
|
|
123
154
|
}
|
|
124
155
|
}
|
|
125
156
|
|
|
126
157
|
override fun onBind(intent: Intent?): IBinder? = null
|
|
127
|
-
}
|
|
158
|
+
}
|
|
@@ -5,170 +5,186 @@ import android.app.NotificationManager
|
|
|
5
5
|
import android.app.PendingIntent
|
|
6
6
|
import android.content.Context
|
|
7
7
|
import android.content.Intent
|
|
8
|
+
import android.graphics.Color
|
|
9
|
+
import android.media.AudioAttributes
|
|
10
|
+
import android.media.RingtoneManager
|
|
8
11
|
import android.os.Build
|
|
12
|
+
import android.os.Handler
|
|
13
|
+
import android.os.Looper
|
|
9
14
|
import androidx.core.app.NotificationCompat
|
|
10
|
-
import
|
|
11
|
-
import android.media.RingtoneManager
|
|
12
|
-
import android.graphics.Color
|
|
13
|
-
import android.app.KeyguardManager
|
|
15
|
+
import androidx.core.app.Person
|
|
14
16
|
|
|
15
17
|
object NativeCallManager {
|
|
18
|
+
// Note: We no longer need the private 'ringtone' variable here
|
|
19
|
+
// because the system NotificationManager handles the sound.
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
const val channelId = "
|
|
19
|
-
|
|
21
|
+
// Incrementing version to V3 to force fresh channel settings on the device
|
|
22
|
+
const val channelId = "CALL_CHANNEL_V6_URGENT"
|
|
20
23
|
private var currentCallData: Map<String, String>? = null
|
|
21
24
|
|
|
22
25
|
fun getCurrentCallData(): Map<String, String>? = currentCallData
|
|
23
26
|
|
|
24
|
-
fun handleIncomingPush(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
val notificationId = uuid.hashCode()
|
|
32
|
-
|
|
33
|
-
val pendingFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
34
|
-
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
|
|
35
|
-
} else {
|
|
36
|
-
PendingIntent.FLAG_UPDATE_CURRENT
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Dummy intent for content click
|
|
40
|
-
val noOpIntent = PendingIntent.getActivity(context, notificationId + 3, Intent(), pendingFlags)
|
|
41
|
-
|
|
42
|
-
// 1. Define the flags properly (DO NOT MIX THEM)
|
|
43
|
-
val mutableFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
44
|
-
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
|
|
45
|
-
} else {
|
|
46
|
-
PendingIntent.FLAG_UPDATE_CURRENT
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
val immutableFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
50
|
-
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
51
|
-
} else {
|
|
52
|
-
PendingIntent.FLAG_UPDATE_CURRENT
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// 2. Setup the Overlay Intent
|
|
56
|
-
val overlayIntent = Intent(context, NotificationOverlayActivity::class.java).apply {
|
|
57
|
-
this.putExtra("EXTRA_CALL_UUID", uuid)
|
|
58
|
-
data.forEach { (k, v) -> this.putExtra(k, v) }
|
|
59
|
-
// No User Action flag prevents the intent from triggering an auto-dismiss of notifications
|
|
60
|
-
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_USER_ACTION)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// 3. Create the Full Screen PendingIntent (Use IMMUTABLE)
|
|
64
|
-
val fullScreenPendingIntent = PendingIntent.getActivity(
|
|
65
|
-
context,
|
|
66
|
-
notificationId,
|
|
67
|
-
overlayIntent,
|
|
68
|
-
immutableFlags // <--- Use the immutable version here
|
|
69
|
-
)
|
|
70
|
-
|
|
27
|
+
fun handleIncomingPush(
|
|
28
|
+
context: Context,
|
|
29
|
+
data: Map<String, String>,
|
|
30
|
+
) {
|
|
31
|
+
Handler(Looper.getMainLooper()).post {
|
|
32
|
+
val uuid = data["callUuid"] ?: return@post
|
|
33
|
+
this.currentCallData = data
|
|
71
34
|
|
|
35
|
+
val name = data["name"] ?: "Incoming Call"
|
|
36
|
+
val notificationId = uuid.hashCode()
|
|
37
|
+
val ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
|
|
72
38
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
39
|
+
val pendingFlags =
|
|
40
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
41
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
42
|
+
} else {
|
|
43
|
+
PendingIntent.FLAG_UPDATE_CURRENT
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 1. Content Intent (Tap notification)
|
|
47
|
+
val noOpIntent =
|
|
48
|
+
Intent("com.rnsnativecall.ACTION_NOTIFICATION_TAP_NOOP").apply {
|
|
49
|
+
`package` = context.packageName
|
|
50
|
+
putExtra("EXTRA_CALL_UUID", uuid)
|
|
51
|
+
}
|
|
52
|
+
val contentIntent = PendingIntent.getBroadcast(context, notificationId + 3, noOpIntent, pendingFlags)
|
|
53
|
+
|
|
54
|
+
// 2. Full Screen Intent (Wake lockscreen)
|
|
55
|
+
val overlayIntent =
|
|
56
|
+
Intent(context, NotificationOverlayActivity::class.java).apply {
|
|
57
|
+
putExtra("EXTRA_CALL_UUID", uuid)
|
|
58
|
+
data.forEach { (k, v) -> putExtra(k, v) }
|
|
59
|
+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_USER_ACTION)
|
|
60
|
+
}
|
|
61
|
+
val fullScreenPendingIntent = PendingIntent.getActivity(context, notificationId, overlayIntent, pendingFlags)
|
|
62
|
+
|
|
63
|
+
// 3. Answer Action
|
|
64
|
+
val answerIntent =
|
|
65
|
+
Intent(context, CallActionReceiver::class.java).apply {
|
|
66
|
+
action = "ACTION_ANSWER"
|
|
67
|
+
putExtra("EXTRA_CALL_UUID", uuid)
|
|
68
|
+
data.forEach { (k, v) -> putExtra(k, v) }
|
|
69
|
+
}
|
|
70
|
+
val answerPendingIntent = PendingIntent.getBroadcast(context, notificationId + 1, answerIntent, pendingFlags)
|
|
71
|
+
|
|
72
|
+
// 4. Reject Action
|
|
73
|
+
val rejectIntent =
|
|
74
|
+
Intent(context, CallActionReceiver::class.java).apply {
|
|
75
|
+
action = "ACTION_REJECT"
|
|
76
|
+
putExtra("EXTRA_CALL_UUID", uuid)
|
|
77
|
+
data.forEach { (k, v) -> putExtra(k, v) }
|
|
78
|
+
}
|
|
79
|
+
val rejectPendingIntent = PendingIntent.getBroadcast(context, notificationId + 2, rejectIntent, pendingFlags)
|
|
80
|
+
|
|
81
|
+
// Setup Channel with System-Managed Sound
|
|
82
|
+
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
83
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
84
|
+
val channel =
|
|
85
|
+
NotificationChannel(channelId, "Incoming Calls", NotificationManager.IMPORTANCE_HIGH).apply {
|
|
86
|
+
lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
|
|
87
|
+
enableVibration(true)
|
|
88
|
+
setBypassDnd(true)
|
|
89
|
+
// System-managed sound ensures the "pill" pops up correctly
|
|
90
|
+
setSound(
|
|
91
|
+
ringtoneUri,
|
|
92
|
+
AudioAttributes
|
|
93
|
+
.Builder()
|
|
94
|
+
.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
|
|
95
|
+
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
|
96
|
+
.build(),
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
notificationManager.createNotificationChannel(channel)
|
|
100
|
+
}
|
|
82
101
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
102
|
+
val caller =
|
|
103
|
+
Person
|
|
104
|
+
.Builder()
|
|
105
|
+
.setName(name)
|
|
106
|
+
.setImportant(true)
|
|
107
|
+
.build()
|
|
108
|
+
|
|
109
|
+
val builder =
|
|
110
|
+
NotificationCompat
|
|
111
|
+
.Builder(context, channelId)
|
|
112
|
+
.setSmallIcon(context.applicationInfo.icon)
|
|
113
|
+
.setPriority(NotificationCompat.PRIORITY_MAX)
|
|
114
|
+
.setCategory(NotificationCompat.CATEGORY_CALL)
|
|
115
|
+
.setOngoing(true)
|
|
116
|
+
.setAutoCancel(false)
|
|
117
|
+
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
118
|
+
.setContentIntent(contentIntent)
|
|
119
|
+
.setFullScreenIntent(fullScreenPendingIntent, true)
|
|
120
|
+
.setStyle(
|
|
121
|
+
NotificationCompat.CallStyle
|
|
122
|
+
.forIncomingCall(caller, rejectPendingIntent, answerPendingIntent)
|
|
123
|
+
.setAnswerButtonColorHint(Color.GREEN)
|
|
124
|
+
.setDeclineButtonColorHint(Color.RED),
|
|
125
|
+
).setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
|
|
126
|
+
|
|
127
|
+
// Sending this notification will now trigger the system ringtone automatically
|
|
128
|
+
notificationManager.notify(notificationId, builder.build())
|
|
88
129
|
}
|
|
89
|
-
|
|
90
|
-
val rejectPendingIntent = PendingIntent.getBroadcast(context, notificationId + 2, rejectIntent, mutableFlags)
|
|
130
|
+
}
|
|
91
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Stop the ringtone by canceling the notification itself.
|
|
134
|
+
* The system manages the sound, so when the notification dies, the sound dies.
|
|
135
|
+
*/
|
|
136
|
+
fun dismissIncomingCall(
|
|
137
|
+
context: Context,
|
|
138
|
+
uuid: String?,
|
|
139
|
+
) {
|
|
140
|
+
this.currentCallData = null
|
|
92
141
|
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
val channel = NotificationChannel(channelId, "Incoming Calls", NotificationManager.IMPORTANCE_HIGH).apply {
|
|
96
|
-
enableVibration(true)
|
|
97
|
-
vibrationPattern = longArrayOf(0, 500, 500, 500)
|
|
98
|
-
lightColor = Color.GREEN
|
|
99
|
-
setBypassDnd(true)
|
|
100
|
-
lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
|
|
101
|
-
setSound(null, null)
|
|
102
|
-
}
|
|
103
|
-
notificationManager.createNotificationChannel(channel)
|
|
142
|
+
if (uuid != null) {
|
|
143
|
+
notificationManager.cancel(uuid.hashCode())
|
|
104
144
|
}
|
|
105
|
-
|
|
106
|
-
val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
|
107
|
-
val isLocked = keyguardManager.isKeyguardLocked
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
val builder = NotificationCompat.Builder(context, channelId)
|
|
111
|
-
.setSmallIcon(context.applicationInfo.icon)
|
|
112
|
-
.setContentTitle("Incoming $callType call")
|
|
113
|
-
.setContentText(name)
|
|
114
|
-
.setPriority(NotificationCompat.PRIORITY_MAX)
|
|
115
|
-
.setCategory(NotificationCompat.CATEGORY_CALL)
|
|
116
|
-
.setOngoing(true)
|
|
117
|
-
.setAutoCancel(false)
|
|
118
|
-
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
119
|
-
.setContentIntent(noOpIntent)
|
|
120
|
-
.addAction(0, "Answer", answerPendingIntent)
|
|
121
|
-
.addAction(0, "Decline", rejectPendingIntent)
|
|
122
|
-
.setDefaults(NotificationCompat.DEFAULT_ALL)
|
|
123
|
-
|
|
124
|
-
builder.setFullScreenIntent(fullScreenPendingIntent, true)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
notificationManager.notify(notificationId, builder.build())
|
|
128
|
-
|
|
129
|
-
try {
|
|
130
|
-
val ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
|
|
131
|
-
ringtone = RingtoneManager.getRingtone(context, ringtoneUri)
|
|
132
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) ringtone?.isLooping = true
|
|
133
|
-
ringtone?.play()
|
|
134
|
-
} catch (e: Exception) { e.printStackTrace() }
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
fun stopRingtone() {
|
|
138
|
-
try {
|
|
139
|
-
ringtone?.let { if (it.isPlaying) it.stop() }
|
|
140
|
-
ringtone = null
|
|
141
|
-
} catch (e: Exception) { ringtone = null }
|
|
142
145
|
}
|
|
143
146
|
|
|
144
|
-
fun connecting(
|
|
147
|
+
fun connecting(
|
|
148
|
+
context: Context,
|
|
149
|
+
uuid: String,
|
|
150
|
+
name: String,
|
|
151
|
+
callType: String,
|
|
152
|
+
) {
|
|
145
153
|
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
146
|
-
val builder =
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
154
|
+
val builder =
|
|
155
|
+
NotificationCompat
|
|
156
|
+
.Builder(context, channelId)
|
|
157
|
+
.setSmallIcon(context.applicationInfo.icon)
|
|
158
|
+
.setContentTitle(name)
|
|
159
|
+
.setContentText("Connecting…")
|
|
160
|
+
.setPriority(NotificationCompat.PRIORITY_MAX)
|
|
161
|
+
.setCategory(NotificationCompat.CATEGORY_CALL)
|
|
162
|
+
.setOngoing(true)
|
|
163
|
+
.setStyle(NotificationCompat.BigTextStyle().bigText("Connecting…"))
|
|
153
164
|
notificationManager.notify(uuid.hashCode(), builder.build())
|
|
154
165
|
}
|
|
155
166
|
|
|
156
|
-
fun aborting(
|
|
167
|
+
fun aborting(
|
|
168
|
+
context: Context,
|
|
169
|
+
uuid: String,
|
|
170
|
+
name: String,
|
|
171
|
+
callType: String,
|
|
172
|
+
) {
|
|
157
173
|
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
158
|
-
val builder =
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
174
|
+
val builder =
|
|
175
|
+
NotificationCompat
|
|
176
|
+
.Builder(context, channelId)
|
|
177
|
+
.setSmallIcon(context.applicationInfo.icon)
|
|
178
|
+
.setContentTitle(name)
|
|
179
|
+
.setContentText("Aborting…")
|
|
180
|
+
.setPriority(NotificationCompat.PRIORITY_MAX)
|
|
181
|
+
.setCategory(NotificationCompat.CATEGORY_CALL)
|
|
182
|
+
.setOngoing(true)
|
|
165
183
|
notificationManager.notify(uuid.hashCode(), builder.build())
|
|
166
184
|
}
|
|
167
185
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
172
|
-
if (uuid != null) notificationManager.cancel(uuid.hashCode())
|
|
186
|
+
// Deprecated manual method - kept for signature compatibility but does nothing
|
|
187
|
+
fun stopRingtone() {
|
|
188
|
+
// No-op: Sound is now managed by the Notification Channel life-cycle
|
|
173
189
|
}
|
|
174
|
-
}
|
|
190
|
+
}
|
package/package.json
CHANGED
package/withNativeCallVoip.js
CHANGED
|
@@ -9,7 +9,9 @@ function withMainActivityDataFix(config) {
|
|
|
9
9
|
'import android.content.Intent',
|
|
10
10
|
'import android.os.Bundle',
|
|
11
11
|
'import android.view.WindowManager',
|
|
12
|
-
'import android.os.Build'
|
|
12
|
+
'import android.os.Build',
|
|
13
|
+
'import androidx.core.app.ActivityCompat', // ADDED
|
|
14
|
+
'import android.Manifest' // ADDED
|
|
13
15
|
];
|
|
14
16
|
|
|
15
17
|
// Add imports if they don't exist
|
|
@@ -22,8 +24,18 @@ function withMainActivityDataFix(config) {
|
|
|
22
24
|
const onCreateCode = `
|
|
23
25
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
24
26
|
super.onCreate(savedInstanceState)
|
|
27
|
+
|
|
28
|
+
// Request Notification Permissions for Android 13+ (Required for Pill UI)
|
|
29
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
30
|
+
ActivityCompat.requestPermissions(
|
|
31
|
+
this,
|
|
32
|
+
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
|
33
|
+
101
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
25
37
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
|
26
|
-
setShowWhenLocked(
|
|
38
|
+
setShowWhenLocked(true) // Set to TRUE so call UI can show over lockscreen
|
|
27
39
|
setTurnScreenOn(true)
|
|
28
40
|
} else {
|
|
29
41
|
window.addFlags(
|
|
@@ -32,6 +44,7 @@ function withMainActivityDataFix(config) {
|
|
|
32
44
|
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
|
|
33
45
|
)
|
|
34
46
|
}
|
|
47
|
+
|
|
35
48
|
if (intent.getBooleanExtra("background_wake", false)) {
|
|
36
49
|
moveTaskToBack(true)
|
|
37
50
|
}
|
|
@@ -43,7 +56,6 @@ function withMainActivityDataFix(config) {
|
|
|
43
56
|
setIntent(intent)
|
|
44
57
|
}`;
|
|
45
58
|
|
|
46
|
-
// Use a more flexible regex for the class definition
|
|
47
59
|
const classRegex = /class MainActivity\s*:\s*ReactActivity\(\)\s*\{/;
|
|
48
60
|
|
|
49
61
|
if (!contents.includes('override fun onCreate')) {
|
|
@@ -51,7 +63,6 @@ function withMainActivityDataFix(config) {
|
|
|
51
63
|
}
|
|
52
64
|
|
|
53
65
|
if (!contents.includes('override fun onNewIntent')) {
|
|
54
|
-
// Re-run match check because contents might have changed from onCreate injection
|
|
55
66
|
contents = contents.replace(classRegex, (match) => `${match}${onNewIntentCode}`);
|
|
56
67
|
}
|
|
57
68
|
|
|
@@ -70,7 +81,7 @@ function withAndroidConfig(config) {
|
|
|
70
81
|
const mainActivity = application.activity.find((a) => a.$['android:name'] === '.MainActivity');
|
|
71
82
|
if (mainActivity) {
|
|
72
83
|
mainActivity.$['android:launchMode'] = 'singleTop';
|
|
73
|
-
mainActivity.$['android:showWhenLocked'] = '
|
|
84
|
+
mainActivity.$['android:showWhenLocked'] = 'true'; // Changed to true
|
|
74
85
|
mainActivity.$['android:turnScreenOn'] = 'true';
|
|
75
86
|
}
|
|
76
87
|
|
|
@@ -82,19 +93,17 @@ function withAndroidConfig(config) {
|
|
|
82
93
|
'android.permission.POST_NOTIFICATIONS',
|
|
83
94
|
'android.permission.WAKE_LOCK',
|
|
84
95
|
'android.permission.DISABLE_KEYGUARD',
|
|
85
|
-
'android.permission.
|
|
96
|
+
'android.permission.MANAGE_ONGOING_CALLS'
|
|
86
97
|
];
|
|
87
98
|
|
|
88
|
-
// Initialize permissions array if missing
|
|
89
99
|
manifest.manifest['uses-permission'] = manifest.manifest['uses-permission'] || [];
|
|
90
|
-
|
|
91
100
|
permissions.forEach((perm) => {
|
|
92
101
|
if (!manifest.manifest['uses-permission'].some((p) => p.$['android:name'] === perm)) {
|
|
93
102
|
manifest.manifest['uses-permission'].push({ $: { 'android:name': perm } });
|
|
94
103
|
}
|
|
95
104
|
});
|
|
96
105
|
|
|
97
|
-
//
|
|
106
|
+
// Components Setup
|
|
98
107
|
application.service = application.service || [];
|
|
99
108
|
application.activity = application.activity || [];
|
|
100
109
|
application.receiver = application.receiver || [];
|
|
@@ -117,7 +126,7 @@ function withAndroidConfig(config) {
|
|
|
117
126
|
}
|
|
118
127
|
});
|
|
119
128
|
|
|
120
|
-
// 2. Activities (
|
|
129
|
+
// 2. Activities (NotificationOverlayActivity is critical for Foldables)
|
|
121
130
|
const activities = [
|
|
122
131
|
{
|
|
123
132
|
name: 'com.rnsnativecall.AcceptCallActivity',
|
|
@@ -158,26 +167,6 @@ function withAndroidConfig(config) {
|
|
|
158
167
|
});
|
|
159
168
|
}
|
|
160
169
|
|
|
161
|
-
// ADD THIS: UnlockReceiver with USER_PRESENT filter
|
|
162
|
-
if (!application.receiver.some(r => r.$['android:name'] === 'com.rnsnativecall.UnlockReceiver')) {
|
|
163
|
-
application.receiver.push({
|
|
164
|
-
$: {
|
|
165
|
-
'android:name': 'com.rnsnativecall.UnlockReceiver',
|
|
166
|
-
'android:exported': 'true',
|
|
167
|
-
'android:enabled': 'true',
|
|
168
|
-
'android:directBootAware': 'true', // Helps bypass some background restrictions
|
|
169
|
-
},
|
|
170
|
-
'intent-filter': [
|
|
171
|
-
{
|
|
172
|
-
$: { 'android:priority': '1000' },
|
|
173
|
-
action: [
|
|
174
|
-
{ $: { 'android:name': 'android.intent.action.USER_PRESENT' } }
|
|
175
|
-
]
|
|
176
|
-
}
|
|
177
|
-
]
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
|
|
181
170
|
return config;
|
|
182
171
|
});
|
|
183
172
|
}
|
|
@@ -203,4 +192,4 @@ module.exports = (config) => {
|
|
|
203
192
|
withMainActivityDataFix,
|
|
204
193
|
withIosConfig
|
|
205
194
|
]);
|
|
206
|
-
};
|
|
195
|
+
};
|