rns-nativecall 0.4.5 → 0.4.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.
|
@@ -15,8 +15,6 @@ import com.google.firebase.messaging.FirebaseMessagingService
|
|
|
15
15
|
import com.google.firebase.messaging.RemoteMessage
|
|
16
16
|
import com.facebook.react.HeadlessJsTaskService
|
|
17
17
|
|
|
18
|
-
import android.os.PowerManager
|
|
19
|
-
|
|
20
18
|
class CallMessagingService : FirebaseMessagingService() {
|
|
21
19
|
|
|
22
20
|
companion object {
|
|
@@ -41,36 +39,33 @@ class CallMessagingService : FirebaseMessagingService() {
|
|
|
41
39
|
}
|
|
42
40
|
|
|
43
41
|
if (isAppInForeground(context)) {
|
|
42
|
+
// When in foreground, we just tell JS to show the in-app UI
|
|
44
43
|
CallModule.sendEventToJS("onCallReceived", data)
|
|
45
44
|
} else {
|
|
46
|
-
// 1.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
data.forEach { (k, v) -> putString(k, v) }
|
|
50
|
-
})
|
|
51
|
-
}
|
|
45
|
+
// 1. Show the Notification IMMEDIATELY
|
|
46
|
+
// Do not wait for HeadlessJS. Android requires immediate feedback for background starts.
|
|
47
|
+
NativeCallManager.handleIncomingPush(context, data)
|
|
52
48
|
|
|
53
|
-
// 2. Start
|
|
54
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
55
|
-
context.startForegroundService(headlessIntent)
|
|
56
|
-
} else {
|
|
57
|
-
context.startService(headlessIntent)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// 3. WakeLock
|
|
49
|
+
// 2. Start HeadlessJS Task to sync Javascript state (WebRTC initialization, etc.)
|
|
61
50
|
try {
|
|
51
|
+
val headlessIntent = Intent(context, CallHeadlessTask::class.java).apply {
|
|
52
|
+
putExtras(Bundle().apply {
|
|
53
|
+
data.forEach { (k, v) -> putString(k, v) }
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Use a standard startService. If CallHeadlessTask is a HeadlessJsTaskService,
|
|
58
|
+
// it handles its own foreground/background lifecycle.
|
|
59
|
+
context.startService(headlessIntent)
|
|
62
60
|
HeadlessJsTaskService.acquireWakeLockNow(context)
|
|
63
|
-
} catch (e: Exception) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
val wakeLock = powerManager.newWakeLock(
|
|
67
|
-
PowerManager.FULL_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP or PowerManager.ON_AFTER_RELEASE,
|
|
68
|
-
"NativeCall:IncomingCallWakeLock"
|
|
69
|
-
)
|
|
70
|
-
wakeLock.acquire(3000) // Wake screen for 3 seconds to show notification
|
|
61
|
+
} catch (e: Exception) {
|
|
62
|
+
e.printStackTrace()
|
|
63
|
+
}
|
|
71
64
|
|
|
72
|
-
//
|
|
65
|
+
// 3. Backup Safety: Ensure if JS fails, the notification is eventually handled
|
|
73
66
|
val showNotificationRunnable = Runnable {
|
|
67
|
+
// If the notification was somehow canceled or not showing, re-trigger
|
|
68
|
+
// (Usually not needed if handleIncomingPush is called above, but kept for your logic)
|
|
74
69
|
if (!isAppInForeground(context)) {
|
|
75
70
|
NativeCallManager.handleIncomingPush(context, data)
|
|
76
71
|
}
|
|
@@ -1,115 +1,140 @@
|
|
|
1
|
-
package com.rnsnativecall
|
|
2
|
-
|
|
3
|
-
import android.app.Notification
|
|
4
1
|
import android.app.NotificationChannel
|
|
5
2
|
import android.app.NotificationManager
|
|
6
3
|
import android.app.PendingIntent
|
|
7
4
|
import android.content.Context
|
|
8
5
|
import android.content.Intent
|
|
9
|
-
import android.graphics.Color
|
|
10
|
-
import android.media.AudioAttributes
|
|
11
|
-
import android.media.RingtoneManager
|
|
12
6
|
import android.os.Build
|
|
13
7
|
import androidx.core.app.NotificationCompat
|
|
8
|
+
import android.media.Ringtone
|
|
9
|
+
import android.media.RingtoneManager
|
|
10
|
+
import android.graphics.Color
|
|
14
11
|
import androidx.core.app.Person
|
|
15
|
-
import
|
|
12
|
+
import androidx.core.app.NotificationCompat.CallStyle
|
|
16
13
|
|
|
17
14
|
object NativeCallManager {
|
|
18
15
|
|
|
16
|
+
private var ringtone: Ringtone? = null
|
|
19
17
|
const val CALL_CHANNEL_ID = "CALL_CHANNEL_ID"
|
|
20
18
|
|
|
21
19
|
fun handleIncomingPush(context: Context, data: Map<String, String>) {
|
|
22
20
|
val uuid = data["callUuid"] ?: return
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
val
|
|
27
|
-
|
|
28
|
-
//
|
|
29
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
|
34
|
-
.build()
|
|
35
|
-
|
|
36
|
-
val channel = NotificationChannel(CALL_CHANNEL_ID, "Incoming Calls", NotificationManager.IMPORTANCE_HIGH).apply {
|
|
37
|
-
setSound(ringtoneUri, audioAttributes)
|
|
38
|
-
enableVibration(true)
|
|
39
|
-
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
|
40
|
-
setBypassDnd(true) // Optional: Break through Do Not Disturb
|
|
41
|
-
}
|
|
42
|
-
notificationManager.createNotificationChannel(channel)
|
|
21
|
+
stopRingtone()
|
|
22
|
+
|
|
23
|
+
val name = data["name"] ?: "Someone"
|
|
24
|
+
val callType = data["callType"] ?: "audio"
|
|
25
|
+
|
|
26
|
+
// Use FLAG_IMMUTABLE for security unless you need to modify the intent in the system
|
|
27
|
+
val pendingFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
28
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
|
|
29
|
+
} else {
|
|
30
|
+
PendingIntent.FLAG_UPDATE_CURRENT
|
|
43
31
|
}
|
|
44
32
|
|
|
45
|
-
//
|
|
46
|
-
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
|
47
|
-
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
48
|
-
else PendingIntent.FLAG_UPDATE_CURRENT
|
|
49
|
-
|
|
50
|
-
// A. Full Screen Intent (Triggers the Heads-up logic)
|
|
51
|
-
// We point this to AcceptCallActivity, which has showWhenLocked="false"
|
|
52
|
-
// Result: System tries to launch, hits the lock restriction, and falls back to showing the Notification Pill.
|
|
33
|
+
// 1. Full Screen Intent (Opens the UI when phone is locked or heads-up is clicked)
|
|
53
34
|
val intentToActivity = Intent(context, AcceptCallActivity::class.java).apply {
|
|
54
|
-
action = "
|
|
55
|
-
data.forEach { (
|
|
35
|
+
action = "ACTION_SHOW_UI_$uuid"
|
|
36
|
+
data.forEach { (key, value) -> putExtra(key, value) }
|
|
37
|
+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
|
56
38
|
}
|
|
57
|
-
val fullScreenPendingIntent = PendingIntent.getActivity(
|
|
39
|
+
val fullScreenPendingIntent = PendingIntent.getActivity(
|
|
40
|
+
context, uuid.hashCode(), intentToActivity, pendingFlags
|
|
41
|
+
)
|
|
58
42
|
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
43
|
+
// 2. Reject Intent (Broadcast)
|
|
44
|
+
val rejectIntent = Intent(context, CallActionReceiver::class.java).apply {
|
|
45
|
+
action = "ACTION_REJECT_$uuid"
|
|
46
|
+
putExtra("EXTRA_CALL_UUID", uuid)
|
|
47
|
+
data.forEach { (key, value) -> putExtra(key, value) }
|
|
48
|
+
}
|
|
49
|
+
val rejectPendingIntent = PendingIntent.getBroadcast(
|
|
50
|
+
context, uuid.hashCode() + 1, rejectIntent, pendingFlags
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
// 3. Answer Intent (Broadcast - Process the logic before opening UI)
|
|
54
|
+
val answerIntent = Intent(context, CallActionReceiver::class.java).apply {
|
|
55
|
+
action = "ACTION_ANSWER_$uuid"
|
|
56
|
+
putExtra("EXTRA_CALL_UUID", uuid)
|
|
57
|
+
data.forEach { (key, value) -> putExtra(key, value) }
|
|
69
58
|
}
|
|
70
|
-
val answerPendingIntent = PendingIntent.
|
|
59
|
+
val answerPendingIntent = PendingIntent.getBroadcast(
|
|
60
|
+
context, uuid.hashCode() + 2, answerIntent, pendingFlags
|
|
61
|
+
)
|
|
71
62
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
63
|
+
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
64
|
+
|
|
65
|
+
// Create Channel for Android O+
|
|
66
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
67
|
+
val channel = NotificationChannel(
|
|
68
|
+
CALL_CHANNEL_ID,
|
|
69
|
+
"Incoming Call",
|
|
70
|
+
NotificationManager.IMPORTANCE_HIGH
|
|
71
|
+
).apply {
|
|
72
|
+
setBypassDnd(true)
|
|
73
|
+
lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
|
|
74
|
+
enableVibration(true)
|
|
75
|
+
setSound(null, null) // Sound is handled manually via RingtoneManager
|
|
76
|
+
}
|
|
77
|
+
notificationManager.createNotificationChannel(channel)
|
|
76
78
|
}
|
|
77
|
-
val rejectPendingIntent = PendingIntent.getBroadcast(context, uuid.hashCode() + 2, rejectIntent, flags)
|
|
78
79
|
|
|
79
|
-
//
|
|
80
|
-
val
|
|
80
|
+
// 4. Build the "Person" (Required for CallStyle)
|
|
81
|
+
val caller = Person.Builder()
|
|
81
82
|
.setName(name)
|
|
82
|
-
.
|
|
83
|
+
.setImportant(true)
|
|
83
84
|
.build()
|
|
84
85
|
|
|
86
|
+
// 5. Create the Notification with CallStyle
|
|
85
87
|
val builder = NotificationCompat.Builder(context, CALL_CHANNEL_ID)
|
|
86
|
-
.setSmallIcon(context.applicationInfo.icon)
|
|
88
|
+
.setSmallIcon(context.applicationInfo.icon) // Ensure this is a silhouette icon
|
|
89
|
+
.setContentTitle("Incoming $callType call")
|
|
90
|
+
.setContentText(name)
|
|
87
91
|
.setPriority(NotificationCompat.PRIORITY_MAX)
|
|
88
92
|
.setCategory(NotificationCompat.CATEGORY_CALL)
|
|
89
|
-
.setAutoCancel(false)
|
|
90
93
|
.setOngoing(true)
|
|
91
|
-
.
|
|
92
|
-
|
|
93
|
-
.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
.setAutoCancel(false)
|
|
95
|
+
.setFullScreenIntent(fullScreenPendingIntent, true)
|
|
96
|
+
.setColor(Color.parseColor("#28a745"))
|
|
97
|
+
.setColorized(true)
|
|
98
|
+
.setStyle(
|
|
99
|
+
NotificationCompat.CallStyle.forIncomingCall(
|
|
100
|
+
caller,
|
|
101
|
+
rejectPendingIntent,
|
|
102
|
+
answerPendingIntent
|
|
103
|
+
)
|
|
99
104
|
)
|
|
100
|
-
} else {
|
|
101
|
-
// Fallback for Android 10/11
|
|
102
|
-
builder.setContentTitle("Incoming Call")
|
|
103
|
-
.setContentText(name)
|
|
104
|
-
.addAction(android.R.drawable.ic_menu_call, "Answer", answerPendingIntent)
|
|
105
|
-
.addAction(android.R.drawable.ic_menu_close_clear_cancel, "Decline", rejectPendingIntent)
|
|
106
|
-
}
|
|
107
105
|
|
|
108
106
|
notificationManager.notify(uuid.hashCode(), builder.build())
|
|
107
|
+
|
|
108
|
+
// Start Ringtone
|
|
109
|
+
try {
|
|
110
|
+
val ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
|
|
111
|
+
ringtone = RingtoneManager.getRingtone(context, ringtoneUri)
|
|
112
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) ringtone?.isLooping = true
|
|
113
|
+
ringtone?.play()
|
|
114
|
+
} catch (e: Exception) {
|
|
115
|
+
e.printStackTrace()
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
fun stopRingtone() {
|
|
120
|
+
try {
|
|
121
|
+
ringtone?.let {
|
|
122
|
+
if (it.isPlaying) {
|
|
123
|
+
it.stop()
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
ringtone = null
|
|
127
|
+
} catch (e: Exception) {
|
|
128
|
+
e.printStackTrace()
|
|
129
|
+
ringtone = null
|
|
130
|
+
}
|
|
109
131
|
}
|
|
110
132
|
|
|
111
133
|
fun dismissIncomingCall(context: Context, uuid: String?) {
|
|
112
|
-
|
|
113
|
-
|
|
134
|
+
stopRingtone()
|
|
135
|
+
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
136
|
+
if (uuid != null) {
|
|
137
|
+
notificationManager.cancel(uuid.hashCode())
|
|
138
|
+
}
|
|
114
139
|
}
|
|
115
140
|
}
|
package/package.json
CHANGED
package/withNativeCallVoip.js
CHANGED
|
@@ -18,18 +18,29 @@ function withMainActivityDataFix(config) {
|
|
|
18
18
|
}
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
+
imports.forEach(imp => {
|
|
22
|
+
if (!contents.includes(imp)) {
|
|
23
|
+
contents = contents.replace(/package .*/, (match) => `${match}\n${imp}`);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
21
27
|
// FIXED: Corrected Kotlin bitwise OR syntax and logic check
|
|
28
|
+
// We check for "navigatingToCall" so this only runs when Answer is tapped
|
|
22
29
|
const wakeLogic = `super.onCreate(savedInstanceState)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
// }
|
|
30
|
+
if (intent?.getBooleanExtra("navigatingToCall", false) == true) {
|
|
31
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
|
32
|
+
setShowWhenLocked(true)
|
|
33
|
+
setTurnScreenOn(true)
|
|
34
|
+
} else {
|
|
35
|
+
window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON)
|
|
36
|
+
}
|
|
31
37
|
|
|
32
|
-
|
|
38
|
+
// Remove Keyguard so we jump straight to app (optional but recommended)
|
|
39
|
+
val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as? android.app.KeyguardManager
|
|
40
|
+
keyguardManager?.requestDismissKeyguard(this, null)
|
|
41
|
+
}`;
|
|
42
|
+
|
|
43
|
+
|
|
33
44
|
|
|
34
45
|
if (!contents.includes('setShowWhenLocked')) {
|
|
35
46
|
contents = contents.replace(/super\.onCreate\(.*\)/, wakeLogic);
|