Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactored Message input view into it's own Fragment #3792

Merged
merged 1 commit into from
Jun 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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,477 changes: 116 additions & 1,361 deletions app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt

Large diffs are not rendered by default.

821 changes: 821 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()
}
}
}