diff --git a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt index 3cd1013b57..81ffd18f47 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt @@ -31,10 +31,12 @@ package com.nextcloud.talk.chat import android.Manifest import android.animation.ObjectAnimator import android.annotation.SuppressLint +import android.content.BroadcastReceiver import android.content.ClipData import android.content.ClipboardManager import android.content.Context import android.content.Intent +import android.content.IntentFilter import android.content.pm.PackageManager import android.content.res.AssetFileDescriptor import android.content.res.Resources @@ -42,7 +44,9 @@ import android.database.Cursor import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable +import android.media.AudioFocusRequest import android.media.AudioFormat +import android.media.AudioManager import android.media.AudioRecord import android.media.MediaPlayer import android.media.MediaRecorder @@ -384,6 +388,20 @@ class ChatActivity : private var lastRecordMediaPosition: Int = 0 private var lastRecordedSeeked: Boolean = false + private val audioFocusChangeListener = getAudioFocusChangeListener() + + private val noisyAudioStreamReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + chatViewModel.isPausedDueToBecomingNoisy = true + if (isVoicePreviewPlaying) { + pausePreviewVoicePlaying() + } + if (currentlyPlayedVoiceMessage != null) { + pausePlayback(currentlyPlayedVoiceMessage!!) + } + } + } + private lateinit var participantPermissions: ParticipantPermissions private var videoURI: Uri? = null @@ -1099,7 +1117,9 @@ class ChatActivity : binding.messageInputView.micInputCloud.setOnClickListener { if (mediaRecorderState == MediaRecorderState.RECORDING) { - recorder?.stop() + audioFocusRequest(false) { + recorder?.stop() + } mediaRecorderState = MediaRecorderState.INITIAL stopMicInputRecordingAnimation() showPreviewVoiceRecording(true) @@ -1328,8 +1348,11 @@ class ChatActivity : duration = it.duration.toLong() interpolator = LinearInterpolator() } - voicePreviewMediaPlayer!!.start() - voicePreviewObjectAnimator!!.start() + audioFocusRequest(true) { + voicePreviewMediaPlayer!!.start() + voicePreviewObjectAnimator!!.start() + handleBecomingNoisyBroadcast(register = true) + } } setOnCompletionListener { @@ -1343,15 +1366,21 @@ class ChatActivity : if (voicePreviewMediaPlayer == null) { initPreviewVoiceRecording() } else { - voicePreviewMediaPlayer!!.start() - voicePreviewObjectAnimator!!.resume() + audioFocusRequest(true) { + voicePreviewMediaPlayer!!.start() + voicePreviewObjectAnimator!!.resume() + handleBecomingNoisyBroadcast(register = true) + } } } private fun pausePreviewVoicePlaying() { Log.d(TAG, "paused preview voice recording") - voicePreviewMediaPlayer!!.pause() - voicePreviewObjectAnimator!!.pause() + audioFocusRequest(false) { + voicePreviewMediaPlayer!!.pause() + voicePreviewObjectAnimator!!.pause() + handleBecomingNoisyBroadcast(register = false) + } } private fun stopPreviewVoicePlaying() { @@ -1361,9 +1390,12 @@ class ChatActivity : voicePreviewObjectAnimator!!.end() voicePreviewObjectAnimator = null binding.messageInputView.seekBar.clearAnimation() - voicePreviewMediaPlayer!!.stop() - voicePreviewMediaPlayer!!.release() - voicePreviewMediaPlayer = null + audioFocusRequest(false) { + voicePreviewMediaPlayer!!.stop() + voicePreviewMediaPlayer!!.release() + voicePreviewMediaPlayer = null + handleBecomingNoisyBroadcast(register = false) + } } } @@ -1817,6 +1849,73 @@ class ChatActivity : } } + private fun getAudioFocusChangeListener(): AudioManager.OnAudioFocusChangeListener { + return AudioManager.OnAudioFocusChangeListener { flag -> + when (flag) { + AudioManager.AUDIOFOCUS_LOSS -> { + chatViewModel.isPausedDueToBecomingNoisy = false + if (isVoicePreviewPlaying) { + stopPreviewVoicePlaying() + } + if (currentlyPlayedVoiceMessage != null) { + stopMediaPlayer(currentlyPlayedVoiceMessage!!) + } + } + + AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> { + chatViewModel.isPausedDueToBecomingNoisy = false + if (isVoicePreviewPlaying) { + pausePreviewVoicePlaying() + } + if (currentlyPlayedVoiceMessage != null) { + pausePlayback(currentlyPlayedVoiceMessage!!) + } + } + } + } + } + + private fun audioFocusRequest(shouldRequestFocus: Boolean, onGranted: () -> Unit) { + if (chatViewModel.isPausedDueToBecomingNoisy) { + onGranted() + return + } + val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager + val duration = AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE + + val isGranted: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val focusRequest = AudioFocusRequest.Builder(duration) + .setOnAudioFocusChangeListener(audioFocusChangeListener) + .build() + if (shouldRequestFocus) { + audioManager.requestAudioFocus(focusRequest) + } else { + audioManager.abandonAudioFocusRequest(focusRequest) + } + } else { + @Deprecated("This method was deprecated in API level 26.") + if (shouldRequestFocus) { + audioManager.requestAudioFocus(audioFocusChangeListener, AudioManager.STREAM_MUSIC, duration) + } else { + audioManager.abandonAudioFocus(audioFocusChangeListener) + } + } + if (isGranted == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { + onGranted() + } + } + + private fun handleBecomingNoisyBroadcast(register: Boolean) { + if (register && !chatViewModel.receiverRegistered) { + registerReceiver(noisyAudioStreamReceiver, IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)) + chatViewModel.receiverRegistered = true + } else if (!chatViewModel.receiverUnregistered) { + unregisterReceiver(noisyAudioStreamReceiver) + chatViewModel.receiverUnregistered = true + chatViewModel.receiverRegistered = false + } + } + private fun startPlayback(message: ChatMessage) { if (!active) { // don't begin to play voice message if screen is not visible anymore. @@ -1830,8 +1929,10 @@ class ChatActivity : mediaPlayer?.let { if (!it.isPlaying) { - it.start() - Log.d(TAG, "MediaPlayer has Started") + audioFocusRequest(true) { + it.start() + handleBecomingNoisyBroadcast(register = true) + } } mediaPlayerHandler = Handler() @@ -1866,8 +1967,10 @@ class ChatActivity : private fun pausePlayback(message: ChatMessage) { if (mediaPlayer!!.isPlaying) { - mediaPlayer!!.pause() - Log.d(TAG, "MediaPlayer is paused") + audioFocusRequest(false) { + mediaPlayer!!.pause() + handleBecomingNoisyBroadcast(register = false) + } } message.isPlayingVoiceMessage = false @@ -1925,7 +2028,10 @@ class ChatActivity : mediaPlayer?.let { if (it.isPlaying) { Log.d(TAG, "media player is stopped") - it.stop() + audioFocusRequest(false) { + it.stop() + handleBecomingNoisyBroadcast(register = false) + } } } } catch (e: IllegalStateException) { @@ -2141,8 +2247,10 @@ class ChatActivity : private fun stopMicInputRecordingAnimation() { if (micInputAudioRecordThread != null) { Log.d(TAG, "Mic Animation Ended") - micInputAudioRecorder.stop() - micInputAudioRecorder.release() + audioFocusRequest(false) { + micInputAudioRecorder.stop() + micInputAudioRecorder.release() + } isMicInputAudioThreadRunning = false micInputAudioRecordThread = null } @@ -2193,7 +2301,9 @@ class ChatActivity : } try { - start() + audioFocusRequest(true) { + start() + } mediaRecorderState = MediaRecorderState.RECORDING Log.d(TAG, "recording started") } catch (e: IllegalStateException) { @@ -2234,8 +2344,10 @@ class ChatActivity : recorder?.apply { try { if (mediaRecorderState == MediaRecorderState.RECORDING) { - stop() - reset() + audioFocusRequest(false) { + stop() + reset() + } mediaRecorderState = MediaRecorderState.INITIAL Log.d(TAG, "stopped recorder") } diff --git a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt index d285f112af..3bad3aad93 100644 --- a/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt +++ b/app/src/main/java/com/nextcloud/talk/chat/viewmodels/ChatViewModel.kt @@ -48,6 +48,11 @@ class ChatViewModel @Inject constructor(private val repository: ChatRepository) open class GetReminderExistState(val reminder: Reminder) : ViewState private val _getReminderExistState: MutableLiveData = MutableLiveData(GetReminderStartState) + + var isPausedDueToBecomingNoisy = false + var receiverRegistered = false + var receiverUnregistered = false + val getReminderExistState: LiveData get() = _getReminderExistState diff --git a/app/src/main/java/com/nextcloud/talk/fullscreenfile/FullScreenMediaActivity.kt b/app/src/main/java/com/nextcloud/talk/fullscreenfile/FullScreenMediaActivity.kt index 1d6796b522..0dd0b9ba50 100644 --- a/app/src/main/java/com/nextcloud/talk/fullscreenfile/FullScreenMediaActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/fullscreenfile/FullScreenMediaActivity.kt @@ -44,6 +44,7 @@ import androidx.core.view.marginBottom import androidx.core.view.updateLayoutParams import androidx.core.view.updatePadding import androidx.fragment.app.DialogFragment +import androidx.media3.common.AudioAttributes import androidx.media3.common.MediaItem import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.ExoPlayer @@ -165,7 +166,10 @@ class FullScreenMediaActivity : AppCompatActivity() { } private fun initializePlayer() { - player = ExoPlayer.Builder(applicationContext).build() + player = ExoPlayer.Builder(applicationContext) + .setAudioAttributes(AudioAttributes.DEFAULT, true) + .setHandleAudioBecomingNoisy(true) + .build() binding.playerView.player = player } @@ -202,6 +206,7 @@ class FullScreenMediaActivity : AppCompatActivity() { windowInsetsController.show(WindowInsetsCompat.Type.systemBars()) supportActionBar?.show() } + private fun applyWindowInsets() { val playerView = binding.playerView val exoControls = playerView.findViewById(R.id.exo_bottom_bar)