mirror of
https://github.com/nextcloud/talk-android
synced 2025-06-20 12:09:45 +01:00
Merge pull request #1373 from nextcloud/bugfix/1343/stopVoiceMessage
Bugfix/1343/stop voice message
This commit is contained in:
commit
b989c62055
@ -267,7 +267,7 @@ dependencies {
|
|||||||
implementation 'com.novoda:merlin:1.2.1'
|
implementation 'com.novoda:merlin:1.2.1'
|
||||||
|
|
||||||
implementation 'com.github.Kennyc1012:BottomSheet:2.4.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 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
|
||||||
|
|
||||||
implementation('eu.medsea.mimeutil:mime-util:2.1.3', {
|
implementation('eu.medsea.mimeutil:mime-util:2.1.3', {
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
|
|
||||||
package com.nextcloud.talk.adapters.items;
|
package com.nextcloud.talk.adapters.items;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.ColorStateList;
|
import android.content.res.ColorStateList;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
@ -105,6 +106,7 @@ public class ConversationItem extends AbstractFlexibleItem<ConversationItem.Conv
|
|||||||
return new ConversationItemViewHolder(view, adapter);
|
return new ConversationItemViewHolder(view, adapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
@Override
|
@Override
|
||||||
public void bindViewHolder(FlexibleAdapter<IFlexible> adapter, ConversationItemViewHolder holder, int position, List<Object> payloads) {
|
public void bindViewHolder(FlexibleAdapter<IFlexible> adapter, ConversationItemViewHolder holder, int position, List<Object> payloads) {
|
||||||
Context appContext =
|
Context appContext =
|
||||||
|
@ -45,17 +45,20 @@ import eu.davidea.flexibleadapter.utils.FlexibleUtils;
|
|||||||
public class MentionAutocompleteItem extends AbstractFlexibleItem<UserItem.UserItemViewHolder>
|
public class MentionAutocompleteItem extends AbstractFlexibleItem<UserItem.UserItemViewHolder>
|
||||||
implements IFilterable<String> {
|
implements IFilterable<String> {
|
||||||
|
|
||||||
|
public static final String SOURCE_CALLS = "calls";
|
||||||
|
public static final String SOURCE_GUESTS = "guests";
|
||||||
private String objectId;
|
private String objectId;
|
||||||
private String displayName;
|
private String displayName;
|
||||||
private String source;
|
private String source;
|
||||||
private UserEntity currentUser;
|
private UserEntity currentUser;
|
||||||
private Context context;
|
private Context context;
|
||||||
|
|
||||||
public MentionAutocompleteItem(String objectId,
|
public MentionAutocompleteItem(
|
||||||
String displayName,
|
String objectId,
|
||||||
String source,
|
String displayName,
|
||||||
UserEntity currentUser,
|
String source,
|
||||||
Context activityContext) {
|
UserEntity currentUser,
|
||||||
|
Context activityContext) {
|
||||||
this.objectId = objectId;
|
this.objectId = objectId;
|
||||||
this.displayName = displayName;
|
this.displayName = displayName;
|
||||||
this.source = source;
|
this.source = source;
|
||||||
@ -102,19 +105,27 @@ public class MentionAutocompleteItem extends AbstractFlexibleItem<UserItem.UserI
|
|||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
@Override
|
@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(),
|
holder.contactDisplayName.setTextColor(ResourcesCompat.getColor(context.getResources(),
|
||||||
R.color.conversation_item_header,
|
R.color.conversation_item_header,
|
||||||
null));
|
null));
|
||||||
if (adapter.hasFilter()) {
|
if (adapter.hasFilter()) {
|
||||||
FlexibleUtils.highlightText(holder.contactDisplayName, displayName,
|
FlexibleUtils.highlightText(holder.contactDisplayName,
|
||||||
String.valueOf(adapter.getFilter(String.class)), NextcloudTalkApplication.Companion.getSharedApplication()
|
displayName,
|
||||||
.getResources().getColor(R.color.colorPrimary));
|
String.valueOf(adapter.getFilter(String.class)),
|
||||||
|
NextcloudTalkApplication.Companion.getSharedApplication()
|
||||||
|
.getResources().getColor(R.color.colorPrimary));
|
||||||
if (holder.contactMentionId != null) {
|
if (holder.contactMentionId != null) {
|
||||||
FlexibleUtils.highlightText(holder.contactMentionId, "@" + objectId,
|
FlexibleUtils.highlightText(holder.contactMentionId,
|
||||||
String.valueOf(adapter.getFilter(String.class)), NextcloudTalkApplication.Companion.getSharedApplication()
|
"@" + objectId,
|
||||||
.getResources().getColor(R.color.colorPrimary));
|
String.valueOf(adapter.getFilter(String.class)),
|
||||||
|
NextcloudTalkApplication.Companion.getSharedApplication()
|
||||||
|
.getResources().getColor(R.color.colorPrimary));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
holder.contactDisplayName.setText(displayName);
|
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);
|
holder.simpleDraweeView.setImageResource(R.drawable.ic_circular_group);
|
||||||
} else {
|
} else {
|
||||||
String avatarId = objectId;
|
String avatarId = objectId;
|
||||||
String avatarUrl = ApiUtils.getUrlForAvatarWithName(currentUser.getBaseUrl(),
|
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;
|
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);
|
holder.simpleDraweeView.setController(null);
|
||||||
@ -147,8 +161,15 @@ public class MentionAutocompleteItem extends AbstractFlexibleItem<UserItem.UserI
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean filter(String constraint) {
|
public boolean filter(String constraint) {
|
||||||
return objectId != null && Pattern.compile(constraint,
|
return objectId != null &&
|
||||||
Pattern.CASE_INSENSITIVE | Pattern.LITERAL).matcher(objectId).find()
|
Pattern
|
||||||
|| displayName != null && Pattern.compile(constraint, Pattern.CASE_INSENSITIVE | Pattern.LITERAL).matcher(displayName).find();
|
.compile(constraint, Pattern.CASE_INSENSITIVE | Pattern.LITERAL)
|
||||||
|
.matcher(objectId)
|
||||||
|
.find() ||
|
||||||
|
displayName != null &&
|
||||||
|
Pattern
|
||||||
|
.compile(constraint, Pattern.CASE_INSENSITIVE | Pattern.LITERAL)
|
||||||
|
.matcher(displayName)
|
||||||
|
.find();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,23 +25,17 @@
|
|||||||
package com.nextcloud.talk.adapters.messages
|
package com.nextcloud.talk.adapters.messages
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.graphics.drawable.LayerDrawable
|
import android.graphics.drawable.LayerDrawable
|
||||||
import android.media.MediaPlayer
|
|
||||||
import android.os.Handler
|
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.SeekBar
|
import android.widget.SeekBar
|
||||||
import android.widget.SeekBar.OnSeekBarChangeListener
|
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.res.ResourcesCompat
|
import androidx.core.content.res.ResourcesCompat
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.work.Data
|
|
||||||
import androidx.work.OneTimeWorkRequest
|
|
||||||
import androidx.work.WorkInfo
|
import androidx.work.WorkInfo
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import autodagger.AutoInjector
|
import autodagger.AutoInjector
|
||||||
@ -51,14 +45,11 @@ import com.nextcloud.talk.R
|
|||||||
import com.nextcloud.talk.application.NextcloudTalkApplication
|
import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||||
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
|
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
|
||||||
import com.nextcloud.talk.databinding.ItemCustomIncomingVoiceMessageBinding
|
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.models.json.chat.ChatMessage
|
||||||
import com.nextcloud.talk.utils.ApiUtils
|
import com.nextcloud.talk.utils.ApiUtils
|
||||||
import com.nextcloud.talk.utils.DisplayUtils
|
import com.nextcloud.talk.utils.DisplayUtils
|
||||||
import com.nextcloud.talk.utils.preferences.AppPreferences
|
import com.nextcloud.talk.utils.preferences.AppPreferences
|
||||||
import com.stfalcon.chatkit.messages.MessageHolders
|
import com.stfalcon.chatkit.messages.MessageHolders
|
||||||
import java.io.File
|
|
||||||
import java.util.concurrent.ExecutionException
|
import java.util.concurrent.ExecutionException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ -79,11 +70,7 @@ class IncomingVoiceMessageViewHolder(incomingView: View) : MessageHolders
|
|||||||
|
|
||||||
lateinit var message: ChatMessage
|
lateinit var message: ChatMessage
|
||||||
|
|
||||||
lateinit var activity: Activity
|
lateinit var voiceMessageInterface: VoiceMessageInterface
|
||||||
|
|
||||||
var mediaPlayer: MediaPlayer? = null
|
|
||||||
|
|
||||||
lateinit var handler: Handler
|
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
override fun onBind(message: ChatMessage) {
|
override fun onBind(message: ChatMessage) {
|
||||||
@ -101,47 +88,85 @@ class IncomingVoiceMessageViewHolder(incomingView: View) : MessageHolders
|
|||||||
// parent message handling
|
// parent message handling
|
||||||
setParentMessageDataOnMessageItem(message)
|
setParentMessageDataOnMessageItem(message)
|
||||||
|
|
||||||
binding.playBtn.setOnClickListener {
|
updateDownloadState(message)
|
||||||
openOrDownloadFile(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 {
|
if (message.isDownloadingVoiceMessage) {
|
||||||
pausePlayback()
|
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) {
|
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
||||||
// unused atm
|
// unused atm
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartTrackingTouch(seekBar: SeekBar) {
|
override fun onStartTrackingTouch(seekBar: SeekBar) {
|
||||||
// unused atm
|
// unused atm
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
||||||
if (mediaPlayer != null && fromUser) {
|
if (fromUser) {
|
||||||
mediaPlayer!!.seekTo(progress * SEEKBAR_BASE)
|
voiceMessageInterface.updateMediaPlayerProgressBySlider(message, progress)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateDownloadState(message: ChatMessage) {
|
||||||
// check if download worker is already running
|
// check if download worker is already running
|
||||||
val fileId = message.getSelectedIndividualHashMap()["id"]
|
val fileId = message.getSelectedIndividualHashMap()["id"]
|
||||||
val workers = WorkManager.getInstance(
|
val workers = WorkManager.getInstance(context!!).getWorkInfosByTag(fileId!!)
|
||||||
context!!
|
|
||||||
).getWorkInfosByTag(fileId!!)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (workInfo in workers.get()) {
|
for (workInfo in workers.get()) {
|
||||||
if (workInfo.state == WorkInfo.State.RUNNING || workInfo.state == WorkInfo.State.ENQUEUED) {
|
if (workInfo.state == WorkInfo.State.RUNNING || workInfo.state == WorkInfo.State.ENQUEUED) {
|
||||||
binding.progressBar.visibility = View.VISIBLE
|
showVoiceMessageLoading()
|
||||||
binding.playBtn.visibility = View.GONE
|
|
||||||
WorkManager.getInstance(context!!).getWorkInfoByIdLiveData(workInfo.id)
|
WorkManager.getInstance(context!!).getWorkInfoByIdLiveData(workInfo.id)
|
||||||
.observeForever { info: WorkInfo? ->
|
.observeForever { info: WorkInfo? ->
|
||||||
if (info != null) {
|
if (info != null) {
|
||||||
updateViewsByProgress(
|
when (info.state) {
|
||||||
info
|
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) {
|
private fun setAvatarAndAuthorOnMessageItem(message: ChatMessage) {
|
||||||
val author: String = message.actorDisplayName
|
val author: String = message.actorDisplayName
|
||||||
if (!TextUtils.isEmpty(author)) {
|
if (!TextUtils.isEmpty(author)) {
|
||||||
@ -249,155 +284,12 @@ class IncomingVoiceMessageViewHolder(incomingView: View) : MessageHolders
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openOrDownloadFile(message: ChatMessage) {
|
fun assignAdapter(voiceMessageInterface: VoiceMessageInterface) {
|
||||||
val filename = message.getSelectedIndividualHashMap()["name"]
|
this.voiceMessageInterface = voiceMessageInterface
|
||||||
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 -> {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "VoiceInMessageView"
|
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
|
private const val SEEKBAR_START: Int = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,19 +23,15 @@
|
|||||||
package com.nextcloud.talk.adapters.messages
|
package com.nextcloud.talk.adapters.messages
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.PorterDuff
|
import android.graphics.PorterDuff
|
||||||
import android.media.MediaPlayer
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.SeekBar
|
import android.widget.SeekBar
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.work.Data
|
|
||||||
import androidx.work.OneTimeWorkRequest
|
|
||||||
import androidx.work.WorkInfo
|
import androidx.work.WorkInfo
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import autodagger.AutoInjector
|
import autodagger.AutoInjector
|
||||||
@ -44,25 +40,21 @@ import com.nextcloud.talk.R
|
|||||||
import com.nextcloud.talk.application.NextcloudTalkApplication
|
import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||||
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
|
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
|
||||||
import com.nextcloud.talk.databinding.ItemCustomOutcomingVoiceMessageBinding
|
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.ChatMessage
|
||||||
import com.nextcloud.talk.models.json.chat.ReadStatus
|
import com.nextcloud.talk.models.json.chat.ReadStatus
|
||||||
import com.nextcloud.talk.utils.ApiUtils
|
import com.nextcloud.talk.utils.ApiUtils
|
||||||
import com.nextcloud.talk.utils.DisplayUtils
|
import com.nextcloud.talk.utils.DisplayUtils
|
||||||
import com.nextcloud.talk.utils.preferences.AppPreferences
|
import com.nextcloud.talk.utils.preferences.AppPreferences
|
||||||
import com.stfalcon.chatkit.messages.MessageHolders
|
import com.stfalcon.chatkit.messages.MessageHolders
|
||||||
import java.io.File
|
|
||||||
import java.util.concurrent.ExecutionException
|
import java.util.concurrent.ExecutionException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AutoInjector(NextcloudTalkApplication::class)
|
@AutoInjector(NextcloudTalkApplication::class)
|
||||||
class OutcomingVoiceMessageViewHolder(incomingView: View) : MessageHolders
|
class OutcomingVoiceMessageViewHolder(outcomingView: View) : MessageHolders
|
||||||
.OutcomingTextMessageViewHolder<ChatMessage>(incomingView) {
|
.OutcomingTextMessageViewHolder<ChatMessage>(outcomingView) {
|
||||||
|
|
||||||
private val binding: ItemCustomOutcomingVoiceMessageBinding =
|
private val binding: ItemCustomOutcomingVoiceMessageBinding =
|
||||||
ItemCustomOutcomingVoiceMessageBinding.bind(itemView)
|
ItemCustomOutcomingVoiceMessageBinding.bind(itemView)
|
||||||
private val realView: View = itemView
|
|
||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
@Inject
|
@Inject
|
||||||
@ -74,12 +66,10 @@ class OutcomingVoiceMessageViewHolder(incomingView: View) : MessageHolders
|
|||||||
|
|
||||||
lateinit var message: ChatMessage
|
lateinit var message: ChatMessage
|
||||||
|
|
||||||
lateinit var activity: Activity
|
|
||||||
|
|
||||||
var mediaPlayer: MediaPlayer? = null
|
|
||||||
|
|
||||||
lateinit var handler: Handler
|
lateinit var handler: Handler
|
||||||
|
|
||||||
|
lateinit var voiceMessageInterface: VoiceMessageInterface
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
override fun onBind(message: ChatMessage) {
|
override fun onBind(message: ChatMessage) {
|
||||||
super.onBind(message)
|
super.onBind(message)
|
||||||
@ -94,57 +84,56 @@ class OutcomingVoiceMessageViewHolder(incomingView: View) : MessageHolders
|
|||||||
// parent message handling
|
// parent message handling
|
||||||
setParentMessageDataOnMessageItem(message)
|
setParentMessageDataOnMessageItem(message)
|
||||||
|
|
||||||
binding.playBtn.setOnClickListener {
|
updateDownloadState(message)
|
||||||
openOrDownloadFile(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 {
|
if (message.isDownloadingVoiceMessage) {
|
||||||
pausePlayback()
|
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 {
|
binding.seekbar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
||||||
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
||||||
// unused atm
|
// unused atm
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartTrackingTouch(seekBar: SeekBar) {
|
override fun onStartTrackingTouch(seekBar: SeekBar) {
|
||||||
// unused atm
|
// unused atm
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
||||||
if (mediaPlayer != null && fromUser) {
|
if (fromUser) {
|
||||||
mediaPlayer!!.seekTo(progress * SEEKBAR_BASE)
|
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) {
|
val readStatusDrawableInt = when (message.readStatus) {
|
||||||
ReadStatus.READ -> R.drawable.ic_check_all
|
ReadStatus.READ -> R.drawable.ic_check_all
|
||||||
ReadStatus.SENT -> R.drawable.ic_check
|
ReadStatus.SENT -> R.drawable.ic_check
|
||||||
@ -167,6 +156,56 @@ class OutcomingVoiceMessageViewHolder(incomingView: View) : MessageHolders
|
|||||||
binding.checkMark.setContentDescription(readStatusContentDescriptionString)
|
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) {
|
private fun setParentMessageDataOnMessageItem(message: ChatMessage) {
|
||||||
if (!message.isDeleted && message.parentMessage != null) {
|
if (!message.isDeleted && message.parentMessage != null) {
|
||||||
val parentChatMessage = message.parentMessage
|
val parentChatMessage = message.parentMessage
|
||||||
@ -224,156 +263,12 @@ class OutcomingVoiceMessageViewHolder(incomingView: View) : MessageHolders
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openOrDownloadFile(message: ChatMessage) {
|
fun assignAdapter(voiceMessageInterface: VoiceMessageInterface) {
|
||||||
val filename = message.getSelectedIndividualHashMap()["name"]
|
this.voiceMessageInterface = voiceMessageInterface
|
||||||
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 -> {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "VoiceOutMessageView"
|
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
|
private const val SEEKBAR_START: Int = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,9 @@
|
|||||||
|
|
||||||
package com.nextcloud.talk.adapters.messages;
|
package com.nextcloud.talk.adapters.messages;
|
||||||
|
|
||||||
|
import com.nextcloud.talk.controllers.ChatController;
|
||||||
import com.stfalcon.chatkit.commons.ImageLoader;
|
import com.stfalcon.chatkit.commons.ImageLoader;
|
||||||
|
import com.stfalcon.chatkit.commons.ViewHolder;
|
||||||
import com.stfalcon.chatkit.commons.models.IMessage;
|
import com.stfalcon.chatkit.commons.models.IMessage;
|
||||||
import com.stfalcon.chatkit.messages.MessageHolders;
|
import com.stfalcon.chatkit.messages.MessageHolders;
|
||||||
import com.stfalcon.chatkit.messages.MessagesListAdapter;
|
import com.stfalcon.chatkit.messages.MessagesListAdapter;
|
||||||
@ -28,12 +30,29 @@ import com.stfalcon.chatkit.messages.MessagesListAdapter;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class TalkMessagesListAdapter<M extends IMessage> extends MessagesListAdapter<M> {
|
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);
|
super(senderId, holders, imageLoader);
|
||||||
|
this.chatController = chatController;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<MessagesListAdapter.Wrapper> getItems() {
|
public List<MessagesListAdapter.Wrapper> getItems() {
|
||||||
return items;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
@ -34,6 +34,7 @@ import android.content.pm.PackageManager
|
|||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
|
import android.media.MediaPlayer
|
||||||
import android.media.MediaRecorder
|
import android.media.MediaRecorder
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@ -76,6 +77,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.work.Data
|
import androidx.work.Data
|
||||||
import androidx.work.OneTimeWorkRequest
|
import androidx.work.OneTimeWorkRequest
|
||||||
|
import androidx.work.WorkInfo
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import autodagger.AutoInjector
|
import autodagger.AutoInjector
|
||||||
import coil.load
|
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.OutcomingPreviewMessageViewHolder
|
||||||
import com.nextcloud.talk.adapters.messages.OutcomingVoiceMessageViewHolder
|
import com.nextcloud.talk.adapters.messages.OutcomingVoiceMessageViewHolder
|
||||||
import com.nextcloud.talk.adapters.messages.TalkMessagesListAdapter
|
import com.nextcloud.talk.adapters.messages.TalkMessagesListAdapter
|
||||||
|
import com.nextcloud.talk.adapters.messages.VoiceMessageInterface
|
||||||
import com.nextcloud.talk.api.NcApi
|
import com.nextcloud.talk.api.NcApi
|
||||||
import com.nextcloud.talk.application.NextcloudTalkApplication
|
import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||||
import com.nextcloud.talk.callbacks.MentionAutocompleteCallback
|
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.databinding.ControllerChatBinding
|
||||||
import com.nextcloud.talk.events.UserMentionClickEvent
|
import com.nextcloud.talk.events.UserMentionClickEvent
|
||||||
import com.nextcloud.talk.events.WebSocketCommunicationEvent
|
import com.nextcloud.talk.events.WebSocketCommunicationEvent
|
||||||
|
import com.nextcloud.talk.jobs.DownloadFileToCacheWorker
|
||||||
import com.nextcloud.talk.jobs.UploadAndShareFilesWorker
|
import com.nextcloud.talk.jobs.UploadAndShareFilesWorker
|
||||||
import com.nextcloud.talk.models.database.CapabilitiesUtil
|
import com.nextcloud.talk.models.database.CapabilitiesUtil
|
||||||
import com.nextcloud.talk.models.database.UserEntity
|
import com.nextcloud.talk.models.database.UserEntity
|
||||||
@ -175,6 +179,7 @@ import java.util.ArrayList
|
|||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.HashMap
|
import java.util.HashMap
|
||||||
import java.util.Objects
|
import java.util.Objects
|
||||||
|
import java.util.concurrent.ExecutionException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AutoInjector(NextcloudTalkApplication::class)
|
@AutoInjector(NextcloudTalkApplication::class)
|
||||||
@ -186,7 +191,9 @@ class ChatController(args: Bundle) :
|
|||||||
MessagesListAdapter.OnLoadMoreListener,
|
MessagesListAdapter.OnLoadMoreListener,
|
||||||
MessagesListAdapter.Formatter<Date>,
|
MessagesListAdapter.Formatter<Date>,
|
||||||
MessagesListAdapter.OnMessageViewLongClickListener<IMessage>,
|
MessagesListAdapter.OnMessageViewLongClickListener<IMessage>,
|
||||||
ContentChecker<ChatMessage> {
|
ContentChecker<ChatMessage>,
|
||||||
|
VoiceMessageInterface {
|
||||||
|
|
||||||
private val binding: ControllerChatBinding by viewBinding(ControllerChatBinding::bind)
|
private val binding: ControllerChatBinding by viewBinding(ControllerChatBinding::bind)
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
@ -247,6 +254,10 @@ class ChatController(args: Bundle) :
|
|||||||
|
|
||||||
private var recorder: MediaRecorder? = null
|
private var recorder: MediaRecorder? = null
|
||||||
|
|
||||||
|
var mediaPlayer: MediaPlayer? = null
|
||||||
|
lateinit var mediaPlayerHandler: Handler
|
||||||
|
var currentlyPlayedVoiceMessage: ChatMessage? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setHasOptionsMenu(true)
|
setHasOptionsMenu(true)
|
||||||
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
|
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
|
||||||
@ -488,7 +499,8 @@ class ChatController(args: Bundle) :
|
|||||||
.setAutoPlayAnimations(true)
|
.setAutoPlayAnimations(true)
|
||||||
.build()
|
.build()
|
||||||
imageView.controller = draweeController
|
imageView.controller = draweeController
|
||||||
}
|
},
|
||||||
|
this
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
binding.messagesListView.visibility = View.VISIBLE
|
binding.messagesListView.visibility = View.VISIBLE
|
||||||
@ -499,6 +511,22 @@ class ChatController(args: Bundle) :
|
|||||||
adapter?.setDateHeadersFormatter { format(it) }
|
adapter?.setDateHeadersFormatter { format(it) }
|
||||||
adapter?.setOnMessageViewLongClickListener { view, message -> onMessageViewLongClick(view, message) }
|
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) {
|
if (context != null) {
|
||||||
val messageSwipeController = MessageSwipeCallback(
|
val messageSwipeController = MessageSwipeCallback(
|
||||||
activity!!,
|
activity!!,
|
||||||
@ -749,6 +777,151 @@ class ChatController(args: Bundle) :
|
|||||||
super.onViewBound(view)
|
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")
|
@SuppressLint("SimpleDateFormat")
|
||||||
private fun setVoiceRecordFileName() {
|
private fun setVoiceRecordFileName() {
|
||||||
val pattern = "yyyy-MM-dd HH-mm-ss"
|
val pattern = "yyyy-MM-dd HH-mm-ss"
|
||||||
@ -1290,6 +1463,8 @@ class ChatController(args: Bundle) :
|
|||||||
actionBar?.setIcon(null)
|
actionBar?.setIcon(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentlyPlayedVoiceMessage?.let { stopMediaPlayer(it) }
|
||||||
|
|
||||||
adapter = null
|
adapter = null
|
||||||
inConversation = false
|
inConversation = false
|
||||||
}
|
}
|
||||||
@ -2349,5 +2524,7 @@ class ChatController(args: Bundle) :
|
|||||||
private const val SHORT_VIBRATE: Long = 20
|
private const val SHORT_VIBRATE: Long = 20
|
||||||
private const val FULLY_OPAQUE_INT: Int = 255
|
private const val FULLY_OPAQUE_INT: Int = 255
|
||||||
private const val SEMI_TRANSPARENT_INT: Int = 99
|
private const val SEMI_TRANSPARENT_INT: Int = 99
|
||||||
|
private const val VOICE_MESSAGE_SEEKBAR_BASE: Int = 1000
|
||||||
|
private const val SECOND: Long = 1000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -90,6 +90,13 @@ public class ChatMessage implements MessageContentType, MessageContentType.Image
|
|||||||
@JsonField(name = "messageType")
|
@JsonField(name = "messageType")
|
||||||
public String messageType;
|
public String messageType;
|
||||||
|
|
||||||
|
public boolean isDownloadingVoiceMessage;
|
||||||
|
public boolean resetVoiceMessage;
|
||||||
|
public boolean isPlayingVoiceMessage;
|
||||||
|
public int voiceMessageDuration;
|
||||||
|
public int voiceMessagePlayedSeconds;
|
||||||
|
public int voiceMessageDownloadProgress;
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
List<MessageType> messageTypesToIgnore = Arrays.asList(
|
List<MessageType> messageTypesToIgnore = Arrays.asList(
|
||||||
MessageType.REGULAR_TEXT_MESSAGE,
|
MessageType.REGULAR_TEXT_MESSAGE,
|
||||||
@ -133,8 +140,6 @@ public class ChatMessage implements MessageContentType, MessageContentType.Image
|
|||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public String getImageUrl() {
|
public String getImageUrl() {
|
||||||
|
|
||||||
|
|
||||||
if (messageParameters != null && messageParameters.size() > 0) {
|
if (messageParameters != null && messageParameters.size() > 0) {
|
||||||
for (HashMap.Entry<String, HashMap<String, String>> entry : messageParameters.entrySet()) {
|
for (HashMap.Entry<String, HashMap<String, String>> entry : messageParameters.entrySet()) {
|
||||||
Map<String, String> individualHashMap = entry.getValue();
|
Map<String, String> individualHashMap = entry.getValue();
|
||||||
|
@ -66,7 +66,7 @@
|
|||||||
android:textSize="12sp" />
|
android:textSize="12sp" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:gravity="center_vertical">
|
android:gravity="center_vertical">
|
||||||
@ -79,32 +79,20 @@
|
|||||||
android:visibility="gone"/>
|
android:visibility="gone"/>
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/playBtn"
|
android:id="@+id/playPauseBtn"
|
||||||
style="@style/Widget.AppTheme.Button.IconButton"
|
style="@style/Widget.AppTheme.Button.IconButton"
|
||||||
android:layout_width="48dp"
|
android:layout_width="48dp"
|
||||||
android:layout_height="48dp"
|
android:layout_height="48dp"
|
||||||
android:contentDescription="@string/play_voice_message"
|
android:contentDescription="@string/play_pause_voice_message"
|
||||||
android:visibility="visible"
|
android:visibility="visible"
|
||||||
app:cornerRadius="@dimen/button_corner_radius"
|
app:cornerRadius="@dimen/button_corner_radius"
|
||||||
app:icon="@drawable/ic_baseline_play_arrow_voice_message_24"
|
app:icon="@drawable/ic_baseline_play_arrow_voice_message_24"
|
||||||
app:iconSize="40dp"
|
app:iconSize="40dp"
|
||||||
app:iconTint="@color/nc_incoming_text_default" />
|
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
|
<SeekBar
|
||||||
android:id="@+id/seekbar"
|
android:id="@+id/seekbar"
|
||||||
android:layout_width="200dp"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
tools:progress="50" />
|
tools:progress="50" />
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@
|
|||||||
android:visibility="gone" />
|
android:visibility="gone" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:gravity="center_vertical">
|
android:gravity="center_vertical">
|
||||||
@ -60,14 +60,15 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:progressTint="@color/fontAppbar"
|
android:progressTint="@color/fontAppbar"
|
||||||
android:visibility="gone"/>
|
android:visibility="gone"
|
||||||
|
android:indeterminateTint="@color/nc_outcoming_text_default"/>
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/playBtn"
|
android:id="@+id/playPauseBtn"
|
||||||
style="@style/Widget.AppTheme.Button.IconButton"
|
style="@style/Widget.AppTheme.Button.IconButton"
|
||||||
android:layout_width="48dp"
|
android:layout_width="48dp"
|
||||||
android:layout_height="48dp"
|
android:layout_height="48dp"
|
||||||
android:contentDescription="@string/play_voice_message"
|
android:contentDescription="@string/play_pause_voice_message"
|
||||||
android:visibility="visible"
|
android:visibility="visible"
|
||||||
app:rippleColor="#1FFFFFFF"
|
app:rippleColor="#1FFFFFFF"
|
||||||
app:cornerRadius="@dimen/button_corner_radius"
|
app:cornerRadius="@dimen/button_corner_radius"
|
||||||
@ -75,23 +76,10 @@
|
|||||||
app:iconSize="40dp"
|
app:iconSize="40dp"
|
||||||
app:iconTint="@color/nc_outcoming_text_default" />
|
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
|
<SeekBar
|
||||||
android:id="@+id/seekbar"
|
android:id="@+id/seekbar"
|
||||||
style="@style/Nextcloud.Material.Outgoing.SeekBar"
|
style="@style/Nextcloud.Material.Outgoing.SeekBar"
|
||||||
android:layout_width="200dp"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="40dp"
|
android:layout_height="40dp"
|
||||||
android:thumb="@drawable/voice_message_outgoing_seek_bar_slider"
|
android:thumb="@drawable/voice_message_outgoing_seek_bar_slider"
|
||||||
tools:progress="50" />
|
tools:progress="50" />
|
||||||
|
@ -389,8 +389,7 @@
|
|||||||
<string name="nc_voice_message_hold_to_record_info">Hold to record, release to send.</string>
|
<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_description_record_voice">Record voice message</string>
|
||||||
<string name="nc_voice_message_slide_to_cancel"><< Slide to cancel</string>
|
<string name="nc_voice_message_slide_to_cancel"><< Slide to cancel</string>
|
||||||
<string name="play_voice_message">Play voice message</string>
|
<string name="play_pause_voice_message">Play/pause voice message</string>
|
||||||
<string name="pause_voice_message">Pause voice message</string>
|
|
||||||
<string name="nc_voice_message_missing_audio_permission">Permission for audio recording is required</string>
|
<string name="nc_voice_message_missing_audio_permission">Permission for audio recording is required</string>
|
||||||
|
|
||||||
<!-- Phonebook Integration -->
|
<!-- Phonebook Integration -->
|
||||||
|
@ -1 +1 @@
|
|||||||
440
|
439
|
@ -1,2 +1,2 @@
|
|||||||
DO NOT TOUCH; GENERATED BY DRONE
|
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>
|
||||||
|
Loading…
Reference in New Issue
Block a user