From 7f45223d99822b3fe62f1115167ccb710a85899e Mon Sep 17 00:00:00 2001 From: Liviu Timar <65943217+liviu-timar@users.noreply.github.com> Date: Tue, 24 Sep 2024 15:23:55 +0300 Subject: [PATCH] Use device ringtone for incoming call sound (#1179) * Use device ringtone - with fallbacks * Update sound related docs * Revert changes to Sounds and StreamVideoBuilder * Add SoundConfig interface * Make deviceRingtoneSoundConfig default * Safe-call get device ringtone method * Clean up * Shorten list of public sound configs * Refactor SoundConfig factory methods * Update removal version for deprecated Sounds ctor * Update docs & clean-up * Request/abandon audio focus * Use Ringtone or MediaPlayer to play sound * Update with new name and small refactoring of the factory methods * Update compilation errors * Spotless and ApiDump * Update docs, remove unused MP3 in resources, make `context` non-nullable * Improve docs and names --------- Co-authored-by: Aleksandar Apostolov --- .../05-incoming-and-outgoing-call.mdx | 5 +- .../docs/Android/06-advanced/01-ringing.mdx | 139 +++++++++++++++-- .../api/stream-video-android-core.api | 30 ++-- .../video/android/core/StreamVideoBuilder.kt | 4 +- .../internal/service/CallService.kt | 145 +++++++++++++++--- .../android/core/sounds/RingingConfig.kt | 144 +++++++++++++++++ .../video/android/core/sounds/Sounds.kt | 31 ---- .../src/main/res/raw/call_busy_sound.mp3 | Bin 81989 -> 0 bytes 8 files changed, 416 insertions(+), 82 deletions(-) create mode 100644 stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/sounds/RingingConfig.kt delete mode 100644 stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/sounds/Sounds.kt delete mode 100644 stream-video-android-core/src/main/res/raw/call_busy_sound.mp3 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 7778f3e175832eeef3f17ce8cf527e748d813300..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 81989 zcmeFa2Uru^*7!ZCBq4z$w1j2|ML@)aCLl^ekRqUT5m7^xrc&&pB-BtvKm=x*_l0i_HWkOW%gRJG}VRz z`~b!{y12OTBX|ISY;pJB7P84B#C@w5V537P0bkQ_f;7La{Is3kA)$WTwgQxINhE%f z=e8|D?%Vx50=@rsZs?bj{X(~G*%G9q_3fMC{BQa>?%3*JV`~DCC||?hBKkU%zeThe zCf}3z-_X|6Hq-rn;15OsM+b+M{5uup7sT3qr;o0ICRI<9N}=$NeG9x#w%?-v^wV;y z&o+J{KO3M002yln5Ew!jgT+e_q~%Ct<%Q~tmuTtg=`UMBW0+Z3+1NTbxvby7^zioe z3)~tUx^vII1FXmxPC{}@+TqNjIVbW8i%ZK-SJl=xp1;)8($;aU`$pfut-JRh4L^SR zeB#xcnRg#P&kG8}FSDnh%=&_zZ$Syd`V!@rgVsL&`P<=dpS({gUV!K%0Qm8bAOHYE z0ss*IuaLjXkLs`h0BTLQS*b0k2dDpg;V-{;-ltSA+QHuj^nVY_ukQFKp-(Z_?wJd|C@H`>9J3@g6BYcPeh)y zd>r~lwtjh4wM;|i&J(yiZa*RG(4K?(#A7C1q*F;pyJL~mLf}@jw$t&hv!fSG-}O`P zvY)LnEs!z28?_XHmaY_2v~~gK7#u3qMCgmCd(orek~2_i4OJIpBA_4=)5O9auSyu= z_|Km6BeXhR^fgd_>8n@f!Yl@Bxr;@i-7ld+JW7fScj#};4O3n}5qJQQ_cV6*B+YJ!s_Zc4KFX!(#P%KmqWMf z7RAJF z*kueohv)7x3sJ+!g1kg}3f@g^T@(qilIi2!viRKjh;*R-d90|WsiT7hqqwvm)S7x) zK&T^O<}3-f=zc%;YgbNG$u~swJ)GZ-`_h%$vHI5Y9C%Rc$QyW2_kwt-E!R*ud{e_x zV+$oj)F(VBH)SMPqC+RQTx!@MTB65NR%AD>=aw>0z zN@NGDc66=zJjS>=vSeb}Uicdp><+hYN{c$;kA zf8>u8Kp=oE+{%JbP%JnF$vvnYba=hKxHZxH)iB!`Y{@EsdWER)0&Sg6P;L$y3KdCPn7KqPOjF_S}-$k{gO3qH1VJcM}d|_4xdZgK1ko19q(g0%xpcOablr} zG>?}%FmZRU@RDPTvegov75b)Lzj@+s&-%xlOIZmqo8F9qUVvgUp~JJGmbeIUWar}x zgLfKRK?bY=H1d8XRe@!QGI&&urnAb?rIBX9Ar>Amh|mP(Z_0#nzVu6X6k{oDALxqLNz;t}r-?;~%%=)b7@1cYt^@S-TA;|Fp90U<7b z6J{ksoaKuUpcBd8{P)@i3b{#yVq>C7`FzEc>S0bXsXP0uN6+a?%L*F%<4;=y8+ zAo^a41>J|@%k85qp~Z7f9p17-(Vn&^wr7djlkhLZS_L2XZif2GB^N^)6k-6|6UA?Z z9G+Zj&GNa`vHr;du6BDsTK2_vz9*}W?e*DWmr20(zM_YMNvR*gmw(A(em!sgHShd? zlf^%-X91yj(Wza+jMy+f8$O{S0E7rr7QuwGSYNf=KR)wcJn9EPfcv1~A$4oSq+*vS zTM3=BaUgL}WF5NQZacBbK5(MZfLlpBWOVy`8NouQ~R*`H62>$lhyc*D{r%}9FId@Owrjho%FZt z-}B}ksDGE1vcD(@2<0SAwMv+ZeIoJ^eVI}azQ0Fg%^Ns>_e(qUOVI$}a7PCs4V)r8 z4Ze>pfK1AYA&nd$2V8X+`i~?WG=_ZAU`k{~;W52KqffROuV-Ybg2*Rx+^vp@JBQ!t zatfYajgqhlQb+E z3}mhljU1k&QC`2_E@1RN{PU(Ge+?l5T0$yu;o`0?r(^-4bBGyX1=Idd?%xQB=(wXr zXWD_UgcN(a``I?ub|RkWqg5?v5#U7EOC`PB9&N9nsJOCNMMkm9jeGZBumYd5bG;;x z5xFHg7|<_K2(3p8bqpDC`5ynS)%gFXZleh>nsBBNa{-KdonisS8kM|m)|(l+x^yD) zI90N#O21S|D@&F-CPN+9r0zaV5r;Ubig@oho_cMwP|}(96^Q{mT}FLGQm=WkWHlpq z&_v9UkfsUIVmK!XG`SGf&e9LT^m4qbxZ9)V+i@Dfb(0!pv-kGL>h8}771{7P^ysHY zviIf;)9>9q7JhF`Wm;QX+wyY%?U2_e&{#3Hf$9clRu73@oGKvJtjzXg8 zQS=0aT9c-pB3p$o52bvSAqgiUdd=IIZ!!d+C`6yXx$u-MsGTuA~W1j)C< z*{Zg*uffqcW%u`XZko3+-FKJ|tp{X4q=--N?53<}>ZOFm7-pQ{PmbAp_%ao5srs&NtaHzXp)pO(f4sWqD-uCI)n~OHyJE%^_PZK=Oo{5R? zJ7OidDWM{^5`N%8jui3Yw!5P#pPF#D2bPgj7;hS)4)|A`x;f!^CghdNqYI)@rYbBp z&cQ5VBwx%ddZmSQJn!m<*F4_boWyrR|Dsm^!(XSUbDvQbve$hfgq+^CpcV3oTKScb z7&Z@Q!~5P2Q8Z09QrzK#x~iedT2C7DwKkd$P^=<$;cPG(w578tW0u;sLalMDT)_jn zgF~Uubhl!RI*npVJ!KKfvNHOT%&K(ZlB4O@E=c#b*7A66uodav&nVSd^>bT?UF+(Dz7kAo5!y+p5N_#;;W zHv81?EF1PeEm=0~Q_rk4#6*%(RM9cu0wj!9XhO3G+2Tkg;+)DUa9Ir^oW~nEhWbwE z7hV6yW1v<5V1oOBA=oImP*@B>fUu?X?NxD)*ikcoi7cv zR=jhr*d?`i{dLU^Tf9RbIi0>Gy?$Md^PJDk?3))eDtvC9eYs`Y^=8Q6^K4h2`VXhh z!#xhF@7a1he(m|sii6(?{b8Z_H!~0r8pO=tR85b5a{oq1{LW&u=-ZfYg#OK}znn&W zyi+S^pN@61VP3N#{zRadd||c`pWJ5TTe*ueqiixZ|`1~9sy|?jU57pAc_Z{@qo{07!#g|nN;s0(tHy2 z?J`!&ot9aRa(IooH&mMF7!4?75Yk8ab57GPyUctlfg#NhX{A)P?0P$)MkOmSE z$`hR$A(%og52b#UA$0nB5z(18?pGP=5e@}u;3EMSMQsqUSy8s_MT^);J#GB$Eur9A zg-Fe7*F82Y+95N;@w27UhQle&j(0WX%v<$wo9cD);vEv1(qtcs2#$%K5|;xX2u8BC zbtB9)qXrlqO{+A$LA9E~b%oTOeKAYUudOp&+o}CQWsk;dkpkBTvICtHJ* zPgyx7@~^_cjI_luT8%v@J9?f<;zTRVhdx{KU+tMZM||Hz;Js;{F1 z0CEu?79QRt({0^4!BHE%+_AG4+&?7uFhimw13G)l&}PFTIr@5mKq6)Fy#7d#XcjCwDu!m#+%&BSi%!fUEY@;ae@alN@nDl&-zoRK zZd+rW!=2kbf{tb(LheGlJg!CD4a#ruZf@{0Z1z-EVk-1V2{m(JCFHeR--lzuU-K?j zLS*&H`ol`;Yt{;KnhqEIZPWZt=wFk7|J=6)gzDpF?x>nN{L+_05$GrpUOw%AjEDaC zbhnu4zJz-Pm~HX`RW^six~^DA zrp%q~b$Rhodgduz$g8au#(h=)dBC~0qb>K!#|hg`1(<}{zyVvpN9)YM7>{>yF8oVQ zg3UJ1oTpNP@epsrcS8T;IsGpk5D?13Og)io75n_XFNfN{7$rJm_D^N#ACwK?&lWM& z*}kgCM%E-A7cr&tp}}V7^Ek%cJXRDvFTz#)c~wc)=?79&h?ds@naG&e#{huTrH@o58X~}>~`y82ddcj(OHoqOF)T2EyK0-3b>kD z*q+z}AmSiCCeR15k+~5Uai}@h>5btv@=o<@uLlaxtVv&Lj$SFJCkm$`1N6g=EZaJy z)$wB4@$jd^oWj!!DnvFWx|}>~pC)~v|6Zx*<`xiJpLMQX|3fph=i(#Yn@xh*pUZha z1fdGdRJUlW_GjWZEhjn@hu!i^Z~Gqr`6q{#5L7LwVfH$sFLY6RPEs|NOHcyMsT9o! zGu^PnzM>j06lNZVZxO@UZ&^+9eQUL$UAFpx=JvCt6;4HiOBaDm=(e|gO_eoV_8pW{ zN|#*Ad(Pu!2uMA*CnHEf{QfCZr~v4~hk^#I3u0Z*F*a4YF}$M?p`yD{Ev;2H_(3Xz z!@VE|QfVcve5WgQHZj)rh#J|l+Q4L_Z)78Lsd%Ty8 z)vsq}g&U55U+hX4H>WM~jz zH$odk5N&}>ET}==c7fyx2wmn(jfwwS%ZZcnaI1L{-`XKL-l+16OVeVtVcIYYO-S-H zo76-4IMuPD5cNBhE4ZPI%d0I9gkI)NALAkX*)-arpazxI9c~bIjUNmIwuAvB0>w=e znbw*pCefWjidJPO*4;ai^K=}01HmwQYLukxO@r)dvweaw=Qy|Fi#cd-TZdhK$Yw_k-urO{ZHZTNEy=!}$BIIlH)oiH$b zvsWHXqC>duz6M%5d*~o8n;T~2w!l^aYY@(RCBVt!t!Xd=n6NhRGYJ(~5Y-HU=>L>Mfr)sGoj5X92qTHZ~eF!p4n1c7+V9e$okVjczxYOMZjy#4HKs~Y&ms$ zGD^Z$r_%3<2#4PS^0h0EFXx}m`TvQ8003o$1jN7qY8o_vQv`G5)8L6tp8NJ{D?(}v z#l$B` zt^8>sNtt#n{3-9z?aIE>rd7EmyE9Ex;VgBpLUXru#6$M9P8>;ml{#L?gl(_Hs<*xJ zqWa+N#|OIA-BRA>l~gS5<-PsurxXCeg2q0?+jUgIcqsSlh*gBU4ZrOd89IHN1m+kL*%ikdzZjzrDN$V&{>=Ac#Wap}ulwm|AviBM5_&TMB^X7{QSeAVz zhP0c2pgqo93=vzP)_6)*`{mu7Bh5=P%bArjz952aIN^R#7KBFFxSIeV3a7n~pA1E; zc0T^FicA1lBSZ1>q7mQ_7LDLCJUPqES~c+U_(MT6InU#rc=26?{z>2H&p1rwbKp9F zD!KOoTGN+i2r)CPdS%%!9R;-PI(}!{hx=8@8Ng+^bHeER_`#Vr?90RsbGfdj1#rUS zqX&27+ySDmCI?Qa%6l-k_@O1Z6Z#;C>Bzcy4dajYTR%)cZEBE!D5l7(vHL;FIMvy* zbtYPYS9MRY2- zp_VBY%hO_0CLhneIrlTX<0DSLM+gK3AW;6LDHIR1rvzm!WurKQtSCf*;zz})T;Nd&+4J|_^4Sf+rs186K*yorFc#;&aF=sK8aYc_F%GH<;M zYcZ%Xi{3`#GzGa>uVRTeCE}x)>P$%}Ef=+3qY?_b>WBe_UdhgE21hAlGml<6w>IsV z;UK@qcOJLQb_`QVvvA9Qy{lkG%;a|o{TVX!cXgeOo6|UtfA8u=3slv?nd8zz_eiH5% zS-c9(1PIr62P*D)V`@cmoouyxZj*WSZPKbucj1oLBUq=JU#pXRTj*Kl8M!yGB?x^d z`6Xhxht}ME8JRtR{&>EsN6Qu;sP4td^6=_p#ywl-lzzxn)|GDOu54maTHu`^Ea?*1 zUv%`(>?r)Bj@$n{CLqK)II~Z+BC^rSsr|`oF4d zKg4x^68a%9{6-;u68eqG`5}^i68a%9{6-;u68eqG`5}^i68a%9{6-;u68eqG`5}^i z68a%9{6-;u68eqG`5}^i68a%9{6-;u68eqG`5}^i68a%9{6-;u68eqG`5}^i68a%9 z{6-;yWPQp{_q|y*P)e~!3JXt0PBcxpzi`*FN;K-*8W$+uJXX-OzXP#8wK_%v3izAJ zl2zO-iS9l*z)hA^OLlpWhO!FlF&K}eD#YX-as;i>W}x*aEA#C(T0?r zQCCL?&7vc95^L)ouex!Q;eY4$eV2#MgUJh96x9>KT4n5kvXW=Voczv})tHnORG0B~ zp1O4P)ZE=%$f^T|9|~IATp+}vjBQJar>+-y*dk{1Kl|91vbknZI1t4Vq5!s+$UqO) zd9w*t+{0)Er6;qcDH4Z7Lq*XFnM{3;K4eDclSL9)j+dCBxZ@y|>k-*u6dz_&VnjRI z2g-`1(S`9w2HS<_v;4a%PL;-=dE3)+FLEc+)ZO=jDYhMCl)75uXa@Rf^h}$1$qmtN zk)AB7p6LPR&O_>>{2P)gWcc}~Z;pM6>|gAbgs)2~^gg-4dE}jf->rh+w<)#P%SLCm zqV87B%2Ay0LKdrn3{9LJ&7C(a@N(&T{C57xIAG(rF;Pvp%U=TRfVWXz)orufCR7KV zC}k~%NrcOS=qx6}qr{a-S>dHh@*o+yBhr~Tun!|IxSxce1f}61)ZWO|7@~j>FY#TZ z@qz?tZVeYC$tH(Oe$(9|@$7Vi^pIGj0X=xerTx09S<;z#>yEl?#b{qR`cGXn5|Wd>BS zJ1h*2ZQaosJ4G0tJnYfSDhIiAg<0_f1&)07~>rA;(FO zi&wsW7@KCM+&8{*M zl~7af(W_774|u!0kmt=Cmj{2Y@IL428hF;Z#8(<43C|E-@63BYv+OwEi-Do>oH5Vj zhiHfL5!0*97@}dhd>UU&!G&cVx;WgBRQm&hz^@eO?qI$t>&kV(a5H^k(2Z*B1*=h{KbwR=T7i= zAG<^-#)+a_7i&pMHhQsp-}S=zGd48=*OCqfr=kspIIB(csaiP$uOeC7Y@m^p5~6~e zN^Mo%gYdI^6jz%nCCh?ASewn`$zn!43kdkD!B;_eacwtkQkWaO!YH<(ik&m1{!|bC% z=1cKF`18xVW6nAfcH@h>^yDwzTX?LTSRgR%;;-o6#{0yZf4K242UI3lE|-p(Mw&6w zKI6Dw+aW}{8d~I+MZy1TD1u673TcD9gcZS=_%y_4;^>pOkbawlCk{+#SSTiU;VAco zP4_4LG|s4@wtEq`)il8i`+#Kf8EJJNrQ116^X76+ z@;$q_yx ztMh@&HRi8lJ;Aq4nY*~-JE4DH)&E;(0>CPZ4k*4vM9>VCp{J##8Naek!93m~vSzZ4 z&r65JchDFE*-i36-YK$$Ef+#*<5JL@44P4=etG)ldA(q0%SAZ)}vP|H%H<) zbhX`;6Ow+NsmlgJvhI`&NZu^mqC9A$q~}|bKcuwicCk&30`rU*p2a29BMug%Owe&w ztifjcg2QTwN43JnZXUlCd_3TV+unB{->nZg{KB=a{q(Ksmq+g9Et)4yU!Fh1OMKpg zNqMigJeT_nRo>)BvS_7$u()}!Tj%Da%uI^`ig`1pe^oN3VO0MTsqZ9>Dq*S9wwiZE z!?DUEm1=3ZMkc9kG`QjZrQ@l83n^lJcM4$(pdh~Pg*Hv|cvF{oZx$Tq@%>%Q{W*5V zzxl{h7I57tOZcYx4zyR4(kKC;7S7CV9aD$T9zkD42xGlYMDrJqB-45)c>DAMYe#j5 z%;;1I=%Wz^{qT*pH!Z3Ch;CNl6;@%Ib8BjV*{ak$lW0Vty}AS{+9cNk4`q`|$VBn@ zmE=Pfg&fB?ypqKsvPm>bT-7$uq*{&dWkCnUcfFu4EQn#i{1_^N%a1{5g4oHxw^I)v zo|xf%Uf9RPu=JTifHV^ilre8`#CMK|TKi`lC<)w=oa5dBeYLq7myn=R<1I7oAJCfV z9tWtm^`{`t-Z3R_++vC|v&gI<6-11L0C!DdR{C@xrIg~A+eP0i4524lwqLCa1r@l7$iQ+B3OAmoUmmK3R)G%9*f3hn* zrb~KrL#cU?hlbBZO%0W7*7;(>?22{~txSyx|CN??{$__4f}_>lZn{3+x|GVBU7NYr z_1cAZ7bb>d&%(!FFTK|5P#gg(=mTsyz5o^t0-9I~wyMv7izOP-CGxD9R{Ofj=C#@| zk88aVly|h=5xdOVm?ffZpI2!q?`KxJkJ zT0_ISgQTJ11?T^FH2ED-E8f7H?=JhU<$m+I|JM`wbQ~xQwgx4wIhd*+tH0t|=p}>r zDuiS5bBMaR+nP%*VXgY+83WOQk;vwwoTDBTcg!=jFKy9 zvNuq-A(rIdv)p&1yn7hCFg+roN$HKL{a}=Qn`DSVT)R&!LK-l$m;rH&6N`HBM%y9abBo(RLZmQ3(*a_Q z^#tz*(x5r0NEnGw1Ac~}V1+v3>+XWz1+4kxe=}F#s&2tP=?S86KzV2TTJ`(fTU9}h zRQ;4y7bNf|XIN37Xm^Ct@&vlV!~vy3J*q_@f7xp4jzvdzgl=0Lq1z}!!TM|4`v+j$ z%T)>ova?%U0+6M0-TdysDjPE}R3E|oNCfR`5ax{<{1^)w`{SnT6ig!LzS~GjJXnmr zQMsGn27PIV%J{C1LB7y_Fcj_yQN>O}TWunRPZSlb)60jfrPwH91_)_atT0|}Isu7; zQV^FpzjmxRt*QJW>ME)ClZ$}inPUCdnJ94z9~$Blk9&1=e0*W; zJZjEzH4c4_C5u!7nrAug_N~CvZPB!?_OB=Gr+=@E{3gAz(1gFTc zIw?^4MhYsTisEZwV)W^J;Zp0ySbbTGrBoA$oBq6lE+$+Tr}1Ud=Tg%21qHr+mu2o( z8Nx;#`?YO~h^1Ef4T=lgyx!vG2payhPCCj+m90_N8xMmf^9MBOrKARxHQFrx=!Gl} zg@`B$M|e#sO2pr@wN<5aV9-R(#AEWOlVqQR;aUi+NvPSw>pPz!NN5BIJTyBQrh$M% z0d`Q>Zeu6}1`!q-`b-O^M@LS*HGDmLhR*JWzloqAo=19!you!~SU!1JMy$Gcr|D#y zac_()UajJKTZrn>!#TlLYNz9hl_6v+llTW4)?9z-u;h5zsa`UzQxXMI_KAqf527Fv zWi1XH;MM#JEw0I~3Q1md+@eQZ!Vn{e6IvB<4d#ZC(~xsn40ChMv%Kg3=mEbSI$QBw z%l%tDvj4zI0xdTn`c_w$FGJt$yRgtA{&1*`uk^mi(Et82fEIvYngAlqDAq&f7#Q#% zMpy3*mnLs}e^Y;p6I#||P1LLKWDs{D{87wNhe}_?rs$?p*0Pgsb{fPy(~F9QQ;jLl z3}U$yDY*zX{kc&I-~VCl+61)(Xj23vwdt9PE?TyJ#TnkGxko(y z_`jS<1$i*B5!Ot_D1YW48og7od(!9eEZO%IZUu8VTHj--s?YAnv7yI0lKcZfl56e9 zVkhp_`%e{*W#Bt;s!$7cunJzsR4Lu z9k=%(K}qL$>l5}w8fcYTx1L_77-e)?JN5Y@b*_ftz@`@;3>}fH*7kjJPrR6Ibh;+> zlM%&qsmRL5b}oB~arA1={p$D)iw_G!*ppZd4G|*WmjZiQLBYe!x@4V-$3WwyFV;RU z;h0j^J_dL@q6Tu?N>OG(-KeqW9>#JvUqzwY($jWPRK@AM#1&`KY8hzzb*K9qJsmS& zH@IKo?O?*81l)?C%p96qXtGeZ*s9BaL&W>I|LS)c`qw)H|CuucgzDmEN)%eoZyF+f z)pDprW2{X$ll83~`gbnX2ta^xBm_vr)qpz4qhQmZnhnM^V&3)ZApy^vJ+Y1|HTTwH zdRH$E6Ti1Xk)n*&MA~)`k7#2*mS2H%olhQl!BLG{nO~Yp;Cr5Pv|Mgo54*KI?qt@! z0JCxw$8@FR@~190SAD6;Pkft3BwB<-vlg9Pn@9k%x~RP+$%WS>lk9kVZSZ8GwHme< zqkmny6fqh%eYibo=L&>+p%|xslnGNnKgge$@;kXJ7{?F0cJdv}MS+CB5#nuL`JK=| zf6D)O3WdV;rPu;85X8KPFL>wvla~!cN}R~s9>gE;5D+R%n%*tXpHgtI{7Ohz-22xf z)>263j(gRc1K^vrYQ4+-JwL2qa%GRaIJ8eES1J153EsH9sbH*Qo5zr#+qy^#{31vQ z{386#Ljpb~=H7>gCwLzPW~Z~4HQg;{-HF}a{YIe=AMRmEp7xpxciC)sFojtGTjk*! zd?Fi>?KESL_MuE=-Q2(Vsy-9IEmL9!N8FnpshEO3yJ}-BwWa;FY;RKngorywv*@ zqj-k4WO^FKR%oABP_D4@e|v84s`u(QYYw!5+?jV$_y%2G0DnMpH9Fh-I`34MV0tZ! zH`k&8Y9hl+NrcB-RJxcE0Oelc0FU!lGG%koyMriX8jVGk6$6M~!Qo}wK9y-#J%(At zw)Bx6Jo;B`^;3XcgG?+CMIa4JWtuaN5Et?e$&??AIT}Awn3>3(TTtH}+!$A17<1-b zma;mNq%G=&Yvd;E-sM*@pwR(=lgn2xPZySf4yYJH5DDN;=*WP88=ufyM1i6ezo&rX zswqfi12FNlo+63WJP+Dbkqn)t{)~5G?DE_KzQr|>wqSmT%0tjvO5J1^@g9BGiQE35 zh!X@6+31=>heB@f&Ys(Ief#(c-o*oRpZSl&3EroD%haN|UZ0%a+}M;JJ~tl>3L)|9 zCkp~_GoXpwPJ~O72VtWBa^%$9o9)xbI97CC$gbpdFrm})$c4%Sn0-n zbr`o6k9x&7>^^mm_auD&RaWeL$&=hffI*rmb+4%@f2==TdB?___wL!u^;>6oo96D! z@G_DgInxw2&E=dqwfGk^)_+IU^UpaAFu=DF+E61z0eli$BjQCHt3pM+iYd5yNjIUqfX{XendS66)H|XX5?&ywrMtqGt1WVMirx9!OaF9dA_H z*RD?Y(ImDiSKD6+EGfC|-Q^(#cVB{%hK7bIwBLk-DhAu4!5m-_1qt|2RJdm;p{%_I zX*ZLolu$*jMQ{U(YE%8voc=w_jB=;%u?+Ft(LrIbaz1YSKHsz4lhtdNpW=I#%T|h8 zSZk=YVozGZ!aD!K3w`}k!NF6ny+dC3b&fyu^S?VXcEgI)GwHRcE zGMLhQ8nOH7SYA-K&v?((JR-KK2j0~`8r8aH!?Ta2H?T_OhYuL`r_SM=!t0;T+L0{- z@Nf{mfJVZvHWiOoh5(%5);Nblx6K&i-(wF;IY6=d}r%-4&DC&fXA35WjPUJYHO8Vs zKtWVNlpW{}Bodg^7fuldkWn@|Aua}(O*K3ozjJVz_v*P=((e7#%eKviaHZAbhXC?!usSYLR_WuvQVNlQ1#oY&VT zjp`m1dO#_NxkXi`RZwWHcuHB5*diF0`v}719s^a*J~0dyK`UD1^b)mNx|o%e6s9#m@10o+bKnD zQWCp<-O5ba<9X0TlL^wRV@!+KbhmcZ!PTjG3tdKZ7iAPP-1ogP8h^sv605jlyiOSN zQ1w9##pH@&+;|;Y>ay9(vYUO!$L(vASBY7JI#=~;x%uU7TVxsGt4olAe2D2mWOkq$ zudB?At>}xcoz)G0cZ+vc;vW+FH7))x zsBOPd%^axa-n@mqX+PO}m-(4T7?%jTPH7nQ3%s4?+m3f>3qs%T`h0;5S;%s2+wtUi zw7pGKD?@smn7e3?uYDHGVRt*x=#Z}eS>=gMj(WCzJv9+`7WS2$v~qM55PB>6esHZZ zY^o)$SSiOqt@!Im} z_AG3}iW4IFp6e_o)XlwMmBqc>4GD-+aU8i{jy`C3(sBHlL-`J`Hg&>bknF*9&f!4}X7=$Ga7NaegdAfAsojvi?}cI&c18vY!8)Q&v`1{@L|T)fF~2 z&fWTV+A^-&=9bXuq%!c=H!^837z$-Ajz_JobYVEa(0Yq|PM4%Ub98qa9z1GfZ#pjq zHGJ*oF?BAViXsJbvQ@wiT3QQ{y-HF9JtYWSlC;Czx(lwF5@!uR4$lA*SVAe2b62e0 zz+E!jtro-}fC?xBkZ@wv5qS>jDxRbO+_OcJa0tE|_eYPdL$9_Di|a0IzKNW4lZ5Trz3?3GcnVTQdc!0t15)^u5peY>C@39Z{1{3u4z}|N_77`d-Y&@ z>@wbmy!`&9_V5H7T~b7+LkP;8w30K>*_R4^vr_9d|E>Bt|80EpvDAb%q~)ddX;#o?%6Pie0q$&Ek(h_&2DK#0c@YO+`?@rxz6Og#1@ zv6KbsTId2XpaCbZMX@3onto zvhP!v(kvAwoMqvWf^6`bNxT&JBD3CoC{dh=1<)I6P)Y~~paIl^Rg!9fl*1V>Am&H8 z?ZZLTQW82uD@@Y`1T?pBujI3;?UXmBL|#d8wa~&Qo94SZS10JuUGa@)) zEPP;R4VnKi@{QiZdAtM35AzRXE7Jpazk_1T71Ydt*jSewfF>?!EA%vw%@JY+9H9{d zt*z6Zu!}*FY=G~Ze#k`9yhe z`#Y)=P6PycV<*aAzIE_($Az%9G>V0Ed#;?SxE6w|Ck*E51r0`|Ni-o_C)E@}k`!*! zJ3yjpIQ#9#)epz@HTsr6;@|Rl$2zrR$Djjsg$dQZ#7tSLmhOt(_Zv2;WuTNtwwF6D z?8u{-r$*@{;P4hHq{p%>k*e946f(*Z2PUoT$g{5W*r>ZAtVb)_R&4iv{d1n+S+N?} zXX_Rmxv{{ukS(^RQa%lR0m|4fl<1W~A*Wx_1tDF$#B}lDiw0^Q-Ee#K{^-lZt4LZ@ zGsy&)M6=uw)?_Ht1>Q8ZwT8EiH+ON{6gp)7SuWhuNJ&>2IaiqaL2j^$RS0 zvt!|NztVBrhjnW;4hsm;IVSh9SH!}HNUoeC>Ov;25|je$EB=yG{{&*|7Am0_-tn6E z+_j(QRUJN;HlJN8m*}VtUS?oz+eD9&q~vF+GR@H}wtLVG-l|WrdHDu8F_iXn$8B4*_=+4M{8^(O>cL>ma*}5PWSNR z^$Uv}w%g8RSo7#r<~xXK<+3ngc8Z%U63a>n<1f$$N))!I;H$8*No9u?xjM{`G_3hl zHq6-8|AKI!NOxh8u15db8k>#Rb%WHfDdH?GA`--3mB}gu=aT8RUk*9)%^YAQ`}FwB z9Orw%Cl;YCZo^U;FUwj;y(bex92FL`OPg0RJfEvx0-4`^uXFC^tz8wToX|7F~}>|%#S3R9*sToMuq#e9nzH1%OaE?_s8O?rr-$c z4bplqrFBC2i{6T`aOm*V&_}iqSP1&`TI@pt2X!R;s{W<<0{QOO(+hZ;X$iZ{BA1n# zt~htKdV#p^mNpEGw0gRtz`}k^ z?q~|7gG}_|KtTB(xdeEcqDyPeLne&BEV3$H!%1WR(bIm|Vg2&3J=KZ&IpMferk{U~_p$Zwa7%^c`aVs+ICW$WUr6@bOpZ2L$2n838 zoZLSqCvXeF<2~ZN+hM^afMvP1P*v_rp_yiiRL59py2W+;IJ9$}{7kc{aHW6{12e5g z6XL`-9+pQnNE?yi z8K@?Qe8sDO^G3kG{y+c4b2a{tws{GAf_<@R-~ss-@Q`1qXG=qW&>m+MzhooWJp10;{!guz`B(E15XwlJR+8nLK72Q0MNFz; zHF&JtuQK$n<|+6-Nb7dIpCpZ|2+VMez>{20OjHjh`9hBv&9tWn*4n%tKtJGk%9FsQ z4(N8Yz3t*t6Gq3D)!x4|&#TBXS(1tz9Tr~!&&NSoRw|M`R!Sq57R5n6yW{K!yYlp^ zW*_YFbu=S@`MpSmnoG4atNj-~P8REV zkmwBP**w+gN^-R(I-vv8x-1r2gJ6;t;)cpJ*QdVkDxL&T*8P9%y$4(q+tvp4ON;-6A=pu%>V&GiZqojib_|kgeo8+MGzGgqzH-?u!4CL zujSnP?(uy0e&>A0_j`_#%*@VAcGiE^+H23O^-tjg=7FkHfg9h$^qUfER8~~sjJIb93%?oc&SuJdvQ$4*Ri<>wZcZYz2F+i zK{y3v1nq+`#`yZA+-cw({kVXZMb$i2@pPE>O{0q< zKE(TM6=rAk2Fe#5(h@O`@cBqhylI>p9-H;Z{Ou>#sq1+@0btx&PR?N~I-Q;hMxKR( zG3#Fg(N13+4k!edA_H*2eO@t-iD7LRvVzjSuYh~w*;cRsV$u}+6clW2ZS8Eg6m45t z!~9?XP$0?xmJBBKkZlf1_}}$-?cM3S#b*UQf;qdzR=}5}iUQE#ID8IU@47&WOiTQ5muR`d5j4=zL!K4Kyjs)sZ@fU>0NBxPGaM73~>q{L=5-t8C?I=sPC_2WClbTz|QI>xQY02m?HVPl3G6utS`q zA~fUVkVHZYo{PVa8g{DV0oP&sK^w~{zKa}==QapLj3PQlkKT5wtTmDAUZH(*@xU=Z zqc@*OW3;(|m2$_k9eOr{DM-snQ8A1fO0cq=njc82z!>+SqA~LZb#0Nk zy#N1#PC*akLVzQr0b7ojh0+NqKflL`0}17g@EXKGMScehp%LW#UNM%ZQ2j-RgvZ)3 zFXjWby5L z7cq<+$yJE}ky{8S!vVkMkel1YaVK{vNL)fUSxEWmh1a+8Z-f*z@{NO&;-PoYR8#mVux0Nh=~z_V#47k zzs=eu5YcfrDfkeg-b`HU33Orrc??dZJvAzkp6)WV)14|dJ3Q-Rpqf~M59=#9yZ4UU zxyFIP2mYat8v-6*$*K%YSrrj<;l*%Jq#zm@YaCmnamMwJp4w9;J$1zoGjSVNmeLdL^IyU+mM)8xd^B5~S z?9zCpu<=qeG{Lc0?)lqGS7}+r@+!>L`{X~{6=usmyyK;-Q^!MsX)j}9o0iuzO)oRH zz(~qmyp+3wvoVE6kXVl=kW$_W#gYM|t^{}N_}jOSH4hey$Aw++ue2Z+$8dE^y=RlX z>$XX+JjL>Yo0DkyXt}z`3=Ik-6{bb5b>v}>FXKMA*}}fB!=E0IS*2*1a~FH}$ZfC# zGdsL|6w3s;W6pv;47h#!-zTShv-^MT3b1+xAhQr^Ikcc}(HgsC@D(ARcoC3J;l$T* z$d_6{4ufiui{bzeeN^7|$jJji4@?&V2NOp>e0uQf_|81APIh{n33q3+9D^7>5f)J?Q$V`0Dbx z!Rson-j*DQwKCWoGj*9D8BQ49Hb{nzlg*M3S`>?Eo;~!B%Y`ZMeLnQpg7N!jXCc&_ zxNujIr2f4aD{ADQQo(=!=s#|`8VCc-pfy-_1O$mHM-e!;X+;T_$DG#j&11w&3q(pH z3MHtP9b3%vOlUsy!8&TyEjCw7PF!*~K8ZU_ERH{hS?h~MsO?TJ=CXrbq3B_Y3qi%$`0oHL%JYH zNq?h}`#K9{h07Ho=U0o7z;USXizyKC3_S|zujleHkxg7;^lh0l}=B47>a3MG(C&I6;y znLxF5BrUqWdwz&H$411l_M3t-db=R#2S>5JM58zus2*NhWt2Sd>_+L8Bi0skXRMX_ zV|z4+@!TiOHSUq_SbJ^nyWwoEO5ix!$TmTxi2KUm*Q}>B_v5NJuIP(1E_SPRecwDO z7eSznz0&=N5L~nG*&8AM``M({l%|8Q@mG-0Sj2IieR+ImJ~b^eS^oA`>p$7q{$r!_ zub*Hc)Ighe@5fDUC2w+bZ2LHwnrbWsI6a@B#lx~ksA$z=s zJ>b59t#}@E&YVR?5nVWhyANGSc)7CFtW8Uct$8s0iV^KtEj?mZ`*33})IBsS>&ul$ebQ$i8 z-BP4-Om8V<8>H*rW347J5jbL1VTm*^km(q6)T*ZZz{uVAU4wcxHjVBlvQ|p>1Y<&p zLHQktlldkJgUJnpT4SXuy!FLbIZN*9uD=nJ6Bu9(G|>?&cu92Rzj&bAS~=Vo{DJqDU@Yz;y_hLl=b%kK zrP-QYrd2|qZ+iQYncG?y_uX12I@)@f6TAF&et)a==%jWEz0W6%2gtlqPn|h+=Ztgv z$$Y+9r?ux~dFqEVXB!0L?PTTL?QEgO34mo!&e#~p)|C#S;9plrMJ0C^A_K~-o&{9P zoojI6-{jrHHsamaQ@je*jjz6S)(CCg>E*C{cE{Gbur-?1&-HHR?#vnP?Tvi8HefuU zf&{mcVZ-x5_&6zvaoManlK$PL=&j6ae{+r`H#m;45W15%f65X>=o<~K@V;@(GbV-d z#mWhAO+XlYVNd~lGv@kqot$TRwsE=1v3J)J1p4%Sui1zc1_UgOUp!eJp&G}TPC^LT zo$Eh8MujUv;k+g*<+q2bg@@WfQ3?WZHdK02`}VUbVxci`5euA-yuSN-IG$a29}^>_ zvY-5s#ElVmIlyz7D1w1XJOsy`W&8yTKBx)A7FG^*VKagC@%XY=_;!ermOH}ekc1tS zs07Trm5&#dciv2B;tm$?s1L$$e}XKmta{;oVb>`g8tLg_D=SaGOwYaG3`ss$Rc$kP zW9q4j+dnpS^)^=qi1H$mVYaT19Li!H_EjZ09kpGFIK&ZKw*NRd5Iy;(Lq8ZQ{-_C{ zV#!yaOXP@z0FP_QTHE)0NcA6|(+iA4Aw2Ek+zv#VXWyDW6r8W$8G`A`V zeXQks;M3|YO(QO~hg+`cJ-fP3l7|L?QYd}<-WBNM-~vby_6P1DwO`wYP0z1KgPOI| zObaX+KIxGVtLH`MZSl%QNO*V)Ukx(Rxf^&(7FLO!P*~cAFX5-5RFJC&Jq*qLECXWCM18DnCDM3>+UZ07iHXlR4DFt;5D_uy zN=$m@))3fuA*^v=YIvw}yQbRMHg;2EBf4pssrVoGau}2=KRsKghmR{hyTs{`Ll8L@l03m}=EZH8URYr( zBfm|#)Gt|_^H+n-pZ@;PAb7SY_TA&=AboU zV&V=avk%3eyo&FNnY0dQ zwU;`)64R$<=ktu#G%OxbrVWtp>$-VgsLN86DR$K*YXe`HFk<$1_eIdqFrEh2xOmW-^IJ6p=$$cO$w3# z1RML&S}S?2{L8r+IrXk2&qpyPyqVJCwUBF>3cS3hb5D50H>wA-nM6$&Li^K|Fd-DA zqd`B{gH+Dpc2~7_=P9&ZU)pX3F(Fb>o1;A6b>yw%*sSC-_4tsrPbM-uEk{#VstppV znfrr!Fk6m4++>6na*?B6mtD#0lhm*NDX;0HDRVIT)8Bea;j>%E(xH2_1tanLj7!}@ zUv;Q@H0_Mfp|3jhum0cvueQJG(BJi|SqS~g1pk5be&ZiM-f%&ga3UE;ju8|+jj!g5KVj0&y!TV>4R0*rAF>e6>l$B z4StxrykXM}vkB8E_#yhZi_&_9BfWOL*L@r`1ZP)0C~2^CduH)w)q^@~=Z3qvBTWG} z0?unEMCkB2V>631s5A@bG|d?Qgksw~AJ-Z<}*U4Gp4&AFxz03a}1 z4psse7PwVOKvEJ+Dsx&|0H3stF@>Z4hV~6p7t2E^!Sx>x45svlixl6pZ&@y_OdYko zib~Q6N)v{8Qji9~V@6*pjwd!J)ty%;F~B^ai!E;Q4PAmjiV(u#sxPp+;F>*rFvVDF zb<8?#d6GbsFV2obt`;eg>phbokxNtKv-ZZUQ}3!l$<<+0+2zYU-t5tmk{R|y;3iog zn62TEFDK&+Ji?KM;mv~zaTx=gII!GOT!yPg59=e?7{H2swbfDQo(67uNZ|&43F|&Z z8;>ZfN{3XJFtilD#{)ox3!wm1*TGj~!HjAFFpXN219053q2GBoChZoT;&WUHlDbNP zXu-}xh)I9DRqF)*{pQzSo)2N)7vJ4Z82%ckK(SA6Mhm3tb9=@SHZX=(cv=PW9B$d1 z7-|)F0;%M8Ui#3WY`A;obVhD>ZM2kSMp+G-c2HN|X;rt}V)(AWa(TI7=d$gBgVpUj ze6toG^nB0|u3cC1>`I=1|5l%8k~>hY`0>TDCjx$2J+?B1ZAnRBd2T*OQc{wbsIp8& zqM(3=yNgRP`DBUljmwc_ERe$nc(>mbckf-NHrKjWLir6NnL>n)5aGV6NEm{ItUAHm z7f_Dl5RQLI3gak1IY35X^Kdds9lpR>yzdx{mugTAXzJxaOr{gcWAzMq$%N`)1f@_M zv2jI#H%WkxGRJK1Rrh``fIt4pZ|my(9MNdV!8UH@LGU3y^TWk5@O3HdKX^X=M{x{4 zY-a^DPErs}#@Z>n#-J>Oz_RVU%15ChA3%TrjacyIcTRu=D1;mZ;_Do7h#5B3qe#?z zX+{1WUXT!TQ>`!wy+s=WN)Tb;Pk}OAu(#<@!Qilgwgd1WJ=5A zLX5(Llm?7Ss+p>Zuqhr{67{U&nm!gMuizf}SkePtQZn@UOCKE-{3(VZFrYD%uZ?Fv z83f~V)pqW-MD7jW^Nha_qXc$}CV~=2)}vT>h_LA$(3&{6eI0XPCyB?i(X}z1-Qj^Q2M4oO1ANxC~RPlrNVoiOmea?j)DSqlfPpQ#U zap<1bdv0_dZa}!ThP`Dfb8+QxSwmF&MTm1FxIUDlV}H$QT!2z<6VtQ=1ZiW< zUx5%oi%~(ap&TVp84;?IP)B{Dzpajf9;eo%DMnR^dWnBXWOJ(jyXGrb?n9(Wt$NhiIw zN@R%ns-X58Lce23{_@={goge#Tuz8Sg?aI6n(_r9RwuZ@&_lf#w)Of58r1Bb=( z0Jt117>TN3$e&yEesA7Z$HqK;&)}TRlnX1I&ISx|lwv#1X!wm+d^j4G(~yk|Fi!xoXz$h`$i*qx3< z24##waF=vh4;QzcTrcNXau$@$+%%mXqlAJJjjhF0!x%BW2kGE<+^gPwp;^!WiJ)4CAC{*NuZVnXwjI)s?S->4PoTFYtVPFIH9cB&`0_QBtaz zKpWMTlWnccQ_@pXrc97sb^2K@STVtc1eRgth)dyROXk~?O_yj+^jkQ0Xe;C+b2*wR z{8tw6`W@i@d3PfVp#kKgqbdn3##;Qv+2+AeQ9KJBUl9TgTsXH8Bu6+52v>?_BSmJE zwx)*J^S_C87f78zV^`Lhwb5@G>qIrqx~OlJwsMAxLaY6i&86l#-xDs^7X+9SwB6yg+=wkl=kA z?z^|OZ`M7v?%-{`pd^XsFWyEkk5<>J6HFjPm@lLgVFDfFErzwqks1-y(VRxI_nYE|?c*frY|1Dxr>VDQ`_gM7q$>-6-wMd2aQd7c;Z zkB~}t^h{*QM}YC{38DvbpOEWG5`q4xV?>7RdtQnP$g9!dEi79u{61N_JG)AzW!r4Z z>QL{S^=RG$-iMw^FPmEWHfY`rn%(0zv`L%$pb?4G?zq-t@CM%MlylX4K8I^Y1`nBL zTsSNz!#WMBBZw?uB#X>E((kJt4zI5t$rp`lt>(0S`$j#j&3f#Sc4}#_UmX&9;>kYU zEd9_Ak)u1bSJUn7?JX3QU0T%(#(TpG;-oNf(jbdM!Q)9!Z7^|&c@LcJuy_?hF#xa= zq*nRV8~?W;^xJEIg-|ej+uo7~}dr(%j(Mi>#2#!5UIYF~YOmH+fGahZG0g zDX`-`+!hj3=+?4YGX&kwLlNvBgHn@`c#48Z4DgO#hze|*) zOkW|ruU``>bKi6@>pD6hKoww9Il9qpD&^1)!b>H*G8=@=<|raj=sqYz))LLUce@q1 z1#N}k;B^pXv=OY-YQ8vmVm>GzRTnkw4@_?h_C zA@DU;&rWQ^AV8WGfe3c!qhfyIg!Dg7%c}1CpzNnWC=2~Mn$sxxdx962fD>xLhJ z?DxCqu+mzPqF5>)_wrbt9Z0|iA-hvvr?3PgPN|6o-(JkfJ|E)aBf!@ZtP)juRaDw}DF6s;V7&q+0}(rXD)=U;1EU(!4~QhrJKpr`%YsWP3X zW$C&>7yKGQTD(j23#FI1SH|-+Rtao>zrX1G6KQjm%x>9%JoBJam!A`796@3QHr!3- zoS5H2wI`$+DZ}>$?8n5*wZdo7%lFqa7d}QanXe9H{Y5$i;5w0HH03mzn`%S0fr@+Z z#T|%9;0^W`*}2xYBsGqF8eZ=Cc>F|60&5&9L%n`4^jp3QwDNZD4U6CALlgYskh6hM ze_mZ%s+8ALlf<0E5fT_T`$J( zBYNk8R_x01slE&FdF-U7XPvToX7*Z^q%^FbWreogqeu zkJVYP@k~Iw?1imPE8Dc{#%5ze9wd}tOm!q^G@BDvK?4Y~lpX>H11$jX-V3s>+2x_r z^~TD|YDcIHt#lZUlYt*{;2@4z&?n9LHX3?*?!6%*vL=~+#5JpO%dqpAIrocoJ~t24 zHZ(XX^juPxICbA?Rc>U@ediBl9jnupqhHJ~xh{Tu$7IG|{6C(tvXlV;GG+MyX^0tI z9Wn_sVj*-3xv*EhP5pb4B;Wm2oIKS#zgjt%83e{&AV2S0?7^>|(4ww5=IFpS$6G=M zAD2u-Ijzx)O%xSc;oqKlon4AzW1%ck|L8-s*s$KTSG=3Xeui;&BOE@Eksm znsN54!Q8w^+}cBrL|kZ6^@Y#M*in@(*Pn{)XU&n4=OzUACv~(FC!q{Al(=6b$w)U;Aor*dgp40kjwjy&jA#ADKUkmf+4&>gps%tzHUjkR!o4pBUpY{@`B zjsDP&vC5^2nKl0q%&h(Jb6B|!+QPW1vBSr2X{_kU?U;Xi=k&9^TLNI7j1+1LLC3I# z;DubHA-lw*ezWPddR%8C!6(LCwi0^kpp=UE`zL|Tjlm0jqEM$ZIzCpk?GS3#xXVi)B?FXerKGxt$olc-S3DiX^A&Dteh+H4!#{Hk1xjW%i#|ia19*m^+G){ zMfG-<4XU1cmCskXa4ygjzcL$|&PKxH3#G;#iu(HENRIGzk2mn)@{WWtKWt>a3a;Kq zbXE-uf8WqLWs5cn=4I9$kTG-Mw-qaivK!R3Q(u`rA^*sM&$13iDZOuDtYH?P+BSMF z;`ZqI{mg^oooyaV36h&x=OVx%A$FLq|NGOn_&l5ZL4u^z7==u*qa7sWPTxR8+ zZwUQ2qw)Xt7=Xw@Wg*l_TSO_629J-VeVq^Wc%itXi@$mb2pE;R7H%FR!S*U1#nyb? zVv09^PyTqubB+F(F?4QxG4#C68S;BM8;XFgZZH*h?5JZpo|KxKX$HOtVBrF0X#^x3 z%Eo~QCp$7`he6s+)|$p|i4|Cnfx@u>*dBZzJ=?skEW6?NC> zrz+{*x3{r_7&2ElNh8TD}HnL(}n@pC8XBMs+HUqG{W5!r4(J@D!T!A%hIg$Hvt!Mt0 zWYK)W*vp*#ln9BePyRZJhYJ%{kLI!=aDckjiB}@hm!qMZj3cHykFV!cn=>@EL&?Qm zE)afAxp;U&AU{4*7kb)^EpsO~pST`7RN{clfccPp_ZgD{hbn{pLXn3g8QJ_JgQn3? z@iZ^%6_dzNgZ&!24~=&rG#pG~@D&#OJ`UavZs8%F3-uLIKgtg=kQEtWn~_ARMQf z1CV{OBE)q$c&YA!_p4EPBYtbeWLw0sR^-0i`ZRCV3kBW2r*1**X;01$-K$>W1~8$4 zejT*)pF}F?K(ZZvm{eA5-93Dt zNL)>0U#%Q!E$At{?D*=-;W^?^u6`#^+jWOlwyHg?$samdo9)t2DUc12-+@W_xL_iN zMS@`9kWqDuE3?@Pe!b21)kfQa$KK2CGau~e*IY0&8*Q5-9dFv#ca#LRA8u@~rnOMS z%_JZdf|jHU{$<%pvV-c2_mZ-F^0%qETudY!6tSMFkFrcId3WxJpru`pzrqRof@=;M z0#8J*o5(y#P{v`9O3g$AOAP^8AsJGtxY=PFf0=V;>Diw*vUE!i-@&J3N?6A{eaBL8~9W^yh8s!HagRdQ)NsQ(v@DDG~AHZ zPvra)5CWjBh~TS)XMz)Ug4}AyuSkEmFs!pZ#0aOlsM;$it;(ONw#&wAKD<@Rd7j7@ zhG#EX)lu}ZgP@zHv~~`~(xJPl^O6G1u;u2$FXNCX+p*U>F5l?X*BD5Mo!8R)j1YI?nFBF)#kv9z@2VpPKzrEVj+`c`S1{u&*HJ*u1HU&7-O7S1XslsZ*TsE(BBTh zA9UasLVqBjzrFbvLVr61f6#$n2>pS8{^n+w1+e3=5UkWwtI1))Urnekca7G`Sgkb@ zNm@c7QzdXHp~wr5BS`T-t`;8av-YNR%A72Jf6-N;gUWW=r#!Z!FMS@;n(ny(g{$*v z9@h2I-Kue2-BstIh5?YoX+`3_B+}7!YW8;GJ;jR`j_K}v)YZHB^{dJYVG6cvW{)fj z5ZGSUe8}Kmi-H5nPykK=a9c5xdHON1TlLS+kD7Y_vHOoCn1R^n<43?2FZ`4 zG81aznyYnI5W=<9u;;Wwh={myIJ84S2qLJ4@3W=S6GTpWdl3OcRI6f`JADpEBVr` zE~!{vTH_{~;B)zmXo^F`8BNVILuGY0oXRSDY8#GBZBlt59x|3^p6>PHyE+q z3)w8S*a1cKnDXBIcb<0_h$x?zYn(dN-hHx@!*Cb*OJY5qN4 zp*8i z4el(GRNvtqT7q_0YGu0&;fDxt^23o_8L$y?FhHKO4uXZ)+Cr67VMb=q?j2`~7`7EP zc%t0~Ff2G4lnYoH6uQ$Oc^Y8wltcUyI}Eo)bSS(%lAvNX&;bcHpH6QKN^A{%yi~lK z@yMcku~-)~C0^X|(IIR8zP@8Y)sC_hd#??5DLL-?Sg@od8za{AIEuZKl|_VKuTbAaTp4-D@{V6+Hoeye^~7YDyz$P5n9WaK8H%<1<~ zKyngh$NG6yos_+f4Qs#9>lj+*z@pu6YM!oW=40<^*(i`U;}=ctUhzz(-C0tGk`W|#(o$Xp&)~j{L!7rZ zgCVDguR8QUI3oaHF&+jm_dqRCFNgyFAOyQ1Hh1?5qd@Wa*j+>pBMcqk(4s-FpXO|8 zK3>A1L<3Hhp*vPS_Q~wxmQXbm<}lXhv}gYc{{{UqhK z?6Y>D4FQ*-xdCmfq5Oi}IB{XjWh5vQCNqo4yu|!OalOj`GDuaZWhsKZAveJ}{)Lw6 z>}FM=dVK<)DV88>=&EM;Y}U=z?KyZ~spoms56-^WkYb-?^6bmoLbQUA7Q0;ggVd2|h|I>ZLha z4U`%dZ#PsF$ThQ9lx9CMoszM3Q3^K{RYog|#+=0*J?0W& zd7{;M|Mn^Ha#{TEbci6x2qy4Q@(GA$IKi`R_4 zQ8>tV^Of9JD~I5wk(FVoFOg&`Q_#Xt`fBWnX;!p3OTL?P5ti!vV_~pf7joppHs8SI zhNmx*g>-wAU9GoVVP0H(+aaVo(wcei0rUJ7rlNW7(WVJ5;igpOGDTgBup{gbc-q(n zurBNQyTuD!*Ing3piJHaFvvVW8aa(7nCG>qCC?>60nY^l#lssKNA`tj zkQ-reatAO%jshs0kag(cwQDStl_h!XoVo?I(uzDlDuUR;AW3mEMTp%tZi9*_e}g_P zLBsg?JY!><20=j=1y}#vJf}@rZg-9(1P*cEJ4y3kFN;vp;uu97XWbiJRka(Lw;|Qunu^uaZG54u#uXosp*wT@Pchp zy|X4>y9D~0AP(HtSqDWD_$cqSZpEx8z=2ByMFuv|7kJ5L{+b*LDD8`C z;O=~0$VS7AOWgeobpMLA={eh{F4%!fGp6uH;Ue?}-4oUy9Aq?LNv#z}giTwwDB z$9lF@0W~OpS3uZ|Xw@N+V)cTD`Ebp3ja#)ROgN*qucJ08tGur7Ev@_5Yy zjQnBOv%cL)dszr|(-r}Rc8BlYHg`D7%6;?Y%EvL9zG4-K#}QT{ss&O}Ings-f>a7{ zIzTeovj%(iKJX2ooWOw*l4&3sGavZBh&-COLH z#5Pw;EA1)}7JL#|o3LxW$k8mzavv~=Ue>uxE71N)zTOt+hTW^~F!VTSBx9mc%L${R zA|$N>8R<9^u0c$?&Lc6g%_ZyPUND34{Am*{?sCJ7giw*k67i;+)oIhvhvCy&oo~z8 z$4H3WgZc2YJ8-W$tmQ5VWhJKXP+V{B5H*bTNDz6v;q$VC0VWFp6n&n==6NE82q0k- zEbV!@Peq=+nL)&W>qB$@hY0<9tUn{fu^6%1=+g1;f%P;?j0!Wx_6IXoL?a9`2d4By zfVV=I<4<_b@ zKAn|$y%l7?$oks>`W(>MfSgBLwDB+TR)6snF!hNn;w8op5c>CcJ2?^5D7OiE1_}fx zs4yY@8eSdWNr`vbizh*66nz z{gr5${S$r|WNW)I=|m5IiNY@LRl4<@tfH)vf`Ue@D}}Xu`uh=LsZd4AyoVUbckzuG z3l$Xd+Z@T?1LHs2k8<{x0*H?ZTiPP^T^lBK;IfgrVuq(w9e^(F(&MUSii8^~s`dv} zMc1r=(xg9KM zEcZWDp{xEdr~=y{Y#+3h?HvF^Es13u>-XXJD+{Z0ja-?*eG3xXq>ZB26D%)xzw?sF zp#$vR)^aLSO&M9UgCn#i1Qa*fmDWNgNaPS>f;`}AH*L7UwEoy;j_5YY8L*&lwxo)r zA}Fp3&2A{@cTJOJAtU5~*gOwmj**?+9dp|_ybk-gr|5Zbz=OgX@61D01*DFwQrvzzk-snANaiFa$vn7L##)Y~WUOgh)Sgy!D8rzipdriV$l11m zU;p{pCgwXEe6R+~nUnZt<^B@)?SF7mmJU_X=555`}Nb<@Q_0cw0ZB*v^nFzH^YXYGDFC_;~h@$N4Mi6A3KI4c)Bgc*cBhX&4hRVqjv870WvqhNtUKy$cDfhax$&JyDDPeHvGJcZxR~rfr#@B zqlMH8Cv@rN_m>8aSEp}xk*WyYH?MTAG*L7ZhshF$Lhx-A(qRo6+e(c@5l_;I{w{~E zdPhMldIc}LMa*?JcYMgU-tLd<7i0cWE%$lSPl3=y`U1NGm?QaZ!piMP#qfO3!25>K zf3*VOAZbA8Tn>ssXJa$crbkU^MSH!_8jj^vWn*}*CA{q8jOAHt)jF!4L~J2OINo+% z@oDF1qH?OLktsGm`}&li&+EiIo%R5wz1fP-x16{X4fViC0o5pqJJJddH)YN;nb}Nc zK1kB(LNXT(jP=CPjZj0xih|p(eI!ByL#>!JR<>-&4 zVPhf6$12KD1rh_rVtXEK#3m6vk2R=xZR|?-O~`wjdx6g*_%q){A;-KM)~Isp_gmY+ zcHFC9ZUeqJ3g$Yu^EYi>x8KDn?(T4-L(xIqu^1g2=?X)GI8KZi_1-Znf&vPsKm-S9HsNpnfUD*JDRu(1F)}{aXzVyKUK1;ylob z$&>>bxGe^x$ax;8Gtc0KcH}v`UJN?#e=d9H@{U29>>bBJk}8m%)&LR^6>SqH9wN*c}Yv-4DkoOUA{8Ypt{a}3}aWmK6Ud_BKr;Rs@z)$Ylzm@sQ z;!i~Ak1p`gHUNH4`o_i_hCL#jF(TmdXjR3`5X;KlMJ~<|j4mzhDgJ8ZszEk|?>Zz% ztG^wYW0+5pZArCJTywW4Uxr9o-!W&cEzmzj!=~Z2LgeVkmG!L+(gCHwB!6jHR7U6Xo5(v=(cr^Ia!__w+)V|P4CmA z#96dV8OvfH`#v?Wz2iY4`^{N!#(m~UpxQcG$g)w3x)F)dUw-`us9la=VPK9j`FhUk z)?Ff*>?I{Ekd1C<>WKmBbM*9@&3o>k}rkvJ8OC zqaoI+1)2^d0I4HG- zWE@~Ha$vgSs@8WhLipdSeFqcWW{7Y#YR_8kI_bLdl0XaKg#{5sLEWlpjZTgM7Yd|= zI=UL&fZbT_3GGQS<|JzOnXepO1 zyl(NmMIH?5Jm#D|c8q5nyw7f$c&=}&zst+>X`9bBxcC5vOze4hocVYxxrK6ed26bi z{m^{EoXaGd%SEK*vW7}v!Py18!Uxn_C#aZc8BVZpcFkb3OJ?Da(DPJiQt+M=O!i-6;WEN<}tPYt~qN5YZ4*~E zQS0fdDe`M9{DAtrHXq*dQfOUNuj8*H9l5%dLR-5pFI0_LD%JeC1;#&c>UnZuwSd#* z^)A1TJ#2cliorKl<3gw3Jcf(6ds=TCeR5i(Epwf< zld@h}v)Ei&)=){&*?2RP;aiqweukR{W^BdcixQ2xnAMfcjMKvjCTbLt&J0ydYKiJOavX3Bt;n8Hg*U}ojB0R(8KG&l91)#lG4_3 zl2>q&l(C3Pnuf=CWnwN9NShI(zP7mjV?xOPzLh9!unzVLej2)Y`E z1W)qjnWWjZXs1qUa(RAvau|>-rAHA6ilLu8U;OkexhuU=U@=r`v`RmBA_o~m3e+lp zA?7%+IWSns_^elE`f^smc+r8Aj7JVJiLbS|&Q#g2XtJocSlF`2kYl1pQm8YttHEMr zVOfI)g@6bLg$hYVja7k|Rt8kfR zjob7dpN?u1)A#Q>bI?O-!PQHuf?BB|Q_F5?t$lSh{{IztBoM0SX(bPep(g$>;u>=f zM<*LWV+hzKS^{jyDL`Y0R(1~wa|cBX5!$OiBtnu_jvS34T7@jN%pHv(YK0`N%o&X# zT7@jN%pHv(YK0`N%o&X#T7@jN%pHv(YK0`N%o&X#T7@jN%pHv(YK0`N%o&X#T7@jN O%pHv(YK7#mH3tB{C1Lvj