Added PDF file preview in conversation (message bubble + documents list)

This commit is contained in:
Sylvain Berfini 2025-09-17 12:17:06 +02:00
parent 99936e8f75
commit 808dc92cd7
3 changed files with 82 additions and 14 deletions

View file

@ -14,6 +14,7 @@ Group changes to describe their impact on the project, as follows:
### Added ### Added
- Added the ability to edit/delete chat messages sent less than 24 hours ago. - Added the ability to edit/delete chat messages sent less than 24 hours ago.
- Added PDF preview in conversation (message bubble & documents list)
- Added hover effect when using a mouse (useful for tablets or devices with desktop mode) - Added hover effect when using a mouse (useful for tablets or devices with desktop mode)
- Support right click on some items to open bottom sheet/menu - Support right click on some items to open bottom sheet/menu
- Added toggle speaker action in active call notification - Added toggle speaker action in active call notification

View file

@ -19,9 +19,11 @@
*/ */
package org.linphone.ui.main.chat.model package org.linphone.ui.main.chat.model
import android.graphics.pdf.PdfRenderer
import android.media.MediaMetadataRetriever import android.media.MediaMetadataRetriever
import android.media.MediaMetadataRetriever.METADATA_KEY_DURATION import android.media.MediaMetadataRetriever.METADATA_KEY_DURATION
import android.media.ThumbnailUtils import android.media.ThumbnailUtils
import android.os.ParcelFileDescriptor
import android.provider.MediaStore import android.provider.MediaStore
import androidx.annotation.AnyThread import androidx.annotation.AnyThread
import androidx.annotation.UiThread import androidx.annotation.UiThread
@ -35,6 +37,9 @@ import org.linphone.core.tools.Log
import org.linphone.utils.FileUtils import org.linphone.utils.FileUtils
import org.linphone.utils.TimestampUtils import org.linphone.utils.TimestampUtils
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.graphics.createBitmap
import kotlinx.coroutines.withContext
import java.io.File
class FileModel class FileModel
@AnyThread @AnyThread
@ -97,6 +102,9 @@ class FileModel
if (!isWaitingToBeDownloaded) { if (!isWaitingToBeDownloaded) {
val extension = FileUtils.getExtensionFromFileName(path) val extension = FileUtils.getExtensionFromFileName(path)
isPdf = extension == "pdf" isPdf = extension == "pdf"
if (isPdf) {
loadPdfPreview()
}
val mime = FileUtils.getMimeTypeFromExtension(extension) val mime = FileUtils.getMimeTypeFromExtension(extension)
mimeTypeString = mime mimeTypeString = mime
@ -168,21 +176,65 @@ class FileModel
} }
@AnyThread @AnyThread
private fun loadVideoPreview() { private fun loadPdfPreview() {
try { scope.launch {
Log.i("$TAG Try to create an image preview of video file [$path]") withContext(Dispatchers.IO) {
val previewBitmap = ThumbnailUtils.createVideoThumbnail( try {
path, val pdfFileDescriptor = ParcelFileDescriptor.open(
MediaStore.Images.Thumbnails.MINI_KIND File(path),
) ParcelFileDescriptor.MODE_READ_ONLY
if (previewBitmap != null) { )
val previewPath = FileUtils.storeBitmap(previewBitmap, fileName) if (pdfFileDescriptor == null) {
Log.i("$TAG Preview of video file [$path] available at [$previewPath]") Log.e("$TAG Failed to get a file descriptor for PDF at [$path]")
mediaPreview.postValue(previewPath) return@withContext
mediaPreviewAvailable.postValue(true) }
val pdfRenderer = PdfRenderer(pdfFileDescriptor)
val pdfFirstPage = pdfRenderer.openPage(0)
val previewBitmap = createBitmap(pdfFirstPage.width, pdfFirstPage.height)
pdfFirstPage.render(
previewBitmap,
null,
null,
PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY
)
val previewPath = FileUtils.storeBitmap(previewBitmap, fileName)
Log.i("$TAG Preview of PDF file [$path] available at [$previewPath]")
mediaPreview.postValue(previewPath)
mediaPreviewAvailable.postValue(true)
previewBitmap.recycle()
pdfFirstPage.close()
pdfRenderer.close()
pdfFileDescriptor.close()
} catch (e: Exception) {
Log.e("$TAG Failed to get image preview for PDF file [$path]: $e")
}
}
}
}
@AnyThread
private fun loadVideoPreview() {
scope.launch {
withContext(Dispatchers.IO) {
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")
}
} }
} catch (e: Exception) {
Log.e("$TAG Failed to get image preview for file [$path]: $e")
} }
} }

View file

@ -64,6 +64,21 @@
app:layout_constraintEnd_toEndOf="@id/left_background" app:layout_constraintEnd_toEndOf="@id/left_background"
app:tint="?attr/color_main2_600" /> app:tint="?attr/color_main2_600" />
<ImageView
android:id="@+id/file_pdf_preview"
android:layout_width="@dimen/chat_bubble_grid_file_size"
android:layout_height="@dimen/chat_bubble_grid_file_size"
android:onClick="@{() -> model.onClick()}"
android:onLongClick="@{onLongClickListener}"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
android:contentDescription="@string/content_description_chat_bubble_file"
android:visibility="@{model.isPdf &amp;&amp; model.mediaPreviewAvailable ? View.VISIBLE : View.GONE, default=gone}"
coilBubbleGrid="@{model.mediaPreview}"
coilBubbleFallback="@{fileIcon}"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<androidx.appcompat.widget.AppCompatTextView <androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_600" style="@style/default_text_style_600"
android:id="@+id/audio_duration" android:id="@+id/audio_duration"