瀏覽代碼

move mediaPlayer and download-logic for voicemessages to ChatController

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
Marcel Hibbe 3 年之前
父節點
當前提交
d47deb42c8

+ 64 - 170
app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt

@@ -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
+            )
+        }
+
+        if (message.isDownloadingVoiceMessage) {
+            binding.playPauseBtn.visibility = View.GONE
+            binding.progressBar.visibility = View.VISIBLE
+        } else {
+            binding.progressBar.visibility = View.GONE
         }
 
-        binding.pauseBtn.setOnClickListener {
-            pausePlayback()
+        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
     }
 }

+ 86 - 187
app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingVoiceMessageViewHolder.kt

@@ -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
+            )
+        }
+
+        if (message.isDownloadingVoiceMessage) {
+            binding.playPauseBtn.visibility = View.GONE
+            binding.progressBar.visibility = View.VISIBLE
+        } else {
+            binding.progressBar.visibility = View.GONE
         }
 
-        binding.pauseBtn.setOnClickListener {
-            pausePlayback()
+        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
     }
 }

+ 23 - 1
app/src/main/java/com/nextcloud/talk/adapters/messages/TalkMessagesListAdapter.java

@@ -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);
+        }
+
+    }
 }

+ 7 - 0
app/src/main/java/com/nextcloud/talk/adapters/messages/VoiceMessageInterface.kt

@@ -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)
+}

+ 177 - 2
app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt

@@ -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
     }
 }

+ 18 - 2
app/src/main/java/com/nextcloud/talk/models/json/chat/ChatMessage.java

@@ -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();

+ 1 - 13
app/src/main/res/layout/item_custom_incoming_voice_message.xml

@@ -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"

+ 1 - 14
app/src/main/res/layout/item_custom_outcoming_voice_message.xml

@@ -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"