rns-nativecall 0.7.9 → 0.8.0
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 +13 -62
- package/android/src/main/java/com/rnsnativecall/CallActionReceiver.kt +46 -25
- package/android/src/main/java/com/rnsnativecall/CallForegroundService.kt +54 -31
- package/android/src/main/java/com/rnsnativecall/CallMessagingService.kt +0 -1
- package/android/src/main/java/com/rnsnativecall/CallModule.kt +26 -5
- package/android/src/main/java/com/rnsnativecall/NativeCallManager.kt +89 -98
- package/android/src/main/java/com/rnsnativecall/NotificationOverlayActivity.kt +26 -0
- package/android/src/main/java/com/rnsnativecall/UnlockReceiver.kt +22 -0
- package/android/src/main/res/drawable/circle_background.xml +4 -0
- package/android/src/main/res/drawable/ic_call_answer_white.xml +9 -0
- package/android/src/main/res/drawable/ic_call_end_white.xml +9 -0
- package/android/src/main/res/layout/activity_incoming_call.xml +139 -0
- package/index.d.ts +4 -0
- package/index.js +10 -0
- package/package.json +1 -1
- package/withNativeCallVoip.js +130 -68
- package/android/src/main/java/com/rnsnativecall/UnlockPromptActivity.kt +0 -55
|
@@ -1,77 +1,28 @@
|
|
|
1
1
|
package com.rnsnativecall
|
|
2
2
|
|
|
3
3
|
import android.app.Activity
|
|
4
|
-
import android.app.NotificationManager
|
|
5
|
-
import android.content.Context
|
|
6
4
|
import android.content.Intent
|
|
7
5
|
import android.os.Bundle
|
|
8
6
|
import android.view.WindowManager
|
|
9
|
-
import android.
|
|
7
|
+
import android.os.Build
|
|
10
8
|
|
|
11
9
|
class AcceptCallActivity : Activity() {
|
|
12
|
-
|
|
13
10
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
14
11
|
super.onCreate(savedInstanceState)
|
|
15
12
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
// Check: Did the user actually press "Answer" or did the system auto-launch?
|
|
26
|
-
// If the action is null or doesn't match your Answer action,
|
|
27
|
-
// it means the system is just "preparing" the activity.
|
|
28
|
-
if (intent.action?.startsWith("ACTION_ANSWER") != true) {
|
|
29
|
-
// If it's just an auto-launch, we don't fire the JS event!
|
|
30
|
-
// We can either finish() or show a tiny "Swipe to Answer" UI.
|
|
31
|
-
return
|
|
32
|
-
}
|
|
33
|
-
processCallIntent(intent)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
private fun processCallIntent(intent: Intent) {
|
|
37
|
-
NativeCallManager.stopRingtone()
|
|
38
|
-
|
|
39
|
-
val extras = intent.extras
|
|
40
|
-
val uuid = extras?.getString("callUuid")
|
|
41
|
-
|
|
42
|
-
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
43
|
-
uuid?.let { notificationManager.cancel(it.hashCode()) }
|
|
44
|
-
|
|
45
|
-
// ✅ WE STOP SENDING THE JS EVENT HERE.
|
|
46
|
-
// Instead, we pass the intent to MainActivity with a specific ACTION.
|
|
47
|
-
openMainApp(extras)
|
|
48
|
-
finish()
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
private fun openMainApp(extras: Bundle?) {
|
|
52
|
-
try {
|
|
53
|
-
// Get the actual MainActivity class name (e.g., com.yourapp.MainActivity)
|
|
54
|
-
val mainActivityClassName = "${packageName}.MainActivity"
|
|
55
|
-
|
|
56
|
-
val intent = Intent().apply {
|
|
57
|
-
setClassName(packageName, mainActivityClassName)
|
|
58
|
-
action = "com.rnsnativecall.ACTION_ANSWER"
|
|
59
|
-
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
|
60
|
-
|
|
61
|
-
// Ensure extras are carried over
|
|
62
|
-
extras?.let { putExtras(it) }
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
startActivity(intent)
|
|
66
|
-
} catch (e: Exception) {
|
|
67
|
-
// Fallback: If explicit mapping fails, try the launch intent but force the action
|
|
68
|
-
val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
|
|
69
|
-
launchIntent?.apply {
|
|
70
|
-
action = "com.rnsnativecall.ACTION_ANSWER"
|
|
71
|
-
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
|
72
|
-
extras?.let { putExtras(it) }
|
|
73
|
-
startActivity(this)
|
|
13
|
+
val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
|
|
14
|
+
if (launchIntent != null) {
|
|
15
|
+
launchIntent.apply {
|
|
16
|
+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or
|
|
17
|
+
Intent.FLAG_ACTIVITY_SINGLE_TOP or
|
|
18
|
+
Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
|
|
19
|
+
putExtras(intent.extras ?: Bundle())
|
|
20
|
+
putExtra("navigatingToCall", true)
|
|
74
21
|
}
|
|
22
|
+
startActivity(launchIntent)
|
|
75
23
|
}
|
|
24
|
+
|
|
25
|
+
finish()
|
|
26
|
+
overridePendingTransition(0, 0)
|
|
76
27
|
}
|
|
77
28
|
}
|
|
@@ -4,49 +4,70 @@ import android.content.BroadcastReceiver
|
|
|
4
4
|
import android.content.Context
|
|
5
5
|
import android.content.Intent
|
|
6
6
|
import android.app.NotificationManager
|
|
7
|
-
import android.
|
|
7
|
+
import android.os.Bundle
|
|
8
8
|
|
|
9
9
|
class CallActionReceiver : BroadcastReceiver() {
|
|
10
10
|
override fun onReceive(context: Context, intent: Intent) {
|
|
11
|
+
// Stop sound immediately
|
|
11
12
|
NativeCallManager.stopRingtone()
|
|
12
13
|
|
|
13
14
|
val uuid = intent.getStringExtra("EXTRA_CALL_UUID") ?: return
|
|
14
15
|
val action = intent.action ?: ""
|
|
16
|
+
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
val dataMap = mutableMapOf<String, String>()
|
|
18
|
+
// 1. Reconstruct the full data map from Intent Extras
|
|
19
|
+
val fullDataMap = mutableMapOf<String, String>()
|
|
20
20
|
intent.extras?.keySet()?.forEach { key ->
|
|
21
|
-
intent.extras?.get(key)?.let {
|
|
21
|
+
intent.extras?.get(key)?.let { fullDataMap[key] = it.toString() }
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
val name =
|
|
25
|
-
val callType =
|
|
26
|
-
|
|
27
|
-
val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
|
24
|
+
val name = fullDataMap["name"] ?: "Someone"
|
|
25
|
+
val callType = fullDataMap["callType"] ?: "audio"
|
|
28
26
|
|
|
29
|
-
|
|
27
|
+
// --- HANDLE REJECT ---
|
|
28
|
+
if (action == "ACTION_REJECT") {
|
|
30
29
|
if (CallModule.isReady()) {
|
|
31
|
-
CallModule.sendEventToJS("onCallRejected",
|
|
30
|
+
CallModule.sendEventToJS("onCallRejected", fullDataMap)
|
|
32
31
|
notificationManager.cancel(uuid.hashCode())
|
|
32
|
+
CallForegroundService.stop(context)
|
|
33
33
|
} else {
|
|
34
|
-
|
|
35
|
-
CallModule.setPendingCallData("onCallRejected_pending", mapOf("callUuid" to uuid))
|
|
36
|
-
// Update notification pill to "Aborting…" state
|
|
34
|
+
CallModule.setPendingCallData("onCallRejected_pending", fullDataMap)
|
|
37
35
|
NativeCallManager.aborting(context, uuid, name, callType)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// --- HANDLE ANSWER ---
|
|
40
|
+
if (action == "ACTION_ANSWER") {
|
|
41
|
+
// Dismiss the "Incoming" pill immediately
|
|
42
|
+
notificationManager.cancel(uuid.hashCode())
|
|
38
43
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
if (CallModule.isReady()) {
|
|
45
|
+
// Inform JS with full payload
|
|
46
|
+
CallModule.sendEventToJS("onCallAccepted", fullDataMap)
|
|
47
|
+
CallForegroundService.stop(context)
|
|
48
|
+
|
|
49
|
+
// Open the app
|
|
50
|
+
launchApp(context, intent.extras)
|
|
51
|
+
} else {
|
|
52
|
+
// Cold start: Queue full payload and show connecting
|
|
53
|
+
CallModule.setPendingCallData("onCallAccepted_pending", fullDataMap)
|
|
54
|
+
NativeCallManager.connecting(context, uuid, name, callType)
|
|
55
|
+
|
|
56
|
+
launchApp(context, intent.extras)
|
|
49
57
|
}
|
|
50
58
|
}
|
|
51
59
|
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
private fun launchApp(context: Context, extras: Bundle?) {
|
|
63
|
+
val intent = Intent(context, AcceptCallActivity::class.java).apply {
|
|
64
|
+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or
|
|
65
|
+
Intent.FLAG_ACTIVITY_SINGLE_TOP or
|
|
66
|
+
Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
|
67
|
+
|
|
68
|
+
if (extras != null) putExtras(extras)
|
|
69
|
+
}
|
|
70
|
+
context.startActivity(intent)
|
|
52
71
|
}
|
|
72
|
+
|
|
73
|
+
}
|
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
package com.rnsnativecall
|
|
2
2
|
|
|
3
3
|
import android.app.*
|
|
4
|
+
import android.content.BroadcastReceiver
|
|
4
5
|
import android.content.Context
|
|
5
6
|
import android.content.Intent
|
|
7
|
+
import android.content.IntentFilter
|
|
6
8
|
import android.content.pm.ServiceInfo
|
|
7
9
|
import android.os.Build
|
|
8
10
|
import android.os.Bundle
|
|
9
11
|
import android.os.IBinder
|
|
10
12
|
import androidx.core.app.NotificationCompat
|
|
11
13
|
import com.facebook.react.HeadlessJsTaskService
|
|
12
|
-
|
|
13
|
-
import android.os.
|
|
14
|
-
import android.os.Looper // Added
|
|
14
|
+
import android.os.Handler
|
|
15
|
+
import android.os.Looper
|
|
15
16
|
|
|
16
17
|
class CallForegroundService : Service() {
|
|
17
18
|
|
|
19
|
+
private var unlockReceiver: UnlockReceiver? = null // Store reference for unregistering
|
|
20
|
+
|
|
18
21
|
companion object {
|
|
19
22
|
private const val NOTIFICATION_ID = 101
|
|
20
23
|
private const val CHANNEL_ID = "incoming_call_service"
|
|
@@ -25,6 +28,23 @@ class CallForegroundService : Service() {
|
|
|
25
28
|
}
|
|
26
29
|
}
|
|
27
30
|
|
|
31
|
+
override fun onCreate() {
|
|
32
|
+
super.onCreate()
|
|
33
|
+
|
|
34
|
+
// --- DYNAMIC REGISTRATION FIX ---
|
|
35
|
+
// Registering here makes the system allow the USER_PRESENT broadcast
|
|
36
|
+
// because the app is already in the Foreground (via this service).
|
|
37
|
+
unlockReceiver = UnlockReceiver()
|
|
38
|
+
val filter = IntentFilter(Intent.ACTION_USER_PRESENT)
|
|
39
|
+
|
|
40
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
41
|
+
// Android 13+ requires the RECEIVER_EXPORTED flag for system broadcasts
|
|
42
|
+
registerReceiver(unlockReceiver, filter, Context.RECEIVER_EXPORTED)
|
|
43
|
+
} else {
|
|
44
|
+
registerReceiver(unlockReceiver, filter)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
28
48
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
29
49
|
val data = intent?.extras
|
|
30
50
|
val name = data?.getString("name") ?: "Someone"
|
|
@@ -33,14 +53,13 @@ class CallForegroundService : Service() {
|
|
|
33
53
|
|
|
34
54
|
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
|
|
35
55
|
.setContentTitle(name)
|
|
36
|
-
.setContentText("
|
|
56
|
+
.setContentText("Incoming Call...")
|
|
37
57
|
.setSmallIcon(applicationInfo.icon)
|
|
38
58
|
.setCategory(NotificationCompat.CATEGORY_CALL)
|
|
39
59
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
|
40
60
|
.setOngoing(true)
|
|
41
61
|
.build()
|
|
42
62
|
|
|
43
|
-
// --- ANDROID 14 FIX START ---
|
|
44
63
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
45
64
|
startForeground(
|
|
46
65
|
NOTIFICATION_ID,
|
|
@@ -50,11 +69,9 @@ class CallForegroundService : Service() {
|
|
|
50
69
|
} else {
|
|
51
70
|
startForeground(NOTIFICATION_ID, notification)
|
|
52
71
|
}
|
|
53
|
-
// --- ANDROID 14 FIX END ---
|
|
54
72
|
|
|
55
73
|
// Launch the Headless Task
|
|
56
74
|
val headlessIntent = Intent(this, CallHeadlessTask::class.java).apply {
|
|
57
|
-
// Safe bundle copy
|
|
58
75
|
val bundle = Bundle()
|
|
59
76
|
data?.let { b ->
|
|
60
77
|
for (key in b.keySet()) {
|
|
@@ -67,38 +84,44 @@ class CallForegroundService : Service() {
|
|
|
67
84
|
try {
|
|
68
85
|
this.startService(headlessIntent)
|
|
69
86
|
HeadlessJsTaskService.acquireWakeLockNow(this)
|
|
70
|
-
} catch (e: Exception) {
|
|
71
|
-
e.printStackTrace()
|
|
72
|
-
}
|
|
87
|
+
} catch (e: Exception) { e.printStackTrace() }
|
|
73
88
|
|
|
89
|
+
// Auto-stop after 30s
|
|
90
|
+
Handler(Looper.getMainLooper()).postDelayed({
|
|
91
|
+
try { stopSelf() } catch (e: Exception) {}
|
|
92
|
+
}, 30000)
|
|
74
93
|
|
|
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
94
|
return START_NOT_STICKY
|
|
84
95
|
}
|
|
85
96
|
|
|
97
|
+
override fun onDestroy() {
|
|
98
|
+
// --- CLEANUP ---
|
|
99
|
+
// Unregister to prevent memory leaks once the call ends or service stops
|
|
100
|
+
try {
|
|
101
|
+
unlockReceiver?.let { unregisterReceiver(it) }
|
|
102
|
+
} catch (e: Exception) { e.printStackTrace() }
|
|
103
|
+
|
|
104
|
+
try { stopForeground(true) } catch (_: Exception) {}
|
|
105
|
+
super.onDestroy()
|
|
106
|
+
}
|
|
107
|
+
|
|
86
108
|
private fun createNotificationChannel() {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
109
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
110
|
+
val channel = NotificationChannel(
|
|
111
|
+
CHANNEL_ID,
|
|
112
|
+
"Call Service",
|
|
113
|
+
NotificationManager.IMPORTANCE_LOW
|
|
114
|
+
).apply {
|
|
115
|
+
description = "Handles incoming call connection state"
|
|
116
|
+
setSound(null, null)
|
|
117
|
+
enableVibration(false)
|
|
118
|
+
setBypassDnd(true)
|
|
119
|
+
lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
|
|
120
|
+
}
|
|
121
|
+
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
122
|
+
manager.createNotificationChannel(channel)
|
|
97
123
|
}
|
|
98
|
-
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
99
|
-
manager.createNotificationChannel(channel)
|
|
100
124
|
}
|
|
101
|
-
}
|
|
102
125
|
|
|
103
126
|
override fun onBind(intent: Intent?): IBinder? = null
|
|
104
127
|
}
|
|
@@ -34,7 +34,6 @@ class CallMessagingService : FirebaseMessagingService() {
|
|
|
34
34
|
CallState.clear(uuid, context)
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
// Dismiss the "Connecting..." or "Incoming Call" UI
|
|
38
37
|
NativeCallManager.dismissIncomingCall(context, uuid)
|
|
39
38
|
showMissedCallNotification(context, data, uuid)
|
|
40
39
|
return
|
|
@@ -5,6 +5,9 @@ import android.content.Context
|
|
|
5
5
|
import com.facebook.react.bridge.*
|
|
6
6
|
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
7
7
|
import android.content.Intent
|
|
8
|
+
import android.os.Build
|
|
9
|
+
import android.provider.Settings
|
|
10
|
+
import android.net.Uri
|
|
8
11
|
|
|
9
12
|
class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
|
|
10
13
|
|
|
@@ -51,6 +54,29 @@ class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
|
|
|
51
54
|
}
|
|
52
55
|
}
|
|
53
56
|
|
|
57
|
+
// Inside your CallModule class
|
|
58
|
+
@ReactMethod
|
|
59
|
+
fun checkOverlayPermission(promise: Promise) {
|
|
60
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
61
|
+
promise.resolve(Settings.canDrawOverlays(reactApplicationContext))
|
|
62
|
+
} else {
|
|
63
|
+
promise.resolve(true)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@ReactMethod
|
|
68
|
+
fun requestOverlayPermission() {
|
|
69
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
70
|
+
if (!Settings.canDrawOverlays(reactApplicationContext)) {
|
|
71
|
+
val intent = Intent(
|
|
72
|
+
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
|
|
73
|
+
Uri.parse("package:${reactApplicationContext.packageName}")
|
|
74
|
+
)
|
|
75
|
+
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
76
|
+
reactApplicationContext.startActivity(intent)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
54
80
|
|
|
55
81
|
/**
|
|
56
82
|
* Combined Validity Check:
|
|
@@ -108,11 +134,6 @@ class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
|
|
|
108
134
|
notificationManager.cancel(101)
|
|
109
135
|
}
|
|
110
136
|
|
|
111
|
-
@ReactMethod
|
|
112
|
-
fun checkTelecomPermissions(promise: Promise) {
|
|
113
|
-
promise.resolve(true)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
137
|
@ReactMethod fun addListener(eventName: String) {}
|
|
117
138
|
@ReactMethod fun removeListeners(count: Int) {}
|
|
118
139
|
|
|
@@ -10,85 +10,89 @@ import androidx.core.app.NotificationCompat
|
|
|
10
10
|
import android.media.Ringtone
|
|
11
11
|
import android.media.RingtoneManager
|
|
12
12
|
import android.graphics.Color
|
|
13
|
-
|
|
14
13
|
import android.app.KeyguardManager
|
|
15
14
|
|
|
16
|
-
import com.rnsnativecall.CallMessagingService
|
|
17
|
-
|
|
18
15
|
object NativeCallManager {
|
|
19
16
|
|
|
20
17
|
private var ringtone: Ringtone? = null
|
|
21
18
|
const val channelId = "CALL_CHANNEL_ID"
|
|
22
19
|
|
|
20
|
+
private var currentCallData: Map<String, String>? = null
|
|
21
|
+
|
|
22
|
+
fun getCurrentCallData(): Map<String, String>? = currentCallData
|
|
23
|
+
|
|
23
24
|
fun handleIncomingPush(context: Context, data: Map<String, String>) {
|
|
24
25
|
val uuid = data["callUuid"] ?: return
|
|
26
|
+
this.currentCallData = data
|
|
25
27
|
stopRingtone()
|
|
26
28
|
|
|
27
29
|
val name = data["name"] ?: "Incoming Call"
|
|
28
30
|
val callType = data["callType"] ?: "audio"
|
|
29
31
|
val notificationId = uuid.hashCode()
|
|
30
32
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// --- LOCK SCREEN GATEKEEPER ---
|
|
35
|
-
val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
|
36
|
-
if (keyguardManager.isKeyguardLocked) {
|
|
37
|
-
// Device is locked? We bounce it!
|
|
38
|
-
CallState.markCanceled(uuid, context)
|
|
39
|
-
showMissedCallNotification(context, data, uuid)
|
|
40
|
-
return // 🛑 Flow stops here
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
33
|
val pendingFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
46
34
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
|
|
47
35
|
} else {
|
|
48
36
|
PendingIntent.FLAG_UPDATE_CURRENT
|
|
49
37
|
}
|
|
50
38
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
// --- ANSWER ACTION ---
|
|
74
|
+
val answerIntent = Intent(context, CallActionReceiver::class.java).apply {
|
|
75
|
+
this.action = "ACTION_ANSWER"
|
|
76
|
+
this.putExtra("EXTRA_CALL_UUID", uuid)
|
|
77
|
+
|
|
78
|
+
data.forEach { (key, value) -> this.putExtra(key, value) } // Pass full data
|
|
62
79
|
}
|
|
63
80
|
|
|
64
|
-
val
|
|
65
|
-
context,
|
|
66
|
-
notificationId,
|
|
67
|
-
intentToActivity,
|
|
68
|
-
pendingFlags
|
|
69
|
-
)
|
|
81
|
+
val answerPendingIntent = PendingIntent.getBroadcast(context, notificationId + 1, answerIntent, mutableFlags)
|
|
70
82
|
|
|
83
|
+
// --- REJECT ACTION ---
|
|
71
84
|
val rejectIntent = Intent(context, CallActionReceiver::class.java).apply {
|
|
72
|
-
this.action = "
|
|
85
|
+
this.action = "ACTION_REJECT"
|
|
73
86
|
this.putExtra("EXTRA_CALL_UUID", uuid)
|
|
74
|
-
data.forEach { (key, value) -> this.putExtra(key, value) }
|
|
87
|
+
data.forEach { (key, value) -> this.putExtra(key, value) } // Pass full data
|
|
75
88
|
}
|
|
76
|
-
|
|
77
|
-
val rejectPendingIntent = PendingIntent.getBroadcast(
|
|
78
|
-
context,
|
|
79
|
-
notificationId,
|
|
80
|
-
rejectIntent,
|
|
81
|
-
pendingFlags
|
|
82
|
-
)
|
|
89
|
+
|
|
90
|
+
val rejectPendingIntent = PendingIntent.getBroadcast(context, notificationId + 2, rejectIntent, mutableFlags)
|
|
83
91
|
|
|
84
92
|
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
85
93
|
|
|
86
94
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
87
|
-
val channel = NotificationChannel(
|
|
88
|
-
channelId,
|
|
89
|
-
"Incoming Calls",
|
|
90
|
-
NotificationManager.IMPORTANCE_HIGH // NotificationManager.IMPORTANCE_HIGH
|
|
91
|
-
).apply {
|
|
95
|
+
val channel = NotificationChannel(channelId, "Incoming Calls", NotificationManager.IMPORTANCE_HIGH).apply {
|
|
92
96
|
enableVibration(true)
|
|
93
97
|
vibrationPattern = longArrayOf(0, 500, 500, 500)
|
|
94
98
|
lightColor = Color.GREEN
|
|
@@ -99,83 +103,70 @@ object NativeCallManager {
|
|
|
99
103
|
notificationManager.createNotificationChannel(channel)
|
|
100
104
|
}
|
|
101
105
|
|
|
106
|
+
val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
|
107
|
+
val isLocked = keyguardManager.isKeyguardLocked
|
|
108
|
+
|
|
109
|
+
|
|
102
110
|
val builder = NotificationCompat.Builder(context, channelId)
|
|
103
111
|
.setSmallIcon(context.applicationInfo.icon)
|
|
104
112
|
.setContentTitle("Incoming $callType call")
|
|
105
113
|
.setContentText(name)
|
|
106
|
-
.setPriority(NotificationCompat.PRIORITY_MAX)
|
|
107
|
-
.setCategory(NotificationCompat.CATEGORY_CALL)
|
|
114
|
+
.setPriority(NotificationCompat.PRIORITY_MAX)
|
|
115
|
+
.setCategory(NotificationCompat.CATEGORY_CALL)
|
|
108
116
|
.setOngoing(true)
|
|
109
117
|
.setAutoCancel(false)
|
|
110
118
|
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
111
|
-
.setFullScreenIntent(fullScreenPendingIntent, true)
|
|
112
119
|
.setContentIntent(noOpIntent)
|
|
113
|
-
.addAction(0, "Answer",
|
|
120
|
+
.addAction(0, "Answer", answerPendingIntent)
|
|
114
121
|
.addAction(0, "Decline", rejectPendingIntent)
|
|
122
|
+
.setDefaults(NotificationCompat.DEFAULT_ALL)
|
|
123
|
+
|
|
124
|
+
builder.setFullScreenIntent(fullScreenPendingIntent, true)
|
|
125
|
+
|
|
115
126
|
|
|
116
127
|
notificationManager.notify(notificationId, builder.build())
|
|
117
128
|
|
|
118
129
|
try {
|
|
119
130
|
val ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
|
|
120
131
|
ringtone = RingtoneManager.getRingtone(context, ringtoneUri)
|
|
121
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
|
|
122
|
-
ringtone?.isLooping = true
|
|
123
|
-
}
|
|
132
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) ringtone?.isLooping = true
|
|
124
133
|
ringtone?.play()
|
|
125
|
-
} catch (e: Exception) {
|
|
126
|
-
e.printStackTrace()
|
|
127
|
-
}
|
|
134
|
+
} catch (e: Exception) { e.printStackTrace() }
|
|
128
135
|
}
|
|
129
136
|
|
|
130
137
|
fun stopRingtone() {
|
|
131
138
|
try {
|
|
132
139
|
ringtone?.let { if (it.isPlaying) it.stop() }
|
|
133
140
|
ringtone = null
|
|
134
|
-
} catch (e: Exception) {
|
|
135
|
-
ringtone = null
|
|
136
|
-
}
|
|
141
|
+
} catch (e: Exception) { ringtone = null }
|
|
137
142
|
}
|
|
138
143
|
|
|
139
|
-
fun connecting(context: Context, uuid: String, name: String, callType: String) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
.
|
|
149
|
-
|
|
150
|
-
.setOngoing(true)
|
|
151
|
-
.setAutoCancel(false)
|
|
152
|
-
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
153
|
-
.setProgress(0, 0, true) // ✅ system activity indicator (indeterminate progress bar)
|
|
154
|
-
|
|
155
|
-
notificationManager.notify(notificationId, builder.build())
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
fun aborting(context: Context, uuid: String, name: String, callType: String) {
|
|
159
|
-
val notificationId = uuid.hashCode()
|
|
160
|
-
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
161
|
-
|
|
162
|
-
val builder = NotificationCompat.Builder(context, channelId)
|
|
163
|
-
.setSmallIcon(context.applicationInfo.icon)
|
|
164
|
-
.setContentTitle("Incoming $callType call")
|
|
165
|
-
.setContentText("Aborting…") // ✅ show aborting text
|
|
166
|
-
.setSubText("Aborting…") // status line
|
|
167
|
-
.setPriority(NotificationCompat.PRIORITY_MAX)
|
|
168
|
-
.setCategory(NotificationCompat.CATEGORY_CALL)
|
|
169
|
-
.setOngoing(true)
|
|
170
|
-
.setAutoCancel(false)
|
|
171
|
-
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
172
|
-
.setProgress(0, 0, true) // ✅ indeterminate progress indicator
|
|
173
|
-
|
|
174
|
-
notificationManager.notify(notificationId, builder.build())
|
|
175
|
-
}
|
|
144
|
+
fun connecting(context: Context, uuid: String, name: String, callType: String) {
|
|
145
|
+
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
146
|
+
val builder = NotificationCompat.Builder(context, channelId)
|
|
147
|
+
.setSmallIcon(context.applicationInfo.icon)
|
|
148
|
+
.setContentTitle("Incoming $callType call")
|
|
149
|
+
.setContentText("Connecting…")
|
|
150
|
+
.setPriority(NotificationCompat.PRIORITY_MAX)
|
|
151
|
+
.setOngoing(true)
|
|
152
|
+
.setProgress(0, 0, true)
|
|
153
|
+
notificationManager.notify(uuid.hashCode(), builder.build())
|
|
154
|
+
}
|
|
176
155
|
|
|
156
|
+
fun aborting(context: Context, uuid: String, name: String, callType: String) {
|
|
157
|
+
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
158
|
+
val builder = NotificationCompat.Builder(context, channelId)
|
|
159
|
+
.setSmallIcon(context.applicationInfo.icon)
|
|
160
|
+
.setContentTitle("Incoming $callType call")
|
|
161
|
+
.setContentText("Aborting…")
|
|
162
|
+
.setPriority(NotificationCompat.PRIORITY_MAX)
|
|
163
|
+
.setOngoing(true)
|
|
164
|
+
.setProgress(0, 0, true)
|
|
165
|
+
notificationManager.notify(uuid.hashCode(), builder.build())
|
|
166
|
+
}
|
|
177
167
|
|
|
178
168
|
fun dismissIncomingCall(context: Context, uuid: String?) {
|
|
169
|
+
this.currentCallData = null
|
|
179
170
|
stopRingtone()
|
|
180
171
|
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
181
172
|
if (uuid != null) notificationManager.cancel(uuid.hashCode())
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
package com.rnsnativecall
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.os.Build
|
|
5
|
+
import android.os.Bundle
|
|
6
|
+
import android.view.WindowManager
|
|
7
|
+
|
|
8
|
+
class NotificationOverlayActivity : Activity() {
|
|
9
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
10
|
+
super.onCreate(savedInstanceState)
|
|
11
|
+
|
|
12
|
+
// These flags allow the notification pill to show over the lockscreen
|
|
13
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
|
14
|
+
setShowWhenLocked(true)
|
|
15
|
+
setTurnScreenOn(true)
|
|
16
|
+
} else {
|
|
17
|
+
@Suppress("DEPRECATION")
|
|
18
|
+
window.addFlags(
|
|
19
|
+
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
|
|
20
|
+
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
|
|
21
|
+
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
finish()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
package com.rnsnativecall
|
|
2
|
+
|
|
3
|
+
import android.content.BroadcastReceiver
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.Intent
|
|
6
|
+
|
|
7
|
+
class UnlockReceiver : BroadcastReceiver() {
|
|
8
|
+
override fun onReceive(context: Context, intent: Intent) {
|
|
9
|
+
android.util.Log.d("UnlockReceiver", "Device Unlocked! Action: ${intent.action}")
|
|
10
|
+
|
|
11
|
+
if (intent.action == Intent.ACTION_USER_PRESENT) {
|
|
12
|
+
val activeData = NativeCallManager.getCurrentCallData()
|
|
13
|
+
|
|
14
|
+
if (activeData != null) {
|
|
15
|
+
android.util.Log.d("UnlockReceiver", "Re-triggering call for: ${activeData["name"]}")
|
|
16
|
+
NativeCallManager.handleIncomingPush(context, activeData)
|
|
17
|
+
} else {
|
|
18
|
+
android.util.Log.d("UnlockReceiver", "No active call data found to re-trigger.")
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
2
|
+
android:width="24dp"
|
|
3
|
+
android:height="24dp"
|
|
4
|
+
android:viewportWidth="24"
|
|
5
|
+
android:viewportHeight="24">
|
|
6
|
+
<path
|
|
7
|
+
android:fillColor="#FFFFFF"
|
|
8
|
+
android:pathData="M20,15.5c-1.25,0 -2.45,-0.2 -3.57,-0.57a1.02,1.02 0,0 0,-1.02 0.24l-2.2,2.2a15.05,15.05 0,0 1,-6.59 -6.59l2.2,-2.2a1.02,1.02 0,0 0,0.24 -1.02A11.36,11.36 0,0 1,8.5 4c0,-0.55 -0.45,-1 -1,-1H4c-0.55,0 -1,0.45 -1,1c0,9.39 7.61,17 17,17c0.55,0 1,-0.45 1,-1v-3.5c0,-0.55 -0.45,-1 -1,-1z" />
|
|
9
|
+
</vector>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
2
|
+
android:width="24dp"
|
|
3
|
+
android:height="24dp"
|
|
4
|
+
android:viewportWidth="24"
|
|
5
|
+
android:viewportHeight="24">
|
|
6
|
+
<path
|
|
7
|
+
android:fillColor="#FFFFFF"
|
|
8
|
+
android:pathData="M12,9c-1.6,0 -3.15,0.25 -4.6,0.72v3.1c0,0.39 -0.23,0.74 -0.56,0.9 -0.98,0.49 -1.65,1.45 -1.65,2.58c0,1.65 1.34,3 3,3h2v-3H8.1c0.01,-0.99 0.87,-1.8 1.85,-1.8c0.51,0 1,-0.21 1.35,-0.56l1.8,-1.8C13.04,9.3 12.55,9 12,9z" />
|
|
9
|
+
</vector>
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<androidx.constraintlayout.widget.ConstraintLayout
|
|
3
|
+
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
4
|
+
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
5
|
+
xmlns:tools="http://schemas.android.com/tools"
|
|
6
|
+
android:layout_width="match_parent"
|
|
7
|
+
android:layout_height="match_parent"
|
|
8
|
+
android:background="#212121">
|
|
9
|
+
|
|
10
|
+
<!-- Profile Section -->
|
|
11
|
+
<ImageView
|
|
12
|
+
android:id="@+id/profileImage"
|
|
13
|
+
android:layout_width="200dp"
|
|
14
|
+
android:layout_height="200dp"
|
|
15
|
+
android:scaleType="centerCrop"
|
|
16
|
+
android:background="@drawable/circle_background"
|
|
17
|
+
app:layout_constraintTop_toTopOf="parent"
|
|
18
|
+
app:layout_constraintStart_toStartOf="parent"
|
|
19
|
+
app:layout_constraintEnd_toEndOf="parent"
|
|
20
|
+
app:layout_constraintBottom_toTopOf="@id/usernameText"
|
|
21
|
+
app:layout_constraintVertical_chainStyle="packed"
|
|
22
|
+
app:layout_constraintVertical_bias="0.35"
|
|
23
|
+
tools:src="@drawable/ic_profile_placeholder" />
|
|
24
|
+
|
|
25
|
+
<!-- Optional: Blur effect for discreet mode (apply programmatically) -->
|
|
26
|
+
<!-- You can use RenderScript or BlurView library for real blur -->
|
|
27
|
+
|
|
28
|
+
<TextView
|
|
29
|
+
android:id="@+id/usernameText"
|
|
30
|
+
android:layout_width="wrap_content"
|
|
31
|
+
android:layout_height="wrap_content"
|
|
32
|
+
android:text="John Doe"
|
|
33
|
+
android:textColor="#FFFFFF"
|
|
34
|
+
android:textSize="32sp"
|
|
35
|
+
android:fontFamily="sans-serif-medium"
|
|
36
|
+
android:layout_marginTop="20dp"
|
|
37
|
+
app:layout_constraintTop_toBottomOf="@id/profileImage"
|
|
38
|
+
app:layout_constraintStart_toStartOf="parent"
|
|
39
|
+
app:layout_constraintEnd_toEndOf="parent"
|
|
40
|
+
tools:text="John Doe" />
|
|
41
|
+
|
|
42
|
+
<TextView
|
|
43
|
+
android:id="@+id/callStatusText"
|
|
44
|
+
android:layout_width="wrap_content"
|
|
45
|
+
android:layout_height="wrap_content"
|
|
46
|
+
android:text="Incoming Video Call..."
|
|
47
|
+
android:textColor="#FFFFFF"
|
|
48
|
+
android:textSize="18sp"
|
|
49
|
+
android:alpha="0.8"
|
|
50
|
+
android:layout_marginTop="10dp"
|
|
51
|
+
app:layout_constraintTop_toBottomOf="@id/usernameText"
|
|
52
|
+
app:layout_constraintStart_toStartOf="parent"
|
|
53
|
+
app:layout_constraintEnd_toEndOf="parent" />
|
|
54
|
+
|
|
55
|
+
<!-- Action Buttons Container -->
|
|
56
|
+
<LinearLayout
|
|
57
|
+
android:id="@+id/buttonContainer"
|
|
58
|
+
android:layout_width="match_parent"
|
|
59
|
+
android:layout_height="wrap_content"
|
|
60
|
+
android:orientation="horizontal"
|
|
61
|
+
android:gravity="center"
|
|
62
|
+
android:paddingBottom="100dp"
|
|
63
|
+
app:layout_constraintBottom_toBottomOf="parent"
|
|
64
|
+
app:layout_constraintStart_toStartOf="parent"
|
|
65
|
+
app:layout_constraintEnd_toEndOf="parent">
|
|
66
|
+
|
|
67
|
+
<!-- Decline Button -->
|
|
68
|
+
<LinearLayout
|
|
69
|
+
android:layout_width="wrap_content"
|
|
70
|
+
android:layout_height="wrap_content"
|
|
71
|
+
android:orientation="vertical"
|
|
72
|
+
android:gravity="center"
|
|
73
|
+
android:layout_marginEnd="60dp">
|
|
74
|
+
|
|
75
|
+
<androidx.cardview.widget.CardView
|
|
76
|
+
android:layout_width="70dp"
|
|
77
|
+
android:layout_height="70dp"
|
|
78
|
+
app:cardCornerRadius="35dp"
|
|
79
|
+
app:cardElevation="8dp"
|
|
80
|
+
app:cardBackgroundColor="#FF3B30">
|
|
81
|
+
|
|
82
|
+
<ImageView
|
|
83
|
+
android:layout_width="match_parent"
|
|
84
|
+
android:layout_height="match_parent"
|
|
85
|
+
android:src="@drawable/ic_call_end_white"
|
|
86
|
+
android:padding="18dp"
|
|
87
|
+
android:tint="#FFFFFF" />
|
|
88
|
+
|
|
89
|
+
</androidx.cardview.widget.CardView>
|
|
90
|
+
|
|
91
|
+
<TextView
|
|
92
|
+
android:layout_width="wrap_content"
|
|
93
|
+
android:layout_height="wrap_content"
|
|
94
|
+
android:text="Decline"
|
|
95
|
+
android:textColor="#FFFFFF"
|
|
96
|
+
android:textSize="16sp"
|
|
97
|
+
android:fontFamily="sans-serif-medium"
|
|
98
|
+
android:layout_marginTop="10dp" />
|
|
99
|
+
|
|
100
|
+
</LinearLayout>
|
|
101
|
+
|
|
102
|
+
<!-- Accept Button -->
|
|
103
|
+
<LinearLayout
|
|
104
|
+
android:layout_width="wrap_content"
|
|
105
|
+
android:layout_height="wrap_content"
|
|
106
|
+
android:orientation="vertical"
|
|
107
|
+
android:gravity="center"
|
|
108
|
+
android:layout_marginStart="60dp">
|
|
109
|
+
|
|
110
|
+
<androidx.cardview.widget.CardView
|
|
111
|
+
android:layout_width="70dp"
|
|
112
|
+
android:layout_height="70dp"
|
|
113
|
+
app:cardCornerRadius="35dp"
|
|
114
|
+
app:cardElevation="8dp"
|
|
115
|
+
app:cardBackgroundColor="#4CD964">
|
|
116
|
+
|
|
117
|
+
<ImageView
|
|
118
|
+
android:layout_width="match_parent"
|
|
119
|
+
android:layout_height="match_parent"
|
|
120
|
+
android:src="@drawable/ic_call_answer_white"
|
|
121
|
+
android:padding="18dp"
|
|
122
|
+
android:tint="#FFFFFF" />
|
|
123
|
+
|
|
124
|
+
</androidx.cardview.widget.CardView>
|
|
125
|
+
|
|
126
|
+
<TextView
|
|
127
|
+
android:layout_width="wrap_content"
|
|
128
|
+
android:layout_height="wrap_content"
|
|
129
|
+
android:text="Accept"
|
|
130
|
+
android:textColor="#FFFFFF"
|
|
131
|
+
android:textSize="16sp"
|
|
132
|
+
android:fontFamily="sans-serif-medium"
|
|
133
|
+
android:layout_marginTop="10dp" />
|
|
134
|
+
|
|
135
|
+
</LinearLayout>
|
|
136
|
+
|
|
137
|
+
</LinearLayout>
|
|
138
|
+
|
|
139
|
+
</androidx.constraintlayout.widget.ConstraintLayout>
|
package/index.d.ts
CHANGED
|
@@ -78,6 +78,10 @@ export interface CallHandlerType {
|
|
|
78
78
|
*/
|
|
79
79
|
checkCallStatus(uuid: string): Promise<CallStatus>;
|
|
80
80
|
|
|
81
|
+
requestOverlayPermission(): Promise<boolean>;
|
|
82
|
+
|
|
83
|
+
checkOverlayPermission(): Promise<boolean>;
|
|
84
|
+
|
|
81
85
|
/**
|
|
82
86
|
* Forcefully dismisses the native call UI, stops the ringtone, and clears state.
|
|
83
87
|
* Use this when the call is hung up or timed out.
|
package/index.js
CHANGED
|
@@ -47,6 +47,16 @@ export const CallHandler = {
|
|
|
47
47
|
},
|
|
48
48
|
// ------------------------------------
|
|
49
49
|
|
|
50
|
+
requestOverlayPermission: async () => {
|
|
51
|
+
if (!CallModule?.requestOverlayPermission) return false;
|
|
52
|
+
return await CallModule.requestOverlayPermission();
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
checkOverlayPermission: async () => {
|
|
56
|
+
if (!CallModule?.checkOverlayPermission) return false;
|
|
57
|
+
return await CallModule.checkOverlayPermission();
|
|
58
|
+
},
|
|
59
|
+
|
|
50
60
|
displayCall: async (uuid, name, callType = "audio") => {
|
|
51
61
|
if (!CallModule) return false;
|
|
52
62
|
return await CallModule.displayIncomingCall(uuid.toLowerCase().trim(), name, callType);
|
package/package.json
CHANGED
package/withNativeCallVoip.js
CHANGED
|
@@ -5,35 +5,54 @@ function withMainActivityDataFix(config) {
|
|
|
5
5
|
return withMainActivity(config, (config) => {
|
|
6
6
|
let contents = config.modResults.contents;
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
const imports = [
|
|
9
|
+
'import android.content.Intent',
|
|
10
|
+
'import android.os.Bundle',
|
|
11
|
+
'import android.view.WindowManager',
|
|
12
|
+
'import android.os.Build'
|
|
13
|
+
];
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
// Add imports if they don't exist
|
|
16
|
+
imports.forEach(imp => {
|
|
17
|
+
if (!contents.includes(imp)) {
|
|
18
|
+
contents = contents.replace(/package .*/, (match) => `${match}\n${imp}`);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
19
21
|
|
|
20
22
|
const onCreateCode = `
|
|
21
23
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
22
24
|
super.onCreate(savedInstanceState)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
|
26
|
+
setShowWhenLocked(false)
|
|
27
|
+
setTurnScreenOn(true)
|
|
28
|
+
} else {
|
|
29
|
+
window.addFlags(
|
|
30
|
+
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
|
|
31
|
+
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or
|
|
32
|
+
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
|
|
33
|
+
)
|
|
34
|
+
}
|
|
25
35
|
if (intent.getBooleanExtra("background_wake", false)) {
|
|
26
36
|
moveTaskToBack(true)
|
|
27
37
|
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
38
|
+
}`;
|
|
39
|
+
|
|
40
|
+
const onNewIntentCode = `
|
|
41
|
+
override fun onNewIntent(intent: Intent) {
|
|
42
|
+
super.onNewIntent(intent)
|
|
43
|
+
setIntent(intent)
|
|
44
|
+
}`;
|
|
45
|
+
|
|
46
|
+
// Use a more flexible regex for the class definition
|
|
47
|
+
const classRegex = /class MainActivity\s*:\s*ReactActivity\(\)\s*\{/;
|
|
48
|
+
|
|
34
49
|
if (!contents.includes('override fun onCreate')) {
|
|
35
|
-
|
|
36
|
-
|
|
50
|
+
contents = contents.replace(classRegex, (match) => `${match}${onCreateCode}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!contents.includes('override fun onNewIntent')) {
|
|
54
|
+
// Re-run match check because contents might have changed from onCreate injection
|
|
55
|
+
contents = contents.replace(classRegex, (match) => `${match}${onNewIntentCode}`);
|
|
37
56
|
}
|
|
38
57
|
|
|
39
58
|
config.modResults.contents = contents;
|
|
@@ -47,79 +66,115 @@ function withAndroidConfig(config) {
|
|
|
47
66
|
const manifest = config.modResults;
|
|
48
67
|
const application = manifest.manifest.application[0];
|
|
49
68
|
|
|
50
|
-
//
|
|
69
|
+
// Ensure MainActivity flags
|
|
70
|
+
const mainActivity = application.activity.find((a) => a.$['android:name'] === '.MainActivity');
|
|
71
|
+
if (mainActivity) {
|
|
72
|
+
mainActivity.$['android:launchMode'] = 'singleTop';
|
|
73
|
+
mainActivity.$['android:showWhenLocked'] = 'false';
|
|
74
|
+
mainActivity.$['android:turnScreenOn'] = 'true';
|
|
75
|
+
}
|
|
76
|
+
|
|
51
77
|
const permissions = [
|
|
52
78
|
'android.permission.USE_FULL_SCREEN_INTENT',
|
|
53
79
|
'android.permission.VIBRATE',
|
|
54
80
|
'android.permission.FOREGROUND_SERVICE',
|
|
55
81
|
'android.permission.FOREGROUND_SERVICE_PHONE_CALL',
|
|
56
82
|
'android.permission.POST_NOTIFICATIONS',
|
|
57
|
-
'android.permission.SYSTEM_ALERT_WINDOW',
|
|
58
83
|
'android.permission.WAKE_LOCK',
|
|
59
84
|
'android.permission.DISABLE_KEYGUARD',
|
|
60
|
-
'android.permission.
|
|
85
|
+
'android.permission.SYSTEM_ALERT_WINDOW'
|
|
61
86
|
];
|
|
62
87
|
|
|
88
|
+
// Initialize permissions array if missing
|
|
63
89
|
manifest.manifest['uses-permission'] = manifest.manifest['uses-permission'] || [];
|
|
90
|
+
|
|
64
91
|
permissions.forEach((perm) => {
|
|
65
92
|
if (!manifest.manifest['uses-permission'].some((p) => p.$['android:name'] === perm)) {
|
|
66
93
|
manifest.manifest['uses-permission'].push({ $: { 'android:name': perm } });
|
|
67
94
|
}
|
|
68
95
|
});
|
|
69
96
|
|
|
97
|
+
// Initialize components if missing
|
|
98
|
+
application.service = application.service || [];
|
|
70
99
|
application.activity = application.activity || [];
|
|
71
|
-
|
|
72
|
-
if (!application.activity.some(a => a.$['android:name'] === 'com.rnsnativecall.AcceptCallActivity')) {
|
|
73
|
-
application.activity.push({
|
|
74
|
-
$: {
|
|
75
|
-
'android:name': 'com.rnsnativecall.AcceptCallActivity',
|
|
76
|
-
'android:theme': '@android:style/Theme.Translucent.NoTitleBar',
|
|
77
|
-
'android:excludeFromRecents': 'true',
|
|
78
|
-
'android:noHistory': 'true',
|
|
79
|
-
'android:exported': 'false',
|
|
80
|
-
'android:launchMode': 'singleInstance',
|
|
81
|
-
'android:showWhenLocked': 'true',
|
|
82
|
-
'android:turnScreenOn': 'true'
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
}
|
|
100
|
+
application.receiver = application.receiver || [];
|
|
86
101
|
|
|
87
|
-
|
|
102
|
+
// 1. Services
|
|
103
|
+
const services = [
|
|
104
|
+
{ name: 'com.rnsnativecall.CallMessagingService', exported: 'false', filter: 'com.google.firebase.MESSAGING_EVENT' },
|
|
105
|
+
{ name: 'com.rnsnativecall.CallForegroundService', type: 'phoneCall' },
|
|
106
|
+
{ name: 'com.rnsnativecall.CallHeadlessTask' }
|
|
107
|
+
];
|
|
88
108
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
109
|
+
services.forEach(svc => {
|
|
110
|
+
if (!application.service.some(s => s.$['android:name'] === svc.name)) {
|
|
111
|
+
const entry = { $: { 'android:name': svc.name, 'android:exported': svc.exported || 'false' } };
|
|
112
|
+
if (svc.type) entry.$['android:foregroundServiceType'] = svc.type;
|
|
113
|
+
if (svc.filter) {
|
|
114
|
+
entry['intent-filter'] = [{ action: [{ $: { 'android:name': svc.filter } }] }];
|
|
115
|
+
}
|
|
116
|
+
application.service.push(entry);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
97
119
|
|
|
98
|
-
// 2.
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
120
|
+
// 2. Activities (Trampoline & Overlay)
|
|
121
|
+
const activities = [
|
|
122
|
+
{
|
|
123
|
+
name: 'com.rnsnativecall.AcceptCallActivity',
|
|
124
|
+
theme: '@android:style/Theme.Translucent.NoTitleBar',
|
|
125
|
+
launchMode: 'singleInstance'
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: 'com.rnsnativecall.NotificationOverlayActivity',
|
|
129
|
+
theme: '@android:style/Theme.NoTitleBar.Fullscreen',
|
|
130
|
+
launchMode: 'singleInstance'
|
|
131
|
+
}
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
activities.forEach(act => {
|
|
135
|
+
if (!application.activity.some(a => a.$['android:name'] === act.name)) {
|
|
136
|
+
application.activity.push({
|
|
137
|
+
$: {
|
|
138
|
+
'android:name': act.name,
|
|
139
|
+
'android:theme': act.theme,
|
|
140
|
+
'android:exported': 'false',
|
|
141
|
+
'android:showWhenLocked': 'true',
|
|
142
|
+
'android:turnScreenOn': 'true',
|
|
143
|
+
'android:excludeFromRecents': 'true',
|
|
144
|
+
'android:noHistory': 'true',
|
|
145
|
+
'android:launchMode': act.launchMode
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
});
|
|
105
150
|
|
|
106
|
-
// 3.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
application.service.push({
|
|
151
|
+
// 3. Receiver
|
|
152
|
+
if (!application.receiver.some(r => r.$['android:name'] === 'com.rnsnativecall.CallActionReceiver')) {
|
|
153
|
+
application.receiver.push({
|
|
110
154
|
$: {
|
|
111
|
-
'android:name':
|
|
112
|
-
'android:foregroundServiceType': 'phoneCall',
|
|
155
|
+
'android:name': 'com.rnsnativecall.CallActionReceiver',
|
|
113
156
|
'android:exported': 'false'
|
|
114
157
|
}
|
|
115
158
|
});
|
|
116
159
|
}
|
|
117
160
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (!application.receiver.some(r => r.$['android:name'] === receiverName)) {
|
|
161
|
+
// ADD THIS: UnlockReceiver with USER_PRESENT filter
|
|
162
|
+
if (!application.receiver.some(r => r.$['android:name'] === 'com.rnsnativecall.UnlockReceiver')) {
|
|
121
163
|
application.receiver.push({
|
|
122
|
-
$: {
|
|
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
|
+
]
|
|
123
178
|
});
|
|
124
179
|
}
|
|
125
180
|
|
|
@@ -132,13 +187,20 @@ function withIosConfig(config) {
|
|
|
132
187
|
return withInfoPlist(config, (config) => {
|
|
133
188
|
const infoPlist = config.modResults;
|
|
134
189
|
if (!infoPlist.UIBackgroundModes) infoPlist.UIBackgroundModes = [];
|
|
135
|
-
|
|
136
|
-
|
|
190
|
+
|
|
191
|
+
['voip', 'audio', 'remote-notification'].forEach(mode => {
|
|
192
|
+
if (!infoPlist.UIBackgroundModes.includes(mode)) {
|
|
193
|
+
infoPlist.UIBackgroundModes.push(mode);
|
|
194
|
+
}
|
|
137
195
|
});
|
|
138
196
|
return config;
|
|
139
197
|
});
|
|
140
198
|
}
|
|
141
199
|
|
|
142
200
|
module.exports = (config) => {
|
|
143
|
-
return withPlugins(config, [
|
|
144
|
-
|
|
201
|
+
return withPlugins(config, [
|
|
202
|
+
withAndroidConfig,
|
|
203
|
+
withMainActivityDataFix,
|
|
204
|
+
withIosConfig
|
|
205
|
+
]);
|
|
206
|
+
};
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
package com.rnsnativecall
|
|
2
|
-
|
|
3
|
-
import android.app.Activity
|
|
4
|
-
import android.app.KeyguardManager
|
|
5
|
-
import android.content.Context
|
|
6
|
-
import android.content.Intent
|
|
7
|
-
import android.os.Build
|
|
8
|
-
import android.os.Bundle
|
|
9
|
-
import android.view.WindowManager
|
|
10
|
-
|
|
11
|
-
class UnlockPromptActivity : Activity() {
|
|
12
|
-
override fun onCreate(savedInstanceState: Bundle?) {
|
|
13
|
-
super.onCreate(savedInstanceState)
|
|
14
|
-
|
|
15
|
-
// Ensure this activity shows over the lockscreen to trigger the prompt
|
|
16
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
|
17
|
-
setShowWhenLocked(true)
|
|
18
|
-
setTurnScreenOn(true)
|
|
19
|
-
} else {
|
|
20
|
-
window.addFlags(
|
|
21
|
-
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
|
|
22
|
-
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
|
|
23
|
-
)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
val km = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
|
27
|
-
|
|
28
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
29
|
-
km.requestDismissKeyguard(this, object : KeyguardManager.KeyguardDismissCallback() {
|
|
30
|
-
override fun onDismissSucceeded() {
|
|
31
|
-
super.onDismissSucceeded()
|
|
32
|
-
// Unlock successful, launch the main app
|
|
33
|
-
val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
|
|
34
|
-
startActivity(launchIntent)
|
|
35
|
-
finish()
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
override fun onDismissCancelled() {
|
|
39
|
-
super.onDismissCancelled()
|
|
40
|
-
finish()
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
override fun onDismissError() {
|
|
44
|
-
super.onDismissError()
|
|
45
|
-
finish()
|
|
46
|
-
}
|
|
47
|
-
})
|
|
48
|
-
} else {
|
|
49
|
-
// Fallback for older Android versions
|
|
50
|
-
val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
|
|
51
|
-
startActivity(launchIntent)
|
|
52
|
-
finish()
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|