move mediaPlayer and download-logic for voicemessages to ChatController

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
This commit is contained in:
Marcel Hibbe 2021-06-28 14:39:29 +02:00
parent f9449db82a
commit d47deb42c8
No known key found for this signature in database
GPG Key ID: C793F8B59F43CE7B
8 changed files with 377 additions and 389 deletions

View File

@ -29,19 +29,14 @@ import android.app.Activity
import android.content.Context
import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
import android.media.MediaPlayer
import android.os.Handler
import android.text.TextUtils
import android.util.Log
import android.view.View
import android.widget.SeekBar
import android.widget.SeekBar.OnSeekBarChangeListener
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.ViewCompat
import androidx.work.Data
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkInfo
import androidx.work.WorkManager
import autodagger.AutoInjector
@ -51,14 +46,11 @@ import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.databinding.ItemCustomIncomingVoiceMessageBinding
import com.nextcloud.talk.jobs.DownloadFileToCacheWorker
import com.nextcloud.talk.models.database.CapabilitiesUtil
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.utils.preferences.AppPreferences
import com.stfalcon.chatkit.messages.MessageHolders
import java.io.File
import java.util.concurrent.ExecutionException
import javax.inject.Inject
@ -81,9 +73,7 @@ class IncomingVoiceMessageViewHolder(incomingView: View) : MessageHolders
lateinit var activity: Activity
var mediaPlayer: MediaPlayer? = null
lateinit var handler: Handler
lateinit var voiceMessageInterface: VoiceMessageInterface
@SuppressLint("SetTextI18n")
override fun onBind(message: ChatMessage) {
@ -101,47 +91,94 @@ class IncomingVoiceMessageViewHolder(incomingView: View) : MessageHolders
// parent message handling
setParentMessageDataOnMessageItem(message)
binding.playBtn.setOnClickListener {
openOrDownloadFile(message)
updateDownloadState(message)
binding.seekbar.max = message.voiceMessageDuration
if (message.isPlayingVoiceMessage) {
binding.progressBar.visibility = View.GONE
binding.playPauseBtn.visibility = View.VISIBLE
binding.playPauseBtn.icon =
ContextCompat.getDrawable(context!!, R.drawable.ic_baseline_pause_voice_message_24)
binding.seekbar.progress = message.voiceMessagePlayedSeconds
} else {
binding.playPauseBtn.visibility = View.VISIBLE
binding.playPauseBtn.icon = ContextCompat.getDrawable(
context!!, R.drawable
.ic_baseline_play_arrow_voice_message_24
)
}
binding.pauseBtn.setOnClickListener {
pausePlayback()
if (message.isDownloadingVoiceMessage) {
binding.playPauseBtn.visibility = View.GONE
binding.progressBar.visibility = View.VISIBLE
} else {
binding.progressBar.visibility = View.GONE
}
if (message.resetVoiceMessage) {
binding.playPauseBtn.visibility = View.VISIBLE
binding.playPauseBtn.icon = ContextCompat.getDrawable(
context!!, R.drawable
.ic_baseline_play_arrow_voice_message_24
)
binding.seekbar.progress = SEEKBAR_START
message.resetVoiceMessage = false
}
activity = itemView.context as Activity
binding.seekbar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
binding.seekbar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onStopTrackingTouch(seekBar: SeekBar) {
// unused atm
}
override fun onStartTrackingTouch(seekBar: SeekBar) {
// unused atm
}
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
if (mediaPlayer != null && fromUser) {
mediaPlayer!!.seekTo(progress * SEEKBAR_BASE)
if (fromUser) {
voiceMessageInterface.updateMediaPlayerProgressBySlider(message, progress)
}
}
})
}
private fun updateDownloadState(message: ChatMessage) {
// check if download worker is already running
val fileId = message.getSelectedIndividualHashMap()["id"]
val workers = WorkManager.getInstance(
context!!
).getWorkInfosByTag(fileId!!)
val workers = WorkManager.getInstance(context!!).getWorkInfosByTag(fileId!!)
try {
for (workInfo in workers.get()) {
if (workInfo.state == WorkInfo.State.RUNNING || workInfo.state == WorkInfo.State.ENQUEUED) {
binding.progressBar.visibility = View.VISIBLE
binding.playBtn.visibility = View.GONE
binding.playPauseBtn.visibility = View.GONE
WorkManager.getInstance(context!!).getWorkInfoByIdLiveData(workInfo.id)
.observeForever { info: WorkInfo? ->
if (info != null) {
updateViewsByProgress(
info
)
when (info.state) {
WorkInfo.State.RUNNING -> {
Log.d(TAG, "WorkInfo.State.RUNNING in ViewHolder")
binding.playPauseBtn.visibility = View.GONE
binding.progressBar.visibility = View.VISIBLE
}
WorkInfo.State.SUCCEEDED -> {
Log.d(TAG, "WorkInfo.State.SUCCEEDED in ViewHolder")
binding.playPauseBtn.visibility = View.VISIBLE
binding.progressBar.visibility = View.GONE
}
WorkInfo.State.FAILED -> {
Log.d(TAG, "WorkInfo.State.FAILED in ViewHolder")
binding.playPauseBtn.visibility = View.VISIBLE
binding.progressBar.visibility = View.GONE
}
else -> {
}
}
}
}
}
@ -249,155 +286,12 @@ class IncomingVoiceMessageViewHolder(incomingView: View) : MessageHolders
}
}
private fun openOrDownloadFile(message: ChatMessage) {
val filename = message.getSelectedIndividualHashMap()["name"]
val file = File(context!!.cacheDir, filename!!)
if (file.exists()) {
binding.progressBar.visibility = View.GONE
startPlayback(message)
} else {
binding.playBtn.visibility = View.GONE
binding.progressBar.visibility = View.VISIBLE
downloadFileToCache(message)
}
}
private fun startPlayback(message: ChatMessage) {
initMediaPlayer(message)
if (!mediaPlayer!!.isPlaying) {
mediaPlayer!!.start()
}
handler = Handler()
activity.runOnUiThread(object : Runnable {
override fun run() {
if (mediaPlayer != null) {
val currentPosition: Int = mediaPlayer!!.currentPosition / SEEKBAR_BASE
binding.seekbar.progress = currentPosition
}
handler.postDelayed(this, SECOND)
}
})
binding.progressBar.visibility = View.GONE
binding.playBtn.visibility = View.GONE
binding.pauseBtn.visibility = View.VISIBLE
}
private fun pausePlayback() {
if (mediaPlayer!!.isPlaying) {
mediaPlayer!!.pause()
}
binding.playBtn.visibility = View.VISIBLE
binding.pauseBtn.visibility = View.GONE
}
private fun initMediaPlayer(message: ChatMessage) {
val fileName = message.getSelectedIndividualHashMap()["name"]
val absolutePath = context!!.cacheDir.absolutePath + "/" + fileName
if (mediaPlayer == null) {
mediaPlayer = MediaPlayer().apply {
setDataSource(absolutePath)
prepare()
}
}
binding.seekbar.max = mediaPlayer!!.duration / SEEKBAR_BASE
mediaPlayer!!.setOnCompletionListener {
binding.playBtn.visibility = View.VISIBLE
binding.pauseBtn.visibility = View.GONE
binding.seekbar.progress = SEEKBAR_START
handler.removeCallbacksAndMessages(null)
mediaPlayer?.stop()
mediaPlayer?.release()
mediaPlayer = null
}
}
@SuppressLint("LongLogTag")
private fun downloadFileToCache(message: ChatMessage) {
val baseUrl = message.activeUser.baseUrl
val userId = message.activeUser.userId
val attachmentFolder = CapabilitiesUtil.getAttachmentFolder(message.activeUser)
val fileName = message.getSelectedIndividualHashMap()["name"]
var size = message.getSelectedIndividualHashMap()["size"]
if (size == null) {
size = "-1"
}
val fileSize = Integer.valueOf(size)
val fileId = message.getSelectedIndividualHashMap()["id"]
val path = message.getSelectedIndividualHashMap()["path"]
// check if download worker is already running
val workers = WorkManager.getInstance(
context!!
).getWorkInfosByTag(fileId!!)
try {
for (workInfo in workers.get()) {
if (workInfo.state == WorkInfo.State.RUNNING || workInfo.state == WorkInfo.State.ENQUEUED) {
Log.d(TAG, "Download worker for " + fileId + " is already running or scheduled")
return
}
}
} catch (e: ExecutionException) {
Log.e(TAG, "Error when checking if worker already exists", e)
} catch (e: InterruptedException) {
Log.e(TAG, "Error when checking if worker already exists", e)
}
val data: Data = Data.Builder()
.putString(DownloadFileToCacheWorker.KEY_BASE_URL, baseUrl)
.putString(DownloadFileToCacheWorker.KEY_USER_ID, userId)
.putString(DownloadFileToCacheWorker.KEY_ATTACHMENT_FOLDER, attachmentFolder)
.putString(DownloadFileToCacheWorker.KEY_FILE_NAME, fileName)
.putString(DownloadFileToCacheWorker.KEY_FILE_PATH, path)
.putInt(DownloadFileToCacheWorker.KEY_FILE_SIZE, fileSize)
.build()
val downloadWorker: OneTimeWorkRequest = OneTimeWorkRequest.Builder(DownloadFileToCacheWorker::class.java)
.setInputData(data)
.addTag(fileId)
.build()
WorkManager.getInstance().enqueue(downloadWorker)
WorkManager.getInstance(context!!).getWorkInfoByIdLiveData(downloadWorker.id)
.observeForever { workInfo: WorkInfo ->
updateViewsByProgress(
workInfo
)
}
}
private fun updateViewsByProgress(workInfo: WorkInfo) {
when (workInfo.state) {
WorkInfo.State.RUNNING -> {
val progress = workInfo.progress.getInt(DownloadFileToCacheWorker.PROGRESS, -1)
if (progress > -1) {
binding.playBtn.visibility = View.GONE
binding.progressBar.visibility = View.VISIBLE
}
}
WorkInfo.State.SUCCEEDED -> {
startPlayback(message)
}
WorkInfo.State.FAILED -> {
binding.progressBar.visibility = View.GONE
binding.playBtn.visibility = View.VISIBLE
}
else -> {
}
}
fun assignAdapter(voiceMessageInterface: VoiceMessageInterface) {
this.voiceMessageInterface = voiceMessageInterface
}
companion object {
private const val TAG = "VoiceInMessageView"
private const val SECOND: Long = 1000
private const val SEEKBAR_BASE: Int = 1000
private const val SEEKBAR_START: Int = 0
}
}

View File

@ -27,15 +27,13 @@ import android.app.Activity
import android.content.Context
import android.graphics.PorterDuff
import android.media.MediaPlayer
import android.net.Uri
import android.os.Handler
import android.util.Log
import android.view.View
import android.widget.SeekBar
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.work.Data
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkInfo
import androidx.work.WorkManager
import autodagger.AutoInjector
@ -44,21 +42,18 @@ import com.nextcloud.talk.R
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
import com.nextcloud.talk.databinding.ItemCustomOutcomingVoiceMessageBinding
import com.nextcloud.talk.jobs.DownloadFileToCacheWorker
import com.nextcloud.talk.models.database.CapabilitiesUtil
import com.nextcloud.talk.models.json.chat.ChatMessage
import com.nextcloud.talk.models.json.chat.ReadStatus
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.DisplayUtils
import com.nextcloud.talk.utils.preferences.AppPreferences
import com.stfalcon.chatkit.messages.MessageHolders
import java.io.File
import java.util.concurrent.ExecutionException
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class OutcomingVoiceMessageViewHolder(incomingView: View) : MessageHolders
.OutcomingTextMessageViewHolder<ChatMessage>(incomingView) {
class OutcomingVoiceMessageViewHolder(outcomingView: View) : MessageHolders
.OutcomingTextMessageViewHolder<ChatMessage>(outcomingView) {
private val binding: ItemCustomOutcomingVoiceMessageBinding =
ItemCustomOutcomingVoiceMessageBinding.bind(itemView)
@ -80,6 +75,8 @@ class OutcomingVoiceMessageViewHolder(incomingView: View) : MessageHolders
lateinit var handler: Handler
lateinit var voiceMessageInterface: VoiceMessageInterface
@SuppressLint("SetTextI18n")
override fun onBind(message: ChatMessage) {
super.onBind(message)
@ -94,57 +91,59 @@ class OutcomingVoiceMessageViewHolder(incomingView: View) : MessageHolders
// parent message handling
setParentMessageDataOnMessageItem(message)
binding.playBtn.setOnClickListener {
openOrDownloadFile(message)
updateDownloadState(message)
binding.seekbar.max = message.voiceMessageDuration
if (message.isPlayingVoiceMessage) {
binding.progressBar.visibility = View.GONE
binding.playPauseBtn.visibility = View.VISIBLE
binding.playPauseBtn.icon =
ContextCompat.getDrawable(context!!, R.drawable.ic_baseline_pause_voice_message_24)
binding.seekbar.progress = message.voiceMessagePlayedSeconds
} else {
binding.playPauseBtn.visibility = View.VISIBLE
binding.playPauseBtn.icon = ContextCompat.getDrawable(
context!!, R.drawable
.ic_baseline_play_arrow_voice_message_24
)
}
binding.pauseBtn.setOnClickListener {
pausePlayback()
if (message.isDownloadingVoiceMessage) {
binding.playPauseBtn.visibility = View.GONE
binding.progressBar.visibility = View.VISIBLE
} else {
binding.progressBar.visibility = View.GONE
}
if (message.resetVoiceMessage) {
binding.playPauseBtn.visibility = View.VISIBLE
binding.playPauseBtn.icon = ContextCompat.getDrawable(
context!!, R.drawable
.ic_baseline_play_arrow_voice_message_24
)
binding.seekbar.progress = SEEKBAR_START
message.resetVoiceMessage = false
}
activity = itemView.context as Activity
binding.seekbar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onStopTrackingTouch(seekBar: SeekBar) {
// unused atm
}
override fun onStartTrackingTouch(seekBar: SeekBar) {
// unused atm
}
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
if (mediaPlayer != null && fromUser) {
mediaPlayer!!.seekTo(progress * SEEKBAR_BASE)
if (fromUser) {
voiceMessageInterface.updateMediaPlayerProgressBySlider(message, progress)
}
}
})
// check if download worker is already running
val fileId = message.getSelectedIndividualHashMap()["id"]
val workers = WorkManager.getInstance(
context!!
).getWorkInfosByTag(fileId!!)
try {
for (workInfo in workers.get()) {
if (workInfo.state == WorkInfo.State.RUNNING || workInfo.state == WorkInfo.State.ENQUEUED) {
binding.progressBar.visibility = View.VISIBLE
binding.playBtn.visibility = View.GONE
WorkManager.getInstance(context!!).getWorkInfoByIdLiveData(workInfo.id)
.observeForever { info: WorkInfo? ->
if (info != null) {
updateViewsByProgress(
info
)
}
}
}
}
} catch (e: ExecutionException) {
Log.e(TAG, "Error when checking if worker already exists", e)
} catch (e: InterruptedException) {
Log.e(TAG, "Error when checking if worker already exists", e)
}
val readStatusDrawableInt = when (message.readStatus) {
ReadStatus.READ -> R.drawable.ic_check_all
ReadStatus.SENT -> R.drawable.ic_check
@ -167,6 +166,50 @@ class OutcomingVoiceMessageViewHolder(incomingView: View) : MessageHolders
binding.checkMark.setContentDescription(readStatusContentDescriptionString)
}
private fun updateDownloadState(message: ChatMessage) {
// check if download worker is already running
val fileId = message.getSelectedIndividualHashMap()["id"]
val workers = WorkManager.getInstance(context!!).getWorkInfosByTag(fileId!!)
try {
for (workInfo in workers.get()) {
if (workInfo.state == WorkInfo.State.RUNNING || workInfo.state == WorkInfo.State.ENQUEUED) {
binding.progressBar.visibility = View.VISIBLE
binding.playPauseBtn.visibility = View.GONE
WorkManager.getInstance(context!!).getWorkInfoByIdLiveData(workInfo.id)
.observeForever { info: WorkInfo? ->
if (info != null) {
when (info.state) {
WorkInfo.State.RUNNING -> {
Log.d(TAG, "WorkInfo.State.RUNNING in ViewHolder")
binding.playPauseBtn.visibility = View.GONE
binding.progressBar.visibility = View.VISIBLE
}
WorkInfo.State.SUCCEEDED -> {
Log.d(TAG, "WorkInfo.State.SUCCEEDED in ViewHolder")
binding.playPauseBtn.visibility = View.VISIBLE
binding.progressBar.visibility = View.GONE
}
WorkInfo.State.FAILED -> {
Log.d(TAG, "WorkInfo.State.FAILED in ViewHolder")
binding.playPauseBtn.visibility = View.VISIBLE
binding.progressBar.visibility = View.GONE
}
else -> {
}
}
}
}
}
}
} catch (e: ExecutionException) {
Log.e(TAG, "Error when checking if worker already exists", e)
} catch (e: InterruptedException) {
Log.e(TAG, "Error when checking if worker already exists", e)
}
}
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
if (!message.isDeleted && message.parentMessage != null) {
val parentChatMessage = message.parentMessage
@ -224,156 +267,12 @@ class OutcomingVoiceMessageViewHolder(incomingView: View) : MessageHolders
}
}
private fun openOrDownloadFile(message: ChatMessage) {
val filename = message.getSelectedIndividualHashMap()["name"]
val file = File(context!!.cacheDir, filename!!)
if (file.exists()) {
binding.progressBar.visibility = View.GONE
startPlayback(message)
} else {
binding.playBtn.visibility = View.GONE
binding.progressBar.visibility = View.VISIBLE
downloadFileToCache(message)
}
}
private fun startPlayback(message: ChatMessage) {
initMediaPlayer(message)
if (!mediaPlayer!!.isPlaying) {
mediaPlayer!!.start()
}
handler = Handler()
activity.runOnUiThread(object : Runnable {
override fun run() {
if (mediaPlayer != null) {
val currentPosition: Int = mediaPlayer!!.currentPosition / SEEKBAR_BASE
binding.seekbar.progress = currentPosition
}
handler.postDelayed(this, SECOND)
}
})
binding.progressBar.visibility = View.GONE
binding.playBtn.visibility = View.GONE
binding.pauseBtn.visibility = View.VISIBLE
}
private fun pausePlayback() {
if (mediaPlayer!!.isPlaying) {
mediaPlayer!!.pause()
}
binding.playBtn.visibility = View.VISIBLE
binding.pauseBtn.visibility = View.GONE
}
private fun initMediaPlayer(message: ChatMessage) {
val fileName = message.getSelectedIndividualHashMap()["name"]
val absolutePath = context!!.cacheDir.absolutePath + "/" + fileName
if (mediaPlayer == null) {
mediaPlayer = MediaPlayer().apply {
setDataSource(context!!, Uri.parse(absolutePath))
prepare()
}
}
binding.seekbar.max = mediaPlayer!!.duration / SEEKBAR_BASE
mediaPlayer!!.setOnCompletionListener {
binding.playBtn.visibility = View.VISIBLE
binding.pauseBtn.visibility = View.GONE
binding.seekbar.progress = SEEKBAR_START
handler.removeCallbacksAndMessages(null)
mediaPlayer?.stop()
mediaPlayer?.release()
mediaPlayer = null
}
}
@SuppressLint("LongLogTag")
private fun downloadFileToCache(message: ChatMessage) {
val baseUrl = message.activeUser.baseUrl
val userId = message.activeUser.userId
val attachmentFolder = CapabilitiesUtil.getAttachmentFolder(message.activeUser)
val fileName = message.getSelectedIndividualHashMap()["name"]
var size = message.getSelectedIndividualHashMap()["size"]
if (size == null) {
size = "-1"
}
val fileSize = Integer.valueOf(size)
val fileId = message.getSelectedIndividualHashMap()["id"]
val path = message.getSelectedIndividualHashMap()["path"]
// check if download worker is already running
val workers = WorkManager.getInstance(
context!!
).getWorkInfosByTag(fileId!!)
try {
for (workInfo in workers.get()) {
if (workInfo.state == WorkInfo.State.RUNNING || workInfo.state == WorkInfo.State.ENQUEUED) {
Log.d(TAG, "Download worker for " + fileId + " is already running or scheduled")
return
}
}
} catch (e: ExecutionException) {
Log.e(TAG, "Error when checking if worker already exists", e)
} catch (e: InterruptedException) {
Log.e(TAG, "Error when checking if worker already exists", e)
}
val data: Data = Data.Builder()
.putString(DownloadFileToCacheWorker.KEY_BASE_URL, baseUrl)
.putString(DownloadFileToCacheWorker.KEY_USER_ID, userId)
.putString(DownloadFileToCacheWorker.KEY_ATTACHMENT_FOLDER, attachmentFolder)
.putString(DownloadFileToCacheWorker.KEY_FILE_NAME, fileName)
.putString(DownloadFileToCacheWorker.KEY_FILE_PATH, path)
.putInt(DownloadFileToCacheWorker.KEY_FILE_SIZE, fileSize)
.build()
val downloadWorker: OneTimeWorkRequest = OneTimeWorkRequest.Builder(DownloadFileToCacheWorker::class.java)
.setInputData(data)
.addTag(fileId)
.build()
WorkManager.getInstance().enqueue(downloadWorker)
WorkManager.getInstance(context!!).getWorkInfoByIdLiveData(downloadWorker.id)
.observeForever { workInfo: WorkInfo ->
updateViewsByProgress(
workInfo
)
}
}
private fun updateViewsByProgress(workInfo: WorkInfo) {
when (workInfo.state) {
WorkInfo.State.RUNNING -> {
val progress = workInfo.progress.getInt(DownloadFileToCacheWorker.PROGRESS, -1)
if (progress > -1) {
binding.playBtn.visibility = View.GONE
binding.progressBar.visibility = View.VISIBLE
}
}
WorkInfo.State.SUCCEEDED -> {
startPlayback(message)
}
WorkInfo.State.FAILED -> {
binding.progressBar.visibility = View.GONE
binding.playBtn.visibility = View.VISIBLE
}
else -> {
}
}
fun assignAdapter(voiceMessageInterface: VoiceMessageInterface) {
this.voiceMessageInterface = voiceMessageInterface
}
companion object {
private const val TAG = "VoiceOutMessageView"
private const val SECOND: Long = 1000
private const val SEEKBAR_BASE: Int = 1000
private const val SEEKBAR_START: Int = 0
}
}

View File

@ -20,7 +20,9 @@
package com.nextcloud.talk.adapters.messages;
import com.nextcloud.talk.controllers.ChatController;
import com.stfalcon.chatkit.commons.ImageLoader;
import com.stfalcon.chatkit.commons.ViewHolder;
import com.stfalcon.chatkit.commons.models.IMessage;
import com.stfalcon.chatkit.messages.MessageHolders;
import com.stfalcon.chatkit.messages.MessagesListAdapter;
@ -28,12 +30,32 @@ import com.stfalcon.chatkit.messages.MessagesListAdapter;
import java.util.List;
public class TalkMessagesListAdapter<M extends IMessage> extends MessagesListAdapter<M> {
private final ChatController chatController;
public TalkMessagesListAdapter(String senderId, MessageHolders holders, ImageLoader imageLoader) {
public TalkMessagesListAdapter(
String senderId,
MessageHolders holders,
ImageLoader imageLoader,
ChatController chatController) {
super(senderId, holders, imageLoader);
this.chatController = chatController;
}
public List<MessagesListAdapter.Wrapper> getItems() {
return items;
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
super.onBindViewHolder(holder, position);
if (holder instanceof IncomingVoiceMessageViewHolder) {
((IncomingVoiceMessageViewHolder) holder).assignAdapter(chatController);
}
if (holder instanceof OutcomingVoiceMessageViewHolder) {
((OutcomingVoiceMessageViewHolder) holder).assignAdapter(chatController);
}
}
}

View File

@ -0,0 +1,7 @@
package com.nextcloud.talk.adapters.messages
import com.nextcloud.talk.models.json.chat.ChatMessage
interface VoiceMessageInterface {
fun updateMediaPlayerProgressBySlider(message : ChatMessage, progress : Int)
}

View File

@ -34,6 +34,7 @@ import android.content.pm.PackageManager
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.drawable.ColorDrawable
import android.media.MediaPlayer
import android.media.MediaRecorder
import android.net.Uri
import android.os.Build
@ -76,6 +77,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.work.Data
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkInfo
import androidx.work.WorkManager
import autodagger.AutoInjector
import coil.load
@ -102,6 +104,7 @@ import com.nextcloud.talk.adapters.messages.OutcomingLocationMessageViewHolder
import com.nextcloud.talk.adapters.messages.OutcomingPreviewMessageViewHolder
import com.nextcloud.talk.adapters.messages.OutcomingVoiceMessageViewHolder
import com.nextcloud.talk.adapters.messages.TalkMessagesListAdapter
import com.nextcloud.talk.adapters.messages.VoiceMessageInterface
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.callbacks.MentionAutocompleteCallback
@ -112,6 +115,7 @@ import com.nextcloud.talk.controllers.util.viewBinding
import com.nextcloud.talk.databinding.ControllerChatBinding
import com.nextcloud.talk.events.UserMentionClickEvent
import com.nextcloud.talk.events.WebSocketCommunicationEvent
import com.nextcloud.talk.jobs.DownloadFileToCacheWorker
import com.nextcloud.talk.jobs.UploadAndShareFilesWorker
import com.nextcloud.talk.models.database.CapabilitiesUtil
import com.nextcloud.talk.models.database.UserEntity
@ -175,6 +179,7 @@ import java.util.ArrayList
import java.util.Date
import java.util.HashMap
import java.util.Objects
import java.util.concurrent.ExecutionException
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
@ -186,7 +191,8 @@ class ChatController(args: Bundle) :
MessagesListAdapter.OnLoadMoreListener,
MessagesListAdapter.Formatter<Date>,
MessagesListAdapter.OnMessageViewLongClickListener<IMessage>,
ContentChecker<ChatMessage> {
ContentChecker<ChatMessage>, VoiceMessageInterface {
private val binding: ControllerChatBinding by viewBinding(ControllerChatBinding::bind)
@Inject
@ -247,6 +253,10 @@ class ChatController(args: Bundle) :
private var recorder: MediaRecorder? = null
var mediaPlayer: MediaPlayer? = null
lateinit var mediaPlayerHandler: Handler
var currentlyPlayedVoiceMessage: ChatMessage? = null
init {
setHasOptionsMenu(true)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
@ -488,7 +498,7 @@ class ChatController(args: Bundle) :
.setAutoPlayAnimations(true)
.build()
imageView.controller = draweeController
}
}, this
)
} else {
binding.messagesListView.visibility = View.VISIBLE
@ -499,6 +509,22 @@ class ChatController(args: Bundle) :
adapter?.setDateHeadersFormatter { format(it) }
adapter?.setOnMessageViewLongClickListener { view, message -> onMessageViewLongClick(view, message) }
adapter?.registerViewClickListener(
R.id.playPauseBtn
) { view, message ->
val filename = message.getSelectedIndividualHashMap()["name"]
val file = File(context!!.cacheDir, filename!!)
if (file.exists()) {
if (message.isPlayingVoiceMessage) {
pausePlayback(message)
} else {
startPlayback(message)
}
} else {
downloadFileToCache(message)
}
}
if (context != null) {
val messageSwipeController = MessageSwipeCallback(
activity!!,
@ -749,6 +775,151 @@ class ChatController(args: Bundle) :
super.onViewBound(view)
}
private fun startPlayback(message: ChatMessage) {
if (!this.isAttached) {
// don't begin to play voice message if screen is not visible anymore.
// this situation might happen if file is downloading but user already left the chatview.
// If user returns to chatview, the old chatview instance is not attached anymore
// and he has to click the play button again (which is considered to be okay)
return
}
initMediaPlayer(message)
if (!mediaPlayer!!.isPlaying) {
mediaPlayer!!.start()
}
mediaPlayerHandler = Handler()
activity?.runOnUiThread(object : Runnable {
override fun run() {
if (mediaPlayer != null) {
val currentPosition: Int = mediaPlayer!!.currentPosition / VOICE_MESSAGE_SEEKBAR_BASE
message.voiceMessagePlayedSeconds = currentPosition
adapter?.update(message)
}
mediaPlayerHandler.postDelayed(this, SECOND)
}
})
message.isDownloadingVoiceMessage = false
message.isPlayingVoiceMessage = true
adapter?.update(message)
}
private fun pausePlayback(message: ChatMessage) {
if (mediaPlayer!!.isPlaying) {
mediaPlayer!!.pause()
}
message.isPlayingVoiceMessage = false
adapter?.update(message)
}
private fun initMediaPlayer(message: ChatMessage) {
if (message != currentlyPlayedVoiceMessage) {
currentlyPlayedVoiceMessage?.let { stopMediaPlayer(it) }
}
if (mediaPlayer == null) {
val fileName = message.getSelectedIndividualHashMap()["name"]
val absolutePath = context!!.cacheDir.absolutePath + "/" + fileName
mediaPlayer = MediaPlayer().apply {
setDataSource(absolutePath)
prepare()
}
currentlyPlayedVoiceMessage = message
message.voiceMessageDuration = mediaPlayer!!.duration / VOICE_MESSAGE_SEEKBAR_BASE
mediaPlayer!!.setOnCompletionListener {
stopMediaPlayer(message)
}
} else {
Log.e(TAG, "mediaPlayer was not null. This should not happen!")
}
}
private fun stopMediaPlayer(message: ChatMessage) {
message.isPlayingVoiceMessage = false
message.resetVoiceMessage = true
adapter?.update(message)
currentlyPlayedVoiceMessage = null
mediaPlayerHandler.removeCallbacksAndMessages(null)
mediaPlayer?.stop()
mediaPlayer?.release()
mediaPlayer = null
}
override fun updateMediaPlayerProgressBySlider(messageWithSlidedProgress: ChatMessage, progress: Int) {
if (mediaPlayer != null) {
if (messageWithSlidedProgress == currentlyPlayedVoiceMessage) {
mediaPlayer!!.seekTo(progress * VOICE_MESSAGE_SEEKBAR_BASE)
}
}
}
@SuppressLint("LongLogTag")
private fun downloadFileToCache(message: ChatMessage) {
message.isDownloadingVoiceMessage = true
adapter?.update(message)
val baseUrl = message.activeUser.baseUrl
val userId = message.activeUser.userId
val attachmentFolder = CapabilitiesUtil.getAttachmentFolder(message.activeUser)
val fileName = message.getSelectedIndividualHashMap()["name"]
var size = message.getSelectedIndividualHashMap()["size"]
if (size == null) {
size = "-1"
}
val fileSize = Integer.valueOf(size)
val fileId = message.getSelectedIndividualHashMap()["id"]
val path = message.getSelectedIndividualHashMap()["path"]
// check if download worker is already running
val workers = WorkManager.getInstance(
context!!
).getWorkInfosByTag(fileId!!)
try {
for (workInfo in workers.get()) {
if (workInfo.state == WorkInfo.State.RUNNING || workInfo.state == WorkInfo.State.ENQUEUED) {
Log.d(TAG, "Download worker for " + fileId + " is already running or scheduled")
return
}
}
} catch (e: ExecutionException) {
Log.e(TAG, "Error when checking if worker already exists", e)
} catch (e: InterruptedException) {
Log.e(TAG, "Error when checking if worker already exists", e)
}
val data: Data = Data.Builder()
.putString(DownloadFileToCacheWorker.KEY_BASE_URL, baseUrl)
.putString(DownloadFileToCacheWorker.KEY_USER_ID, userId)
.putString(DownloadFileToCacheWorker.KEY_ATTACHMENT_FOLDER, attachmentFolder)
.putString(DownloadFileToCacheWorker.KEY_FILE_NAME, fileName)
.putString(DownloadFileToCacheWorker.KEY_FILE_PATH, path)
.putInt(DownloadFileToCacheWorker.KEY_FILE_SIZE, fileSize)
.build()
val downloadWorker: OneTimeWorkRequest = OneTimeWorkRequest.Builder(DownloadFileToCacheWorker::class.java)
.setInputData(data)
.addTag(fileId)
.build()
WorkManager.getInstance().enqueue(downloadWorker)
WorkManager.getInstance(context!!).getWorkInfoByIdLiveData(downloadWorker.id)
.observeForever { workInfo: WorkInfo ->
if (workInfo.state == WorkInfo.State.SUCCEEDED) {
startPlayback(message)
}
}
}
@SuppressLint("SimpleDateFormat")
private fun setVoiceRecordFileName() {
val pattern = "yyyy-MM-dd HH-mm-ss"
@ -1265,6 +1436,8 @@ class ChatController(args: Bundle) :
if (mentionAutocomplete != null && mentionAutocomplete!!.isPopupShowing) {
mentionAutocomplete?.dismissPopup()
}
currentlyPlayedVoiceMessage?.let { stopMediaPlayer(it) }
}
override val title: String
@ -2345,5 +2518,7 @@ class ChatController(args: Bundle) :
private const val SHORT_VIBRATE: Long = 20
private const val FULLY_OPAQUE_INT: Int = 255
private const val SEMI_TRANSPARENT_INT: Int = 99
private const val VOICE_MESSAGE_SEEKBAR_BASE: Int = 1000
private const val SECOND: Long = 1000
}
}

View File

@ -90,6 +90,22 @@ public class ChatMessage implements MessageContentType, MessageContentType.Image
@JsonField(name = "messageType")
public String messageType;
public boolean isDownloadingVoiceMessage;
public boolean resetVoiceMessage;
public boolean isPlayingVoiceMessage;
public int voiceMessageDuration;
public int voiceMessagePlayedSeconds;
public VoiceMessageDownloadState voiceMessageDownloadState;
public int voiceMessageDownloadProgress;
public enum VoiceMessageDownloadState {
NOT_STARTED,
RUNNING,
SUCCEEDED,
FAILED
}
@JsonIgnore
List<MessageType> messageTypesToIgnore = Arrays.asList(
MessageType.REGULAR_TEXT_MESSAGE,
@ -100,6 +116,8 @@ public class ChatMessage implements MessageContentType, MessageContentType.Image
MessageType.SINGLE_NC_GEOLOCATION_MESSAGE,
MessageType.VOICE_MESSAGE);
public boolean hasFileAttachment() {
if (messageParameters != null && messageParameters.size() > 0) {
for (HashMap.Entry<String, HashMap<String, String>> entry : messageParameters.entrySet()) {
@ -133,8 +151,6 @@ public class ChatMessage implements MessageContentType, MessageContentType.Image
@Nullable
@Override
public String getImageUrl() {
if (messageParameters != null && messageParameters.size() > 0) {
for (HashMap.Entry<String, HashMap<String, String>> entry : messageParameters.entrySet()) {
Map<String, String> individualHashMap = entry.getValue();

View File

@ -79,7 +79,7 @@
android:visibility="gone"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/playBtn"
android:id="@+id/playPauseBtn"
style="@style/Widget.AppTheme.Button.IconButton"
android:layout_width="48dp"
android:layout_height="48dp"
@ -90,18 +90,6 @@
app:iconSize="40dp"
app:iconTint="@color/nc_incoming_text_default" />
<com.google.android.material.button.MaterialButton
android:id="@+id/pauseBtn"
style="@style/Widget.AppTheme.Button.IconButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:contentDescription="@string/pause_voice_message"
android:visibility="gone"
app:cornerRadius="@dimen/button_corner_radius"
app:icon="@drawable/ic_baseline_pause_voice_message_24"
app:iconSize="40dp"
app:iconTint="@color/nc_incoming_text_default" />
<SeekBar
android:id="@+id/seekbar"
android:layout_width="200dp"

View File

@ -63,7 +63,7 @@
android:visibility="gone"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/playBtn"
android:id="@+id/playPauseBtn"
style="@style/Widget.AppTheme.Button.IconButton"
android:layout_width="48dp"
android:layout_height="48dp"
@ -75,19 +75,6 @@
app:iconSize="40dp"
app:iconTint="@color/nc_outcoming_text_default" />
<com.google.android.material.button.MaterialButton
android:id="@+id/pauseBtn"
style="@style/Widget.AppTheme.Button.IconButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:contentDescription="@string/pause_voice_message"
android:visibility="gone"
app:rippleColor="#1FFFFFFF"
app:cornerRadius="@dimen/button_corner_radius"
app:icon="@drawable/ic_baseline_pause_voice_message_24"
app:iconSize="40dp"
app:iconTint="@color/nc_outcoming_text_default" />
<SeekBar
android:id="@+id/seekbar"
style="@style/Nextcloud.Material.Outgoing.SeekBar"