Added plain text file viewer

This commit is contained in:
Sylvain Berfini 2023-12-13 15:17:30 +01:00
parent de2f247c5f
commit 5d3d8eeedc
12 changed files with 122 additions and 35 deletions

View file

@ -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")

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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<Boolean>()
val isText = MutableLiveData<Boolean>()
val text = MutableLiveData<String>()
val fileReadyEvent = MutableLiveData<Event<Boolean>>()
val pdfRendererReadyEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
@ -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 {

View file

@ -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 {

View file

@ -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 &amp;&amp; model.firstImagePath.length() >= 0 ? View.VISIBLE : View.GONE, default=gone}"
android:visibility="@{model.filesList.size() == 1 &amp;&amp; model.firstImagePath.length() > 0 ? View.VISIBLE : View.GONE, default=gone}"
coilBubble="@{model.firstImagePath}"/>
<ViewStub

View file

@ -153,7 +153,7 @@
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}"
android:visibility="@{model.filesList.size() == 1 &amp;&amp; model.firstImagePath.length() > 0 ? View.VISIBLE : View.GONE, default=gone}"
coilBubble="@{model.firstImagePath}"/>
<ViewStub

View file

@ -82,6 +82,27 @@
coilFile="@{viewModel.path}"
android:visibility="@{viewModel.isImage ? View.VISIBLE : View.GONE, default=gone}" />
<ScrollView
android:id="@+id/text"
android:background="?attr/color_main2_000"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="@{viewModel.isText ? View.VISIBLE : View.GONE, default=gone}" >
<TextView
style="@style/default_text_style"
android:onClick="@{() -> viewModel.toggleFullScreen()}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:singleLine="false"
android:textIsSelectable="true"
android:textColor="?attr/color_main2_900"
android:text="@{viewModel.text}"/>
</ScrollView>
<ImageView
android:id="@+id/back"
android:onClick="@{backClickListener}"
@ -159,4 +180,5 @@
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -28,6 +28,7 @@
<item name="color_main2_600">@color/gray_main2_400</item>
<item name="color_main2_700">@color/gray_main2_300</item>
<item name="color_main2_800">@color/gray_main2_200</item>
<item name="color_main2_900">@color/white</item>
<item name="color_grey_100">@color/gray_900</item>
<item name="color_grey_200">@color/gray_800</item>

View file

@ -25,6 +25,7 @@
<attr name="color_main2_600" format="color"/>
<attr name="color_main2_700" format="color"/>
<attr name="color_main2_800" format="color"/>
<attr name="color_main2_900" format="color"/>
<attr name="color_grey_100" format="color"/>
<attr name="color_grey_200" format="color"/>

View file

@ -29,6 +29,7 @@
<item name="color_main2_600">@color/gray_main2_600</item>
<item name="color_main2_700">@color/gray_main2_700</item>
<item name="color_main2_800">@color/gray_main2_800</item>
<item name="color_main2_900">@color/black</item>
<item name="color_grey_100">@color/gray_100</item>
<item name="color_grey_200">@color/gray_200</item>