Browse Source

Merge pull request #1373 from nextcloud/bugfix/1343/stopVoiceMessage

Bugfix/1343/stop voice message
Andy Scherzinger 3 years ago
parent
commit
b989c62055

+ 1 - 1
app/build.gradle

@@ -267,7 +267,7 @@ dependencies {
     implementation 'com.novoda:merlin:1.2.1'
 
     implementation 'com.github.Kennyc1012:BottomSheet:2.4.1'
-    implementation 'com.github.nextcloud:PopupBubble:master-SNAPSHOT'
+    implementation 'com.github.nextcloud:PopupBubble:1.0.6'
     implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
 
     implementation('eu.medsea.mimeutil:mime-util:2.1.3', {

+ 2 - 0
app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.java

@@ -22,6 +22,7 @@
 
 package com.nextcloud.talk.adapters.items;
 
+import android.annotation.SuppressLint;
 import android.content.Context;
 import android.content.res.ColorStateList;
 import android.graphics.Color;
@@ -105,6 +106,7 @@ public class ConversationItem extends AbstractFlexibleItem<ConversationItem.Conv
         return new ConversationItemViewHolder(view, adapter);
     }
 
+    @SuppressLint("SetTextI18n")
     @Override
     public void bindViewHolder(FlexibleAdapter<IFlexible> adapter, ConversationItemViewHolder holder, int position, List<Object> payloads) {
         Context appContext =

+ 42 - 21
app/src/main/java/com/nextcloud/talk/adapters/items/MentionAutocompleteItem.java

@@ -45,17 +45,20 @@ import eu.davidea.flexibleadapter.utils.FlexibleUtils;
 public class MentionAutocompleteItem extends AbstractFlexibleItem<UserItem.UserItemViewHolder>
         implements IFilterable<String> {
 
+    public static final String SOURCE_CALLS = "calls";
+    public static final String SOURCE_GUESTS = "guests";
     private String objectId;
     private String displayName;
     private String source;
     private UserEntity currentUser;
     private Context context;
 
-    public MentionAutocompleteItem(String objectId,
-                                   String displayName,
-                                   String source,
-                                   UserEntity currentUser,
-                                   Context activityContext) {
+    public MentionAutocompleteItem(
+            String objectId,
+            String displayName,
+            String source,
+            UserEntity currentUser,
+            Context activityContext) {
         this.objectId = objectId;
         this.displayName = displayName;
         this.source = source;
@@ -102,19 +105,27 @@ public class MentionAutocompleteItem extends AbstractFlexibleItem<UserItem.UserI
 
     @SuppressLint("SetTextI18n")
     @Override
-    public void bindViewHolder(FlexibleAdapter<IFlexible> adapter, UserItem.UserItemViewHolder holder, int position, List<Object> payloads) {
+    public void bindViewHolder(
+            FlexibleAdapter<IFlexible> adapter,
+            UserItem.UserItemViewHolder holder,
+            int position,
+            List<Object> payloads) {
 
         holder.contactDisplayName.setTextColor(ResourcesCompat.getColor(context.getResources(),
-                                                                R.color.conversation_item_header,
-                                                                null));
+                                                                        R.color.conversation_item_header,
+                                                                        null));
         if (adapter.hasFilter()) {
-            FlexibleUtils.highlightText(holder.contactDisplayName, displayName,
-                    String.valueOf(adapter.getFilter(String.class)), NextcloudTalkApplication.Companion.getSharedApplication()
-                            .getResources().getColor(R.color.colorPrimary));
+            FlexibleUtils.highlightText(holder.contactDisplayName,
+                                        displayName,
+                                        String.valueOf(adapter.getFilter(String.class)),
+                                        NextcloudTalkApplication.Companion.getSharedApplication()
+                                                .getResources().getColor(R.color.colorPrimary));
             if (holder.contactMentionId != null) {
-                FlexibleUtils.highlightText(holder.contactMentionId, "@" + objectId,
-                        String.valueOf(adapter.getFilter(String.class)), NextcloudTalkApplication.Companion.getSharedApplication()
-                                .getResources().getColor(R.color.colorPrimary));
+                FlexibleUtils.highlightText(holder.contactMentionId,
+                                            "@" + objectId,
+                                            String.valueOf(adapter.getFilter(String.class)),
+                                            NextcloudTalkApplication.Companion.getSharedApplication()
+                                                    .getResources().getColor(R.color.colorPrimary));
             }
         } else {
             holder.contactDisplayName.setText(displayName);
@@ -123,16 +134,19 @@ public class MentionAutocompleteItem extends AbstractFlexibleItem<UserItem.UserI
             }
         }
 
-        if (source.equals("calls")) {
+        if (SOURCE_CALLS.equals(source)) {
             holder.simpleDraweeView.setImageResource(R.drawable.ic_circular_group);
         } else {
             String avatarId = objectId;
             String avatarUrl = ApiUtils.getUrlForAvatarWithName(currentUser.getBaseUrl(),
-                    avatarId, R.dimen.avatar_size_big);
+                                                                avatarId, R.dimen.avatar_size_big);
 
-            if (source.equals("guests")) {
+            if (SOURCE_GUESTS.equals(source)) {
                 avatarId = displayName;
-                avatarUrl = ApiUtils.getUrlForAvatarWithNameForGuests(currentUser.getBaseUrl(), avatarId, R.dimen.avatar_size_big);
+                avatarUrl = ApiUtils.getUrlForAvatarWithNameForGuests(
+                        currentUser.getBaseUrl(),
+                        avatarId,
+                        R.dimen.avatar_size_big);
             }
 
             holder.simpleDraweeView.setController(null);
@@ -147,8 +161,15 @@ public class MentionAutocompleteItem extends AbstractFlexibleItem<UserItem.UserI
 
     @Override
     public boolean filter(String constraint) {
-        return objectId != null && Pattern.compile(constraint,
-                Pattern.CASE_INSENSITIVE | Pattern.LITERAL).matcher(objectId).find()
-                || displayName != null && Pattern.compile(constraint, Pattern.CASE_INSENSITIVE | Pattern.LITERAL).matcher(displayName).find();
+        return objectId != null &&
+                Pattern
+                        .compile(constraint, Pattern.CASE_INSENSITIVE | Pattern.LITERAL)
+                        .matcher(objectId)
+                        .find() ||
+                displayName != null &&
+                        Pattern
+                                .compile(constraint, Pattern.CASE_INSENSITIVE | Pattern.LITERAL)
+                                .matcher(displayName)
+                                .find();
     }
 }

+ 67 - 175
app/src/main/java/com/nextcloud/talk/adapters/messages/IncomingVoiceMessageViewHolder.kt

@@ -25,23 +25,17 @@
 package com.nextcloud.talk.adapters.messages
 
 import android.annotation.SuppressLint
-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 +45,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
 
@@ -79,11 +70,7 @@ class IncomingVoiceMessageViewHolder(incomingView: View) : MessageHolders
 
     lateinit var message: ChatMessage
 
-    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 +88,85 @@ 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) {
+            showPlayButton()
+            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) {
+            showVoiceMessageLoading()
+        } else {
+            binding.progressBar.visibility = View.GONE
         }
 
-        activity = itemView.context as Activity
+        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
+        }
 
-        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
+                    showVoiceMessageLoading()
                     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")
+                                        showVoiceMessageLoading()
+                                    }
+                                    WorkInfo.State.SUCCEEDED -> {
+                                        Log.d(TAG, "WorkInfo.State.SUCCEEDED in ViewHolder")
+                                        showPlayButton()
+                                    }
+                                    WorkInfo.State.FAILED -> {
+                                        Log.d(TAG, "WorkInfo.State.FAILED in ViewHolder")
+                                        showPlayButton()
+                                    }
+                                    else -> {
+                                    }
+                                }
                             }
                         }
                 }
@@ -153,6 +178,16 @@ class IncomingVoiceMessageViewHolder(incomingView: View) : MessageHolders
         }
     }
 
+    private fun showPlayButton() {
+        binding.playPauseBtn.visibility = View.VISIBLE
+        binding.progressBar.visibility = View.GONE
+    }
+
+    private fun showVoiceMessageLoading() {
+        binding.playPauseBtn.visibility = View.GONE
+        binding.progressBar.visibility = View.VISIBLE
+    }
+
     private fun setAvatarAndAuthorOnMessageItem(message: ChatMessage) {
         val author: String = message.actorDisplayName
         if (!TextUtils.isEmpty(author)) {
@@ -249,155 +284,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
     }
 }

+ 90 - 195
app/src/main/java/com/nextcloud/talk/adapters/messages/OutcomingVoiceMessageViewHolder.kt

@@ -23,19 +23,15 @@
 package com.nextcloud.talk.adapters.messages
 
 import android.annotation.SuppressLint
-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,25 +40,21 @@ 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)
-    private val realView: View = itemView
 
     @JvmField
     @Inject
@@ -74,12 +66,10 @@ class OutcomingVoiceMessageViewHolder(incomingView: View) : MessageHolders
 
     lateinit var message: ChatMessage
 
-    lateinit var activity: Activity
-
-    var mediaPlayer: MediaPlayer? = null
-
     lateinit var handler: Handler
 
+    lateinit var voiceMessageInterface: VoiceMessageInterface
+
     @SuppressLint("SetTextI18n")
     override fun onBind(message: ChatMessage) {
         super.onBind(message)
@@ -94,57 +84,56 @@ 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) {
+            showPlayButton()
+            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) {
+            showVoiceMessageLoading()
+        } else {
+            binding.progressBar.visibility = View.GONE
         }
 
-        activity = itemView.context as Activity
+        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
+        }
 
         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 +156,56 @@ 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) {
+                    showVoiceMessageLoading()
+                    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")
+                                        showVoiceMessageLoading()
+                                    }
+                                    WorkInfo.State.SUCCEEDED -> {
+                                        Log.d(TAG, "WorkInfo.State.SUCCEEDED in ViewHolder")
+                                        showPlayButton()
+                                    }
+                                    WorkInfo.State.FAILED -> {
+                                        Log.d(TAG, "WorkInfo.State.FAILED in ViewHolder")
+                                        showPlayButton()
+                                    }
+                                    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 showPlayButton() {
+        binding.playPauseBtn.visibility = View.VISIBLE
+        binding.progressBar.visibility = View.GONE
+    }
+
+    private fun showVoiceMessageLoading() {
+        binding.playPauseBtn.visibility = View.GONE
+        binding.progressBar.visibility = View.VISIBLE
+    }
+
     private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
         if (!message.isDeleted && message.parentMessage != null) {
             val parentChatMessage = message.parentMessage
@@ -224,156 +263,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
     }
 }

+ 20 - 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,29 @@ 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);
+        } else if (holder instanceof OutcomingVoiceMessageViewHolder) {
+            ((OutcomingVoiceMessageViewHolder) holder).assignAdapter(chatController);
+        }
+    }
 }

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

@@ -0,0 +1,27 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Marcel Hibbe
+ * Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.adapters.messages
+
+import com.nextcloud.talk.models.json.chat.ChatMessage
+
+interface VoiceMessageInterface {
+    fun updateMediaPlayerProgressBySlider(message: ChatMessage, progress: Int)
+}

+ 179 - 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,9 @@ 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 +254,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 +499,8 @@ class ChatController(args: Bundle) :
                         .setAutoPlayAnimations(true)
                         .build()
                     imageView.controller = draweeController
-                }
+                },
+                this
             )
         } else {
             binding.messagesListView.visibility = View.VISIBLE
@@ -499,6 +511,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 +777,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"
@@ -1290,6 +1463,8 @@ class ChatController(args: Bundle) :
             actionBar?.setIcon(null)
         }
 
+        currentlyPlayedVoiceMessage?.let { stopMediaPlayer(it) }
+
         adapter = null
         inConversation = false
     }
@@ -2349,5 +2524,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
     }
 }

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

@@ -90,6 +90,13 @@ 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 int voiceMessageDownloadProgress;
+
     @JsonIgnore
     List<MessageType> messageTypesToIgnore = Arrays.asList(
             MessageType.REGULAR_TEXT_MESSAGE,
@@ -133,8 +140,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();

+ 4 - 16
app/src/main/res/layout/item_custom_incoming_voice_message.xml

@@ -66,7 +66,7 @@
                     android:textSize="12sp" />
 
                 <LinearLayout
-                    android:layout_width="wrap_content"
+                    android:layout_width="match_parent"
                     android:layout_height="wrap_content"
                     android:orientation="horizontal"
                     android:gravity="center_vertical">
@@ -79,32 +79,20 @@
                             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"
-                            android:contentDescription="@string/play_voice_message"
+                            android:contentDescription="@string/play_pause_voice_message"
                             android:visibility="visible"
                             app:cornerRadius="@dimen/button_corner_radius"
                             app:icon="@drawable/ic_baseline_play_arrow_voice_message_24"
                             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"
+                            android:layout_width="match_parent"
                             android:layout_height="wrap_content"
                             tools:progress="50" />
 

+ 6 - 18
app/src/main/res/layout/item_custom_outcoming_voice_message.xml

@@ -49,7 +49,7 @@
             android:visibility="gone" />
 
         <LinearLayout
-            android:layout_width="wrap_content"
+            android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:orientation="horizontal"
             android:gravity="center_vertical">
@@ -60,14 +60,15 @@
                 android:layout_height="wrap_content"
                 android:layout_gravity="center"
                 android:progressTint="@color/fontAppbar"
-                android:visibility="gone"/>
+                android:visibility="gone"
+                android:indeterminateTint="@color/nc_outcoming_text_default"/>
 
             <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"
-                android:contentDescription="@string/play_voice_message"
+                android:contentDescription="@string/play_pause_voice_message"
                 android:visibility="visible"
                 app:rippleColor="#1FFFFFFF"
                 app:cornerRadius="@dimen/button_corner_radius"
@@ -75,23 +76,10 @@
                 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"
-                android:layout_width="200dp"
+                android:layout_width="match_parent"
                 android:layout_height="40dp"
                 android:thumb="@drawable/voice_message_outgoing_seek_bar_slider"
                 tools:progress="50" />

+ 1 - 2
app/src/main/res/values/strings.xml

@@ -389,8 +389,7 @@
     <string name="nc_voice_message_hold_to_record_info">Hold to record, release to send.</string>
     <string name="nc_description_record_voice">Record voice message</string>
     <string name="nc_voice_message_slide_to_cancel">&lt;&lt; Slide to cancel</string>
-    <string name="play_voice_message">Play voice message</string>
-    <string name="pause_voice_message">Pause voice message</string>
+    <string name="play_pause_voice_message">Play/pause voice message</string>
     <string name="nc_voice_message_missing_audio_permission">Permission for audio recording is required</string>
 
     <!-- Phonebook Integration -->

+ 1 - 1
scripts/analysis/findbugs-results.txt

@@ -1 +1 @@
-440
+439

+ 1 - 1
scripts/analysis/lint-results.txt

@@ -1,2 +1,2 @@
 DO NOT TOUCH; GENERATED BY DRONE
-      <span class="mdl-layout-title">Lint Report: 3 errors and 275 warnings</span>
+      <span class="mdl-layout-title">Lint Report: 3 errors and 274 warnings</span>