From e85c97837f95f587b12e09b175c413a7256daca8 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 16 Jan 2025 10:45:20 +0100 Subject: [PATCH] Let user try to open file as plain text if no app on the device is registered to handle MIME type (useful for logs file without extension) --- .../ui/fileviewer/FileViewerActivity.kt | 8 ++ .../ui/fileviewer/viewmodel/FileViewModel.kt | 12 ++ .../ConversationDocumentsListFragment.kt | 41 +++++-- .../chat/fragment/ConversationFragment.kt | 51 +++++++-- .../fragment/ConversationMediaListFragment.kt | 2 +- .../java/org/linphone/utils/DialogUtils.kt | 17 +++ .../main/java/org/linphone/utils/FileUtils.kt | 5 +- .../res/layout/dialog_open_plain_text.xml | 103 ++++++++++++++++++ app/src/main/res/values-fr/strings.xml | 4 + app/src/main/res/values/strings.xml | 4 + 10 files changed, 225 insertions(+), 22 deletions(-) create mode 100644 app/src/main/res/layout/dialog_open_plain_text.xml diff --git a/app/src/main/java/org/linphone/ui/fileviewer/FileViewerActivity.kt b/app/src/main/java/org/linphone/ui/fileviewer/FileViewerActivity.kt index 7b856d775..0e95f8a0b 100644 --- a/app/src/main/java/org/linphone/ui/fileviewer/FileViewerActivity.kt +++ b/app/src/main/java/org/linphone/ui/fileviewer/FileViewerActivity.kt @@ -84,6 +84,14 @@ class FileViewerActivity : GenericActivity() { finish() } + viewModel.showRedToastEvent.observe(this) { + it.consume { pair -> + val message = getString(pair.first) + val icon = pair.second + showRedToast(message, icon) + } + } + viewModel.fileReadyEvent.observe(this) { it.consume { done -> if (!done) { diff --git a/app/src/main/java/org/linphone/ui/fileviewer/viewmodel/FileViewModel.kt b/app/src/main/java/org/linphone/ui/fileviewer/viewmodel/FileViewModel.kt index 850679cb9..31e4e6266 100644 --- a/app/src/main/java/org/linphone/ui/fileviewer/viewmodel/FileViewModel.kt +++ b/app/src/main/java/org/linphone/ui/fileviewer/viewmodel/FileViewModel.kt @@ -138,6 +138,10 @@ class FileViewModel Log.d("$TAG File [$file] seems to be plain text") loadPlainText() } + FileUtils.MimeType.Unknown -> { + Log.w("$TAG Unknown MIME type for file at [$file], opening it as plain text") + loadPlainText() + } else -> { Log.e("$TAG Unexpected MIME type [$mimeType] for file at [$file] with extension [$extension]") fileReadyEvent.value = Event(false) @@ -333,6 +337,14 @@ class FileViewModel // TODO FIXME : improve performances ! } catch (e: Exception) { Log.e("$TAG Exception trying to read file [$filePath] as text: $e") + showRedToastEvent.postValue( + Event( + Pair( + R.string.conversation_file_cant_be_opened_error_toast, + R.drawable.warning_circle + ) + ) + ) } } } diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationDocumentsListFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationDocumentsListFragment.kt index bf6b4cc96..d01a7e959 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationDocumentsListFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationDocumentsListFragment.kt @@ -31,14 +31,14 @@ import androidx.lifecycle.ViewModelProvider import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.LinearLayoutManager -import org.linphone.R import org.linphone.core.tools.Log import org.linphone.databinding.ChatDocumentsFragmentBinding -import org.linphone.ui.GenericActivity import org.linphone.ui.main.chat.adapter.ConversationsFilesAdapter import org.linphone.ui.main.chat.model.FileModel import org.linphone.ui.main.chat.viewmodel.ConversationDocumentsListViewModel import org.linphone.ui.main.fragment.SlidingPaneChildFragment +import org.linphone.utils.ConfirmationDialogModel +import org.linphone.utils.DialogUtils import org.linphone.utils.Event import org.linphone.utils.FileUtils import org.linphone.utils.RecyclerViewHeaderDecoration @@ -149,10 +149,10 @@ class ConversationDocumentsListFragment : SlidingPaneChildFragment() { putBoolean("isEncrypted", fileModel.isEncrypted) putLong("timestamp", fileModel.fileCreationTimestamp) putString("originalPath", fileModel.originalPath) + putBoolean("isMedia", false) } when (FileUtils.getMimeType(mime)) { FileUtils.MimeType.Pdf, FileUtils.MimeType.PlainText -> { - bundle.putBoolean("isMedia", false) sharedViewModel.displayFileEvent.value = Event(bundle) } else -> { @@ -165,13 +165,38 @@ class ConversationDocumentsListFragment : SlidingPaneChildFragment() { requireContext().startActivity(intent) } catch (anfe: ActivityNotFoundException) { Log.e("$TAG Can't open file [$path] in third party app: $anfe") - val message = getString( - R.string.conversation_no_app_registered_to_handle_content_type_error_toast - ) - val icon = R.drawable.file - (requireActivity() as GenericActivity).showRedToast(message, icon) + showOpenAsPlainTextDialog(bundle) } } } } + + private fun showOpenAsPlainTextDialog(bundle: Bundle) { + val model = ConfirmationDialogModel() + val dialog = DialogUtils.getOpenAsPlainTextDialog( + requireActivity(), + model + ) + + model.dismissEvent.observe(viewLifecycleOwner) { + it.consume { + dialog.dismiss() + } + } + + model.cancelEvent.observe(viewLifecycleOwner) { + it.consume { + dialog.dismiss() + } + } + + model.confirmEvent.observe(viewLifecycleOwner) { + it.consume { + sharedViewModel.displayFileEvent.value = Event(bundle) + dialog.dismiss() + } + } + + dialog.show() + } } diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt index 4007cf6cc..dc477869f 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt @@ -69,7 +69,6 @@ import org.linphone.core.tools.Log import org.linphone.databinding.ChatConversationFragmentBinding import org.linphone.databinding.ChatConversationPopupMenuBinding import org.linphone.ui.GenericActivity -import org.linphone.ui.main.MainActivity import org.linphone.ui.main.chat.ConversationScrollListener import org.linphone.ui.main.chat.adapter.ConversationEventAdapter import org.linphone.ui.main.chat.adapter.MessageBottomSheetAdapter @@ -1093,7 +1092,11 @@ open class ConversationFragment : SlidingPaneChildFragment() { val extension = FileUtils.getExtensionFromFileName(path) val mime = FileUtils.getMimeTypeFromExtension(extension) val mimeType = FileUtils.getMimeType(mime) - Log.i("$TAG Extension for file [$path] is [$extension], associated MIME type is [$mimeType]") + if (mimeType == FileUtils.MimeType.Unknown && extension.contains("/")) { + Log.w("$TAG Slash character found in 'extension' [$extension] deduced from file path [$path]; MIME type will be Unknown") + } else { + Log.i("$TAG Extension for file [$path] is [$extension], associated MIME type is [$mimeType]") + } val bundle = Bundle() bundle.apply { @@ -1114,7 +1117,8 @@ open class ConversationFragment : SlidingPaneChildFragment() { sharedViewModel.displayFileEvent.value = Event(bundle) } else -> { - showOpenOrExportFileDialog(path, mime) + bundle.putBoolean("isMedia", false) + showOpenOrExportFileDialog(path, mime, bundle) } } } @@ -1413,7 +1417,7 @@ open class ConversationFragment : SlidingPaneChildFragment() { bottomSheetDialog = unsafeConversationDetailsBottomSheet } - private fun showOpenOrExportFileDialog(path: String, mime: String) { + private fun showOpenOrExportFileDialog(path: String, mime: String, bundle: Bundle) { val model = ConfirmationDialogModel() val dialog = DialogUtils.getOpenOrExportFileDialog( requireActivity(), @@ -1428,7 +1432,7 @@ open class ConversationFragment : SlidingPaneChildFragment() { model.cancelEvent.observe(viewLifecycleOwner) { it.consume { - openFileInAnotherApp(path, mime) + openFileInAnotherApp(path, mime, bundle) dialog.dismiss() } } @@ -1466,7 +1470,7 @@ open class ConversationFragment : SlidingPaneChildFragment() { dialog.show() } - private fun openFileInAnotherApp(path: String, mime: String) { + private fun openFileInAnotherApp(path: String, mime: String, bundle: Bundle) { val intent = Intent(Intent.ACTION_VIEW) val contentUri: Uri = FileUtils.getPublicFilePath(requireContext(), path) @@ -1477,14 +1481,39 @@ open class ConversationFragment : SlidingPaneChildFragment() { requireContext().startActivity(intent) } catch (anfe: ActivityNotFoundException) { Log.e("$TAG Can't open file [$path] in third party app: $anfe") - val message = getString( - R.string.conversation_no_app_registered_to_handle_content_type_error_toast - ) - val icon = R.drawable.file - (requireActivity() as MainActivity).showRedToast(message, icon) + showOpenAsPlainTextDialog(bundle) } } + private fun showOpenAsPlainTextDialog(bundle: Bundle) { + val model = ConfirmationDialogModel() + val dialog = DialogUtils.getOpenAsPlainTextDialog( + requireActivity(), + model + ) + + model.dismissEvent.observe(viewLifecycleOwner) { + it.consume { + dialog.dismiss() + } + } + + model.cancelEvent.observe(viewLifecycleOwner) { + it.consume { + dialog.dismiss() + } + } + + model.confirmEvent.observe(viewLifecycleOwner) { + it.consume { + sharedViewModel.displayFileEvent.value = Event(bundle) + dialog.dismiss() + } + } + + dialog.show() + } + private fun exportFile(path: String, mime: String) { filePathToExport = path diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationMediaListFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationMediaListFragment.kt index 4ae642f08..e8d0d1f68 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationMediaListFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationMediaListFragment.kt @@ -178,10 +178,10 @@ class ConversationMediaListFragment : SlidingPaneChildFragment() { putBoolean("isEncrypted", fileModel.isEncrypted) putLong("timestamp", fileModel.fileCreationTimestamp) putString("originalPath", fileModel.originalPath) + putBoolean("isMedia", true) } when (FileUtils.getMimeType(mime)) { FileUtils.MimeType.Image, FileUtils.MimeType.Video, FileUtils.MimeType.Audio -> { - bundle.putBoolean("isMedia", true) sharedViewModel.displayFileEvent.value = Event(bundle) } else -> { diff --git a/app/src/main/java/org/linphone/utils/DialogUtils.kt b/app/src/main/java/org/linphone/utils/DialogUtils.kt index 7e9a751d3..4dc0843cb 100644 --- a/app/src/main/java/org/linphone/utils/DialogUtils.kt +++ b/app/src/main/java/org/linphone/utils/DialogUtils.kt @@ -47,6 +47,7 @@ import org.linphone.databinding.DialogKickFromConferenceBinding import org.linphone.databinding.DialogManageAccountInternationalPrefixHelpBinding import org.linphone.databinding.DialogMergeCallsIntoConferenceBinding import org.linphone.databinding.DialogOpenExportFileBinding +import org.linphone.databinding.DialogOpenPlainTextBinding import org.linphone.databinding.DialogPickNumberOrAddressBinding import org.linphone.databinding.DialogRemoveAccountBinding import org.linphone.databinding.DialogRemoveAllCallLogsBinding @@ -343,6 +344,22 @@ class DialogUtils { return getDialog(context, binding) } + @UiThread + fun getOpenAsPlainTextDialog( + context: Context, + viewModel: ConfirmationDialogModel + ): Dialog { + val binding: DialogOpenPlainTextBinding = DataBindingUtil.inflate( + LayoutInflater.from(context), + R.layout.dialog_open_plain_text, + null, + false + ) + binding.viewModel = viewModel + + return getDialog(context, binding) + } + @UiThread fun getUpdateAvailableDialog( context: Context, diff --git a/app/src/main/java/org/linphone/utils/FileUtils.kt b/app/src/main/java/org/linphone/utils/FileUtils.kt index 6039aab8f..79c9796ea 100644 --- a/app/src/main/java/org/linphone/utils/FileUtils.kt +++ b/app/src/main/java/org/linphone/utils/FileUtils.kt @@ -115,8 +115,9 @@ class FileUtils { type.endsWith("/log") -> MimeType.PlainText type.startsWith("video/") -> MimeType.Video type.startsWith("audio/") -> MimeType.Audio - type.startsWith("application/pdf") -> MimeType.Pdf - type.startsWith("application/json") -> MimeType.PlainText + type == "application/pdf" -> MimeType.Pdf + type == "application/json" -> MimeType.PlainText + type == "application/xml" -> MimeType.PlainText else -> MimeType.Unknown } Log.d("$TAG MIME type for [$type] is [$mime]") diff --git a/app/src/main/res/layout/dialog_open_plain_text.xml b/app/src/main/res/layout/dialog_open_plain_text.xml new file mode 100644 index 000000000..51d32173d --- /dev/null +++ b/app/src/main/res/layout/dialog_open_plain_text.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 2b9baa643..4d08bc087 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -463,6 +463,9 @@ &appName; ne peut ouvrir ce fichier.\n\nVoulez-vous l\'ouvrir dans une autre app (si possible), ou le sauvegarder sur votre appareil ? Ouvrir le fichier Sauvegarder le fichier + Ouvrir comme texte brut ? + Aucune application trouvée pour lire ce fichier.\n\nVoulez-vous essayer de l\'ouvrir en tant que texte brut ? + Ouvrir comme texte brut Impossible de lire le message vocal ! Message supprimé Échec de création de la conversation ! @@ -483,6 +486,7 @@ Prendre une photo Ouvrir la gallerie Choisir un fichier + Impossible d\'ouvrir le fichier! Participants (%s) Ajouter des participants diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 264693b85..95a788122 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -503,6 +503,9 @@ &appName; can\'t open this file.\n\nDo you want to open it in another app (if possible), or export it on your device? Open file Export file + Open as plain text? + No app found to open this kind of file.\n\nWould you like to try opening it as plain text? + Open as plain text Voice recording cannot be played! Message has been deleted Failed to create conversation! @@ -523,6 +526,7 @@ Take picture Open gallery Pick file + File can\'t be opened! Group members (%s) Add participants