Skip to content

Commit

Permalink
Message Input Refactoring
Browse files Browse the repository at this point in the history
- 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
rapterjet2004 committed May 20, 2024
1 parent b2fe29d commit 90a68e2
Show file tree
Hide file tree
Showing 19 changed files with 2,304 additions and 1,454 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import com.nextcloud.talk.components.filebrowser.webdav.DavUtils
import com.nextcloud.talk.dagger.modules.BusModule
import com.nextcloud.talk.dagger.modules.ContextModule
import com.nextcloud.talk.dagger.modules.DatabaseModule
import com.nextcloud.talk.dagger.modules.ManagerModule
import com.nextcloud.talk.dagger.modules.RepositoryModule
import com.nextcloud.talk.dagger.modules.RestModule
import com.nextcloud.talk.dagger.modules.UtilsModule
Expand Down Expand Up @@ -77,7 +78,8 @@ import javax.inject.Singleton
ViewModelModule::class,
RepositoryModule::class,
UtilsModule::class,
ThemeModule::class
ThemeModule::class,
ManagerModule::class
]
)
@Singleton
Expand Down
1,456 changes: 116 additions & 1,340 deletions app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt

Large diffs are not rendered by default.

807 changes: 807 additions & 0 deletions app/src/main/java/com/nextcloud/talk/chat/MessageInputFragment.kt

Large diffs are not rendered by default.

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)
}
}
}
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()
}
}
}

0 comments on commit 90a68e2

Please sign in to comment.