From cc6ec988468b62f61d8ebedcb4d85fd5a31d9b6f Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 23 Oct 2024 10:48:28 +0200 Subject: [PATCH] Reworked video preview in chat messages to prevent broken layout if thumbnail can't be extracted from video (or if picture is not displayable) --- .../fragment/MediaViewerFragment.kt | 6 ++++ .../file_viewer/viewmodel/MediaViewModel.kt | 13 ++++++- .../linphone/ui/main/chat/model/FileModel.kt | 34 +++++++++++++++++++ .../org/linphone/utils/DataBindingUtils.kt | 27 +++++++++------ .../main/java/org/linphone/utils/FileUtils.kt | 14 ++++++++ app/src/main/res/drawable/file_image.xml | 9 +++++ app/src/main/res/drawable/file_video.xml | 9 +++++ .../drawable/shape_squircle_main2_200_5dp.xml | 5 +++ .../layout/chat_bubble_content_grid_cell.xml | 25 +++++++++++--- .../chat_bubble_single_media_content.xml | 27 ++++++++++++--- ...hat_conversation_attachments_area_cell.xml | 24 ++++++++++--- .../layout/chat_media_content_grid_cell.xml | 25 ++++++++++++-- 12 files changed, 193 insertions(+), 25 deletions(-) create mode 100644 app/src/main/res/drawable/file_image.xml create mode 100644 app/src/main/res/drawable/file_video.xml create mode 100644 app/src/main/res/drawable/shape_squircle_main2_200_5dp.xml diff --git a/app/src/main/java/org/linphone/ui/file_viewer/fragment/MediaViewerFragment.kt b/app/src/main/java/org/linphone/ui/file_viewer/fragment/MediaViewerFragment.kt index 64370e8d4..ba7fbf09a 100644 --- a/app/src/main/java/org/linphone/ui/file_viewer/fragment/MediaViewerFragment.kt +++ b/app/src/main/java/org/linphone/ui/file_viewer/fragment/MediaViewerFragment.kt @@ -95,6 +95,12 @@ class MediaViewerFragment : GenericMainFragment() { binding.videoPlayer.setAspectRatio(width, height) } } + + viewModel.changeFullScreenModeEvent.observe(viewLifecycleOwner) { + it.consume { fullScreenMode -> + sharedViewModel.mediaViewerFullScreenMode.value = fullScreenMode + } + } } override fun onResume() { diff --git a/app/src/main/java/org/linphone/ui/file_viewer/viewmodel/MediaViewModel.kt b/app/src/main/java/org/linphone/ui/file_viewer/viewmodel/MediaViewModel.kt index f9aee49fc..00e975024 100644 --- a/app/src/main/java/org/linphone/ui/file_viewer/viewmodel/MediaViewModel.kt +++ b/app/src/main/java/org/linphone/ui/file_viewer/viewmodel/MediaViewModel.kt @@ -64,6 +64,10 @@ class MediaViewModel @UiThread constructor() : GenericViewModel() { MutableLiveData>>() } + val changeFullScreenModeEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + lateinit var mediaPlayer: MediaPlayer private lateinit var filePath: String @@ -172,11 +176,18 @@ class MediaViewModel @UiThread constructor() : GenericViewModel() { // Leave full screen when playback is done fullScreenMode.postValue(false) + changeFullScreenModeEvent.postValue(Event(false)) } setOnVideoSizeChangedListener { mediaPlayer, width, height -> videoSizeChangedEvent.postValue(Event(Pair(width, height))) } - prepare() + try { + prepare() + } catch (e: Exception) { + fullScreenMode.postValue(false) + changeFullScreenModeEvent.postValue(Event(false)) + Log.e("$TAG Failed to prepare video file: $e") + } } val durationInMillis = mediaPlayer.duration diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/FileModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/FileModel.kt index 3d7ff0244..70f0650bb 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/FileModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/FileModel.kt @@ -21,7 +21,9 @@ package org.linphone.ui.main.chat.model import android.media.MediaMetadataRetriever import android.media.MediaMetadataRetriever.METADATA_KEY_DURATION +import android.media.ThumbnailUtils import android.net.Uri +import android.provider.MediaStore import androidx.annotation.AnyThread import androidx.annotation.UiThread import androidx.lifecycle.MutableLiveData @@ -52,6 +54,10 @@ class FileModel @AnyThread constructor( val transferProgress = MutableLiveData() + val mediaPreview = MutableLiveData() + + val mediaPreviewAvailable = MutableLiveData() + val mimeType: FileUtils.MimeType val mimeTypeString: String @@ -79,6 +85,7 @@ class FileModel @AnyThread constructor( private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) init { + mediaPreviewAvailable.postValue(false) transferProgress.postValue(-1) formattedFileSize.postValue(FileUtils.bytesToDisplayableSize(fileSize)) @@ -93,6 +100,14 @@ class FileModel @AnyThread constructor( isImage = mimeType == FileUtils.MimeType.Image isVideoPreview = mimeType == FileUtils.MimeType.Video isAudio = mimeType == FileUtils.MimeType.Audio + + if (isImage) { + mediaPreview.postValue(path) + mediaPreviewAvailable.postValue(true) + } else if (isVideoPreview) { + loadVideoPreview() + } + if (isVideoPreview || isAudio) { getDuration() } @@ -132,6 +147,25 @@ class FileModel @AnyThread constructor( FileUtils.deleteFile(path) } + @AnyThread + private fun loadVideoPreview() { + try { + Log.i("$TAG Try to create an image preview of video file [$path]") + val previewBitmap = ThumbnailUtils.createVideoThumbnail( + path, + MediaStore.Images.Thumbnails.MINI_KIND + ) + if (previewBitmap != null) { + val previewPath = FileUtils.storeBitmap(previewBitmap, fileName) + Log.i("$TAG Preview of video file [$path] available at [$previewPath]") + mediaPreview.postValue(previewPath) + mediaPreviewAvailable.postValue(true) + } + } catch (e: Exception) { + Log.e("$TAG Failed to get image preview for file [$path]: $e") + } + } + @AnyThread private fun getDuration() { try { diff --git a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt index cee98b1ae..ce44e6298 100644 --- a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt +++ b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt @@ -309,18 +309,23 @@ fun ImageView.loadFileImage(file: String?) { } @UiThread -@BindingAdapter("coilBubble") -fun ImageView.loadImageForChatBubble(file: String?) { - loadImageForChatBubble(this, file, false) +@BindingAdapter("coilBubble", "coilBubbleFallback") +fun ImageView.loadImageForChatBubbleSingle(file: String?, fallback: View?) { + loadImageForChatBubble(this, file, false, fallback) } @UiThread -@BindingAdapter("coilBubbleGrid") -fun ImageView.loadImageForChatBubbleGrid(file: String?) { - loadImageForChatBubble(this, file, true) +@BindingAdapter("coilBubbleGrid", "coilBubbleFallback") +fun ImageView.loadImageForChatBubbleGrid(file: String?, fallback: View?) { + loadImageForChatBubble(this, file, true, fallback) } -private fun loadImageForChatBubble(imageView: ImageView, file: String?, grid: Boolean) { +private fun loadImageForChatBubble( + imageView: ImageView, + file: String?, + grid: Boolean, + fallback: View? +) { if (file.isNullOrEmpty()) return val isImage = FileUtils.isExtensionImage((file)) @@ -339,22 +344,23 @@ private fun loadImageForChatBubble(imageView: ImageView, file: String?, grid: Bo if (isVideo) { imageView.load(file) { - placeholder(R.drawable.image_square) + placeholder(R.drawable.file_video) videoFrameMillis(0) transformations(RoundedCornersTransformation(radius)) size(width, height) listener( onError = { _, result -> Log.e( - "$TAG Error getting preview picture from video? [$file]: ${result.throwable}" + "$TAG Error getting preview picture from video (?) [$file]: ${result.throwable}" ) + fallback?.visibility = View.VISIBLE imageView.visibility = View.GONE } ) } } else { imageView.load(file) { - placeholder(R.drawable.image_square) + placeholder(R.drawable.file_image) // Can't have a transformation for gif file, breaks animation if (FileUtils.getExtensionFromFileName(file) != "gif") { transformations(RoundedCornersTransformation(radius)) @@ -365,6 +371,7 @@ private fun loadImageForChatBubble(imageView: ImageView, file: String?, grid: Bo Log.e( "$TAG Error getting picture from file [$file]: ${result.throwable}" ) + fallback?.visibility = View.VISIBLE imageView.visibility = View.GONE } ) diff --git a/app/src/main/java/org/linphone/utils/FileUtils.kt b/app/src/main/java/org/linphone/utils/FileUtils.kt index ecf94d15c..ac75443e7 100644 --- a/app/src/main/java/org/linphone/utils/FileUtils.kt +++ b/app/src/main/java/org/linphone/utils/FileUtils.kt @@ -22,6 +22,7 @@ package org.linphone.utils import android.content.ContentValues import android.content.Context import android.database.CursorIndexOutOfBoundsException +import android.graphics.Bitmap import android.net.Uri import android.os.Environment import android.os.ParcelFileDescriptor @@ -398,6 +399,19 @@ class FileUtils { return file.exists() } + @AnyThread + fun storeBitmap(bitmap: Bitmap, fileName: String): String { + val path = getFileStorageCacheDir("$fileName.jpg", true) + FileOutputStream(path).use { outputStream -> + bitmap.compress( + Bitmap.CompressFormat.JPEG, + 100, + outputStream + ) + } + return path.absolutePath + } + @AnyThread suspend fun dumpStringToFile(data: String, to: File): Boolean { try { diff --git a/app/src/main/res/drawable/file_image.xml b/app/src/main/res/drawable/file_image.xml new file mode 100644 index 000000000..a58b2a2d0 --- /dev/null +++ b/app/src/main/res/drawable/file_image.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/file_video.xml b/app/src/main/res/drawable/file_video.xml new file mode 100644 index 000000000..64a3b0747 --- /dev/null +++ b/app/src/main/res/drawable/file_video.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/shape_squircle_main2_200_5dp.xml b/app/src/main/res/drawable/shape_squircle_main2_200_5dp.xml new file mode 100644 index 000000000..42dd507a6 --- /dev/null +++ b/app/src/main/res/drawable/shape_squircle_main2_200_5dp.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/chat_bubble_content_grid_cell.xml b/app/src/main/res/layout/chat_bubble_content_grid_cell.xml index 2c78204fd..405713998 100644 --- a/app/src/main/res/layout/chat_bubble_content_grid_cell.xml +++ b/app/src/main/res/layout/chat_bubble_content_grid_cell.xml @@ -26,6 +26,21 @@ android:visibility="@{model.isImage || model.isVideoPreview ? View.GONE : View.VISIBLE}" app:constraint_referenced_ids="file_name, file_background, file_icon" /> + + @@ -50,16 +66,17 @@ android:textColor="@{model.isVideoPreview ? @color/white : @color/main2_600}" android:textSize="12sp" android:visibility="@{model.isVideoPreview && model.audioVideoDuration.length() > 0 ? View.VISIBLE : View.GONE, default=gone}" - app:layout_constraintBottom_toBottomOf="@id/image" - app:layout_constraintStart_toStartOf="@id/image"/> + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent"/> + + + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent"/> + + @@ -40,8 +56,8 @@ android:textColor="@{model.isVideoPreview ? @color/white : @color/main2_600}" android:textSize="12sp" android:visibility="@{model.isVideoPreview && model.audioVideoDuration.length() > 0 ? View.VISIBLE : View.GONE, default=gone}" - app:layout_constraintBottom_toBottomOf="@id/image" - app:layout_constraintStart_toStartOf="@id/image"/> + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent"/> + +