diff --git a/docusaurus/docs/Android/05-ui-cookbook/05-incoming-and-outgoing-call.mdx b/docusaurus/docs/Android/05-ui-cookbook/05-incoming-and-outgoing-call.mdx
index e405a5d0d8..58b13cdb66 100644
--- a/docusaurus/docs/Android/05-ui-cookbook/05-incoming-and-outgoing-call.mdx
+++ b/docusaurus/docs/Android/05-ui-cookbook/05-incoming-and-outgoing-call.mdx
@@ -87,8 +87,5 @@ fun MyRingingCallScreen() {
So you'll be able to render your own composable or navigate to a different screen depending on the call state.
## Sounds
-The SDK plays default sounds for incoming and outgoing calls. You can customize the sounds by passing your own instance of the `Sounds` class to `StreamVideoBuilder`.
-`Sounds` has two properties, `incomingCallSound` and `outgoingCallSound`. You need to assign raw resource identifiers to these properties. These identifiers correspond to audio files in your project's `res/raw` directory.
-
-To disable sounds, pass `null` to `incomingCallSound` or `outgoingCallSound`.
\ No newline at end of file
+The SDK plays sounds for incoming and outgoing calls. Read [here](../06-advanced/01-ringing.mdx#sounds) for more details.
\ No newline at end of file
diff --git a/docusaurus/docs/Android/06-advanced/01-ringing.mdx b/docusaurus/docs/Android/06-advanced/01-ringing.mdx
index 26d86eae60..c16b190b36 100644
--- a/docusaurus/docs/Android/06-advanced/01-ringing.mdx
+++ b/docusaurus/docs/Android/06-advanced/01-ringing.mdx
@@ -2,49 +2,70 @@
title: Ringing
description: How to ring the call and notify all members
---
+
The `Call` object provides several options to ring and notify users about a call.
+
### Create and start a ringing call
-To create a ring call, we need to set the `ring` flag to `true` and provide the list of members we want to call. It is important to note that the caller should also be included in the list of members.
+To create a ring call, we need to set the `ring` flag to `true` and provide the list of members we
+want to call. It is important to note that the caller should also be included in the list of
+members.
For this, you can use the `create` method from the `Call` object.
+
```kotlin
val call = client.call("default", "123")
call.create(ring = true, members = listOf("caller-id", "receiver-1", "receiver-2"))
```
-When ring is `true`, a push notification will be sent to the members, provided you have the required setup for push notifications.
-For more details around push notifications, please check [this page](./02-push-notifications/01-overview.mdx).
+
+When ring is `true`, a push notification will be sent to the members, provided you have the required
+setup for push notifications.
+For more details around push notifications, please
+check [this page](./02-push-notifications/01-overview.mdx).
If ring is `false`, no push notification will be sent.
### Ring an existing call
+
If you are sure that a call exists, you can use the `get` method instead:
+
```kotlin
val call = client.call("default", "123")
call.get()
call.ring()
```
-The `get()` - `ring()` combination is better used for when calls are created and managed externally via another system.
+
+The `get()` - `ring()` combination is better used for when calls are created and managed externally
+via another system.
### Monitor the outgoing call state
+
The state of the ringing call is available via the `StreamVideo` client.
+
```kotlin
val client = StreamVideo.instance()
val ringingCall = client.state.ringingCall
```
+
This will give you a `StateFlow` which can be monitored.
+
```kotlin
ringingCall.collectLatest { call ->
- // There is a ringing call
+ // There is a ringing call
}
```
+
or simply just get a current value.
+
```kotlin
val call = ringingCall.value
```
+
### Canceling an outgoing call
+
To cancel an outgoing call you can simply `reject` the call from the caller side.
The `reject()` method will notify the endpoint that the call is being rejected and corresponding
events will be sent. In order to cleanup on the caller side, a call to `leave()` is required.
-These two usually go together, unless there is a specific reason to keep the channel open for further
+These two usually go together, unless there is a specific reason to keep the channel open for
+further
events.
```kotlin
@@ -60,42 +81,56 @@ will receive a push notification about an incoming call.
By default the SDK will show the push notification (with a call style) with an option to either
accept or decline the call.
-When the user clicks on a push notification. There is an intent fired `ACTION_REJECT_CALL` or `ACTION_ACCEPT_CALL`.
+When the user clicks on a push notification. There is an intent fired `ACTION_REJECT_CALL`
+or `ACTION_ACCEPT_CALL`.
The
-You can learn more about how to setup [push notifications in the docs](./02-push-notifications/01-overview.mdx).
+You can learn more about how to
+setup [push notifications in the docs](./02-push-notifications/01-overview.mdx).
The docs also explain how to customize the notifications.
### Accept an incoming call
The compose SDK provides built-in components to render and handle an incoming call.
-One of them is `StreamCallActivity`. This abstract activity handles everything that is needed for a call.
-Stream also provides a default compose implementation of this activity called `ComposeStreamCallActivity`.
+One of them is `StreamCallActivity`. This abstract activity handles everything that is needed for a
+call.
+Stream also provides a default compose implementation of this activity
+called `ComposeStreamCallActivity`.
-These components are already predefined and registered in the SDK. If you want to customize them you can easily extend them as any other activity in Android.
+These components are already predefined and registered in the SDK. If you want to customize them you
+can easily extend them as any other activity in Android.
For more details check:
+
* [UI Component docs for incoming calls](../04-ui-components/04-call/04-ringing-call.mdx)
-* UI Cookbook how to build [your own incoming call UI](../05-ui-cookbook/05-incoming-and-outgoing-call.mdx)
+* UI Cookbook how to
+ build [your own incoming call UI](../05-ui-cookbook/05-incoming-and-outgoing-call.mdx)
+
+The Stream SDK provides a way to accept a call within the code so if you are building a new UI, you
+can do this via the SDK API.
-The Stream SDK provides a way to accept a call within the code so if you are building a new UI, you can do this via the SDK API.
```kotlin
call.accept()
call.join()
```
+
The above calls are all you need to accept and join a call.
Its important to note that if there is already an ongoing call you first have to leave that call.
+
```kotlin
val client = StreamVideo.instance()
val activeCall = client.start.activeCall.value
if (activeCall != null) {
- activeCall.leave()
+ activeCall.leave()
}
```
+
All this needs to be done with a component that handles the accept action.
+
```xml
+
```
@@ -105,6 +140,7 @@ Clicking the notification will automatically reject the call.
There are certain instances that you might want to do this manually in your code.
Stream offers a simple API to do this.
+
```kotlin
call.reject()
```
@@ -112,7 +148,82 @@ call.reject()
Note that rejecting the call will notify the caller and other members that the participant rejected
the call. However it will not clean up the local `call` state.
For this you need to leave the call by using:
+
```kotlin
call.leave()
```
+## Ringing sounds
+
+The SDK plays sounds for incoming and outgoing calls. The SDK bundles two sounds for this
+purpose.
+
+### Customizing the ringing sounds
+
+The ringing sounds can be customized in two ways:
+1. Override the bundled resources inside your application.
+2. Provide your own `RingingConfig` to the `StreamVideoBuilder`.
+
+#### Override the bundled resources
+
+The resources are: `/raw/call_incoming_sound.mp3` and `/raw/call_outgoing_sound.mp3`.
+You can place your own `call_incoming_sound.mp3` and `call_outgoing_sound.mp3` files in the `res/raw` directory of your app.
+
+#### Provide your own `RingingConfig`
+
+You can customize the sounds by creating a `RingingConfig`.
+
+:::note
+Currently the SDK accepts a `Sounds` object in the builder, so once you have a `RingingConfig`, you can
+create a `Sounds` object via `ringingConfig.toSounds()` and pass it to the `StreamVideoBuilder`.
+:::
+
+:::caution
+The `Sounds` class is deprecated and will entirely be replaced by `RingingConfig` in the future.
+The current `Sounds` constructor which accepts two integers will always return an `emptyRingingConfig()`
+with muted sounds.
+:::
+
+The `RingingConfig` interface defines two properties:
+
+- `incomingCallSoundUri`: The URI for the incoming call sound.
+- `outgoingCallSoundUri`: The URI for the outgoing call sound.
+
+You can implement this interface and provide your own values for the properties (e.g. a user chosen
+URI). After that, create a `Sounds` object using the `RingingConfig` instance (e.g. `Sounds(ringingConfig)`) and pass the `Sounds` object to the SDK builder.
+If one of the `Uri`s is null the SDK will simply not play that sound and log this fact instead.
+
+For example, to create a `RingingConfig` that only includes an incoming sound you can extend it as shown below, setting `outgoingCallSoundUri` is `null`:
+
+```kotlin
+class IncomingOnlyRingingConfig : RingingConfig {
+ override val incomingCallSoundUri: Uri =
+ Uri.parse("android.resource://$packageName/${R.raw.custom_incoming_sound}")
+
+ override val outgoingCallSoundUri: Uri? = null // Outgoing sound will be muted
+}
+```
+
+`RingingConfig` can also be created via several factory methods:
+
+- `defaultResourcesRingingConfig` - This method returns a `RingingConfig` that uses the SDK's
+ default sounds for both incoming and outgoing calls
+- `resRingingConfig` - This method returns a `RingingConfig` that uses a resource identifier for both incoming and outgoing calls.
+- `uriRingingConfig(Uri, Uri)` - Returns a `RingingConfig` that is configured with two `Uri` objects for the corresponding sounds.
+- `emptyRingingConfig` - The SDK will not play any sounds for incoming and outgoing calls.
+
+For further customization its best to provide your own `RingingConfig` implementation.
+Such use cases may include a user setting to choose a sound or a custom sound that is not bundled.
+
+For example:
+```kotlin
+data class UserRingingConfig(
+ val incomingCallSoundUri: Uri,
+ val outgoingCallSoundUri: Uri
+) : RingingConfig
+```
+
+:::note
+If the sound resources cannot be found (Uri or resource ID) the SDK will simply not play any sound and
+log the error.
+:::
\ No newline at end of file
diff --git a/stream-video-android-core/api/stream-video-android-core.api b/stream-video-android-core/api/stream-video-android-core.api
index 2b05f0c989..efe1132d02 100644
--- a/stream-video-android-core/api/stream-video-android-core.api
+++ b/stream-video-android-core/api/stream-video-android-core.api
@@ -4476,17 +4476,29 @@ public final class io/getstream/video/android/core/socket/SocketState$NotConnect
public fun toString ()Ljava/lang/String;
}
+public abstract interface class io/getstream/video/android/core/sounds/RingingConfig {
+ public abstract fun getIncomingCallSoundUri ()Landroid/net/Uri;
+ public abstract fun getOutgoingCallSoundUri ()Landroid/net/Uri;
+}
+
+public final class io/getstream/video/android/core/sounds/RingingConfigKt {
+ public static final fun defaultResourcesRingingConfig (Landroid/content/Context;)Lio/getstream/video/android/core/sounds/RingingConfig;
+ public static final fun deviceRingtoneRingingConfig (Landroid/content/Context;)Lio/getstream/video/android/core/sounds/RingingConfig;
+ public static final fun emptyRingingConfig ()Lio/getstream/video/android/core/sounds/RingingConfig;
+ public static final fun resRingingConfig (Landroid/content/Context;II)Lio/getstream/video/android/core/sounds/RingingConfig;
+ public static final fun toSounds (Lio/getstream/video/android/core/sounds/RingingConfig;)Lio/getstream/video/android/core/sounds/Sounds;
+ public static final fun uriRingingConfig (Landroid/net/Uri;Landroid/net/Uri;)Lio/getstream/video/android/core/sounds/RingingConfig;
+}
+
public final class io/getstream/video/android/core/sounds/Sounds {
- public fun ()V
- public fun (Ljava/lang/Integer;Ljava/lang/Integer;)V
- public synthetic fun (Ljava/lang/Integer;Ljava/lang/Integer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
- public final fun component1 ()Ljava/lang/Integer;
- public final fun component2 ()Ljava/lang/Integer;
- public final fun copy (Ljava/lang/Integer;Ljava/lang/Integer;)Lio/getstream/video/android/core/sounds/Sounds;
- public static synthetic fun copy$default (Lio/getstream/video/android/core/sounds/Sounds;Ljava/lang/Integer;Ljava/lang/Integer;ILjava/lang/Object;)Lio/getstream/video/android/core/sounds/Sounds;
+ public fun (II)V
+ public synthetic fun (IIILkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public fun (Lio/getstream/video/android/core/sounds/RingingConfig;)V
+ public final fun component1 ()Lio/getstream/video/android/core/sounds/RingingConfig;
+ public final fun copy (Lio/getstream/video/android/core/sounds/RingingConfig;)Lio/getstream/video/android/core/sounds/Sounds;
+ public static synthetic fun copy$default (Lio/getstream/video/android/core/sounds/Sounds;Lio/getstream/video/android/core/sounds/RingingConfig;ILjava/lang/Object;)Lio/getstream/video/android/core/sounds/Sounds;
public fun equals (Ljava/lang/Object;)Z
- public final fun getIncomingCallSound ()Ljava/lang/Integer;
- public final fun getOutgoingCallSound ()Ljava/lang/Integer;
+ public final fun getRingingConfig ()Lio/getstream/video/android/core/sounds/RingingConfig;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.kt
index 180dc5ccb0..a19e1c9399 100644
--- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.kt
+++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/StreamVideoBuilder.kt
@@ -34,6 +34,8 @@ import io.getstream.video.android.core.notifications.internal.storage.DeviceToke
import io.getstream.video.android.core.permission.android.DefaultStreamPermissionCheck
import io.getstream.video.android.core.permission.android.StreamPermissionCheck
import io.getstream.video.android.core.sounds.Sounds
+import io.getstream.video.android.core.sounds.defaultResourcesRingingConfig
+import io.getstream.video.android.core.sounds.toSounds
import io.getstream.video.android.model.ApiKey
import io.getstream.video.android.model.User
import io.getstream.video.android.model.UserToken
@@ -99,7 +101,7 @@ public class StreamVideoBuilder @JvmOverloads constructor(
private val runForegroundServiceForCalls: Boolean = true,
private val callServiceConfig: CallServiceConfig? = null,
private val localSfuAddress: String? = null,
- private val sounds: Sounds = Sounds(),
+ private val sounds: Sounds = defaultResourcesRingingConfig(context).toSounds(),
private val crashOnMissingPermission: Boolean = false,
private val permissionCheck: StreamPermissionCheck = DefaultStreamPermissionCheck(),
private val audioUsage: Int = defaultAudioUsage,
diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt
index 897e0868ab..e1385b962d 100644
--- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt
+++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt
@@ -24,9 +24,16 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.ServiceInfo
+import android.media.AudioAttributes
+import android.media.AudioFocusRequest
+import android.media.AudioManager
import android.media.MediaPlayer
+import android.media.Ringtone
+import android.media.RingtoneManager
+import android.net.Uri
+import android.os.Build
import android.os.IBinder
-import androidx.annotation.RawRes
+import androidx.annotation.RequiresApi
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import io.getstream.log.StreamLog
@@ -77,6 +84,9 @@ internal open class CallService : Service() {
// Call sounds
private var mediaPlayer: MediaPlayer? = null
+ private var audioManager: AudioManager? = null
+ private var audioFocusRequest: AudioFocusRequest? = null
+ private var ringtone: Ringtone? = null
internal companion object {
private const val TAG = "CallServiceCompanion"
@@ -429,7 +439,9 @@ internal open class CallService : Service() {
when (it) {
is RingingState.Incoming -> {
if (!it.acceptedByMe) {
- playCallSound(streamVideo.sounds.incomingCallSound)
+ playCallSound(
+ streamVideo.sounds.ringingConfig.incomingCallSoundUri,
+ )
} else {
stopCallSound() // Stops sound sooner than Active. More responsive.
}
@@ -437,7 +449,9 @@ internal open class CallService : Service() {
is RingingState.Outgoing -> {
if (!it.acceptedByCallee) {
- playCallSound(streamVideo.sounds.outgoingCallSound)
+ playCallSound(
+ streamVideo.sounds.ringingConfig.outgoingCallSoundUri,
+ )
} else {
stopCallSound() // Stops sound sooner than Active. More responsive.
}
@@ -464,37 +478,114 @@ internal open class CallService : Service() {
}
}
- private fun playCallSound(@RawRes sound: Int?) {
- sound?.let {
- try {
- mediaPlayer?.let {
- if (!it.isPlaying) {
- setMediaPlayerDataSource(it, sound)
- it.start()
+ private fun playCallSound(soundUri: Uri?) {
+ try {
+ requestAudioFocus(
+ context = applicationContext,
+ onGranted = {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ playWithRingtone(soundUri)
+ } else {
+ playWithMediaPlayer(soundUri)
}
+ },
+ )
+ } catch (e: Exception) {
+ logger.d { "[Sounds] Error playing call sound: ${e.message}" }
+ }
+ }
+
+ private fun requestAudioFocus(context: Context, onGranted: () -> Unit) {
+ if (audioManager == null) {
+ audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
+ }
+
+ val isGranted = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ if (audioFocusRequest == null) {
+ audioFocusRequest = AudioFocusRequest
+ .Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)
+ .setAudioAttributes(
+ AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
+ .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+ .build(),
+ )
+ .setAcceptsDelayedFocusGain(false)
+ .build()
+ }
+
+ audioFocusRequest?.let {
+ audioManager?.requestAudioFocus(it) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
+ } ?: false
+ } else {
+ audioManager?.requestAudioFocus(
+ null,
+ AudioManager.STREAM_RING,
+ AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE,
+ ) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
+ }
+
+ logger.d { "[Sounds] Audio focus " + if (isGranted) "granted" else "not granted" }
+ if (isGranted) onGranted()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.P)
+ private fun playWithRingtone(soundUri: Uri?) {
+ soundUri?.let {
+ if (ringtone?.isPlaying == true) ringtone?.stop()
+ ringtone = RingtoneManager.getRingtone(applicationContext, soundUri)
+ if (ringtone?.isPlaying == false) {
+ ringtone?.isLooping = true
+ ringtone?.play()
+
+ logger.d { "[Sounds] Sound playing with Ringtone" }
+ }
+ }
+ }
+
+ private fun playWithMediaPlayer(soundUri: Uri?) {
+ soundUri?.let {
+ mediaPlayer?.let { mediaPlayer ->
+ if (!mediaPlayer.isPlaying) {
+ setMediaPlayerDataSource(mediaPlayer, soundUri)
+ mediaPlayer.start()
+
+ logger.d { "[Sounds] Sound playing with MediaPlayer" }
}
- } catch (e: IllegalStateException) {
- logger.d { "Error playing call sound." }
}
}
}
- private fun setMediaPlayerDataSource(mediaPlayer: MediaPlayer, @RawRes resId: Int) {
+ private fun setMediaPlayerDataSource(mediaPlayer: MediaPlayer, uri: Uri) {
mediaPlayer.reset()
- val afd = resources.openRawResourceFd(resId)
- if (afd != null) {
- mediaPlayer.setDataSource(afd.fileDescriptor, afd.startOffset, afd.length)
- afd.close()
- }
+ mediaPlayer.setDataSource(applicationContext, uri)
mediaPlayer.isLooping = true
mediaPlayer.prepare()
}
private fun stopCallSound() {
try {
- if (mediaPlayer?.isPlaying == true) mediaPlayer?.stop()
- } catch (e: IllegalStateException) {
- logger.d { "Error stopping call sound. MediaPlayer might have already been released." }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ logger.d { "[Sounds] Stopping Ringtone sound" }
+ if (ringtone?.isPlaying == true) ringtone?.stop()
+ } else {
+ logger.d { "[Sounds] Stopping MediaPlayer sound" }
+ if (mediaPlayer?.isPlaying == true) mediaPlayer?.stop()
+ }
+ } catch (e: Exception) {
+ logger.d { "[Sounds] Error stopping call sound: ${e.message}" }
+ } finally {
+ abandonAudioFocus()
+ }
+ }
+
+ private fun abandonAudioFocus() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ audioFocusRequest?.let {
+ audioManager?.abandonAudioFocusRequest(it)
+ }
+ } else {
+ audioManager?.abandonAudioFocus(null)
}
}
@@ -675,7 +766,7 @@ internal open class CallService : Service() {
unregisterToggleCameraBroadcastReceiver()
// Call sounds
- clearMediaPlayer()
+ cleanAudioResources()
// Stop any jobs
serviceScope.cancel()
@@ -684,9 +775,17 @@ internal open class CallService : Service() {
stopSelf()
}
- private fun clearMediaPlayer() {
+ private fun cleanAudioResources() {
+ logger.d { "[Sounds] Cleaning audio resources" }
+
+ if (ringtone?.isPlaying == true) ringtone?.stop()
+ ringtone = null
+
mediaPlayer?.release()
mediaPlayer = null
+
+ audioManager = null
+ audioFocusRequest = null
}
// This service does not return a Binder
diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/sounds/RingingConfig.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/sounds/RingingConfig.kt
new file mode 100644
index 0000000000..9042748f31
--- /dev/null
+++ b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/sounds/RingingConfig.kt
@@ -0,0 +1,144 @@
+/*
+ * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved.
+ *
+ * Licensed under the Stream License;
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://github.com/GetStream/stream-video-android/blob/main/LICENSE
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.getstream.video.android.core.sounds
+
+import android.content.Context
+import android.media.RingtoneManager
+import android.net.Uri
+import androidx.annotation.RawRes
+import io.getstream.log.StreamLog
+import io.getstream.video.android.core.R
+import io.getstream.video.android.core.utils.safeCall
+import org.jetbrains.annotations.ApiStatus
+
+// Interface & API
+/**
+ * Interface representing a ringing configuration.
+ *
+ * @see defaultResourcesRingingConfig
+ * @see deviceRingtoneRingingConfig
+ * @see emptyRingingConfig
+ * @see resRingingConfig
+ * @see uriRingingConfig
+ */
+public interface RingingConfig {
+ val incomingCallSoundUri: Uri?
+ val outgoingCallSoundUri: Uri?
+}
+
+/**
+ * Contains all the sounds that the SDK uses.
+ */
+@Deprecated(
+ message = "Sounds will be deprecated in the future and replaced with RingingConfig. It is recommended to use one of the factory methods along with toSounds() to create the Sounds object.",
+ replaceWith = ReplaceWith("SoundConfig"),
+ level = DeprecationLevel.WARNING,
+)
+public data class Sounds(val ringingConfig: RingingConfig) {
+ @ApiStatus.ScheduledForRemoval(inVersion = "1.0.18")
+ @Deprecated(
+ message = "Deprecated. This Constructor will now return a sound configuration with no sounds. Use constructor with SoundConfig parameter instead.",
+ replaceWith = ReplaceWith("defaultResourcesRingingConfig(context).toSounds()"),
+ level = DeprecationLevel.ERROR,
+ )
+ constructor(
+ @RawRes incomingCallSound: Int = R.raw.call_incoming_sound,
+ @RawRes outgoingCallSound: Int = R.raw.call_outgoing_sound,
+ ) : this(emptyRingingConfig())
+}
+
+// Factories
+/**
+ * Returns a ringing config that uses the SDK default sounds for incoming and outgoing calls.
+ *
+ * @param context Context used for retrieving the sounds.
+ */
+public fun defaultResourcesRingingConfig(context: Context): RingingConfig = object : RingingConfig {
+ override val incomingCallSoundUri: Uri? = R.raw.call_incoming_sound.toUriOrNUll(context)
+ override val outgoingCallSoundUri: Uri? = R.raw.call_outgoing_sound.toUriOrNUll(context)
+}
+
+/**
+ * Returns a ringing config that uses the device ringtone for incoming calls and the SDK default ringing tone for outgoing calls.
+ *
+ * @param context Context used for retrieving the sounds.
+ */
+public fun deviceRingtoneRingingConfig(context: Context): RingingConfig = object : RingingConfig {
+ private val streamResSoundConfig = defaultResourcesRingingConfig(context)
+ override val incomingCallSoundUri: Uri?
+ get() = safeCall(default = null) {
+ RingtoneManager.getActualDefaultRingtoneUri(
+ context,
+ RingtoneManager.TYPE_RINGTONE,
+ )
+ } ?: streamResSoundConfig.incomingCallSoundUri
+ override val outgoingCallSoundUri: Uri? = streamResSoundConfig.outgoingCallSoundUri
+}
+
+/**
+ * Returns a ringing config that uses custom resources for incoming and outgoing call sounds.
+ *
+ * @param context Context used for retrieving the sounds.
+ * @param incomingCallSoundResId The resource ID for the incoming call sound.
+ * @param outgoingCallSoundResId The resource ID for the outgoing call sound.
+ */
+public fun resRingingConfig(
+ context: Context,
+ @RawRes incomingCallSoundResId: Int,
+ @RawRes outgoingCallSoundResId: Int,
+) = object : RingingConfig {
+ override val incomingCallSoundUri: Uri? = incomingCallSoundResId.toUriOrNUll(context)
+ override val outgoingCallSoundUri: Uri? = outgoingCallSoundResId.toUriOrNUll(context)
+}
+
+/**
+ * Returns a ringing config that uses custom URIs for incoming and outgoing call sounds.
+ *
+ * @param incomingCallSoundUri The URI for the incoming call sound.
+ * @param outgoingCallSoundUri The URI for the outgoing call sound.
+ */
+public fun uriRingingConfig(
+ incomingCallSoundUri: Uri,
+ outgoingCallSoundUri: Uri,
+) = object : RingingConfig {
+ override val incomingCallSoundUri: Uri = incomingCallSoundUri
+ override val outgoingCallSoundUri: Uri = outgoingCallSoundUri
+}
+
+/**
+ * Returns a ringing config that mutes (disables) incoming and outgoing call sounds.
+ */
+public fun emptyRingingConfig(): RingingConfig = object : RingingConfig {
+ override val incomingCallSoundUri: Uri? = null
+ override val outgoingCallSoundUri: Uri? = null
+}
+
+/**
+ * Converts a ringing config to a [Sounds] object.
+ */
+public fun RingingConfig.toSounds() = Sounds(this)
+
+// Internal utilities
+private fun Int?.toUriOrNUll(context: Context): Uri? =
+ safeCall(default = null) {
+ if (this != null) {
+ Uri.parse("android.resource://${context.packageName}/$this")
+ } else {
+ StreamLog.w("RingingConfig") { "Resource ID is null. Returning null URI." }
+ null
+ }
+ }
diff --git a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/sounds/Sounds.kt b/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/sounds/Sounds.kt
deleted file mode 100644
index ececedd5a9..0000000000
--- a/stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/sounds/Sounds.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved.
- *
- * Licensed under the Stream License;
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://github.com/GetStream/stream-video-android/blob/main/LICENSE
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package io.getstream.video.android.core.sounds
-
-import androidx.annotation.RawRes
-import io.getstream.video.android.core.R
-
-/**
- * Contains all the sounds that the SDK uses.
- *
- * @param incomingCallSound Resource used as a ringtone for incoming calls.
- * @param outgoingCallSound Resource used as a ringing tone for outgoing calls.
- */
-data class Sounds(
- @RawRes val incomingCallSound: Int? = R.raw.call_incoming_sound,
- @RawRes val outgoingCallSound: Int? = R.raw.call_outgoing_sound,
-)
diff --git a/stream-video-android-core/src/main/res/raw/call_busy_sound.mp3 b/stream-video-android-core/src/main/res/raw/call_busy_sound.mp3
deleted file mode 100644
index 7778f3e175..0000000000
Binary files a/stream-video-android-core/src/main/res/raw/call_busy_sound.mp3 and /dev/null differ