Skip to content

Commit

Permalink
WIP replicating functionality 12 - Previewing code working + still ha…
Browse files Browse the repository at this point in the history
…s some glitches though

Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>
  • Loading branch information
rapterjet2004 committed Apr 16, 2024
1 parent c87687f commit 8c32597
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 40 deletions.
2 changes: 0 additions & 2 deletions app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -616,7 +616,6 @@ class ChatActivity :
}
this.lifecycle.addObserver(AudioUtils)
this.lifecycle.addObserver(chatViewModel)
this.lifecycle.addObserver(messageInputViewModel)
}

override fun onSaveInstanceState(outState: Bundle) {
Expand Down Expand Up @@ -656,7 +655,6 @@ class ChatActivity :
}
this.lifecycle.removeObserver(AudioUtils)
this.lifecycle.removeObserver(chatViewModel)
this.lifecycle.removeObserver(messageInputViewModel)
}

@SuppressLint("NotifyDataSetChanged")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,9 @@ class MessageInputFragment : Fragment() {
}

voiceRecordStartTime = System.currentTimeMillis()
binding.fragmentMessageInputView.audioRecordDuration.base = SystemClock.elapsedRealtime()
val base = SystemClock.elapsedRealtime()
binding.fragmentMessageInputView.audioRecordDuration.base = base
chatActivity.messageInputViewModel.setRecordingTime(base)
binding.fragmentMessageInputView.audioRecordDuration.start()
originX = event.x
originY = event.y
Expand All @@ -298,6 +300,7 @@ class MessageInputFragment : Fragment() {
if (chatActivity.chatViewModel.getVoiceRecordingLocked.value != true) { // can also be null
chatActivity.chatViewModel.stopAndDiscardAudioRecording()
}

resetSlider()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package com.nextcloud.talk.chat

import android.animation.ObjectAnimator
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.SeekBar
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.databinding.FragmentMessageInputVoiceRecordingBinding
Expand All @@ -28,6 +31,7 @@ class MessageInputVoiceRecordingFragment : Fragment() {

lateinit var binding: FragmentMessageInputVoiceRecordingBinding
private lateinit var chatActivity: ChatActivity
private var voicePreviewObjectAnimator: ObjectAnimator? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Expand All @@ -44,25 +48,34 @@ class MessageInputVoiceRecordingFragment : Fragment() {
themeVoiceRecordingView()
initVoiceRecordingView()
initObservers()
this.lifecycle.addObserver(chatActivity.messageInputViewModel)
return binding.root
}

override fun onDestroyView() {
super.onDestroyView()
// TODO destroy observers
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.isPlayerFinished.observe(viewLifecycleOwner) {
if (it) {
binding.playPauseBtn.icon = ContextCompat.getDrawable(
requireContext(),
R.drawable.ic_baseline_play_arrow_voice_message_24
)
}
}
}

private fun initVoiceRecordingView() {
binding.deleteVoiceRecording.setOnClickListener {
chatActivity.chatViewModel.stopAndDiscardAudioRecording()
chatActivity.chatViewModel.setVoiceRecordingLocked(false)
clear()
}

binding.sendVoiceRecording.setOnClickListener {
Expand All @@ -71,19 +84,20 @@ class MessageInputVoiceRecordingFragment : Fragment() {
chatActivity.currentConversation!!.displayName!!,
MessageInputFragment.VOICE_MESSAGE_META_DATA
)
chatActivity.chatViewModel.setVoiceRecordingLocked(false)
clear()
}

// FIXME this stops working after 2nd run
binding.micInputCloud.setOnClickListener {
togglePreviewVisibility()
}

binding.playPauseBtn.setOnClickListener {
// TODO stop mediaRecorder before starting the player.
togglePausePlay()
}

// TODO set chronometer too
binding.audioRecordDuration.base = chatActivity.messageInputViewModel.getRecordingTime.value ?: 0L
binding.audioRecordDuration.start()
// TODO fix up the swipe up to record being wonky

binding.seekbar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
Expand All @@ -100,19 +114,49 @@ class MessageInputVoiceRecordingFragment : Fragment() {
})
}

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.startMicInput(requireContext())
chatActivity.chatViewModel.startAudioRecording(requireContext(), chatActivity.currentConversation!!)
binding.audioRecordDuration.visibility = View.VISIBLE
binding.audioRecordDuration.start()
View.GONE
} else {
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ class AudioRecorderManager : LifecycleAwareManager {
private const val AUDIO_INTERVAL = 50L
}

private val scope = MainScope()

private var scope = MainScope()
private var loop = false
private var audioRecorder: AudioRecord? = null
private val bufferSize = AudioRecord.getMinBufferSize(
SAMPLE_RATE,
Expand All @@ -57,9 +57,14 @@ class AudioRecorderManager : LifecycleAwareManager {
if (audioRecorder == null || audioRecorder!!.state == AudioRecord.STATE_UNINITIALIZED) {
initAudioRecorder(context)
}
Log.d("Julius", "AudioRecorder started")
audioRecorder!!.startRecording()
scope.launch {
micInputObserver(callback)
loop = true
scope = MainScope().apply {
launch {
Log.d("Julius", "MicInputObserver started")
micInputObserver(callback)
}
}
}

Expand All @@ -71,13 +76,18 @@ class AudioRecorderManager : LifecycleAwareManager {
Log.e(TAG, "Stopped AudioRecord on invalid state ")
return
}
scope.cancel()
Log.d("Julius", "AudioRecorder stopped")
loop = false
audioRecorder!!.stop()
audioRecorder!!.release()
audioRecorder = null
}

private suspend fun micInputObserver(callback: MutableLiveData<Pair<Float, Float>>) = withContext(Dispatchers.IO) {
while (true) {
if (!loop) {
return@withContext
}
val byteArr = ByteArray(bufferSize / 2)
audioRecorder!!.read(byteArr, 0, byteArr.size)
val x = abs(byteArr[0].toFloat())
Expand All @@ -101,6 +111,7 @@ class AudioRecorderManager : LifecycleAwareManager {
)

if (permissionCheck == PermissionChecker.PERMISSION_GRANTED) {
Log.d("Julius", "AudioRecorder init")
audioRecorder = AudioRecord(
MediaRecorder.AudioSource.MIC,
SAMPLE_RATE,
Expand All @@ -112,14 +123,15 @@ class AudioRecorderManager : LifecycleAwareManager {
}

override fun handleOnPause() {
stop()
// unused atm
}

override fun handleOnResume() {
// unused atm
}

override fun handleOnStop() {
// unused atm
scope.cancel()
stop()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ package com.nextcloud.talk.chat.data.io
import android.media.MediaPlayer
import android.os.Build
import android.util.Log
import androidx.lifecycle.MutableLiveData
import com.nextcloud.talk.chat.ChatActivity

/**
Expand All @@ -20,54 +21,53 @@ class MediaPlayerManager : LifecycleAwareManager {
companion object {
val TAG: String = MediaPlayerManager::class.java.simpleName
private const val ONE_SECOND_IN_MILLIS = 1000L
private const val DIVIDER = 100f
}

private var mediaPlayer: MediaPlayer? = null
private var mediaPlayerPosition: Int = 0
var mediaPlayerDuration: Int = 0

/**
* Starts playing audio from the given path, initializes or resumes if the player is already created.
*/
fun start(path: String) {
if (mediaPlayer == null) {
init(path)
}
mediaPlayer!!.start()
fun start(path: String, callback: MutableLiveData<Boolean>) {
init(path, callback)
}

/**
* Pauses the player, but does not destroy it. That occurs implicitly on lifecycle change.
* Stop and destroys the player.
*/
fun stop() {
if (mediaPlayer != null && mediaPlayer!!.isPlaying) {
mediaPlayer!!.pause()
if (mediaPlayer != null) {
Log.d("Julius", "media player destroyed")
mediaPlayer!!.stop()
mediaPlayer!!.release()
mediaPlayer = null
}
}

/**
* Seeks the player to the given position, saves position for resynchronization.
*/
fun seekTo(progress: Int) {
if (mediaPlayer != null && mediaPlayer!!.isPlaying) {
mediaPlayer!!.seekTo(progress)
mediaPlayerPosition = progress
}
}

private fun destroy() {
if (mediaPlayer != null) {
mediaPlayer!!.stop()
mediaPlayer!!.release()
mediaPlayer = null
val pos = mediaPlayer!!.duration * (progress / DIVIDER)
mediaPlayer!!.seekTo(pos.toInt())
mediaPlayerPosition = progress
}
}

@Suppress("Detekt.TooGenericExceptionCaught")
private fun init(path: String) {
private fun init(path: String, callback: MutableLiveData<Boolean>) {
try {
mediaPlayer = MediaPlayer().apply {
setDataSource(path)
prepare()
prepareAsync()
setOnPreparedListener {
mediaPlayerDuration = it.duration
start()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
setOnMediaTimeDiscontinuityListener { mp, _ ->
if (mediaPlayerPosition > ONE_SECOND_IN_MILLIS) {
Expand All @@ -77,21 +77,24 @@ class MediaPlayerManager : LifecycleAwareManager {
// this ensures that audio can be resumed at a given position
this.seekTo(mediaPlayerPosition)
}
setOnCompletionListener {
callback.postValue(true)
}
}
} catch (e: Exception) {
Log.e(ChatActivity.TAG, "failed to initialize mediaPlayer", e)
}
}

override fun handleOnPause() {
stop()
// unused atm
}

override fun handleOnResume() {
// unused atm
}

override fun handleOnStop() {
destroy()
stop()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,12 @@ class ChatViewModel @Inject constructor(
_getVoiceRecordingInProgress.postValue(true)
}

fun stopAudioRecording() {
mediaRecorderManager.stop()
_getVoiceRecordingInProgress.postValue(false)
Log.d("Julius", "Recording stopped")
}

fun stopAndSendAudioRecording(room: String, displayName: String, metaData: String) {
mediaRecorderManager.stop()
_getVoiceRecordingInProgress.postValue(false)
Expand All @@ -595,6 +601,10 @@ class ChatViewModel @Inject constructor(
cachedFile.delete()
}

fun getCurrentVoiceRecordFile(): String {
return mediaRecorderManager.currentVoiceRecordFile
}

fun uploadFile(fileUri: String, room: String, displayName: String, metaData: String) {
try {
require(fileUri.isNotEmpty())
Expand Down
Loading

0 comments on commit 8c32597

Please sign in to comment.