Skip to content

Commit

Permalink
WIP created classes + xml
Browse files Browse the repository at this point in the history
Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>

WIP setup refactoring stage

Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>

WIP replicating functionality 1

Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>

WIP replicating functionality 2

Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>

WIP replicating functionality 3 - got it displaying but not working

Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>

WIP replicating functionality 4 - got it working - iterating through

Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>

WIP replicating functionality 5 - replying + editing

Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>

WIP replicating functionality 6 - mentioning + voice recording fragment xml

Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>

WIP replicating functionality 7 - MediaRecorderManager, and set up MessageInputVoiceRecordingFragment

Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>

Analysis: update lint results to reflect reduced error/warning count

Signed-off-by: github-actions <github-actions@github.com>

WIP replicating functionality 8 - Setting up recording button + Got recording partially working

Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>

WIP replicating functionality 9 - Got the recorder slider working

Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>

WIP replicating functionality 10 - AudioRecorderManager + MicInputCloud + Got fragment transactions working + Got uploading working + Got continuous recording working

Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>

WIP replicating functionality 11 - MediaPlayerManager + ManagerModule + Made all managers lifecycle aware

Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>

WIP replicating functionality 12 - Previewing code working + still has some glitches though

Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>

WIP replicating functionality 13 - Seekbar is functional - still buggy though

Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>

WIP replicating functionality - preview player bug fix

Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>

WIP replicating functionality 15 - setup AudioFocusRequestManager + cleaning up observer code

Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>

WIP replicating functionality 16 - AudioFocusManager 2

Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>

WIP replicating functionality 17 - mass delete (Except for typing indicator logic)

Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>

WIP replicating functionality 18 - bug fixes

Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>

WIP replicating functionality 19 - bug fixes

Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>

WIP replicating functionality 20 - bug fixes

Signed-off-by: rapterjet2004 <juliuslinus1@gmail.com>
  • Loading branch information
rapterjet2004 committed Apr 26, 2024
1 parent bbcd773 commit e9bbbd8
Show file tree
Hide file tree
Showing 19 changed files with 2,242 additions and 1,325 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,336 changes: 92 additions & 1,244 deletions app/src/main/java/com/nextcloud/talk/chat/ChatActivity.kt

Large diffs are not rendered by default.

795 changes: 795 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,209 @@
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.startMicInput(requireContext())
chatActivity.chatViewModel.startAudioRecording(requireContext(), chatActivity.currentConversation!!)
binding.audioRecordDuration.visibility = View.VISIBLE
binding.audioRecordDuration.base = SystemClock.elapsedRealtime()
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)
}

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,108 @@
/*
* 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 {
var 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 var receiverRegistered = false
private var receiverUnregistered = 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) {
if (register && !receiverRegistered) {
context.registerReceiver(noisyAudioStreamReceiver, IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY))
receiverRegistered = true
} else if (!receiverUnregistered) {
context.unregisterReceiver(noisyAudioStreamReceiver)
receiverUnregistered = true
receiverRegistered = false
}
}
}
Loading

0 comments on commit e9bbbd8

Please sign in to comment.