Skip to content

Commit d6e4585

Browse files
committed
feat: added android ringtone support
1 parent 5542009 commit d6e4585

File tree

7 files changed

+158
-119
lines changed

7 files changed

+158
-119
lines changed

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,13 @@ import android.os.IBinder
2626
import android.telecom.DisconnectCause
2727
import android.util.Log
2828
import androidx.annotation.RequiresApi
29+
import com.callingx.model.Call
30+
import com.callingx.model.CallAction
31+
import com.callingx.notifications.CallNotificationManager
2932
import kotlinx.coroutines.CoroutineScope
3033
import kotlinx.coroutines.SupervisorJob
3134
import kotlinx.coroutines.cancel
3235
import kotlinx.coroutines.launch
33-
import com.callingx.model.Call
34-
import com.callingx.model.CallAction
35-
import com.callingx.notifications.CallNotificationManager
3636

3737
/**
3838
* This service handles the app call logic (show notification, record mic, display audio, etc..). It
@@ -104,6 +104,7 @@ class CallService : Service(), CallRepository.Listener {
104104
Log.d(TAG, "[service] onDestroy: TelecomCallService destroyed")
105105

106106
notificationManager.cancelNotifications()
107+
notificationManager.stopRingtone()
107108
telecomRepository.release()
108109
headlessJSManager.release()
109110

@@ -173,6 +174,11 @@ class CallService : Service(), CallRepository.Listener {
173174
TAG,
174175
"[service] updateServiceState: Call registered - Active: ${call.isActive}, OnHold: ${call.isOnHold}, Muted: ${call.isMuted}"
175176
)
177+
178+
if (call.isIncoming()) {
179+
if (!call.isActive) notificationManager.startRingtone()
180+
else notificationManager.stopRingtone()
181+
}
176182
// Update the call state.
177183
if (isInForeground) {
178184
Log.d(
@@ -199,6 +205,7 @@ class CallService : Service(), CallRepository.Listener {
199205
isInForeground = false
200206
}
201207

208+
notificationManager.stopRingtone()
202209
stopSelf()
203210
}
204211
is Call.None -> {
@@ -209,6 +216,7 @@ class CallService : Service(), CallRepository.Listener {
209216
stopForeground(STOP_FOREGROUND_REMOVE)
210217
isInForeground = false
211218
}
219+
notificationManager.stopRingtone()
212220
}
213221
}
214222
}

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

Lines changed: 42 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,57 @@
11
import android.content.Context
22
import android.media.RingtoneManager
33
import android.net.Uri
4+
import android.util.Log
5+
import androidx.core.net.toUri
46

5-
class ResourceUtils {
6-
companion object {
7-
private val resourceIdCache = mutableMapOf<String, Int>()
87

9-
private fun getResourceIdByName(context: Context, name: String?, type: String): Int {
10-
if (name.isNullOrEmpty()) {
11-
return 0
12-
}
8+
object ResourceUtils {
9+
private const val TAG = "[Callingx] ResourceUtils"
1310

14-
val normalizedName = name.lowercase().replace("-", "_")
15-
val key = "${normalizedName}_$type"
11+
private val resourceIdCache = mutableMapOf<String, Int>()
1612

17-
synchronized(resourceIdCache) {
18-
resourceIdCache[key]?.let {
19-
return it
20-
}
13+
private fun getResourceIdByName(context: Context, name: String?, type: String): Int {
14+
if (name.isNullOrEmpty()) {
15+
return 0
16+
}
2117

22-
val packageName = context.packageName
18+
val normalizedName = name.lowercase().replace("-", "_")
19+
val key = "${normalizedName}_$type"
2320

24-
val id = context.resources.getIdentifier(normalizedName, type, packageName)
25-
resourceIdCache[key] = id
26-
return id
21+
synchronized(resourceIdCache) {
22+
resourceIdCache[key]?.let {
23+
return it
2724
}
25+
26+
val packageName = context.packageName
27+
28+
val id = context.resources.getIdentifier(normalizedName, type, packageName)
29+
resourceIdCache[key] = id
30+
return id
2831
}
32+
}
33+
34+
fun getSoundUri(context: Context, sound: String?): Uri? {
35+
Log.d(TAG, "getSoundUri: Getting sound URI for: $sound")
36+
return when {
37+
sound == null -> null
38+
sound.contains("://") -> sound.toUri()
39+
sound.equals("default", ignoreCase = true) -> {
40+
RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
41+
}
2942

30-
fun getSoundUri(context: Context, sound: String?): Uri? {
31-
return when {
32-
sound == null -> null
33-
sound.contains("://") -> Uri.parse(sound)
34-
sound.equals("default", ignoreCase = true) -> {
35-
RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
36-
}
37-
else -> {
38-
// The API user is attempting to set a sound by file name, verify it exists
39-
var soundResourceId = getResourceIdByName(context, sound, "raw")
40-
if (soundResourceId == 0 && sound.contains(".")) {
41-
soundResourceId = getResourceIdByName(context, sound.substringBeforeLast('.'), "raw")
42-
}
43-
if (soundResourceId == 0) {
44-
null
45-
} else {
46-
// Use the actual sound name vs the resource ID, to obtain a stable URI, Issue #341
47-
Uri.parse("android.resource://${context.packageName}/raw/$sound")
48-
}
49-
}
43+
else -> {
44+
// The API user is attempting to set a sound by file name, verify it exists
45+
var soundResourceId = getResourceIdByName(context, sound, "raw")
46+
if (soundResourceId == 0 && sound.contains(".")) {
47+
soundResourceId = getResourceIdByName(context, sound.substringBeforeLast('.'), "raw")
48+
}
49+
if (soundResourceId == 0) {
50+
null
51+
} else {
52+
// Use the actual sound name vs the resource ID, to obtain a stable URI, Issue #341
53+
"android.resource://${context.packageName}/raw/$sound".toUri()
54+
}
5055
}
5156
}
5257
}

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

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package com.callingx.notifications
22

33
import android.app.Notification
44
import android.content.Context
5+
import android.media.Ringtone
6+
import android.media.RingtoneManager
57
import android.os.Build
68
import android.telecom.DisconnectCause
79
import android.util.Log
@@ -37,6 +39,8 @@ class CallNotificationManager(
3739

3840
private var notificationsConfig = NotificationsConfig.loadNotificationsConfig(context)
3941

42+
private var ringtone: Ringtone? = null
43+
4044
fun createNotification(call: Call.Registered): Notification {
4145
Log.d(TAG, "createNotification: Creating notification for call ID: ${call.id}")
4246

@@ -58,7 +62,7 @@ class CallNotificationManager(
5862
.setCategory(NotificationCompat.CATEGORY_CALL)
5963
.setPriority(NotificationCompat.PRIORITY_MAX)
6064
.setOngoing(true)
61-
65+
6266
call.displayOptions?.let {
6367
if (it.containsKey(CallService.EXTRA_DISPLAY_SUBTITLE)) {
6468
builder.setContentText(it.getString(CallService.EXTRA_DISPLAY_SUBTITLE))
@@ -86,6 +90,32 @@ class CallNotificationManager(
8690
notificationManager.cancel(NOTIFICATION_ID)
8791
}
8892

93+
fun startRingtone() {
94+
if (ringtone?.isPlaying == true) {
95+
Log.d(TAG, "startRingtone: Ringtone already playing")
96+
return
97+
}
98+
99+
val soundUri =
100+
ResourceUtils.getSoundUri(context, notificationsConfig.incomingChannel.sound)
101+
?: RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)
102+
103+
ringtone = RingtoneManager.getRingtone(context, soundUri)
104+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
105+
ringtone?.isLooping = true
106+
}
107+
ringtone?.play()
108+
Log.d(TAG, "startRingtone: Ringtone started")
109+
}
110+
111+
fun stopRingtone() {
112+
if (ringtone?.isPlaying == true) {
113+
ringtone?.stop()
114+
Log.d(TAG, "stopRingtone: Ringtone stopped")
115+
}
116+
ringtone = null
117+
}
118+
89119
private fun getChannelId(call: Call.Registered): String {
90120
return if (call.isIncoming() && !call.isActive) {
91121
notificationsConfig.incomingChannel.id

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class NotificationChannelsManager(
1414
NotificationManagerCompat.from(context)
1515
) {
1616

17-
private var notificationsConfig: NotificationsConfig.ChannelsConfig? = null
17+
private var notificationsConfig: NotificationsConfig.Channels? = null
1818

1919
companion object {
2020
private const val TAG = "[Callingx] NotificationChannelsManager"
@@ -28,7 +28,7 @@ class NotificationChannelsManager(
2828
val isOutgoingChannelEnabled: Boolean,
2929
)
3030

31-
fun setNotificationsConfig(notificationsConfig: NotificationsConfig.ChannelsConfig) {
31+
fun setNotificationsConfig(notificationsConfig: NotificationsConfig.Channels) {
3232
this.notificationsConfig = notificationsConfig
3333
}
3434

@@ -71,12 +71,11 @@ class NotificationChannelsManager(
7171
private fun createNotificationChannel(
7272
config: NotificationsConfig.ChannelParams
7373
): NotificationChannelCompat {
74-
Log.d(TAG, "createNotificationChannel: Creating notification channel: ${config.id}, ${config.name}, ${config.importance}, ${config.vibration}, ${config.sound}")
7574
return NotificationChannelCompat.Builder(config.id, config.importance)
7675
.apply {
7776
setName(config.name)
7877
setVibrationEnabled(config.vibration)
79-
ResourceUtils.getSoundUri(context, config.sound)?.let { setSound(it, null) }
78+
setSound(null, null)
8079
}
8180
.build()
8281
}

0 commit comments

Comments
 (0)