Fixed missing 'play' icon above video preview if it is the only file in a bubble + added video duration if info is available + improved onLongClick on bubbles

This commit is contained in:
Sylvain Berfini 2024-02-05 11:31:01 +01:00
parent c62ac4359a
commit 32145980f4
9 changed files with 183 additions and 42 deletions

View file

@ -1,10 +1,15 @@
package org.linphone.ui.main.chat.model
import android.media.MediaMetadataRetriever
import android.media.MediaMetadataRetriever.METADATA_KEY_DURATION
import android.net.Uri
import androidx.annotation.AnyThread
import androidx.annotation.UiThread
import androidx.lifecycle.MutableLiveData
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.tools.Log
import org.linphone.utils.FileUtils
import org.linphone.utils.TimestampUtils
class FileModel @AnyThread constructor(
val file: String,
@ -19,22 +24,23 @@ class FileModel @AnyThread constructor(
val formattedFileSize = MutableLiveData<String>()
val path = MutableLiveData<String>()
val downloadProgress = MutableLiveData<Int>()
val mimeType: FileUtils.MimeType
val isMedia: Boolean
val isImage: Boolean
val isVideoPreview: Boolean
val videoDuration = MutableLiveData<String>()
val isPdf: Boolean
val isAudio: Boolean
init {
path.postValue(file)
downloadProgress.postValue(-1)
formattedFileSize.postValue(FileUtils.bytesToDisplayableSize(fileSize))
@ -46,6 +52,9 @@ class FileModel @AnyThread constructor(
mimeType = FileUtils.getMimeType(mime)
isImage = mimeType == FileUtils.MimeType.Image
isVideoPreview = mimeType == FileUtils.MimeType.Video
if (isVideoPreview) {
getDuration()
}
isAudio = mimeType == FileUtils.MimeType.Audio
Log.d(
"$TAG File has already been downloaded, extension is [$extension], MIME is [$mime]"
@ -57,6 +66,8 @@ class FileModel @AnyThread constructor(
isVideoPreview = false
isAudio = false
}
isMedia = isVideoPreview || isImage
}
@UiThread
@ -69,4 +80,19 @@ class FileModel @AnyThread constructor(
Log.i("$TAG Deleting file [$file]")
FileUtils.deleteFile(file)
}
private fun getDuration() {
try {
val retriever = MediaMetadataRetriever()
retriever.setDataSource(coreContext.context, Uri.parse(file))
val durationInMs = retriever.extractMetadata(METADATA_KEY_DURATION)?.toInt() ?: 0
val seconds = durationInMs / 1000
val duration = TimestampUtils.durationToString(seconds)
Log.d("$TAG Duration for file [$file] is $duration")
videoDuration.postValue(duration)
retriever.release()
} catch (e: Exception) {
Log.e("$TAG Failed to get duration for file [$file]: $e")
}
}
}

View file

@ -24,6 +24,7 @@ import android.text.SpannableStringBuilder
import android.text.Spanned
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.media.AudioFocusRequestCompat
import java.text.SimpleDateFormat
@ -109,7 +110,7 @@ class MessageModel @WorkerThread constructor(
val filesList = MutableLiveData<ArrayList<FileModel>>()
val firstImagePath = MutableLiveData<String>()
val firstFileModel = MediatorLiveData<FileModel>()
val isSelected = MutableLiveData<Boolean>()
@ -231,6 +232,12 @@ class MessageModel @WorkerThread constructor(
updateReactionsList()
computeContentsList()
coreContext.postOnMainThread {
firstFileModel.addSource(filesList) {
firstFileModel.value = it.firstOrNull()
}
}
}
@WorkerThread
@ -296,7 +303,6 @@ class MessageModel @WorkerThread constructor(
Log.d("$TAG Computing message contents list")
text.postValue(Spannable.Factory.getInstance().newSpannable(""))
filesList.postValue(arrayListOf())
firstImagePath.postValue("")
var displayableContentFound = false
var filesContentCount = 0
@ -349,10 +355,6 @@ class MessageModel @WorkerThread constructor(
}
filesPath.add(fileModel)
if (filesContentCount == 1) {
firstImagePath.postValue(path)
}
displayableContentFound = true
}
else -> {
@ -376,9 +378,6 @@ class MessageModel @WorkerThread constructor(
if (name.isNotEmpty()) {
val fileModel = if (isOutgoing && chatMessage.isFileTransferInProgress) {
val path = content.filePath ?: ""
if (filesContentCount == 1) {
firstImagePath.postValue(path)
}
FileModel(path, name, content.fileSize.toLong(), false) { model ->
onContentClicked?.invoke(model.file)
}

View file

@ -83,6 +83,17 @@ fun <T> setEntries(
viewGroup: ViewGroup,
entries: List<T>?,
layoutId: Int
) {
setEntries(viewGroup, entries, layoutId, null)
}
@UiThread
@BindingAdapter("entries", "layout", "onLongClick")
fun <T> setEntries(
viewGroup: ViewGroup,
entries: List<T>?,
layoutId: Int,
onLongClick: View.OnLongClickListener?
) {
viewGroup.removeAllViews()
if (!entries.isNullOrEmpty()) {
@ -96,6 +107,7 @@ fun <T> setEntries(
)
binding.setVariable(BR.model, entry)
binding.setVariable(BR.onLongClickListener, onLongClick)
// This is a bit hacky...
if (viewGroup.context as? LifecycleOwner != null) {

View file

@ -5,6 +5,9 @@
<data>
<import type="android.view.View" />
<variable
name="onLongClickListener"
type="View.OnLongClickListener" />
<variable
name="model"
type="org.linphone.ui.main.chat.model.FileModel" />
@ -12,7 +15,6 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:onClick="@{() -> model.onClick()}"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
@ -25,16 +27,31 @@
<ImageView
android:id="@+id/image"
android:onClick="@{() -> model.onClick()}"
android:onLongClick="@{onLongClickListener}"
android:layout_width="@dimen/chat_bubble_grid_image_size"
android:layout_height="@dimen/chat_bubble_grid_image_size"
android:layout_margin="1dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
android:visibility="@{model.isImage || model.isVideoPreview ? View.VISIBLE : View.GONE}"
coilBubbleGrid="@{model.path}"
coilBubbleGrid="@{model.file}"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_600"
android:id="@+id/video_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:text="@{model.videoDuration, default=`00:42`}"
android:textColor="@color/white"
android:textSize="12sp"
android:visibility="@{model.isVideoPreview &amp;&amp; model.videoDuration.length() > 0 ? View.VISIBLE : View.GONE, default=gone}"
app:layout_constraintBottom_toBottomOf="@id/image"
app:layout_constraintStart_toStartOf="@id/image"/>
<ImageView
android:id="@+id/video_preview"
android:layout_width="@dimen/icon_size"
@ -45,12 +62,14 @@
app:layout_constraintBottom_toBottomOf="@id/image"
app:layout_constraintStart_toStartOf="@id/image"
app:layout_constraintEnd_toEndOf="@id/image"
app:tint="?attr/color_main1_500" />
app:tint="@color/white" />
<ImageView
android:id="@+id/file_icon"
android:layout_width="@dimen/chat_bubble_grid_image_size"
android:layout_height="@dimen/chat_bubble_grid_image_size"
android:onClick="@{() -> model.onClick()}"
android:onLongClick="@{onLongClickListener}"
android:adjustViewBounds="true"
android:padding="18dp"
android:src="@{model.isWaitingToBeDownloaded ? @drawable/download_simple : model.isPdf ? @drawable/file_pdf : model.isAudio ? @drawable/file_audio : @drawable/file, default=@drawable/file_pdf}"
@ -78,6 +97,7 @@
<View
android:id="@+id/file_background"
android:onClick="@{() -> model.onClick()}"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@drawable/shape_squircle_white_right"
@ -89,6 +109,7 @@
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_700"
android:id="@+id/file_name"
android:onClick="@{() -> model.onClick()}"
android:layout_width="@dimen/chat_bubble_grid_file_name_max_width"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
@ -108,6 +129,7 @@
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_300"
android:id="@+id/file_size"
android:onClick="@{() -> model.onClick()}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"

View file

@ -173,26 +173,26 @@
<com.google.android.flexbox.FlexboxLayout
android:id="@+id/files_grid"
android:onLongClick="@{onLongClickListener}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onLongClick="@{onLongClickListener}"
android:visibility="@{model.filesList.size() >= 2 || (model.filesList.size() >= 1 &amp;&amp; model.firstImagePath.length() == 0) ? View.VISIBLE : View.GONE, default=gone}"
android:visibility="@{model.filesList.size() >= 2 || (model.filesList.size() == 1 &amp;&amp; !model.firstFileModel.isMedia) ? View.VISIBLE : View.GONE, default=gone}"
app:alignItems="center"
app:flexWrap="wrap"
app:justifyContent="@{model.outgoing ? JustifyContent.FLEX_END : JustifyContent.FLEX_START}"
entries="@{model.filesList}"
layout="@{@layout/chat_bubble_content_grid_cell}"/>
layout="@{@layout/chat_bubble_content_grid_cell}"
onLongClick="@{onLongClickListener}"/>
<ImageView
android:id="@+id/single_image"
android:onClick="@{() -> model.firstImageClicked()}"
android:onLongClick="@{onLongClickListener}"
<ViewStub
android:id="@+id/single_media"
android:layout_width="wrap_content"
android:layout_height="@dimen/chat_bubble_big_image_max_size"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
android:visibility="@{model.filesList.size() == 1 &amp;&amp; model.firstImagePath.length() > 0 ? View.VISIBLE : View.GONE, default=gone}"
coilBubble="@{model.firstImagePath}"/>
android:layout_height="wrap_content"
android:layout="@layout/chat_bubble_single_media_content"
android:visibility="@{model.filesList.size() == 1 &amp;&amp; model.firstFileModel.isMedia ? View.VISIBLE : View.GONE, default=gone}"
bind:inflatedVisibility="@{model.filesList.size() == 1 &amp;&amp; model.firstFileModel.isMedia? View.VISIBLE : View.GONE}"
bind:model="@{model.firstFileModel}"
bind:onLongClickListener="@{onLongClickListener}"/>
<ViewStub
android:id="@+id/meeting_info"
@ -201,7 +201,8 @@
android:layout="@layout/chat_bubble_meeting_invite_content"
android:visibility="@{model.meetingFound ? View.VISIBLE : View.GONE, default=gone}"
bind:inflatedVisibility="@{model.meetingFound ? View.VISIBLE : View.GONE}"
bind:model="@{model}"/>
bind:model="@{model}"
bind:onLongClickListener="@{onLongClickListener}"/>
<ViewStub
android:id="@+id/voice_record"
@ -210,7 +211,8 @@
android:layout="@layout/chat_bubble_voice_record_content"
android:visibility="@{model.isVoiceRecord ? View.VISIBLE : View.GONE, default=gone}"
bind:inflatedVisibility="@{model.isVoiceRecord ? View.VISIBLE : View.GONE}"
bind:model="@{model}" />
bind:model="@{model}"
bind:onLongClickListener="@{onLongClickListener}" />
<org.linphone.ui.main.chat.view.ChatBubbleTextView
style="@style/default_text_style"
@ -235,6 +237,7 @@
style="@style/default_text_style_300"
android:id="@+id/date_time"
android:onClick="@{showDeliveryInfoClickListener}"
android:onLongClick="@{onLongClickListener}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{model.time, default=`13:40`}"
@ -244,6 +247,7 @@
style="@style/default_text_style_300"
android:id="@+id/delivery_status"
android:onClick="@{showDeliveryInfoClickListener}"
android:onLongClick="@{onLongClickListener}"
android:layout_width="@dimen/small_icon_size"
android:layout_height="@dimen/small_icon_size"
android:layout_marginStart="5dp"

View file

@ -4,6 +4,9 @@
<data>
<import type="android.view.View" />
<variable
name="onLongClickListener"
type="View.OnLongClickListener" />
<variable
name="model"
type="org.linphone.ui.main.chat.model.MessageModel" />
@ -15,6 +18,7 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="@dimen/chat_bubble_meeting_invite_width"
android:layout_height="wrap_content"
android:onLongClick="@{onLongClickListener}"
android:background="@drawable/shape_squircle_white_r10_background"
android:visibility="@{inflatedVisibility == View.VISIBLE ? View.VISIBLE : View.GONE}"
inflatedLifecycleOwner="@{true}">

View file

@ -139,23 +139,23 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onLongClick="@{onLongClickListener}"
android:visibility="@{model.filesList.size() >= 2 || (model.filesList.size() >= 1 &amp;&amp; model.firstImagePath.length() == 0) ? View.VISIBLE : View.GONE, default=gone}"
android:visibility="@{model.filesList.size() >= 2 || (model.filesList.size() == 1 &amp;&amp; !model.firstFileModel.isMedia) ? View.VISIBLE : View.GONE, default=gone}"
app:alignItems="center"
app:flexWrap="wrap"
app:justifyContent="@{model.outgoing ? JustifyContent.FLEX_END : JustifyContent.FLEX_START}"
entries="@{model.filesList}"
layout="@{@layout/chat_bubble_content_grid_cell}"/>
layout="@{@layout/chat_bubble_content_grid_cell}"
onLongClick="@{onLongClickListener}"/>
<ImageView
android:id="@+id/single_image"
android:onClick="@{() -> model.firstImageClicked()}"
android:onLongClick="@{onLongClickListener}"
<ViewStub
android:id="@+id/single_media"
android:layout_width="wrap_content"
android:layout_height="@dimen/chat_bubble_big_image_max_size"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
android:visibility="@{model.filesList.size() == 1 &amp;&amp; model.firstImagePath.length() > 0 ? View.VISIBLE : View.GONE, default=gone}"
coilBubble="@{model.firstImagePath}"/>
android:layout_height="wrap_content"
android:layout="@layout/chat_bubble_single_media_content"
android:visibility="@{model.filesList.size() == 1 &amp;&amp; model.firstFileModel.isMedia ? View.VISIBLE : View.GONE, default=gone}"
bind:inflatedVisibility="@{model.filesList.size() == 1 &amp;&amp; model.firstFileModel.isMedia? View.VISIBLE : View.GONE}"
bind:model="@{model.firstFileModel}"
bind:onLongClickListener="@{onLongClickListener}"/>
<ViewStub
android:id="@+id/meeting_info"
@ -164,7 +164,8 @@
android:layout="@layout/chat_bubble_meeting_invite_content"
android:visibility="@{model.meetingFound ? View.VISIBLE : View.GONE, default=gone}"
bind:inflatedVisibility="@{model.meetingFound ? View.VISIBLE : View.GONE}"
bind:model="@{model}"/>
bind:model="@{model}"
bind:onLongClickListener="@{onLongClickListener}"/>
<ViewStub
android:id="@+id/voice_record"
@ -173,7 +174,8 @@
android:layout="@layout/chat_bubble_voice_record_content"
android:visibility="@{model.isVoiceRecord ? View.VISIBLE : View.GONE, default=gone}"
bind:inflatedVisibility="@{model.isVoiceRecord ? View.VISIBLE : View.GONE}"
bind:model="@{model}" />
bind:model="@{model}"
bind:onLongClickListener="@{onLongClickListener}" />
<org.linphone.ui.main.chat.view.ChatBubbleTextView
style="@style/default_text_style"
@ -198,6 +200,7 @@
style="@style/default_text_style_300"
android:id="@+id/date_time"
android:onClick="@{showDeliveryInfoClickListener}"
android:onLongClick="@{onLongClickListener}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{model.time, default=`13:40`}"
@ -207,6 +210,7 @@
style="@style/default_text_style_300"
android:id="@+id/delivery_status"
android:onClick="@{showDeliveryInfoClickListener}"
android:onLongClick="@{onLongClickListener}"
android:layout_width="@dimen/small_icon_size"
android:layout_height="@dimen/small_icon_size"
android:layout_marginStart="5dp"

View file

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:bind="http://schemas.android.com/tools">
<data>
<import type="android.view.View" />
<variable
name="onLongClickListener"
type="View.OnLongClickListener" />
<variable
name="model"
type="org.linphone.ui.main.chat.model.FileModel" />
<variable
name="inflatedVisibility"
type="Integer" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="@{inflatedVisibility == View.VISIBLE ? View.VISIBLE : View.GONE}"
inflatedLifecycleOwner="@{true}">
<ImageView
android:id="@+id/image"
android:onClick="@{() -> model.onClick()}"
android:onLongClick="@{onLongClickListener}"
android:layout_width="wrap_content"
android:layout_height="@dimen/chat_bubble_big_image_max_size"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
coilBubble="@{model.file}"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_600"
android:id="@+id/video_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:text="@{model.videoDuration, default=`00:42`}"
android:textColor="@color/white"
android:textSize="12sp"
android:visibility="@{model.isVideoPreview &amp;&amp; model.videoDuration.length() > 0 ? View.VISIBLE : View.GONE, default=gone}"
app:layout_constraintBottom_toBottomOf="@id/image"
app:layout_constraintStart_toStartOf="@id/image"/>
<ImageView
android:id="@+id/video_preview"
android:layout_width="@dimen/icon_size"
android:layout_height="@dimen/icon_size"
android:src="@drawable/play_fill"
android:visibility="@{model.isVideoPreview ? View.VISIBLE : View.GONE, default=gone}"
app:layout_constraintTop_toTopOf="@id/image"
app:layout_constraintBottom_toBottomOf="@id/image"
app:layout_constraintStart_toStartOf="@id/image"
app:layout_constraintEnd_toEndOf="@id/image"
app:tint="@color/white" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -5,6 +5,9 @@
<data>
<import type="android.view.View" />
<variable
name="onLongClickListener"
type="View.OnLongClickListener" />
<variable
name="model"
type="org.linphone.ui.main.chat.model.MessageModel" />
@ -16,6 +19,7 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="@dimen/chat_bubble_voice_record_width"
android:layout_height="wrap_content"
android:onLongClick="@{onLongClickListener}"
android:visibility="@{inflatedVisibility == View.VISIBLE ? View.VISIBLE : View.GONE}"
inflatedLifecycleOwner="@{true}">