diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/SoundPlayer.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/SoundPlayer.kt index ff83c10df3..a43fac29e3 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/SoundPlayer.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/SoundPlayer.kt @@ -2,17 +2,15 @@ package chat.simplex.common.helpers import android.media.* import android.net.Uri -import android.os.VibrationEffect -import android.os.Vibrator +import android.os.* import androidx.core.content.ContextCompat import chat.simplex.common.R -import chat.simplex.common.platform.SoundPlayerInterface -import chat.simplex.common.platform.androidAppContext +import chat.simplex.common.platform.* import kotlinx.coroutines.* object SoundPlayer: SoundPlayerInterface { private var player: MediaPlayer? = null - var playing = false + private var playing = false override fun start(scope: CoroutineScope, sound: Boolean) { player?.reset() @@ -32,7 +30,7 @@ object SoundPlayer: SoundPlayerInterface { scope.launch { while (playing) { if (sound) player?.start() - vibrator?.vibrate(effect) + vibrator?.vibrateApiVersionAware(effect) delay(3500) } } @@ -43,3 +41,61 @@ object SoundPlayer: SoundPlayerInterface { player?.stop() } } + +object CallSoundsPlayer: CallSoundsPlayerInterface { + private var player: MediaPlayer? = null + private var playing = false + + override fun startWaitingAnswerSound(scope: CoroutineScope) { + player?.reset() + player = MediaPlayer().apply { + setAudioAttributes( + AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING) + .build() + ) + setDataSource(androidAppContext, Uri.parse("android.resource://" + androidAppContext.packageName + "/" + R.raw.call_sound_before_answer)) + prepare() + } + playing = true + scope.launch { + while (playing) { + player?.start() + delay(2000) + } + } + } + + override fun vibrate() { + val vibrator = ContextCompat.getSystemService(androidAppContext, Vibrator::class.java) + val effect = VibrationEffect.createOneShot(50, VibrationEffect.DEFAULT_AMPLITUDE) + vibrator?.vibrateApiVersionAware(effect) + } + + override fun stop() { + playing = false + player?.stop() + } +} + +private fun Vibrator.vibrateApiVersionAware(effect: VibrationEffect) { + if (Build.VERSION.SDK_INT >= 33) { + vibrate( + effect, + VibrationAttributes.Builder() + .setUsage(VibrationAttributes.USAGE_ALARM) + .build() + ) + } else if (Build.VERSION.SDK_INT >= 29) { + vibrate( + effect, + AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .setUsage(AudioAttributes.USAGE_ALARM) + .build() + ) + } else { + vibrate(effect) + } +} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/RecAndPlay.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/RecAndPlay.android.kt index 5b0d3c778f..5cb10ff070 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/RecAndPlay.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/RecAndPlay.android.kt @@ -296,6 +296,7 @@ actual object AudioPlayer: AudioPlayerInterface { } actual typealias SoundPlayer = chat.simplex.common.helpers.SoundPlayer +actual typealias CallSoundsPlayer = chat.simplex.common.helpers.CallSoundsPlayer class CryptoMediaSource(val data: ByteArray) : MediaDataSource() { override fun readAt(position: Long, buffer: ByteArray, offset: Int, size: Int): Int { diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt index b0c4beded0..2a11c09c95 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt @@ -69,6 +69,8 @@ fun activeCallDestroyWebView() = withApi { @SuppressLint("SourceLockedOrientationActivity") @Composable actual fun ActiveCallView() { + val call = remember { chatModel.activeCall }.value + val scope = rememberCoroutineScope() val audioViaBluetooth = rememberSaveable { mutableStateOf(false) } val proximityLock = remember { val pm = (androidAppContext.getSystemService(Context.POWER_SERVICE) as PowerManager) @@ -78,6 +80,16 @@ actual fun ActiveCallView() { null } } + val wasConnected = rememberSaveable { mutableStateOf(false) } + LaunchedEffect(call) { + if (call?.callState == CallState.Connected && !wasConnected.value) { + CallSoundsPlayer.vibrate() + wasConnected.value = true + } + if ((call?.callState ?: CallState.WaitCapabilities) >= CallState.OfferReceived) { + CallSoundsPlayer.stop() + } + } DisposableEffect(Unit) { val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager var btDeviceCount = 0 @@ -106,7 +118,15 @@ actual fun ActiveCallView() { } } am.registerAudioDeviceCallback(audioCallback, null) + if (call?.callState == CallState.WaitCapabilities || call?.callState == CallState.InvitationSent) { + setCallSound(call.soundSpeaker, audioViaBluetooth) + CallSoundsPlayer.startWaitingAnswerSound(scope) + } onDispose { + CallSoundsPlayer.stop() + if (wasConnected.value) { + CallSoundsPlayer.vibrate() + } dropAudioManagerOverrides() am.unregisterAudioDeviceCallback(audioCallback) if (proximityLock?.isHeld == true) { @@ -122,8 +142,6 @@ actual fun ActiveCallView() { if (proximityLock?.isHeld == false) proximityLock.acquire() } } - val scope = rememberCoroutineScope() - val call = chatModel.activeCall.value Box(Modifier.fillMaxSize()) { WebRTCView(chatModel.callCommand) { apiMsg -> Log.d(TAG, "received from WebRTCView: $apiMsg") diff --git a/apps/multiplatform/common/src/androidMain/res/raw/call_sound_before_answer.mp3 b/apps/multiplatform/common/src/androidMain/res/raw/call_sound_before_answer.mp3 new file mode 100644 index 0000000000..b3aa169840 Binary files /dev/null and b/apps/multiplatform/common/src/androidMain/res/raw/call_sound_before_answer.mp3 differ diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/RecAndPlay.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/RecAndPlay.kt index 0e0f769487..66c6bad03f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/RecAndPlay.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/RecAndPlay.kt @@ -39,4 +39,12 @@ interface SoundPlayerInterface { fun stop() } +interface CallSoundsPlayerInterface { + fun startWaitingAnswerSound(scope: CoroutineScope) + fun stop() + fun vibrate() +} + expect object SoundPlayer: SoundPlayerInterface + +expect object CallSoundsPlayer: CallSoundsPlayerInterface diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt index 50d143aaf5..e936b35789 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt @@ -228,3 +228,35 @@ actual object SoundPlayer: SoundPlayerInterface { AudioPlayer.stop() } } + +actual object CallSoundsPlayer: CallSoundsPlayerInterface { + private var player: MediaPlayer? = null + private var playing = false + + override fun startWaitingAnswerSound(scope: CoroutineScope) { + val tmpFile = File(tmpDir, UUID.randomUUID().toString()) + tmpFile.deleteOnExit() + SoundPlayer::class.java.getResource("/media/call_sound_before_answer.mp3").openStream()!!.use { it.copyTo(tmpFile.outputStream()) } + SoundPlayer.playing = true + scope.launch { + while (SoundPlayer.playing) { + AudioPlayer.play(CryptoFile.plain(tmpFile.absolutePath), mutableStateOf(true), mutableStateOf(0), mutableStateOf(0), true) + delay(3500) + } + } + playing = true + scope.launch { + while (playing) { + player?.start() + delay(3500) + } + } + } + + override fun vibrate() {} + + override fun stop() { + playing = false + AudioPlayer.stop() + } +} diff --git a/apps/multiplatform/common/src/desktopMain/resources/media/call_sound_before_answer.mp3 b/apps/multiplatform/common/src/desktopMain/resources/media/call_sound_before_answer.mp3 new file mode 100644 index 0000000000..b3aa169840 Binary files /dev/null and b/apps/multiplatform/common/src/desktopMain/resources/media/call_sound_before_answer.mp3 differ