Added save on disk for plain text file, reworked share button to use native sharing

This commit is contained in:
Sylvain Berfini 2024-03-14 10:34:10 +01:00
parent 70e25b7792
commit c35025aedb
5 changed files with 168 additions and 77 deletions

View file

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

View file

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

View file

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

View file

@ -30,6 +30,8 @@ class FileViewModel @UiThread constructor() : ViewModel() {
val fileName = MutableLiveData<String>()
val mimeType = MutableLiveData<String>()
val fullScreenMode = MutableLiveData<Boolean>()
val isPdf = MutableLiveData<Boolean>()
@ -38,14 +40,16 @@ class FileViewModel @UiThread constructor() : ViewModel() {
val pdfPages = MutableLiveData<String>()
val isAudio = MutableLiveData<Boolean>()
val isText = MutableLiveData<Boolean>()
val text = MutableLiveData<String>()
val fileReadyEvent = MutableLiveData<Event<Boolean>>()
val exportPlainTextFileEvent: MutableLiveData<Event<String>> by lazy {
MutableLiveData<Event<String>>()
}
val pdfRendererReadyEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
@ -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

View file

@ -24,6 +24,8 @@ class MediaViewModel @UiThread constructor() : ViewModel() {
val isVideoPlaying = MutableLiveData<Boolean>()
val isAudio = MutableLiveData<Boolean>()
val toggleVideoPlayPauseEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
@ -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 -> { }
}
}