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

Bugfix/1343/stop voice message
This commit is contained in:
Andy Scherzinger 2021-06-30 13:53:13 +02:00 committed by GitHub
commit b989c62055
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 448 additions and 435 deletions

View File

@ -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', {

View File

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

View File

@ -45,13 +45,16 @@ 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,
public MentionAutocompleteItem(
String objectId,
String displayName,
String source,
UserEntity currentUser,
@ -102,18 +105,26 @@ 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));
if (adapter.hasFilter()) {
FlexibleUtils.highlightText(holder.contactDisplayName, displayName,
String.valueOf(adapter.getFilter(String.class)), NextcloudTalkApplication.Companion.getSharedApplication()
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()
FlexibleUtils.highlightText(holder.contactMentionId,
"@" + objectId,
String.valueOf(adapter.getFilter(String.class)),
NextcloudTalkApplication.Companion.getSharedApplication()
.getResources().getColor(R.color.colorPrimary));
}
} else {
@ -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);
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();
}
}

View File

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

View File

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

View File

@ -20,7 +20,9 @@
package com.nextcloud.talk.adapters.messages;
import com.nextcloud.talk.controllers.ChatController;
import com.stfalcon.chatkit.commons.ImageLoader;
import com.stfalcon.chatkit.commons.ViewHolder;
import com.stfalcon.chatkit.commons.models.IMessage;
import com.stfalcon.chatkit.messages.MessageHolders;
import com.stfalcon.chatkit.messages.MessagesListAdapter;
@ -28,12 +30,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);
}
}
}

View File

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

View File

@ -34,6 +34,7 @@ import android.content.pm.PackageManager
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.drawable.ColorDrawable
import android.media.MediaPlayer
import android.media.MediaRecorder
import android.net.Uri
import android.os.Build
@ -76,6 +77,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.work.Data
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkInfo
import androidx.work.WorkManager
import autodagger.AutoInjector
import coil.load
@ -102,6 +104,7 @@ import com.nextcloud.talk.adapters.messages.OutcomingLocationMessageViewHolder
import com.nextcloud.talk.adapters.messages.OutcomingPreviewMessageViewHolder
import com.nextcloud.talk.adapters.messages.OutcomingVoiceMessageViewHolder
import com.nextcloud.talk.adapters.messages.TalkMessagesListAdapter
import com.nextcloud.talk.adapters.messages.VoiceMessageInterface
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.callbacks.MentionAutocompleteCallback
@ -112,6 +115,7 @@ import com.nextcloud.talk.controllers.util.viewBinding
import com.nextcloud.talk.databinding.ControllerChatBinding
import com.nextcloud.talk.events.UserMentionClickEvent
import com.nextcloud.talk.events.WebSocketCommunicationEvent
import com.nextcloud.talk.jobs.DownloadFileToCacheWorker
import com.nextcloud.talk.jobs.UploadAndShareFilesWorker
import com.nextcloud.talk.models.database.CapabilitiesUtil
import com.nextcloud.talk.models.database.UserEntity
@ -175,6 +179,7 @@ import java.util.ArrayList
import java.util.Date
import java.util.HashMap
import java.util.Objects
import java.util.concurrent.ExecutionException
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
@ -186,7 +191,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
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1 +1 @@
440
439

View File

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