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