From 5d3d8eeedc8d231eefa335095a9efd5f510f2570 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 13 Dec 2023 15:17:30 +0100 Subject: [PATCH] Added plain text file viewer --- .../notifications/NotificationsManager.kt | 3 +- .../fragment/ConversationsListFragment.kt | 5 +- .../linphone/ui/main/chat/model/FileModel.kt | 3 +- .../viewer/fragment/FileViewerFragment.kt | 16 ++++ .../ui/main/viewer/viewmodel/FileViewModel.kt | 85 ++++++++++++++----- .../main/java/org/linphone/utils/FileUtils.kt | 16 ++-- .../main/res/layout/chat_bubble_incoming.xml | 2 +- .../main/res/layout/chat_bubble_outgoing.xml | 2 +- .../main/res/layout/file_viewer_fragment.xml | 22 +++++ app/src/main/res/values-night/themes.xml | 1 + app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/themes.xml | 1 + 12 files changed, 122 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index fa79a4b9d..4a415e5be 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -30,7 +30,6 @@ import android.content.Intent import android.content.pm.PackageManager import android.graphics.Bitmap import android.net.Uri -import android.webkit.MimeTypeMap import androidx.annotation.AnyThread import androidx.annotation.MainThread import androidx.annotation.WorkerThread @@ -749,7 +748,7 @@ class NotificationsManager @MainThread constructor(private val context: Context) val filePath = contentUri.toString() val extension = FileUtils.getExtensionFromFileName(filePath) if (extension.isNotEmpty()) { - val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) + val mime = FileUtils.getMimeTypeFromExtension(extension) notifiableMessage.filePath = contentUri notifiableMessage.fileMime = mime Log.i("$TAG Added file $contentUri with MIME $mime to notification") diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationsListFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationsListFragment.kt index f68c00769..f4a8fdcf2 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationsListFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationsListFragment.kt @@ -28,7 +28,6 @@ import android.view.View import android.view.ViewGroup import android.view.animation.Animation import android.view.animation.AnimationUtils -import android.webkit.MimeTypeMap import androidx.annotation.UiThread import androidx.core.view.doOnPreDraw import androidx.lifecycle.ViewModelProvider @@ -244,9 +243,9 @@ class ConversationsListFragment : AbstractTopBarFragment() { if (findNavController().currentDestination?.id == R.id.conversationsListFragment) { Log.i("$TAG Navigating to file viewer fragment with path [$path]") val extension = FileUtils.getExtensionFromFileName(path) - val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) + val mime = FileUtils.getMimeTypeFromExtension(extension) when (FileUtils.getMimeType(mime)) { - FileUtils.MimeType.Image, FileUtils.MimeType.Video, FileUtils.MimeType.Pdf -> { + FileUtils.MimeType.Image, FileUtils.MimeType.Video, FileUtils.MimeType.Pdf, FileUtils.MimeType.PlainText -> { val action = FileViewerFragmentDirections.actionGlobalFileViewerFragment(path) findNavController().navigate(action) 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 37f432c5d..7d0f138cd 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 @@ -1,6 +1,5 @@ package org.linphone.ui.main.chat.model -import android.webkit.MimeTypeMap import androidx.annotation.AnyThread import androidx.annotation.UiThread import androidx.lifecycle.MutableLiveData @@ -41,7 +40,7 @@ class FileModel @AnyThread constructor( val extension = FileUtils.getExtensionFromFileName(file) isPdf = extension == "pdf" - val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) + val mime = FileUtils.getMimeTypeFromExtension(extension) mimeType = FileUtils.getMimeType(mime) isImage = mimeType == FileUtils.MimeType.Image isVideoPreview = mimeType == FileUtils.MimeType.Video diff --git a/app/src/main/java/org/linphone/ui/main/viewer/fragment/FileViewerFragment.kt b/app/src/main/java/org/linphone/ui/main/viewer/fragment/FileViewerFragment.kt index 7136abef6..9f1499065 100644 --- a/app/src/main/java/org/linphone/ui/main/viewer/fragment/FileViewerFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/viewer/fragment/FileViewerFragment.kt @@ -60,6 +60,7 @@ class FileViewerFragment : GenericFragment() { } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + postponeEnterTransition() super.onViewCreated(view, savedInstanceState) viewModel = ViewModelProvider(this)[FileViewModel::class.java] @@ -75,6 +76,21 @@ class FileViewerFragment : GenericFragment() { goBack() } + viewModel.fileReadyEvent.observe(viewLifecycleOwner) { + it.consume { done -> + if (done) { + (view.parent as? ViewGroup)?.doOnPreDraw { + startPostponedEnterTransition() + } + } else { + (view.parent as? ViewGroup)?.doOnPreDraw { + Log.e("$TAG Failed to open file, going back") + goBack() + } + } + } + } + binding.setShareClickListener { lifecycleScope.launch { val filePath = FileUtils.getProperFilePath(path) diff --git a/app/src/main/java/org/linphone/ui/main/viewer/viewmodel/FileViewModel.kt b/app/src/main/java/org/linphone/ui/main/viewer/viewmodel/FileViewModel.kt index 5b38237de..5897570cf 100644 --- a/app/src/main/java/org/linphone/ui/main/viewer/viewmodel/FileViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/viewer/viewmodel/FileViewModel.kt @@ -8,14 +8,17 @@ import android.net.Uri import android.os.Environment import android.os.ParcelFileDescriptor import android.provider.MediaStore -import android.webkit.MimeTypeMap +import android.text.PrecomputedText import android.widget.ImageView import androidx.annotation.UiThread import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import java.io.BufferedReader import java.io.File +import java.io.FileReader import java.lang.IllegalStateException +import java.lang.StringBuilder import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -50,6 +53,12 @@ class FileViewModel @UiThread constructor() : ViewModel() { val isVideoPlaying = MutableLiveData() + val isText = MutableLiveData() + + val text = MutableLiveData() + + val fileReadyEvent = MutableLiveData>() + val pdfRendererReadyEvent: MutableLiveData> by lazy { MutableLiveData>() } @@ -100,45 +109,34 @@ class FileViewModel @UiThread constructor() : ViewModel() { fileName.value = name val extension = FileUtils.getExtensionFromFileName(name) - val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) + val mime = FileUtils.getMimeTypeFromExtension(extension) when (FileUtils.getMimeType(mime)) { FileUtils.MimeType.Pdf -> { Log.i("$TAG File [$file] seems to be a PDF") - isPdf.value = true - - viewModelScope.launch { - withContext(Dispatchers.IO) { - val input = ParcelFileDescriptor.open( - File(file), - ParcelFileDescriptor.MODE_READ_ONLY - ) - pdfRenderer = PdfRenderer(input) - val count = pdfRenderer.pageCount - Log.i("$TAG $count pages in file $file") - pdfPages.postValue(count.toString()) - pdfCurrentPage.postValue("1") - pdfRendererReadyEvent.postValue(Event(true)) - } - } + loadPdf() } FileUtils.MimeType.Image -> { Log.i("$TAG File [$file] seems to be an image") isImage.value = true path.value = file + fileReadyEvent.value = Event(true) } FileUtils.MimeType.Video -> { Log.i("$TAG File [$file] seems to be a video") isVideo.value = true isVideoPlaying.value = false + fileReadyEvent.value = Event(true) } FileUtils.MimeType.Audio -> { // TODO: handle audio files + fileReadyEvent.value = Event(true) } FileUtils.MimeType.PlainText -> { - // TODO: handle plain text files + Log.i("$TAG File [$file] seems to be plain text") + loadPlainText() } else -> { - // TODO: open native app for unsupported files + fileReadyEvent.value = Event(false) } } } @@ -261,6 +259,51 @@ class FileViewModel @UiThread constructor() : ViewModel() { } } + private fun loadPdf() { + isPdf.value = true + + viewModelScope.launch { + withContext(Dispatchers.IO) { + val input = ParcelFileDescriptor.open( + File(filePath), + ParcelFileDescriptor.MODE_READ_ONLY + ) + pdfRenderer = PdfRenderer(input) + val count = pdfRenderer.pageCount + Log.i("$TAG $count pages in file $filePath") + pdfPages.postValue(count.toString()) + pdfCurrentPage.postValue("1") + pdfRendererReadyEvent.postValue(Event(true)) + fileReadyEvent.postValue(Event(true)) + } + } + } + + private fun loadPlainText() { + isText.value = true + + viewModelScope.launch { + withContext(Dispatchers.IO) { + try { + val br = BufferedReader(FileReader(filePath)) + var line: String? + val textBuilder = StringBuilder() + while (br.readLine().also { line = it } != null) { + textBuilder.append(line) + textBuilder.append('\n') + } + br.close() + text.postValue(textBuilder.toString()) + Log.i("$TAG Finished reading file [$filePath]") + fileReadyEvent.postValue(Event(true)) + // TODO FIXME : improve performances ! + } catch (e: Exception) { + Log.e("$TAG Exception trying to read file [$filePath] as text: $e") + } + } + } + } + @UiThread private suspend fun addContentToMediaStore( path: String @@ -285,7 +328,7 @@ class FileViewModel @UiThread constructor() : ViewModel() { val relativePath = "$directory/$appName" val fileName = FileUtils.getNameFromFilePath(path) val extension = FileUtils.getExtensionFromFileName(fileName) - val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) + val mime = FileUtils.getMimeTypeFromExtension(extension) val context = coreContext.context val mediaStoreFilePath = when { diff --git a/app/src/main/java/org/linphone/utils/FileUtils.kt b/app/src/main/java/org/linphone/utils/FileUtils.kt index d56bca118..a71665c95 100644 --- a/app/src/main/java/org/linphone/utils/FileUtils.kt +++ b/app/src/main/java/org/linphone/utils/FileUtils.kt @@ -65,21 +65,21 @@ class FileUtils { @AnyThread fun isExtensionImage(path: String): Boolean { val extension = getExtensionFromFileName(path) - val type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) + val type = getMimeTypeFromExtension(extension) return getMimeType(type) == MimeType.Image } @AnyThread fun isExtensionVideo(path: String): Boolean { val extension = getExtensionFromFileName(path) - val type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) + val type = getMimeTypeFromExtension(extension) return getMimeType(type) == MimeType.Video } @AnyThread fun isExtensionAudio(path: String): Boolean { val extension = getExtensionFromFileName(path) - val type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) + val type = getMimeTypeFromExtension(extension) return getMimeType(type) == MimeType.Audio } @@ -96,12 +96,18 @@ class FileUtils { return extension.lowercase(Locale.getDefault()) } + @AnyThread + fun getMimeTypeFromExtension(extension: String): String { + return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) ?: "file/$extension" + } + @AnyThread fun getMimeType(type: String?): MimeType { if (type.isNullOrEmpty()) return MimeType.Unknown return when { type.startsWith("image/") -> MimeType.Image - type.startsWith("text/plain") -> MimeType.PlainText + type.startsWith("text/") -> MimeType.PlainText + type.endsWith("/log") -> MimeType.PlainText type.startsWith("video/") -> MimeType.Video type.startsWith("audio/") -> MimeType.Audio type.startsWith("application/pdf") -> MimeType.Pdf @@ -220,7 +226,7 @@ class FileUtils { } val extension = getExtensionFromFileName(name) - val type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) + val type = getMimeTypeFromExtension(extension) val isImage = getMimeType(type) == MimeType.Image try { diff --git a/app/src/main/res/layout/chat_bubble_incoming.xml b/app/src/main/res/layout/chat_bubble_incoming.xml index e87a44fa8..8ad1086f7 100644 --- a/app/src/main/res/layout/chat_bubble_incoming.xml +++ b/app/src/main/res/layout/chat_bubble_incoming.xml @@ -190,7 +190,7 @@ android:layout_height="@dimen/chat_bubble_big_image_max_size" android:adjustViewBounds="true" android:scaleType="fitCenter" - android:visibility="@{model.filesList.size() == 1 && model.firstImagePath.length() >= 0 ? View.VISIBLE : View.GONE, default=gone}" + android:visibility="@{model.filesList.size() == 1 && model.firstImagePath.length() > 0 ? View.VISIBLE : View.GONE, default=gone}" coilBubble="@{model.firstImagePath}"/> + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index 50ba1ab74..157b54375 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -28,6 +28,7 @@ @color/gray_main2_400 @color/gray_main2_300 @color/gray_main2_200 + @color/white @color/gray_900 @color/gray_800 diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 213b08bbb..27ab5f9c5 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -25,6 +25,7 @@ + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index bbe4286f9..9a62d92c8 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -29,6 +29,7 @@ @color/gray_main2_600 @color/gray_main2_700 @color/gray_main2_800 + @color/black @color/gray_100 @color/gray_200