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 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)
- Support right click on some items to open bottom sheet/menu
- Added toggle speaker action in active call notification

View file

@ -19,9 +19,11 @@
*/
package org.linphone.ui.main.chat.model
import android.graphics.pdf.PdfRenderer
import android.media.MediaMetadataRetriever
import android.media.MediaMetadataRetriever.METADATA_KEY_DURATION
import android.media.ThumbnailUtils
import android.os.ParcelFileDescriptor
import android.provider.MediaStore
import androidx.annotation.AnyThread
import androidx.annotation.UiThread
@ -35,6 +37,9 @@ import org.linphone.core.tools.Log
import org.linphone.utils.FileUtils
import org.linphone.utils.TimestampUtils
import androidx.core.net.toUri
import androidx.core.graphics.createBitmap
import kotlinx.coroutines.withContext
import java.io.File
class FileModel
@AnyThread
@ -97,6 +102,9 @@ class FileModel
if (!isWaitingToBeDownloaded) {
val extension = FileUtils.getExtensionFromFileName(path)
isPdf = extension == "pdf"
if (isPdf) {
loadPdfPreview()
}
val mime = FileUtils.getMimeTypeFromExtension(extension)
mimeTypeString = mime
@ -168,21 +176,65 @@ class FileModel
}
@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)
private fun loadPdfPreview() {
scope.launch {
withContext(Dispatchers.IO) {
try {
val pdfFileDescriptor = ParcelFileDescriptor.open(
File(path),
ParcelFileDescriptor.MODE_READ_ONLY
)
if (pdfFileDescriptor == null) {
Log.e("$TAG Failed to get a file descriptor for PDF at [$path]")
return@withContext
}
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: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
style="@style/default_text_style_600"
android:id="@+id/audio_duration"