From ff26829d652c4db967986027a3f81316eb46c608 Mon Sep 17 00:00:00 2001 From: ericdecanini Date: Mon, 21 Mar 2022 18:42:07 +0100 Subject: [PATCH] Adds new audio timeline stub --- .../timeline/factory/MessageItemFactory.kt | 9 +- .../detail/timeline/item/MessageAudioItem.kt | 67 +++----- .../detail/timeline/item/MessageFileItem.kt | 10 +- .../detail/timeline/item/MessageVoiceItem.kt | 150 ++++++++++++++++++ .../layout/item_timeline_event_audio_stub.xml | 57 +++---- .../layout/item_timeline_event_file_stub.xml | 2 +- ...em_timeline_event_view_stubs_container.xml | 7 + .../layout/item_timeline_event_voice_stub.xml | 69 ++++++++ 8 files changed, 292 insertions(+), 79 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt create mode 100644 vector/src/main/res/layout/item_timeline_event_voice_stub.xml diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 30e36d33d3..b8d7af2d80 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -54,6 +54,8 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageLocationItem import im.vector.app.features.home.room.detail.timeline.item.MessageLocationItem_ import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem_ +import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceItem +import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceItem_ import im.vector.app.features.home.room.detail.timeline.item.PollItem import im.vector.app.features.home.room.detail.timeline.item.PollItem_ import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState @@ -321,6 +323,7 @@ class MessageItemFactory @Inject constructor( return MessageAudioItem_() .attributes(attributes) + .filename(messageContent.body) .duration(messageContent.audioInfo?.duration ?: 0) .playbackControlButtonClickListener(playbackControlButtonClickListener) .audioMessagePlaybackTracker(audioMessagePlaybackTracker) @@ -357,16 +360,16 @@ class MessageItemFactory @Inject constructor( messageContent: MessageAudioContent, informationData: MessageInformationData, highlight: Boolean, - attributes: AbsMessageItem.Attributes): MessageAudioItem { + attributes: AbsMessageItem.Attributes): MessageVoiceItem { val fileUrl = getAudioFileUrl(messageContent, informationData) val playbackControlButtonClickListener = createOnPlaybackButtonClickListener(messageContent, informationData, params) - return MessageAudioItem_() + return MessageVoiceItem_() .attributes(attributes) .duration(messageContent.audioWaveformInfo?.duration ?: 0) .waveform(messageContent.audioWaveformInfo?.waveform?.toFft().orEmpty()) .playbackControlButtonClickListener(playbackControlButtonClickListener) - .audioMessagePlaybackTracker(audioMessagePlaybackTracker) + .voiceMessagePlaybackTracker(audioMessagePlaybackTracker) .isLocalFile(localFilesHelper.isLocalFile(fileUrl)) .mxcUrl(fileUrl) .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageAudioItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageAudioItem.kt index 29f6ab7d62..101a837b3f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageAudioItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageAudioItem.kt @@ -16,18 +16,15 @@ package im.vector.app.features.home.room.detail.timeline.item -import android.content.Context import android.content.res.ColorStateList import android.graphics.Color import android.text.format.DateUtils -import android.view.View import android.view.ViewGroup import android.widget.ImageButton import android.widget.TextView import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass -import com.visualizer.amplitude.AudioRecordView import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder @@ -39,23 +36,19 @@ import im.vector.app.features.themes.ThemeUtils @EpoxyModelClass(layout = R.layout.item_timeline_event_base) abstract class MessageAudioItem : AbsMessageItem() { + @EpoxyAttribute + var filename: String = "" + @EpoxyAttribute var mxcUrl: String = "" @EpoxyAttribute var duration: Int = 0 - @EpoxyAttribute - var waveform: List = emptyList() - @EpoxyAttribute @JvmField var isLocalFile = false - @EpoxyAttribute - @JvmField - var isVoiceMessage = false - @EpoxyAttribute lateinit var contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder @@ -70,32 +63,34 @@ abstract class MessageAudioItem : AbsMessageItem() { override fun bind(holder: Holder) { super.bind(holder) - renderSendState(holder.audioLayout, null) + renderSendState(holder.rootLayout, null) + bindUploadState(holder) + holder.filenameView.text = filename + applyLayoutTint(holder) + holder.audioPlaybackControlButton.setOnClickListener { playbackControlButtonClickListener?.invoke(it) } + renderStateBasedOnAudioPlayback(holder) + } + + private fun bindUploadState(holder: Holder) { if (!attributes.informationData.sendState.hasFailed()) { contentUploadStateTrackerBinder.bind(attributes.informationData.eventId, isLocalFile, holder.progressLayout) } else { holder.audioPlaybackControlButton.setImageResource(R.drawable.ic_cross) - holder.audioPlaybackControlButton.contentDescription = getUnableToPlayContentDescription(holder.view.context) + holder.audioPlaybackControlButton.contentDescription = holder.view.context.getString(R.string.error_audio_message_unable_to_play) holder.progressLayout.isVisible = false } + } - holder.audioPlaybackWaveform.setOnLongClickListener(attributes.itemLongClickListener) - - holder.audioPlaybackWaveform.post { - holder.audioPlaybackWaveform.recreate() - waveform.forEach { amplitude -> - holder.audioPlaybackWaveform.update(amplitude) - } - } - + private fun applyLayoutTint(holder: Holder) { val backgroundTint = if (attributes.informationData.messageLayout is TimelineMessageLayout.Bubble) Color.TRANSPARENT else ThemeUtils.getColor(holder.view.context, R.attr.vctr_content_quinary) - holder.audioPlaybackLayout.backgroundTintList = ColorStateList.valueOf(backgroundTint) - holder.audioPlaybackControlButton.setOnClickListener { playbackControlButtonClickListener?.invoke(it) } + holder.mainLayout.backgroundTintList = ColorStateList.valueOf(backgroundTint) + } + private fun renderStateBasedOnAudioPlayback(holder: Holder) { audioMessagePlaybackTracker.track(attributes.informationData.eventId, object : AudioMessagePlaybackTracker.Listener { override fun onUpdate(state: AudioMessagePlaybackTracker.Listener.State) { when (state) { @@ -108,33 +103,21 @@ abstract class MessageAudioItem : AbsMessageItem() { }) } - private fun getUnableToPlayContentDescription(context: Context) = context.getString( - if (isVoiceMessage) R.string.error_voice_message_unable_to_play else R.string.error_audio_message_unable_to_play - ) - private fun renderIdleState(holder: Holder) { holder.audioPlaybackControlButton.setImageResource(R.drawable.ic_play_pause_play) - holder.audioPlaybackControlButton.contentDescription = getPlayMessageContentDescription(holder.view.context) + holder.audioPlaybackControlButton.contentDescription = holder.view.context.getString(R.string.a11y_play_audio_message) holder.audioPlaybackTime.text = formatPlaybackTime(duration) } - private fun getPlayMessageContentDescription(context: Context) = context.getString( - if (isVoiceMessage) R.string.a11y_play_voice_message else R.string.a11y_play_audio_message - ) - private fun renderPlayingState(holder: Holder, state: AudioMessagePlaybackTracker.Listener.State.Playing) { holder.audioPlaybackControlButton.setImageResource(R.drawable.ic_play_pause_pause) - holder.audioPlaybackControlButton.contentDescription = getPauseMessageContentDescription(holder.view.context) + holder.audioPlaybackControlButton.contentDescription = holder.view.context.getString(R.string.a11y_pause_audio_message) holder.audioPlaybackTime.text = formatPlaybackTime(state.playbackTime) } - private fun getPauseMessageContentDescription(context: Context) = context.getString( - if (isVoiceMessage) R.string.a11y_pause_voice_message else R.string.a11y_pause_audio_message - ) - private fun renderPausedState(holder: Holder, state: AudioMessagePlaybackTracker.Listener.State.Paused) { holder.audioPlaybackControlButton.setImageResource(R.drawable.ic_play_pause_play) - holder.audioPlaybackControlButton.contentDescription = getPlayMessageContentDescription(holder.view.context) + holder.audioPlaybackControlButton.contentDescription = holder.view.context.getString(R.string.a11y_play_audio_message) holder.audioPlaybackTime.text = formatPlaybackTime(state.playbackTime) } @@ -150,12 +133,12 @@ abstract class MessageAudioItem : AbsMessageItem() { override fun getViewStubId() = STUB_ID class Holder : AbsMessageItem.Holder(STUB_ID) { - val audioPlaybackLayout by bind(R.id.audioPlaybackLayout) - val audioLayout by bind(R.id.audioLayout) + val rootLayout by bind(R.id.messageRootLayout) + val mainLayout by bind(R.id.messageMainInnerLayout) + val filenameView by bind(R.id.messageFilenameView) val audioPlaybackControlButton by bind(R.id.audioPlaybackControlButton) val audioPlaybackTime by bind(R.id.audioPlaybackTime) - val audioPlaybackWaveform by bind(R.id.audioPlaybackWaveform) - val progressLayout by bind(R.id.audioFileUploadProgressLayout) + val progressLayout by bind(R.id.messageFileUploadProgressLayout) } companion object { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageFileItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageFileItem.kt index dbd232bfaf..8a94f927f9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageFileItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageFileItem.kt @@ -47,9 +47,6 @@ abstract class MessageFileItem : AbsMessageItem() { @DrawableRes var iconRes: Int = 0 -// @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) -// var clickListener: ClickListener? = null - @EpoxyAttribute @JvmField var isLocalFile = false @@ -67,13 +64,16 @@ abstract class MessageFileItem : AbsMessageItem() { override fun bind(holder: Holder) { super.bind(holder) renderSendState(holder.fileLayout, holder.filenameView) + if (!attributes.informationData.sendState.hasFailed()) { contentUploadStateTrackerBinder.bind(attributes.informationData.eventId, isLocalFile, holder.progressLayout) } else { holder.fileImageView.setImageResource(R.drawable.ic_cross) holder.progressLayout.isVisible = false } + holder.filenameView.text = filename + if (attributes.informationData.sendState.isSending()) { holder.fileImageView.setImageResource(iconRes) } else { @@ -85,7 +85,7 @@ abstract class MessageFileItem : AbsMessageItem() { holder.fileImageView.setImageResource(R.drawable.ic_download) } } -// holder.view.setOnClickListener(clickListener) + val backgroundTint = if (attributes.informationData.messageLayout is TimelineMessageLayout.Bubble) { Color.TRANSPARENT } else { @@ -109,7 +109,7 @@ abstract class MessageFileItem : AbsMessageItem() { class Holder : AbsMessageItem.Holder(STUB_ID) { val mainLayout by bind(R.id.messageFileMainLayout) - val progressLayout by bind(R.id.audioFileUploadProgressLayout) + val progressLayout by bind(R.id.messageFileUploadProgressLayout) val fileLayout by bind(R.id.messageFileLayout) val fileImageView by bind(R.id.messageFileIconView) val fileImageWrapper by bind(R.id.messageFileImageView) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt new file mode 100644 index 0000000000..06b622d1d6 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.detail.timeline.item + +import android.content.res.ColorStateList +import android.graphics.Color +import android.text.format.DateUtils +import android.view.View +import android.view.ViewGroup +import android.widget.ImageButton +import android.widget.TextView +import androidx.core.view.isVisible +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.visualizer.amplitude.AudioRecordView +import im.vector.app.R +import im.vector.app.core.epoxy.ClickListener +import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder +import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder +import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker +import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout +import im.vector.app.features.themes.ThemeUtils + +@EpoxyModelClass(layout = R.layout.item_timeline_event_base) +abstract class MessageVoiceItem : AbsMessageItem() { + + @EpoxyAttribute + var mxcUrl: String = "" + + @EpoxyAttribute + var duration: Int = 0 + + @EpoxyAttribute + var waveform: List = emptyList() + + @EpoxyAttribute + @JvmField + var isLocalFile = false + + @EpoxyAttribute + @JvmField + var isDownloaded = false + + @EpoxyAttribute + lateinit var contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder + + @EpoxyAttribute + lateinit var contentDownloadStateTrackerBinder: ContentDownloadStateTrackerBinder + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + var playbackControlButtonClickListener: ClickListener? = null + + @EpoxyAttribute + lateinit var voiceMessagePlaybackTracker: AudioMessagePlaybackTracker + + override fun bind(holder: Holder) { + super.bind(holder) + renderSendState(holder.voiceLayout, null) + if (!attributes.informationData.sendState.hasFailed()) { + contentUploadStateTrackerBinder.bind(attributes.informationData.eventId, isLocalFile, holder.progressLayout) + } else { + holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_cross) + holder.voicePlaybackControlButton.contentDescription = holder.view.context.getString(R.string.error_voice_message_unable_to_play) + holder.progressLayout.isVisible = false + } + + holder.voicePlaybackWaveform.setOnLongClickListener(attributes.itemLongClickListener) + + holder.voicePlaybackWaveform.post { + holder.voicePlaybackWaveform.recreate() + waveform.forEach { amplitude -> + holder.voicePlaybackWaveform.update(amplitude) + } + } + + val backgroundTint = if (attributes.informationData.messageLayout is TimelineMessageLayout.Bubble) { + Color.TRANSPARENT + } else { + ThemeUtils.getColor(holder.view.context, R.attr.vctr_content_quinary) + } + holder.voicePlaybackLayout.backgroundTintList = ColorStateList.valueOf(backgroundTint) + holder.voicePlaybackControlButton.setOnClickListener { playbackControlButtonClickListener?.invoke(it) } + + voiceMessagePlaybackTracker.track(attributes.informationData.eventId, object : AudioMessagePlaybackTracker.Listener { + override fun onUpdate(state: AudioMessagePlaybackTracker.Listener.State) { + when (state) { + is AudioMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder) + is AudioMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state) + is AudioMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state) + } + } + }) + } + + private fun renderIdleState(holder: Holder) { + holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_play) + holder.voicePlaybackControlButton.contentDescription = holder.view.context.getString(R.string.a11y_play_voice_message) + holder.voicePlaybackTime.text = formatPlaybackTime(duration) + } + + private fun renderPlayingState(holder: Holder, state: AudioMessagePlaybackTracker.Listener.State.Playing) { + holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_pause) + holder.voicePlaybackControlButton.contentDescription = holder.view.context.getString(R.string.a11y_pause_voice_message) + holder.voicePlaybackTime.text = formatPlaybackTime(state.playbackTime) + } + + private fun renderPausedState(holder: Holder, state: AudioMessagePlaybackTracker.Listener.State.Paused) { + holder.voicePlaybackControlButton.setImageResource(R.drawable.ic_play_pause_play) + holder.voicePlaybackControlButton.contentDescription = holder.view.context.getString(R.string.a11y_play_voice_message) + holder.voicePlaybackTime.text = formatPlaybackTime(state.playbackTime) + } + + private fun formatPlaybackTime(time: Int) = DateUtils.formatElapsedTime((time / 1000).toLong()) + + override fun unbind(holder: Holder) { + super.unbind(holder) + contentUploadStateTrackerBinder.unbind(attributes.informationData.eventId) + contentDownloadStateTrackerBinder.unbind(mxcUrl) + voiceMessagePlaybackTracker.untrack(attributes.informationData.eventId) + } + + override fun getViewStubId() = STUB_ID + + class Holder : AbsMessageItem.Holder(STUB_ID) { + val voicePlaybackLayout by bind(R.id.voicePlaybackLayout) + val voiceLayout by bind(R.id.voiceLayout) + val voicePlaybackControlButton by bind(R.id.voicePlaybackControlButton) + val voicePlaybackTime by bind(R.id.voicePlaybackTime) + val voicePlaybackWaveform by bind(R.id.voicePlaybackWaveform) + val progressLayout by bind(R.id.messageFileUploadProgressLayout) + } + + companion object { + private const val STUB_ID = R.id.messageContentVoiceStub + } +} diff --git a/vector/src/main/res/layout/item_timeline_event_audio_stub.xml b/vector/src/main/res/layout/item_timeline_event_audio_stub.xml index e8b3b80073..d0f4d4c85f 100644 --- a/vector/src/main/res/layout/item_timeline_event_audio_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_audio_stub.xml @@ -2,18 +2,18 @@ + tools:viewBindingIgnore="true"> + + - - + tools:text="0:23" /> - + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_file_stub.xml b/vector/src/main/res/layout/item_timeline_event_file_stub.xml index 82f2c8e886..41e4a118a3 100644 --- a/vector/src/main/res/layout/item_timeline_event_file_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_file_stub.xml @@ -49,7 +49,7 @@ + + + + + + + + + + + + + + + + +