rns-nativecall 0.6.2 → 0.6.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.
@@ -1,135 +1,139 @@
1
1
  package com.rnsnativecall
2
2
 
3
3
  import android.app.ActivityManager
4
- import android.content.Context
5
- import android.content.Intent
6
- import android.os.Bundle
7
- import android.os.Handler
8
- import android.os.Looper
9
4
  import android.app.NotificationManager
10
5
  import android.app.NotificationChannel
11
6
  import android.app.PendingIntent
7
+ import android.content.Context
8
+ import android.content.Intent
12
9
  import android.os.Build
13
- import android.util.Log
10
+ import android.os.Bundle
14
11
  import androidx.core.app.NotificationCompat
15
12
  import androidx.core.content.ContextCompat
16
13
  import com.google.firebase.messaging.FirebaseMessagingService
17
14
  import com.google.firebase.messaging.RemoteMessage
18
15
  import com.facebook.react.HeadlessJsTaskService
19
16
 
20
- class CallMessagingService : FirebaseMessagingService() {
21
-
22
- companion object {
23
- private val handler = Handler(Looper.getMainLooper())
24
- private val pendingNotifications = mutableMapOf<String, Runnable>()
17
+ import com.rnsnativecall.CallState
25
18
 
26
- @JvmStatic
27
- fun stopBackupTimer(uuid: String) {
28
- pendingNotifications[uuid]?.let {
29
- Log.d("CallMessagingService", "Stopping backup timer for UUID: $uuid")
30
- handler.removeCallbacks(it)
31
- pendingNotifications.remove(uuid)
32
- }
33
- }
34
- }
35
-
36
- private fun isAppInForeground(context: Context): Boolean {
37
- val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
38
- val appProcesses = activityManager.runningAppProcesses ?: return false
39
- return appProcesses.any {
40
- it.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND && it.processName == context.packageName
41
- }
42
- }
19
+ class CallMessagingService : FirebaseMessagingService() {
43
20
 
44
21
  override fun onMessageReceived(remoteMessage: RemoteMessage) {
45
22
  val data = remoteMessage.data
46
23
  val context = applicationContext
24
+
47
25
  val uuid = data["callUuid"] ?: return
48
26
  val type = data["type"] ?: ""
49
-
27
+
50
28
  if (type == "CANCEL") {
51
- stopBackupTimer(uuid)
52
- // Stop Headless service if it was running for this call
53
- val stopHeadlessIntent = Intent(context, CallHeadlessTask::class.java)
54
- context.stopService(stopHeadlessIntent)
55
-
56
- NativeCallManager.dismissIncomingCall(context, uuid)
29
+ NativeCallManager.stopRingtone()
30
+ CallState.markCanceled(uuid)
31
+ // Gate future headless runs for this UUID
32
+ if (CallState.getCurrent() == uuid) {
33
+ CallState.clear(uuid)
34
+ }
57
35
  showMissedCallNotification(context, data, uuid)
58
36
  return
59
37
  }
60
-
38
+
39
+ // Inside onMessageReceived
40
+ if (!CallState.setCurrent(uuid)) {
41
+ // We are busy! Start a SILENT headless task to send the WebSocket busy msg
42
+ val busyIntent = Intent(context, CallHeadlessTask::class.java).apply {
43
+ putExtras(Bundle().apply {
44
+ data.forEach { (k, v) -> putString(k, v) }
45
+ putBoolean("isBusySignal", true)
46
+ })
47
+ }
48
+ context.startService(busyIntent)
49
+ return
50
+ }
51
+
61
52
  if (isAppInForeground(context)) {
53
+ // Foreground → send event directly
62
54
  CallModule.sendEventToJS("onCallReceived", data)
63
55
  } else {
64
-
65
- // 2. Start Headless Task
56
+ // Show incoming call notification instantly
57
+ //NativeCallManager.handleIncomingPush(context, data) /// i will use headless instead
58
+ // Background → start foreground service + headless task
59
+ // val serviceIntent = Intent(context, CallForegroundService::class.java).apply {
60
+ // putExtras(Bundle().apply { data.forEach { (k, v) -> putString(k, v) } })
61
+ // }
62
+ // ContextCompat.startForegroundService(context, serviceIntent)
63
+
64
+ val headlessIntent = Intent(context, CallHeadlessTask::class.java).apply {
65
+ putExtras(Bundle().apply { data.forEach { (k, v) -> putString(k, v) } })
66
+ }
66
67
  try {
67
- val headlessIntent = Intent(context, CallHeadlessTask::class.java).apply {
68
- val bundle = Bundle()
69
- data.forEach { (k, v) -> bundle.putString(k, v) }
70
- putExtras(bundle)
71
- }
72
- ContextCompat.startForegroundService(context, headlessIntent)
68
+ context.startService(headlessIntent)
73
69
  HeadlessJsTaskService.acquireWakeLockNow(context)
74
- } catch (e: Exception) {
75
- e.printStackTrace()
70
+ } catch (e: Exception) {
71
+ e.printStackTrace()
76
72
  }
77
73
 
78
- // 3. Set Backup Timer (The Ghost Prevention Logic)
79
- // Remove any old timer for this UUID before setting a new one
80
- stopBackupTimer(uuid)
81
74
 
82
- val showNotificationRunnable = Runnable {
83
- if (!isAppInForeground(context)) {
84
- Log.d("CallMessagingService", "Backup timer triggered for: $uuid")
85
- NativeCallManager.handleIncomingPush(context, data)
86
- // 1. Trigger Notification UI immediately
87
- // NativeCallManager.handleIncomingPush(context, data)
88
-
89
- }
90
- pendingNotifications.remove(uuid)
91
- }
92
-
93
- pendingNotifications[uuid] = showNotificationRunnable
94
- handler.postDelayed(showNotificationRunnable, 18000)
95
75
  }
96
76
  }
97
77
 
98
- private fun showMissedCallNotification(context: Context, data: Map<String, String>, uuid: String) {
78
+ private fun showMissedCallNotification(
79
+ context: Context,
80
+ data: Map<String, String>,
81
+ uuid: String
82
+ ) {
99
83
  val name = data["name"] ?: "Unknown"
100
84
  val callType = data["callType"] ?: "video"
101
- val channelId = "missed_calls"
102
- val article = if (callType.startsWith("a", ignoreCase = true)) "an" else "a"
103
- val appName = context.applicationInfo.loadLabel(context.packageManager).toString()
104
- val capitalizedAppName = appName.replaceFirstChar { it.uppercase() }
85
+ val channelId = "missed_calls"
105
86
 
106
- val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
87
+ val notificationManager =
88
+ context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
107
89
 
108
90
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
109
- val channel = NotificationChannel(channelId, "Missed Calls", NotificationManager.IMPORTANCE_HIGH)
91
+ val channel = NotificationChannel(
92
+ channelId,
93
+ "Missed Calls",
94
+ NotificationManager.IMPORTANCE_DEFAULT
95
+ ).apply { description = "Missed call notifications" }
110
96
  notificationManager.createNotificationChannel(channel)
111
97
  }
112
98
 
113
- val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
114
- val pendingFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
115
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
116
- } else {
117
- PendingIntent.FLAG_UPDATE_CURRENT
118
- }
119
- val contentIntent = PendingIntent.getActivity(context, uuid.hashCode(), launchIntent, pendingFlags)
99
+ val iconResId =
100
+ context.resources.getIdentifier("ic_missed_call", "drawable", context.packageName)
101
+ .takeIf { it != 0 } ?: android.R.drawable.sym_call_missed
102
+ val appName = context.applicationInfo.loadLabel(context.packageManager).toString()
120
103
 
121
- var iconResId = context.resources.getIdentifier("ic_missed_call", "drawable", context.packageName)
122
- if (iconResId == 0) iconResId = android.R.drawable.sym_call_missed
104
+ val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)
105
+ launchIntent?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
106
+ val pendingIntent = PendingIntent.getActivity(
107
+ context,
108
+ uuid.hashCode(),
109
+ launchIntent,
110
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
111
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
112
+ else
113
+ PendingIntent.FLAG_UPDATE_CURRENT
114
+ )
123
115
 
124
116
  val builder = NotificationCompat.Builder(context, channelId)
125
117
  .setSmallIcon(iconResId)
126
- .setContentTitle("$capitalizedAppName missed call")
127
- .setContentText("You missed $article $callType call from $name")
118
+ .setContentTitle("$appName Missed $callType call")
119
+ .setContentText("You missed a call from $name")
128
120
  .setPriority(NotificationCompat.PRIORITY_HIGH)
129
121
  .setAutoCancel(true)
130
- .setContentIntent(contentIntent)
131
- .setCategory(NotificationCompat.CATEGORY_MISSED_CALL)
122
+ .setCategory(NotificationCompat.CATEGORY_MISSED_CALL)
123
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
124
+ .setContentIntent(pendingIntent)
132
125
 
133
126
  notificationManager.notify(uuid.hashCode(), builder.build())
134
127
  }
135
- }
128
+
129
+ private fun isAppInForeground(context: Context): Boolean {
130
+ val activityManager =
131
+ context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
132
+ val appProcesses = activityManager.runningAppProcesses ?: return false
133
+ val packageName = context.packageName
134
+ return appProcesses.any {
135
+ it.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND &&
136
+ it.processName == packageName
137
+ }
138
+ }
139
+ }
@@ -7,23 +7,32 @@ import com.facebook.react.modules.core.DeviceEventManagerModule
7
7
 
8
8
  class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
9
9
 
10
- init {
10
+ init {
11
11
  instance = this
12
+ notifyReady()
12
13
  }
13
14
 
14
15
  override fun getName() = "CallModule"
15
16
 
16
17
  @ReactMethod
17
18
  fun getInitialCallData(promise: Promise) {
18
- val data = pendingCallDataMap?.let { map ->
19
- val writableMap = Arguments.createMap()
20
- map.forEach { (key, value) ->
21
- writableMap.putString(key, value)
22
- }
23
- writableMap
19
+ val writableMap = Arguments.createMap()
20
+
21
+ pendingCallDataMap?.let { map ->
22
+ val eventMap = Arguments.createMap()
23
+ map.forEach { (key, value) -> eventMap.putString(key, value) }
24
+ writableMap.putMap("default", eventMap)
25
+ pendingCallDataMap = null
24
26
  }
25
- promise.resolve(data)
26
- pendingCallDataMap = null
27
+
28
+ pendingEvents.forEach { (eventName, data) ->
29
+ val eventMap = Arguments.createMap()
30
+ data.forEach { (key, value) -> eventMap.putString(key, value) }
31
+ writableMap.putMap(eventName, eventMap)
32
+ }
33
+ pendingEvents.clear()
34
+
35
+ promise.resolve(writableMap)
27
36
  }
28
37
 
29
38
  @ReactMethod
@@ -34,7 +43,6 @@ class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
34
43
  "name" to name,
35
44
  "callType" to callType
36
45
  )
37
- // Use reactApplicationContext safely here
38
46
  NativeCallManager.handleIncomingPush(reactApplicationContext, data)
39
47
  promise.resolve(true)
40
48
  } catch (e: Exception) {
@@ -42,13 +50,47 @@ class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
42
50
  }
43
51
  }
44
52
 
53
+ /**
54
+ * Combined Validity Check:
55
+ * Used by Headless Task to see if it should proceed with the UI.
56
+ */
57
+ @ReactMethod
58
+ fun checkCallValidity(uuid: String, promise: Promise) {
59
+ val isValid = CallState.shouldProceed(uuid)
60
+ val isCanceled = CallState.isCanceled(uuid)
61
+
62
+ val map = Arguments.createMap().apply {
63
+ putBoolean("isValid", isValid)
64
+ putBoolean("isCanceled", isCanceled)
65
+ }
66
+ promise.resolve(map)
67
+ }
68
+
69
+ /**
70
+ * General Status Check:
71
+ * Useful for checking if the UI is still relevant.
72
+ */
73
+ @ReactMethod
74
+ fun checkCallStatus(uuid: String, promise: Promise) {
75
+ val isCanceled = CallState.isCanceled(uuid)
76
+ val isCurrent = CallState.getCurrent() == uuid
77
+
78
+ val map = Arguments.createMap().apply {
79
+ putBoolean("isCanceled", isCanceled)
80
+ putBoolean("isActive", isCurrent)
81
+ putBoolean("shouldDisplay", isCurrent && !isCanceled)
82
+ }
83
+ promise.resolve(map)
84
+ }
85
+
45
86
  @ReactMethod
46
87
  fun endNativeCall(uuid: String) {
47
88
  NativeCallManager.stopRingtone()
89
+ CallState.clear(uuid)
48
90
  pendingCallDataMap = null
49
91
 
50
92
  val notificationManager = reactApplicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
51
- notificationManager.cancel(uuid.hashCode())
93
+ notificationManager.cancel(101)
52
94
  }
53
95
 
54
96
  @ReactMethod
@@ -62,17 +104,47 @@ class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
62
104
  companion object {
63
105
  private var instance: CallModule? = null
64
106
  private var pendingCallDataMap: Map<String, String>? = null
107
+ private var pendingEvents: MutableMap<String, Map<String, String>> = mutableMapOf()
108
+ private val onReadyCallbacks = mutableListOf<() -> Unit>()
65
109
 
66
- // This is the static gate accessible from AcceptCallActivity
67
110
  @JvmStatic
111
+ fun isReady(): Boolean {
112
+ val reactContext = instance?.reactApplicationContext
113
+ return reactContext != null && reactContext.hasActiveCatalystInstance()
114
+ }
115
+
116
+ @JvmStatic
68
117
  fun setPendingCallData(data: Map<String, String>) {
69
- pendingCallDataMap = data
118
+ pendingCallDataMap = data
119
+ }
120
+
121
+ @JvmStatic
122
+ fun setPendingCallData(eventName: String, data: Map<String, String>) {
123
+ pendingEvents[eventName] = data
124
+ }
125
+
126
+ @JvmStatic
127
+ fun registerOnReadyCallback(callback: () -> Unit) {
128
+ if (isReady()) {
129
+ callback()
130
+ } else {
131
+ onReadyCallbacks.add(callback)
132
+ }
133
+ }
134
+
135
+ fun notifyReady() {
136
+ onReadyCallbacks.forEach { it() }
137
+ onReadyCallbacks.clear()
138
+ }
139
+
140
+ @JvmStatic
141
+ fun setPendingEvent(eventName: String, data: Map<String, String>) {
142
+ pendingEvents[eventName] = data
70
143
  }
71
144
 
72
145
  @JvmStatic
73
146
  fun sendEventToJS(eventName: String, params: Any?) {
74
147
  val reactContext = instance?.reactApplicationContext
75
-
76
148
  val bridgeData = when (params) {
77
149
  is Map<*, *> -> {
78
150
  val map = Arguments.createMap()
@@ -84,16 +156,13 @@ class CallModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMo
84
156
  else -> null
85
157
  }
86
158
 
87
- // If app is alive, send via Bridge
88
- if (reactContext != null && reactContext.hasActiveCatalystInstance()) {
159
+ if (isReady()) {
89
160
  reactContext
90
- .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
161
+ ?.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
91
162
  ?.emit(eventName, bridgeData)
92
- }
93
- // If app is in killed state, save for polling
94
- else if (eventName == "onCallAccepted" && params is Map<*, *>) {
163
+ } else if (params is Map<*, *>) {
95
164
  @Suppress("UNCHECKED_CAST")
96
- setPendingCallData(params as Map<String, String>)
165
+ setPendingEvent(eventName, params as Map<String, String>)
97
166
  }
98
167
  }
99
168
  }
@@ -0,0 +1,54 @@
1
+ package com.rnsnativecall
2
+
3
+ object CallState {
4
+ @Volatile private var currentUuid: String? = null
5
+ @Volatile private var canceledUuids = mutableSetOf<String>()
6
+
7
+ @Synchronized
8
+ fun isBusy(): Boolean = currentUuid != null
9
+
10
+ @Synchronized
11
+ fun setCurrent(uuid: String): Boolean {
12
+ if (currentUuid == null) {
13
+ currentUuid = uuid
14
+ return true
15
+ }
16
+ return currentUuid == uuid
17
+ }
18
+
19
+ fun getCurrent(): String? = currentUuid
20
+
21
+ @Synchronized
22
+ fun markCanceled(uuid: String) {
23
+ canceledUuids.add(uuid)
24
+ if (currentUuid == uuid) {
25
+ currentUuid = null
26
+ }
27
+ }
28
+
29
+ // ✅ Fixes the "Unresolved reference 'isCanceled'" error
30
+ @Synchronized
31
+ fun isCanceled(uuid: String?): Boolean {
32
+ if (uuid == null) return false
33
+ return canceledUuids.contains(uuid)
34
+ }
35
+
36
+ @Synchronized
37
+ fun clear(uuid: String?) {
38
+ if (uuid == null) return
39
+ if (currentUuid == uuid) currentUuid = null
40
+ canceledUuids.remove(uuid)
41
+ }
42
+
43
+ @Synchronized
44
+ fun shouldProceed(uuid: String?): Boolean {
45
+ if (uuid == null) return false
46
+ return (currentUuid == uuid) && !canceledUuids.contains(uuid)
47
+ }
48
+
49
+ @Synchronized
50
+ fun clearAll() {
51
+ currentUuid = null
52
+ canceledUuids.clear()
53
+ }
54
+ }
@@ -6,25 +6,23 @@ import android.app.PendingIntent
6
6
  import android.content.Context
7
7
  import android.content.Intent
8
8
  import android.os.Build
9
- import android.os.Bundle
10
9
  import androidx.core.app.NotificationCompat
11
10
  import android.media.Ringtone
12
11
  import android.media.RingtoneManager
13
12
  import android.graphics.Color
14
- import androidx.core.app.Person
15
- import androidx.core.graphics.drawable.IconCompat
16
13
 
17
14
  object NativeCallManager {
18
15
 
19
16
  private var ringtone: Ringtone? = null
20
- const val CALL_CHANNEL_ID = "CALL_CHANNEL_ID"
17
+ const val channelId = "CALL_CHANNEL_ID"
21
18
 
22
19
  fun handleIncomingPush(context: Context, data: Map<String, String>) {
23
20
  val uuid = data["callUuid"] ?: return
24
21
  stopRingtone()
25
-
26
- val name = data["name"] ?: "Someone"
22
+
23
+ val name = data["name"] ?: "Incoming Call"
27
24
  val callType = data["callType"] ?: "audio"
25
+ val notificationId = uuid.hashCode()
28
26
 
29
27
  val pendingFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
30
28
  PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
@@ -32,94 +30,136 @@ object NativeCallManager {
32
30
  PendingIntent.FLAG_UPDATE_CURRENT
33
31
  }
34
32
 
35
- // Prepare Common Bundle
36
- val extras = Bundle().apply {
37
- data.forEach { (k, v) -> putString(k, v) }
38
- putString("EXTRA_CALL_UUID", uuid)
39
- }
33
+ val noOpIntent = PendingIntent.getActivity(
34
+ context,
35
+ notificationId + 1,
36
+ Intent(),
37
+ pendingFlags
38
+ )
40
39
 
41
- // 1. UI Intent (When tapping the notification body)
42
40
  val intentToActivity = Intent(context, AcceptCallActivity::class.java).apply {
43
- action = "ACTION_SHOW_UI_$uuid"
44
- putExtras(extras)
45
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
41
+ this.action = "ACTION_SHOW_UI_$uuid"
42
+ data.forEach { (key, value) -> this.putExtra(key, value) }
43
+ this.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
46
44
  }
45
+
47
46
  val fullScreenPendingIntent = PendingIntent.getActivity(
48
- context, uuid.hashCode(), intentToActivity, pendingFlags
47
+ context,
48
+ notificationId,
49
+ intentToActivity,
50
+ pendingFlags
49
51
  )
50
52
 
51
- // 2. Reject Intent
52
53
  val rejectIntent = Intent(context, CallActionReceiver::class.java).apply {
53
- action = "ACTION_REJECT_$uuid"
54
- putExtras(extras)
54
+ this.action = "ACTION_REJECT_$uuid"
55
+ this.putExtra("EXTRA_CALL_UUID", uuid)
56
+ data.forEach { (key, value) -> this.putExtra(key, value) }
55
57
  }
56
- val rejectPendingIntent = PendingIntent.getBroadcast(
57
- context, uuid.hashCode() + 1, rejectIntent, pendingFlags
58
- )
59
58
 
60
- // 3. Answer Intent
61
- val answerIntent = Intent(context, CallActionReceiver::class.java).apply {
62
- action = "ACTION_ACCEPT_$uuid"
63
- putExtras(extras)
64
- }
65
- val answerPendingIntent = PendingIntent.getBroadcast(
66
- context, uuid.hashCode() + 2, answerIntent, pendingFlags
59
+ val rejectPendingIntent = PendingIntent.getBroadcast(
60
+ context,
61
+ notificationId,
62
+ rejectIntent,
63
+ pendingFlags
67
64
  )
68
65
 
69
66
  val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
70
67
 
71
68
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
72
- val channel = NotificationChannel(CALL_CHANNEL_ID, "Incoming Call", NotificationManager.IMPORTANCE_HIGH).apply {
69
+ val channel = NotificationChannel(
70
+ channelId,
71
+ "Incoming Calls",
72
+ NotificationManager.IMPORTANCE_HIGH // NotificationManager.IMPORTANCE_HIGH
73
+ ).apply {
74
+ enableVibration(true)
75
+ vibrationPattern = longArrayOf(0, 500, 500, 500)
76
+ lightColor = Color.GREEN
73
77
  setBypassDnd(true)
74
78
  lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
75
- enableVibration(true)
76
- setSound(null, null)
79
+ setSound(null, null)
77
80
  }
78
81
  notificationManager.createNotificationChannel(channel)
79
82
  }
80
83
 
81
- val builder = NotificationCompat.Builder(context, CALL_CHANNEL_ID)
84
+ val builder = NotificationCompat.Builder(context, channelId)
82
85
  .setSmallIcon(context.applicationInfo.icon)
83
86
  .setContentTitle("Incoming $callType call")
84
87
  .setContentText(name)
85
- .setPriority(NotificationCompat.PRIORITY_MAX)
86
- .setCategory(NotificationCompat.CATEGORY_CALL)
87
- .setOngoing(true)
88
+ .setPriority(NotificationCompat.PRIORITY_MAX) // PRIORITY_HIGH
89
+ .setCategory(NotificationCompat.CATEGORY_CALL) // CATEGORY_CALL
90
+ .setOngoing(true)
88
91
  .setAutoCancel(false)
89
- .setFullScreenIntent(fullScreenPendingIntent, true) // Essential for waking screen
90
- .setColor(Color.parseColor("#28a745"))
91
- .setColorized(true)
92
- // ADDING ACTIONS MANUALLY TO CONTROL ORDER
93
- // First Action added = Leftmost button
94
- .addAction(0, "Answer", answerPendingIntent)
92
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
93
+ .setFullScreenIntent(fullScreenPendingIntent, true)
94
+ .setContentIntent(noOpIntent)
95
+ .addAction(0, "Answer", fullScreenPendingIntent)
95
96
  .addAction(0, "Decline", rejectPendingIntent)
96
97
 
98
+ notificationManager.notify(notificationId, builder.build())
97
99
 
98
-
99
-
100
- notificationManager.notify(uuid.hashCode(), builder.build())
101
-
102
- // Start Ringtone
103
100
  try {
104
101
  val ringtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
105
102
  ringtone = RingtoneManager.getRingtone(context, ringtoneUri)
106
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) ringtone?.isLooping = true
103
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
104
+ ringtone?.isLooping = true
105
+ }
107
106
  ringtone?.play()
108
- } catch (e: Exception) { e.printStackTrace() }
107
+ } catch (e: Exception) {
108
+ e.printStackTrace()
109
+ }
109
110
  }
110
111
 
111
112
  fun stopRingtone() {
112
113
  try {
113
114
  ringtone?.let { if (it.isPlaying) it.stop() }
114
115
  ringtone = null
115
- } catch (e: Exception) { e.printStackTrace() }
116
+ } catch (e: Exception) {
117
+ ringtone = null
118
+ }
116
119
  }
117
120
 
121
+ fun connecting(context: Context, uuid: String, name: String, callType: String) {
122
+ val notificationId = uuid.hashCode()
123
+ val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
124
+
125
+ val builder = NotificationCompat.Builder(context, channelId)
126
+ .setSmallIcon(context.applicationInfo.icon)
127
+ .setContentTitle("Incoming $callType call")
128
+ .setContentText("Connecting…") // ✅ show connecting text
129
+ .setSubText("Connecting…") // status line
130
+ .setPriority(NotificationCompat.PRIORITY_MAX)
131
+ .setCategory(NotificationCompat.CATEGORY_CALL)
132
+ .setOngoing(true)
133
+ .setAutoCancel(false)
134
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
135
+ .setProgress(0, 0, true) // ✅ system activity indicator (indeterminate progress bar)
136
+
137
+ notificationManager.notify(notificationId, builder.build())
138
+ }
139
+
140
+ fun aborting(context: Context, uuid: String, name: String, callType: String) {
141
+ val notificationId = uuid.hashCode()
142
+ val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
143
+
144
+ val builder = NotificationCompat.Builder(context, channelId)
145
+ .setSmallIcon(context.applicationInfo.icon)
146
+ .setContentTitle("Incoming $callType call")
147
+ .setContentText("Aborting…") // ✅ show aborting text
148
+ .setSubText("Aborting…") // status line
149
+ .setPriority(NotificationCompat.PRIORITY_MAX)
150
+ .setCategory(NotificationCompat.CATEGORY_CALL)
151
+ .setOngoing(true)
152
+ .setAutoCancel(false)
153
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
154
+ .setProgress(0, 0, true) // ✅ indeterminate progress indicator
155
+
156
+ notificationManager.notify(notificationId, builder.build())
157
+ }
158
+
159
+
118
160
  fun dismissIncomingCall(context: Context, uuid: String?) {
119
161
  stopRingtone()
120
162
  val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
121
- if (uuid != null) {
122
- notificationManager.cancel(uuid.hashCode())
123
- }
163
+ if (uuid != null) notificationManager.cancel(uuid.hashCode())
124
164
  }
125
165
  }