From c35025aedb0a3411155eda56a25c5e7e2e0c2249 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 14 Mar 2024 10:34:10 +0100 Subject: [PATCH] Added save on disk for plain text file, reworked share button to use native sharing --- .../linphone/ui/main/chat/model/FileModel.kt | 5 + .../fragment/FileViewerFragment.kt | 68 +++++++++---- .../fragment/MediaListViewerFragment.kt | 66 +++++++++---- .../viewmodel/FileViewModel.kt | 99 +++++++++++-------- .../viewmodel/MediaViewModel.kt | 7 ++ 5 files changed, 168 insertions(+), 77 deletions(-) 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 e2fb7e064..694711961 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 @@ -33,6 +33,8 @@ class FileModel @AnyThread constructor( val mimeType: FileUtils.MimeType + val mimeTypeString: String + val isMedia: Boolean val isImage: Boolean @@ -56,6 +58,8 @@ class FileModel @AnyThread constructor( isPdf = extension == "pdf" val mime = FileUtils.getMimeTypeFromExtension(extension) + mimeTypeString = mime + mimeType = FileUtils.getMimeType(mime) isImage = mimeType == FileUtils.MimeType.Image isVideoPreview = mimeType == FileUtils.MimeType.Video @@ -68,6 +72,7 @@ class FileModel @AnyThread constructor( ) } else { mimeType = FileUtils.MimeType.Unknown + mimeTypeString = "application/octet-stream" isPdf = false isImage = false isVideoPreview = false diff --git a/app/src/main/java/org/linphone/ui/main/file_media_viewer/fragment/FileViewerFragment.kt b/app/src/main/java/org/linphone/ui/main/file_media_viewer/fragment/FileViewerFragment.kt index c49d7d470..f4bac6b5a 100644 --- a/app/src/main/java/org/linphone/ui/main/file_media_viewer/fragment/FileViewerFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/file_media_viewer/fragment/FileViewerFragment.kt @@ -9,12 +9,14 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.annotation.UiThread +import androidx.core.content.FileProvider import androidx.core.view.doOnPreDraw import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback +import java.io.File import kotlinx.coroutines.launch import org.linphone.R import org.linphone.core.tools.Log @@ -23,7 +25,6 @@ import org.linphone.ui.main.MainActivity import org.linphone.ui.main.file_media_viewer.adapter.PdfPagesListAdapter import org.linphone.ui.main.file_media_viewer.viewmodel.FileViewModel import org.linphone.ui.main.fragment.GenericFragment -import org.linphone.utils.Event import org.linphone.utils.FileUtils @UiThread @@ -31,7 +32,7 @@ class FileViewerFragment : GenericFragment() { companion object { private const val TAG = "[File Viewer Fragment]" - private const val EXPORT_PDF = 10 + private const val EXPORT_FILE_AS_DOCUMENT = 10 } private lateinit var binding: FileViewerFragmentBinding @@ -101,17 +102,7 @@ class FileViewerFragment : GenericFragment() { } binding.setShareClickListener { - lifecycleScope.launch { - val filePath = FileUtils.getProperFilePath(path) - val copy = FileUtils.getFilePath(requireContext(), Uri.parse(filePath), false) - if (!copy.isNullOrEmpty()) { - sharedViewModel.filesToShareFromIntent.value = arrayListOf(copy) - Log.i("$TAG Sharing file [$copy], going back to conversations list") - sharedViewModel.closeSlidingPaneEvent.value = Event(true) - } else { - Log.e("$TAG Failed to copy file [$filePath] to share!") - } - } + shareFile() } viewModel.pdfRendererReadyEvent.observe(viewLifecycleOwner) { @@ -126,6 +117,17 @@ class FileViewerFragment : GenericFragment() { } } + viewModel.exportPlainTextFileEvent.observe(viewLifecycleOwner) { + it.consume { name -> + val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "text/plain" + putExtra(Intent.EXTRA_TITLE, name) + } + startActivityForResult(intent, EXPORT_FILE_AS_DOCUMENT) + } + } + viewModel.exportPdfEvent.observe(viewLifecycleOwner) { it.consume { name -> val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { @@ -133,7 +135,7 @@ class FileViewerFragment : GenericFragment() { type = "application/pdf" putExtra(Intent.EXTRA_TITLE, name) } - startActivityForResult(intent, EXPORT_PDF) + startActivityForResult(intent, EXPORT_FILE_AS_DOCUMENT) } } @@ -178,10 +180,10 @@ class FileViewerFragment : GenericFragment() { @Deprecated("Deprecated in Java") override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (requestCode == EXPORT_PDF && resultCode == Activity.RESULT_OK) { + if (requestCode == EXPORT_FILE_AS_DOCUMENT && resultCode == Activity.RESULT_OK) { data?.data?.also { documentUri -> - Log.i("$TAG Exported PDF should be stored in URI [$documentUri]") - viewModel.copyPdfToUri(documentUri) + Log.i("$TAG Exported file should be stored in URI [$documentUri]") + viewModel.copyFileToUri(documentUri) } } super.onActivityResult(requestCode, resultCode, data) @@ -196,4 +198,36 @@ class FileViewerFragment : GenericFragment() { "$TAG Setting screen size ${viewModel.screenWidth}/${viewModel.screenHeight} for PDF renderer" ) } + + private fun shareFile() { + lifecycleScope.launch { + val filePath = FileUtils.getProperFilePath(viewModel.getFilePath()) + val copy = FileUtils.getFilePath( + requireContext(), + Uri.parse(filePath), + overrideExisting = true, + copyToCache = true + ) + if (!copy.isNullOrEmpty()) { + val publicUri = FileProvider.getUriForFile( + requireContext(), + requireContext().getString(R.string.file_provider), + File(copy) + ) + Log.i("$TAG Public URI for file is [$publicUri], starting intent chooser") + + val sendIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_STREAM, publicUri) + putExtra(Intent.EXTRA_SUBJECT, viewModel.fileName.value.orEmpty()) + type = viewModel.mimeType.value.orEmpty() + } + + val shareIntent = Intent.createChooser(sendIntent, null) + startActivity(shareIntent) + } else { + Log.e("$TAG Failed to copy file [$filePath] to share!") + } + } + } } diff --git a/app/src/main/java/org/linphone/ui/main/file_media_viewer/fragment/MediaListViewerFragment.kt b/app/src/main/java/org/linphone/ui/main/file_media_viewer/fragment/MediaListViewerFragment.kt index 09add8ec0..4910c802f 100644 --- a/app/src/main/java/org/linphone/ui/main/file_media_viewer/fragment/MediaListViewerFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/file_media_viewer/fragment/MediaListViewerFragment.kt @@ -19,16 +19,19 @@ */ package org.linphone.ui.main.file_media_viewer.fragment +import android.content.Intent import android.net.Uri import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.content.FileProvider import androidx.core.view.doOnPreDraw import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.navArgs import androidx.viewpager2.widget.ViewPager2 +import java.io.File import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -40,7 +43,6 @@ import org.linphone.ui.main.chat.viewmodel.ConversationMediaListViewModel import org.linphone.ui.main.file_media_viewer.adapter.MediaListAdapter import org.linphone.ui.main.fragment.GenericFragment import org.linphone.utils.AppUtils -import org.linphone.utils.Event import org.linphone.utils.FileUtils class MediaListViewerFragment : GenericFragment() { @@ -135,26 +137,7 @@ class MediaListViewerFragment : GenericFragment() { } binding.setShareClickListener { - val list = viewModel.mediaList.value.orEmpty() - val currentItem = binding.mediaViewPager.currentItem - val model = if (currentItem >= 0 && currentItem < list.size) list[currentItem] else null - if (model != null) { - lifecycleScope.launch { - val filePath = FileUtils.getProperFilePath(model.file) - val copy = FileUtils.getFilePath(requireContext(), Uri.parse(filePath), false) - if (!copy.isNullOrEmpty()) { - sharedViewModel.filesToShareFromIntent.value = arrayListOf(copy) - Log.i("$TAG Sharing file [$copy], going back to conversations list") - sharedViewModel.closeSlidingPaneEvent.value = Event(true) - } else { - Log.e("$TAG Failed to copy file [$filePath] to share!") - } - } - } else { - Log.e( - "$TAG Failed to get FileModel at index [$currentItem], only [${list.size}] items in list" - ) - } + shareFile() } binding.setExportClickListener { @@ -218,4 +201,45 @@ class MediaListViewerFragment : GenericFragment() { super.onDestroy() } + + private fun shareFile() { + val list = viewModel.mediaList.value.orEmpty() + val currentItem = binding.mediaViewPager.currentItem + val model = if (currentItem >= 0 && currentItem < list.size) list[currentItem] else null + if (model != null) { + lifecycleScope.launch { + val filePath = FileUtils.getProperFilePath(model.file) + val copy = FileUtils.getFilePath( + requireContext(), + Uri.parse(filePath), + overrideExisting = true, + copyToCache = true + ) + if (!copy.isNullOrEmpty()) { + val publicUri = FileProvider.getUriForFile( + requireContext(), + requireContext().getString(R.string.file_provider), + File(copy) + ) + Log.i("$TAG Public URI for file is [$publicUri], starting intent chooser") + + val sendIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_STREAM, publicUri) + putExtra(Intent.EXTRA_SUBJECT, model.fileName) + type = model.mimeTypeString + } + + val shareIntent = Intent.createChooser(sendIntent, null) + startActivity(shareIntent) + } else { + Log.e("$TAG Failed to copy file [$filePath] to share!") + } + } + } else { + Log.e( + "$TAG Failed to get FileModel at index [$currentItem], only [${list.size}] items in list" + ) + } + } } diff --git a/app/src/main/java/org/linphone/ui/main/file_media_viewer/viewmodel/FileViewModel.kt b/app/src/main/java/org/linphone/ui/main/file_media_viewer/viewmodel/FileViewModel.kt index a31849c3d..6ce392c39 100644 --- a/app/src/main/java/org/linphone/ui/main/file_media_viewer/viewmodel/FileViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/file_media_viewer/viewmodel/FileViewModel.kt @@ -30,6 +30,8 @@ class FileViewModel @UiThread constructor() : ViewModel() { val fileName = MutableLiveData() + val mimeType = MutableLiveData() + val fullScreenMode = MutableLiveData() val isPdf = MutableLiveData() @@ -38,14 +40,16 @@ class FileViewModel @UiThread constructor() : ViewModel() { val pdfPages = MutableLiveData() - val isAudio = MutableLiveData() - val isText = MutableLiveData() val text = MutableLiveData() val fileReadyEvent = MutableLiveData>() + val exportPlainTextFileEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + val pdfRendererReadyEvent: MutableLiveData> by lazy { MutableLiveData>() } @@ -87,31 +91,27 @@ class FileViewModel @UiThread constructor() : ViewModel() { fun loadFile(file: String, content: String? = null) { fullScreenMode.value = true - filePath = file val name = FileUtils.getNameFromFilePath(file) fileName.value = name if (!content.isNullOrEmpty()) { isText.value = true text.postValue(content) + mimeType.postValue("text/plain") Log.i("$TAG Using pre-loaded content as PlainText") fileReadyEvent.postValue(Event(true)) return } + filePath = file val extension = FileUtils.getExtensionFromFileName(name) val mime = FileUtils.getMimeTypeFromExtension(extension) + mimeType.postValue(mime) when (FileUtils.getMimeType(mime)) { FileUtils.MimeType.Pdf -> { Log.i("$TAG File [$file] seems to be a PDF") loadPdf() } - FileUtils.MimeType.Audio -> { - Log.i("$TAG File [$file] seems to be an audio") - // TODO: handle audio files - isAudio.value = true - fileReadyEvent.value = Event(true) - } FileUtils.MimeType.PlainText -> { Log.i("$TAG File [$file] seems to be plain text") loadPlainText() @@ -174,42 +174,36 @@ class FileViewModel @UiThread constructor() : ViewModel() { } @UiThread - fun exportToMediaStore() { + fun getFilePath(): String { if (::filePath.isInitialized) { - if (isPdf.value == true) { - Log.i("$TAG Exporting PDF as document") - exportPdfEvent.postValue(Event(fileName.value.orEmpty())) - } else { - viewModelScope.launch { - withContext(Dispatchers.IO) { - Log.i("$TAG Export file [$filePath] to Android's MediaStore") - val mediaStorePath = FileUtils.addContentToMediaStore(filePath) - if (mediaStorePath.isNotEmpty()) { - Log.i( - "$TAG File [$filePath] has been successfully exported to MediaStore" - ) - val message = AppUtils.getString( - R.string.toast_file_successfully_exported_to_media_store - ) - showGreenToastEvent.postValue(Event(Pair(message, R.drawable.check))) - } else { - Log.e("$TAG Failed to export file [$filePath] to MediaStore!") - val message = AppUtils.getString( - R.string.toast_export_file_to_media_store_error - ) - showRedToastEvent.postValue(Event(Pair(message, R.drawable.x))) - } - } - } - } + return filePath + } + + Log.i("$TAG File path wasn't initialized, storing memory content as file") + val name = fileName.value.orEmpty() + val file = FileUtils.getFileStorageCacheDir( + fileName = name, + overrideExisting = true + ) + savePlainTextFileToUri(file) + filePath = file.absolutePath + return filePath + } + + @UiThread + fun exportToMediaStore() { + if (isPdf.value == true) { + Log.i("$TAG Exporting PDF as document") + exportPdfEvent.postValue(Event(fileName.value.orEmpty())) } else { - Log.e("$TAG Filepath wasn't initialized!") + Log.i("$TAG Exporting plain text content as document") + exportPlainTextFileEvent.postValue(Event(fileName.value.orEmpty())) } } @UiThread - fun copyPdfToUri(dest: Uri) { - val source = Uri.parse(FileUtils.getProperFilePath(filePath)) + fun copyFileToUri(dest: Uri) { + val source = Uri.parse(FileUtils.getProperFilePath(getFilePath())) Log.i("$TAG Copying file URI [$source] to [$dest]") viewModelScope.launch { withContext(Dispatchers.IO) { @@ -233,6 +227,32 @@ class FileViewModel @UiThread constructor() : ViewModel() { } } + @UiThread + private fun savePlainTextFileToUri(dest: File) { + Log.i("$TAG Saving text to file [${dest.absolutePath}]") + viewModelScope.launch { + withContext(Dispatchers.IO) { + val result = FileUtils.dumpStringToFile(text.value.orEmpty(), dest) + if (result) { + Log.i( + "$TAG Text has been successfully exported to documents" + ) + val message = AppUtils.getString( + R.string.toast_file_successfully_exported_to_documents + ) + showGreenToastEvent.postValue(Event(Pair(message, R.drawable.check))) + } else { + Log.e("$TAG Failed to save text to documents!") + val message = AppUtils.getString( + R.string.toast_export_file_to_documents_error + ) + showRedToastEvent.postValue(Event(Pair(message, R.drawable.x))) + } + } + } + } + + @UiThread private fun loadPdf() { isPdf.value = true @@ -253,6 +273,7 @@ class FileViewModel @UiThread constructor() : ViewModel() { } } + @UiThread private fun loadPlainText() { isText.value = true diff --git a/app/src/main/java/org/linphone/ui/main/file_media_viewer/viewmodel/MediaViewModel.kt b/app/src/main/java/org/linphone/ui/main/file_media_viewer/viewmodel/MediaViewModel.kt index 6eeeacf41..5d5d0787d 100644 --- a/app/src/main/java/org/linphone/ui/main/file_media_viewer/viewmodel/MediaViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/file_media_viewer/viewmodel/MediaViewModel.kt @@ -24,6 +24,8 @@ class MediaViewModel @UiThread constructor() : ViewModel() { val isVideoPlaying = MutableLiveData() + val isAudio = MutableLiveData() + val toggleVideoPlayPauseEvent: MutableLiveData> by lazy { MutableLiveData>() } @@ -49,6 +51,11 @@ class MediaViewModel @UiThread constructor() : ViewModel() { isVideo.value = true isVideoPlaying.value = false } + FileUtils.MimeType.Audio -> { + Log.i("$TAG File [$file] seems to be an audio file") + isAudio.value = true + // TODO: handle audio files + } else -> { } } }