Skip to content

Commit 5542009

Browse files
committed
feat: adjusted notification channels creation and notification posting checks
1 parent 06a19b2 commit 5542009

File tree

9 files changed

+237
-122
lines changed

9 files changed

+237
-122
lines changed

packages/react-native-callingx/android/src/main/java/com/callingx/CallingxModule.kt

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import android.os.IBinder
1313
import android.telecom.DisconnectCause
1414
import android.util.Log
1515
import com.callingx.model.CallAction
16+
import com.callingx.notifications.NotificationChannelsManager
1617
import com.callingx.notifications.NotificationsConfig
1718
import com.facebook.react.bridge.Arguments
1819
import com.facebook.react.bridge.LifecycleEventListener
@@ -50,7 +51,6 @@ class CallingxModule(reactContext: ReactApplicationContext) : NativeCallingxSpec
5051
const val SERVICE_READY_ACTION = "service_ready"
5152
}
5253

53-
// Track binding state carefully
5454
private enum class BindingState {
5555
UNBOUND,
5656
BINDING,
@@ -60,31 +60,19 @@ class CallingxModule(reactContext: ReactApplicationContext) : NativeCallingxSpec
6060
private var callService: CallService? = null
6161
private var bindingState = BindingState.UNBOUND
6262

63-
// private lateinit var notificationsConfig: NotificationsConfig
64-
6563
private var delayedEvents = WritableNativeArray()
6664
private var isModuleInitialized = false
6765

66+
private val notificationChannelsManager = NotificationChannelsManager(reactApplicationContext)
6867
private val callEventBroadcastReceiver = CallEventBroadcastReceiver()
6968
private val appStateListener =
7069
object : LifecycleEventListener {
71-
override fun onHostResume() {
72-
// App resumed - ensure service is bound if needed
73-
Log.d(TAG, "[module] onHostResume: App resumed")
74-
// if (!isBound && shouldServiceBeRunning()) {
75-
// bindToService()
76-
// }
77-
}
70+
override fun onHostResume() {}
7871

79-
override fun onHostPause() {
80-
// App paused - start unbind timer
81-
Log.d(TAG, "[module] onHostPause: App paused")
82-
// startUnbindTimer()
83-
}
72+
override fun onHostPause() {}
8473

8574
override fun onHostDestroy() {
8675
// App destroyed - force unbind
87-
// forceUnbindService()
8876
Log.d(TAG, "[module] onHostDestroy: App destroyed")
8977
unbindServiceSafely()
9078
}
@@ -131,11 +119,18 @@ class CallingxModule(reactContext: ReactApplicationContext) : NativeCallingxSpec
131119

132120
override fun setupAndroid(options: ReadableMap) {
133121
Log.d(TAG, "[module] setupAndroid: Setting up Android: $options")
134-
NotificationsConfig.saveNotificationsConfig(reactApplicationContext, options)
122+
val notificationsConfig =
123+
NotificationsConfig.saveNotificationsConfig(reactApplicationContext, options)
124+
notificationChannelsManager.setNotificationsConfig(notificationsConfig)
125+
notificationChannelsManager.createNotificationChannels()
135126

136127
isModuleInitialized = true
137128
}
138129

130+
override fun canPostNotifications(): Boolean {
131+
return notificationChannelsManager.getNotificationStatus().canPost
132+
}
133+
139134
override fun getInitialEvents(): WritableArray {
140135
// NOTE: writabel native array can be consumed only once, think of getting rid from clear
141136
// event and clear eat immidiate after getting initial events
@@ -166,6 +161,11 @@ class CallingxModule(reactContext: ReactApplicationContext) : NativeCallingxSpec
166161
TAG,
167162
"[module] displayIncomingCall: Displaying incoming call: $callId, $phoneNumber, $callerName, $hasVideo"
168163
)
164+
if (!notificationChannelsManager.getNotificationStatus().canPost) {
165+
promise.reject("ERROR", "Notifications are not granted")
166+
return
167+
}
168+
169169
startCallService(
170170
CallService.ACTION_INCOMING_CALL,
171171
callId,
@@ -199,6 +199,11 @@ class CallingxModule(reactContext: ReactApplicationContext) : NativeCallingxSpec
199199
TAG,
200200
"[module] startCall: Starting outgoing call: $callId, $phoneNumber, $callerName, $hasVideo, $displayOptions"
201201
)
202+
if (!notificationChannelsManager.getNotificationStatus().canPost) {
203+
promise.reject("ERROR", "Notifications are not granted")
204+
return
205+
}
206+
202207
startCallService(
203208
CallService.ACTION_OUTGOING_CALL,
204209
callId,
@@ -218,6 +223,11 @@ class CallingxModule(reactContext: ReactApplicationContext) : NativeCallingxSpec
218223
promise: Promise
219224
) {
220225
Log.d(TAG, "[module] updateDisplay: Updating display: $callId, $phoneNumber, $callerName")
226+
if (!notificationChannelsManager.getNotificationStatus().canPost) {
227+
promise.reject("ERROR", "Notifications are not granted")
228+
return
229+
}
230+
221231
startCallService(
222232
CallService.ACTION_UPDATE_CALL,
223233
callId,

packages/react-native-callingx/android/src/main/java/com/callingx/notifications/CallNotificationManager.kt

Lines changed: 15 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
package com.callingx.notifications
22

3-
import android.Manifest
43
import android.app.Notification
54
import android.content.Context
65
import android.os.Build
76
import android.telecom.DisconnectCause
87
import android.util.Log
98
import androidx.annotation.RequiresApi
10-
import androidx.core.app.NotificationChannelCompat
119
import androidx.core.app.NotificationCompat
1210
import androidx.core.app.NotificationManagerCompat
1311
import androidx.core.app.Person
14-
import androidx.core.content.PermissionChecker
1512
import androidx.core.graphics.drawable.IconCompat
1613
import com.callingx.CallService
1714
import com.callingx.CallingxModule
@@ -36,28 +33,21 @@ class CallNotificationManager(
3633
private const val TAG = "[Callingx] CallNotificationManager"
3734

3835
const val NOTIFICATION_ID = 200
39-
const val NOTIFICATION_ACTION = "notification_action"
4036
}
4137

42-
private var notificationsConfig: NotificationsConfig.ChannelsConfig
43-
44-
init {
45-
notificationsConfig = NotificationsConfig.loadNotificationsConfig(context)
46-
createNotificationChannels(notificationsConfig)
47-
Log.d(TAG, "CallNotificationManager: Notifications config: $notificationsConfig")
48-
}
38+
private var notificationsConfig = NotificationsConfig.loadNotificationsConfig(context)
4939

5040
fun createNotification(call: Call.Registered): Notification {
5141
Log.d(TAG, "createNotification: Creating notification for call ID: ${call.id}")
5242

53-
val contentIntent = NotificationIntentFactory.getLaunchActivityIntent(context, CallingxModule.CALL_ANSWERED_ACTION, call.id)
43+
val contentIntent =
44+
NotificationIntentFactory.getLaunchActivityIntent(
45+
context,
46+
CallingxModule.CALL_ANSWERED_ACTION,
47+
call.id
48+
)
5449
val callStyle = createCallStyle(call)
55-
val channelId =
56-
if (call.isIncoming() && !call.isActive) {
57-
notificationsConfig.incomingChannel.id
58-
} else {
59-
notificationsConfig.outgoingChannel.id
60-
}
50+
val channelId = getChannelId(call)
6151

6252
val builder =
6353
NotificationCompat.Builder(context, channelId)
@@ -68,32 +58,17 @@ class CallNotificationManager(
6858
.setCategory(NotificationCompat.CATEGORY_CALL)
6959
.setPriority(NotificationCompat.PRIORITY_MAX)
7060
.setOngoing(true)
71-
61+
7262
call.displayOptions?.let {
7363
if (it.containsKey(CallService.EXTRA_DISPLAY_SUBTITLE)) {
7464
builder.setContentText(it.getString(CallService.EXTRA_DISPLAY_SUBTITLE))
7565
}
7666
}
7767

78-
// if (call.isOnHold) {
79-
// val activateAction = TelecomCallAction.Activate
80-
// builder.addAction(
81-
// R.drawable.ic_phone_paused_24,
82-
// "Resume",
83-
// getPendingIntent(activateAction)
84-
// )
85-
// }
86-
8768
return builder.build()
8869
}
8970

90-
/** Updates, creates or dismisses a CallStyle notification based on the given [TelecomCall] */
9171
fun updateCallNotification(call: Call) {
92-
if (!canPostNotifications()) {
93-
Log.w(TAG, "updateCallNotification: Notifications are not granted, skipping update")
94-
return
95-
}
96-
9772
when (call) {
9873
Call.None, is Call.Unregistered -> {
9974
Log.d(TAG, "Dismissing notification (call is None or Unregistered)")
@@ -111,27 +86,12 @@ class CallNotificationManager(
11186
notificationManager.cancel(NOTIFICATION_ID)
11287
}
11388

114-
fun createNotificationChannels(notificationsConfig: NotificationsConfig.ChannelsConfig) {
115-
val incomingChannel = createNotificationChannel(notificationsConfig.incomingChannel)
116-
val ongoingChannel = createNotificationChannel(notificationsConfig.outgoingChannel)
117-
118-
notificationManager.createNotificationChannelsCompat(
119-
listOf(
120-
incomingChannel,
121-
ongoingChannel,
122-
),
123-
)
124-
Log.d(TAG, "createNotificationChannels: Notification channels registered")
125-
}
126-
127-
private fun createNotificationChannel(config: NotificationsConfig.ChannelParams): NotificationChannelCompat {
128-
return NotificationChannelCompat.Builder(config.id, config.importance)
129-
.apply {
130-
setName(config.name)
131-
setVibrationEnabled(config.vibration)
132-
ResourceUtils.getSoundUri(context, config.sound)?.let { setSound(it, null) }
133-
}
134-
.build()
89+
private fun getChannelId(call: Call.Registered): String {
90+
return if (call.isIncoming() && !call.isActive) {
91+
notificationsConfig.incomingChannel.id
92+
} else {
93+
notificationsConfig.outgoingChannel.id
94+
}
13595
}
13696

13797
private fun createCallStyle(call: Call.Registered): NotificationCompat.CallStyle {
@@ -184,17 +144,4 @@ class CallNotificationManager(
184144
.setImportant(true)
185145
.build()
186146
}
187-
188-
private fun canPostNotifications(): Boolean {
189-
// POST_NOTIFICATIONS permission is only required on Android 13+
190-
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
191-
return true
192-
}
193-
194-
val permission = PermissionChecker.checkSelfPermission(
195-
context,
196-
Manifest.permission.POST_NOTIFICATIONS,
197-
)
198-
return permission == PermissionChecker.PERMISSION_GRANTED
199-
}
200147
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package com.callingx.notifications
2+
3+
import android.Manifest
4+
import android.content.Context
5+
import android.os.Build
6+
import android.util.Log
7+
import androidx.core.app.NotificationChannelCompat
8+
import androidx.core.app.NotificationManagerCompat
9+
import androidx.core.content.PermissionChecker
10+
11+
class NotificationChannelsManager(
12+
private val context: Context,
13+
private val notificationManager: NotificationManagerCompat =
14+
NotificationManagerCompat.from(context)
15+
) {
16+
17+
private var notificationsConfig: NotificationsConfig.ChannelsConfig? = null
18+
19+
companion object {
20+
private const val TAG = "[Callingx] NotificationChannelsManager"
21+
}
22+
23+
data class NotificationStatus(
24+
val canPost: Boolean,
25+
val hasPermissions: Boolean,
26+
val areNotificationsEnabled: Boolean,
27+
val isIncomingChannelEnabled: Boolean,
28+
val isOutgoingChannelEnabled: Boolean,
29+
)
30+
31+
fun setNotificationsConfig(notificationsConfig: NotificationsConfig.ChannelsConfig) {
32+
this.notificationsConfig = notificationsConfig
33+
}
34+
35+
fun createNotificationChannels() {
36+
notificationsConfig?.let {
37+
val incomingChannel = createNotificationChannel(it.incomingChannel)
38+
val ongoingChannel = createNotificationChannel(it.outgoingChannel)
39+
40+
notificationManager.createNotificationChannelsCompat(
41+
listOf(
42+
incomingChannel,
43+
ongoingChannel,
44+
),
45+
)
46+
Log.d(TAG, "createNotificationChannels: Notification channels registered")
47+
}
48+
}
49+
50+
fun getNotificationStatus(): NotificationStatus {
51+
val areNotificationsEnabled = areNotificationsEnabled()
52+
val hasPermissions = hasNotificationPermissions()
53+
val isIncomingChannelEnabled = isChannelEnabled(notificationsConfig?.incomingChannel?.id)
54+
val isOutgoingChannelEnabled = isChannelEnabled(notificationsConfig?.outgoingChannel?.id)
55+
56+
val canPost =
57+
areNotificationsEnabled &&
58+
hasPermissions &&
59+
isIncomingChannelEnabled &&
60+
isOutgoingChannelEnabled
61+
62+
return NotificationStatus(
63+
canPost,
64+
hasPermissions,
65+
areNotificationsEnabled,
66+
isIncomingChannelEnabled,
67+
isOutgoingChannelEnabled
68+
)
69+
}
70+
71+
private fun createNotificationChannel(
72+
config: NotificationsConfig.ChannelParams
73+
): NotificationChannelCompat {
74+
Log.d(TAG, "createNotificationChannel: Creating notification channel: ${config.id}, ${config.name}, ${config.importance}, ${config.vibration}, ${config.sound}")
75+
return NotificationChannelCompat.Builder(config.id, config.importance)
76+
.apply {
77+
setName(config.name)
78+
setVibrationEnabled(config.vibration)
79+
ResourceUtils.getSoundUri(context, config.sound)?.let { setSound(it, null) }
80+
}
81+
.build()
82+
}
83+
84+
private fun areNotificationsEnabled(): Boolean {
85+
return notificationManager.areNotificationsEnabled()
86+
}
87+
88+
private fun hasNotificationPermissions(): Boolean {
89+
// POST_NOTIFICATIONS permission is only required on Android 13+
90+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
91+
return true
92+
}
93+
94+
val permission =
95+
PermissionChecker.checkSelfPermission(
96+
context,
97+
Manifest.permission.POST_NOTIFICATIONS,
98+
)
99+
return permission == PermissionChecker.PERMISSION_GRANTED
100+
}
101+
102+
private fun isChannelEnabled(channelId: String?): Boolean {
103+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
104+
return true
105+
}
106+
107+
if (channelId == null) {
108+
return false
109+
}
110+
111+
val channel = notificationManager.getNotificationChannel(channelId)
112+
return channel != null && channel.importance != NotificationManagerCompat.IMPORTANCE_NONE
113+
}
114+
}

0 commit comments

Comments
 (0)