-
-
Notifications
You must be signed in to change notification settings - Fork 239
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Added io folder for Abstracting away background work - AudioFocusRequestManager - MediaPlayerManager - MediaRecorderManager - AudioRecorderManager Included new View Models + Fragments to separate concerns - MessageInputFragment - MessageInputVoiceRecordingFragment Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>
- Loading branch information
1 parent
5242f9c
commit 8d9ecef
Showing
19 changed files
with
2,331 additions
and
1,470 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
1,476 changes: 122 additions & 1,354 deletions
1,476
app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt
Large diffs are not rendered by default.
Oops, something went wrong.
821 changes: 821 additions & 0 deletions
821
app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt
Large diffs are not rendered by default.
Oops, something went wrong.
220 changes: 220 additions & 0 deletions
220
app/src/main/java/com/nextcloud/talk/chat/MessageInputVoiceRecordingFragment.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,220 @@ | ||
/* | ||
* Nextcloud Talk - Android Client | ||
* | ||
* SPDX-FileCopyrightText: 2024 Julius Linus juliuslinus1@gmail.com | ||
* SPDX-License-Identifier: GPL-3.0-or-later | ||
*/ | ||
|
||
package com.nextcloud.talk.chat | ||
|
||
import android.os.Bundle | ||
import android.os.SystemClock | ||
import android.view.LayoutInflater | ||
import android.view.View | ||
import android.view.ViewGroup | ||
import android.widget.SeekBar | ||
import android.widget.SeekBar.OnSeekBarChangeListener | ||
import androidx.core.content.ContextCompat | ||
import androidx.fragment.app.Fragment | ||
import autodagger.AutoInjector | ||
import com.nextcloud.android.common.ui.theme.utils.ColorRole | ||
import com.nextcloud.talk.R | ||
import com.nextcloud.talk.application.NextcloudTalkApplication | ||
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication | ||
import com.nextcloud.talk.chat.data.io.AudioFocusRequestManager | ||
import com.nextcloud.talk.databinding.FragmentMessageInputVoiceRecordingBinding | ||
import com.nextcloud.talk.ui.theme.ViewThemeUtils | ||
import javax.inject.Inject | ||
|
||
@AutoInjector(NextcloudTalkApplication::class) | ||
class MessageInputVoiceRecordingFragment : Fragment() { | ||
companion object { | ||
val TAG: String = MessageInputVoiceRecordingFragment::class.java.simpleName | ||
private const val SEEK_LIMIT = 98 | ||
|
||
@JvmStatic | ||
fun newInstance() = MessageInputVoiceRecordingFragment() | ||
} | ||
|
||
@Inject | ||
lateinit var viewThemeUtils: ViewThemeUtils | ||
|
||
lateinit var binding: FragmentMessageInputVoiceRecordingBinding | ||
private lateinit var chatActivity: ChatActivity | ||
private var pause = false | ||
|
||
override fun onCreate(savedInstanceState: Bundle?) { | ||
super.onCreate(savedInstanceState) | ||
sharedApplication!!.componentApplication.inject(this) | ||
} | ||
|
||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { | ||
binding = FragmentMessageInputVoiceRecordingBinding.inflate(inflater) | ||
chatActivity = (requireActivity() as ChatActivity) | ||
themeVoiceRecordingView() | ||
initVoiceRecordingView() | ||
initObservers() | ||
this.lifecycle.addObserver(chatActivity.messageInputViewModel) | ||
return binding.root | ||
} | ||
|
||
override fun onDestroyView() { | ||
super.onDestroyView() | ||
this.lifecycle.removeObserver(chatActivity.messageInputViewModel) | ||
} | ||
|
||
private fun initObservers() { | ||
chatActivity.messageInputViewModel.startMicInput(requireContext()) | ||
chatActivity.messageInputViewModel.micInputAudioObserver.observe(viewLifecycleOwner) { | ||
binding.micInputCloud.setRotationSpeed(it.first, it.second) | ||
} | ||
chatActivity.messageInputViewModel.mediaPlayerSeekbarObserver.observe(viewLifecycleOwner) { progress -> | ||
if (progress >= SEEK_LIMIT) { | ||
togglePausePlay() | ||
binding.seekbar.progress = 0 | ||
} else if (!pause) { | ||
binding.seekbar.progress = progress | ||
} | ||
} | ||
|
||
chatActivity.messageInputViewModel.getAudioFocusChange.observe(viewLifecycleOwner) { state -> | ||
when (state) { | ||
AudioFocusRequestManager.ManagerState.AUDIO_FOCUS_CHANGE_LOSS -> { | ||
if (chatActivity.messageInputViewModel.isVoicePreviewPlaying.value == true) { | ||
chatActivity.messageInputViewModel.stopMediaPlayer() | ||
} | ||
} | ||
AudioFocusRequestManager.ManagerState.AUDIO_FOCUS_CHANGE_LOSS_TRANSIENT -> { | ||
if (chatActivity.messageInputViewModel.isVoicePreviewPlaying.value == true) { | ||
chatActivity.messageInputViewModel.pauseMediaPlayer() | ||
} | ||
} | ||
AudioFocusRequestManager.ManagerState.BROADCAST_RECEIVED -> { | ||
if (chatActivity.messageInputViewModel.isVoicePreviewPlaying.value == true) { | ||
chatActivity.messageInputViewModel.pauseMediaPlayer() | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
private fun initVoiceRecordingView() { | ||
binding.deleteVoiceRecording.setOnClickListener { | ||
chatActivity.chatViewModel.stopAndDiscardAudioRecording() | ||
clear() | ||
} | ||
|
||
binding.sendVoiceRecording.setOnClickListener { | ||
chatActivity.chatViewModel.stopAndSendAudioRecording( | ||
chatActivity.roomToken, | ||
chatActivity.currentConversation!!.displayName!!, | ||
MessageInputFragment.VOICE_MESSAGE_META_DATA | ||
) | ||
clear() | ||
} | ||
|
||
binding.micInputCloud.setOnClickListener { | ||
togglePreviewVisibility() | ||
} | ||
|
||
binding.playPauseBtn.setOnClickListener { | ||
togglePausePlay() | ||
} | ||
|
||
binding.audioRecordDuration.base = chatActivity.messageInputViewModel.getRecordingTime.value ?: 0L | ||
binding.audioRecordDuration.start() | ||
|
||
binding.seekbar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener { | ||
override fun onProgressChanged(seekbar: SeekBar, progress: Int, fromUser: Boolean) { | ||
if (fromUser) { | ||
chatActivity.messageInputViewModel.seekMediaPlayerTo(progress) | ||
} | ||
} | ||
|
||
override fun onStartTrackingTouch(p0: SeekBar) { | ||
pause = true | ||
} | ||
|
||
override fun onStopTrackingTouch(p0: SeekBar) { | ||
pause = false | ||
} | ||
}) | ||
} | ||
|
||
private fun clear() { | ||
chatActivity.chatViewModel.setVoiceRecordingLocked(false) | ||
chatActivity.messageInputViewModel.stopMicInput() | ||
chatActivity.chatViewModel.stopAudioRecording() | ||
chatActivity.messageInputViewModel.stopMediaPlayer() | ||
binding.audioRecordDuration.stop() | ||
binding.audioRecordDuration.clearAnimation() | ||
} | ||
|
||
private fun togglePreviewVisibility() { | ||
val visibility = binding.voicePreviewContainer.visibility | ||
binding.voicePreviewContainer.visibility = if (visibility == View.VISIBLE) { | ||
chatActivity.messageInputViewModel.stopMediaPlayer() | ||
binding.playPauseBtn.icon = ContextCompat.getDrawable( | ||
requireContext(), | ||
R.drawable.ic_baseline_play_arrow_voice_message_24 | ||
) | ||
pause = true | ||
chatActivity.messageInputViewModel.startMicInput(requireContext()) | ||
chatActivity.chatViewModel.startAudioRecording(requireContext(), chatActivity.currentConversation!!) | ||
binding.audioRecordDuration.visibility = View.VISIBLE | ||
binding.audioRecordDuration.base = SystemClock.elapsedRealtime() | ||
binding.audioRecordDuration.start() | ||
View.GONE | ||
} else { | ||
pause = false | ||
binding.seekbar.progress = 0 | ||
chatActivity.messageInputViewModel.stopMicInput() | ||
chatActivity.chatViewModel.stopAudioRecording() | ||
binding.audioRecordDuration.visibility = View.GONE | ||
binding.audioRecordDuration.stop() | ||
View.VISIBLE | ||
} | ||
} | ||
|
||
private fun togglePausePlay() { | ||
val path = chatActivity.chatViewModel.getCurrentVoiceRecordFile() | ||
if (chatActivity.messageInputViewModel.isVoicePreviewPlaying.value == true) { | ||
binding.playPauseBtn.icon = ContextCompat.getDrawable( | ||
requireContext(), | ||
R.drawable.ic_baseline_play_arrow_voice_message_24 | ||
) | ||
chatActivity.messageInputViewModel.stopMediaPlayer() | ||
} else { | ||
binding.playPauseBtn.icon = ContextCompat.getDrawable( | ||
requireContext(), | ||
R.drawable.ic_baseline_pause_voice_message_24 | ||
) | ||
chatActivity.messageInputViewModel.startMediaPlayer(path) | ||
} | ||
} | ||
|
||
private fun themeVoiceRecordingView() { | ||
binding.playPauseBtn.let { | ||
viewThemeUtils.material.colorMaterialButtonText(it) | ||
} | ||
|
||
binding.seekbar.let { | ||
viewThemeUtils.platform.themeHorizontalSeekBar(it) | ||
} | ||
|
||
binding.deleteVoiceRecording.let { | ||
viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY) | ||
} | ||
binding.sendVoiceRecording.let { | ||
viewThemeUtils.platform.colorImageView(it, ColorRole.PRIMARY) | ||
} | ||
|
||
binding.voicePreviewContainer.let { | ||
viewThemeUtils.talk.themeOutgoingMessageBubble(it, true, false) | ||
} | ||
|
||
binding.micInputCloud.let { | ||
viewThemeUtils.talk.themeMicInputCloud(it) | ||
} | ||
} | ||
} |
112 changes: 112 additions & 0 deletions
112
app/src/main/java/com/nextcloud/talk/chat/data/io/AudioFocusRequestManager.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
/* | ||
* Nextcloud Talk - Android Client | ||
* | ||
* SPDX-FileCopyrightText: 2024 Julius Linus <juliuslinus1@gmail.com> | ||
* SPDX-License-Identifier: GPL-3.0-or-later | ||
*/ | ||
|
||
package com.nextcloud.talk.chat.data.io | ||
|
||
import android.content.BroadcastReceiver | ||
import android.content.Context | ||
import android.content.Intent | ||
import android.content.IntentFilter | ||
import android.media.AudioFocusRequest | ||
import android.media.AudioManager | ||
import android.os.Build | ||
import androidx.annotation.RequiresApi | ||
import androidx.lifecycle.LiveData | ||
import androidx.lifecycle.MutableLiveData | ||
|
||
/** | ||
* Abstraction over the [AudioFocusManager](https://developer.android.com/reference/kotlin/android/media/AudioFocusRequest) | ||
* class used to manage audio focus requests automatically | ||
*/ | ||
class AudioFocusRequestManager(private val context: Context) { | ||
companion object { | ||
val TAG: String? = AudioFocusRequestManager::class.java.simpleName | ||
} | ||
|
||
enum class ManagerState { | ||
AUDIO_FOCUS_CHANGE_LOSS, | ||
AUDIO_FOCUS_CHANGE_LOSS_TRANSIENT, | ||
BROADCAST_RECEIVED | ||
} | ||
|
||
private val _getManagerState: MutableLiveData<ManagerState> = MutableLiveData() | ||
val getManagerState: LiveData<ManagerState> | ||
get() = _getManagerState | ||
|
||
private var isPausedDueToBecomingNoisy = false | ||
private val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager | ||
private val duration = AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE | ||
private val audioFocusChangeListener: AudioManager.OnAudioFocusChangeListener = | ||
AudioManager.OnAudioFocusChangeListener { flag -> | ||
when (flag) { | ||
AudioManager.AUDIOFOCUS_LOSS -> { | ||
isPausedDueToBecomingNoisy = false | ||
_getManagerState.value = ManagerState.AUDIO_FOCUS_CHANGE_LOSS | ||
} | ||
|
||
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> { | ||
isPausedDueToBecomingNoisy = false | ||
_getManagerState.value = ManagerState.AUDIO_FOCUS_CHANGE_LOSS_TRANSIENT | ||
} | ||
} | ||
} | ||
private val noisyAudioStreamReceiver = object : BroadcastReceiver() { | ||
override fun onReceive(context: Context?, intent: Intent?) { | ||
isPausedDueToBecomingNoisy = true | ||
_getManagerState.value = ManagerState.BROADCAST_RECEIVED | ||
} | ||
} | ||
|
||
@RequiresApi(Build.VERSION_CODES.O) | ||
private val focusRequest = AudioFocusRequest.Builder(duration) | ||
.setOnAudioFocusChangeListener(audioFocusChangeListener) | ||
.build() | ||
|
||
/** | ||
* Requests the OS for audio focus, before executing the callback on success | ||
*/ | ||
fun audioFocusRequest(shouldRequestFocus: Boolean, onGranted: () -> Unit) { | ||
if (isPausedDueToBecomingNoisy) { | ||
onGranted() | ||
return | ||
} | ||
|
||
val isGranted: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | ||
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() | ||
handleBecomingNoisyBroadcast(shouldRequestFocus) | ||
} | ||
} | ||
|
||
private fun handleBecomingNoisyBroadcast(register: Boolean) { | ||
try { | ||
if (register) { | ||
context.registerReceiver( | ||
noisyAudioStreamReceiver, | ||
IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY) | ||
) | ||
} else { | ||
context.unregisterReceiver(noisyAudioStreamReceiver) | ||
} | ||
} catch (e: IllegalArgumentException) { | ||
e.printStackTrace() | ||
} | ||
} | ||
} |
Oops, something went wrong.