mirror of
https://gitlab.linphone.org/BC/public/linphone-android.git
synced 2026-01-17 11:28:06 +00:00
Added dialog asking user whether to open or export file that app can't display
This commit is contained in:
parent
405596d291
commit
d062910133
7 changed files with 307 additions and 52 deletions
|
|
@ -20,6 +20,7 @@
|
|||
package org.linphone.ui.main.chat.fragment
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.app.Dialog
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.ClipData
|
||||
|
|
@ -87,6 +88,8 @@ import org.linphone.ui.main.chat.viewmodel.ConversationViewModel
|
|||
import org.linphone.ui.main.chat.viewmodel.ConversationViewModel.Companion.SCROLLING_POSITION_NOT_SET
|
||||
import org.linphone.ui.main.chat.viewmodel.SendMessageInConversationViewModel
|
||||
import org.linphone.ui.main.fragment.SlidingPaneChildFragment
|
||||
import org.linphone.ui.main.history.model.ConfirmationDialogModel
|
||||
import org.linphone.utils.DialogUtils
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.FileUtils
|
||||
import org.linphone.utils.RecyclerViewHeaderDecoration
|
||||
|
|
@ -102,6 +105,8 @@ import org.linphone.utils.showKeyboard
|
|||
class ConversationFragment : SlidingPaneChildFragment() {
|
||||
companion object {
|
||||
private const val TAG = "[Conversation Fragment]"
|
||||
|
||||
private const val EXPORT_FILE_AS_DOCUMENT = 10
|
||||
}
|
||||
|
||||
private lateinit var binding: ChatConversationFragmentBinding
|
||||
|
|
@ -120,6 +125,8 @@ class ConversationFragment : SlidingPaneChildFragment() {
|
|||
|
||||
private var bottomSheetDialog: BottomSheetDialogFragment? = null
|
||||
|
||||
private var filePathToExport: String? = null
|
||||
|
||||
private val pickMedia = registerForActivityResult(
|
||||
ActivityResultContracts.PickMultipleVisualMedia()
|
||||
) { list ->
|
||||
|
|
@ -655,6 +662,14 @@ class ConversationFragment : SlidingPaneChildFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
viewModel.showGreenToastEvent.observe(viewLifecycleOwner) {
|
||||
it.consume { pair ->
|
||||
val message = pair.first
|
||||
val icon = pair.second
|
||||
(requireActivity() as GenericActivity).showGreenToast(message, icon)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.showRedToastEvent.observe(viewLifecycleOwner) {
|
||||
it.consume { pair ->
|
||||
val message = pair.first
|
||||
|
|
@ -832,6 +847,29 @@ class ConversationFragment : SlidingPaneChildFragment() {
|
|||
currentChatMessageModelForBottomSheet = null
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode == EXPORT_FILE_AS_DOCUMENT) {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
val filePath = filePathToExport
|
||||
if (filePath != null) {
|
||||
data?.data?.also { documentUri ->
|
||||
Log.i(
|
||||
"$TAG Exported file [$filePath] should be stored in URI [$documentUri]"
|
||||
)
|
||||
viewModel.copyFileToUri(filePath, documentUri)
|
||||
filePathToExport = null
|
||||
}
|
||||
} else {
|
||||
Log.e("$TAG No file path waiting to be exported!")
|
||||
}
|
||||
} else {
|
||||
Log.w("$TAG Export file activity result is [$resultCode], aborting")
|
||||
}
|
||||
}
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
private fun scrollToFirstUnreadMessageOrBottom() {
|
||||
if (adapter.itemCount == 0) {
|
||||
Log.w("$TAG No item in adapter yet, do not scroll")
|
||||
|
|
@ -893,21 +931,7 @@ class ConversationFragment : SlidingPaneChildFragment() {
|
|||
sharedViewModel.displayFileEvent.value = Event(bundle)
|
||||
}
|
||||
else -> {
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
val contentUri: Uri =
|
||||
FileUtils.getPublicFilePath(requireContext(), path)
|
||||
intent.setDataAndType(contentUri, mime)
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
try {
|
||||
requireContext().startActivity(intent)
|
||||
} catch (anfe: ActivityNotFoundException) {
|
||||
Log.e("$TAG Can't open file [$path] in third party app: $anfe")
|
||||
val message = getString(
|
||||
R.string.toast_no_app_registered_to_handle_content_type_error
|
||||
)
|
||||
val icon = R.drawable.file
|
||||
(requireActivity() as MainActivity).showRedToast(message, icon)
|
||||
}
|
||||
showOpenOrExportFileDialog(path, mime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1268,4 +1292,66 @@ class ConversationFragment : SlidingPaneChildFragment() {
|
|||
)
|
||||
bottomSheetDialog = e2eEncryptionDetailsBottomSheet
|
||||
}
|
||||
|
||||
private fun showOpenOrExportFileDialog(path: String, mime: String) {
|
||||
val model = ConfirmationDialogModel()
|
||||
val dialog = DialogUtils.getOpenOrExportFileDialog(
|
||||
requireActivity(),
|
||||
model
|
||||
)
|
||||
|
||||
model.dismissEvent.observe(viewLifecycleOwner) {
|
||||
it.consume {
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
model.cancelEvent.observe(viewLifecycleOwner) {
|
||||
it.consume {
|
||||
openFileInAnotherApp(path, mime)
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
model.confirmEvent.observe(viewLifecycleOwner) {
|
||||
it.consume {
|
||||
exportFile(path, mime)
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
private fun openFileInAnotherApp(path: String, mime: String) {
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
val contentUri: Uri =
|
||||
FileUtils.getPublicFilePath(requireContext(), path)
|
||||
intent.setDataAndType(contentUri, mime)
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
try {
|
||||
Log.i("$TAG Trying to start ACTION_VIEW intent for file [$path]")
|
||||
requireContext().startActivity(intent)
|
||||
} catch (anfe: ActivityNotFoundException) {
|
||||
Log.e("$TAG Can't open file [$path] in third party app: $anfe")
|
||||
val message = getString(
|
||||
R.string.toast_no_app_registered_to_handle_content_type_error
|
||||
)
|
||||
val icon = R.drawable.file
|
||||
(requireActivity() as MainActivity).showRedToast(message, icon)
|
||||
}
|
||||
}
|
||||
|
||||
private fun exportFile(path: String, mime: String) {
|
||||
filePathToExport = path
|
||||
|
||||
Log.i("$TAG Asking where to save file [$filePathToExport] on device")
|
||||
val name = FileUtils.getNameFromFilePath(path)
|
||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
type = mime
|
||||
putExtra(Intent.EXTRA_TITLE, name)
|
||||
}
|
||||
startActivityForResult(intent, EXPORT_FILE_AS_DOCUMENT)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,9 +19,14 @@
|
|||
*/
|
||||
package org.linphone.ui.main.chat.viewmodel
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.annotation.UiThread
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.core.Address
|
||||
|
|
@ -43,6 +48,7 @@ import org.linphone.ui.main.contacts.model.ContactAvatarModel
|
|||
import org.linphone.ui.main.model.isEndToEndEncryptionMandatory
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.FileUtils
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
|
||||
class ConversationViewModel @UiThread constructor() : AbstractConversationViewModel() {
|
||||
|
|
@ -110,6 +116,10 @@ class ConversationViewModel @UiThread constructor() : AbstractConversationViewMo
|
|||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
val showGreenToastEvent: MutableLiveData<Event<Pair<String, Int>>> by lazy {
|
||||
MutableLiveData<Event<Pair<String, Int>>>()
|
||||
}
|
||||
|
||||
val showRedToastEvent: MutableLiveData<Event<Pair<String, Int>>> by lazy {
|
||||
MutableLiveData<Event<Pair<String, Int>>>()
|
||||
}
|
||||
|
|
@ -836,4 +846,30 @@ class ConversationViewModel @UiThread constructor() : AbstractConversationViewMo
|
|||
// Will trigger the conference creation/update automatically
|
||||
conferenceScheduler.info = conferenceInfo
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun copyFileToUri(filePath: String, dest: Uri) {
|
||||
val source = Uri.parse(FileUtils.getProperFilePath(filePath))
|
||||
Log.i("$TAG Copying file URI [$source] to [$dest]")
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
val result = FileUtils.copyFile(source, dest)
|
||||
if (result) {
|
||||
Log.i(
|
||||
"$TAG File [$filePath] 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 export file [$filePath] to documents!")
|
||||
val message = AppUtils.getString(
|
||||
R.string.toast_export_file_to_documents_error
|
||||
)
|
||||
showRedToastEvent.postValue(Event(Pair(message, R.drawable.warning_circle)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -164,43 +164,7 @@ class MediaListViewerFragment : GenericFragment() {
|
|||
}
|
||||
|
||||
binding.setExportClickListener {
|
||||
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) {
|
||||
val filePath = model.file
|
||||
lifecycleScope.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
|
||||
)
|
||||
(requireActivity() as GenericActivity).showGreenToast(
|
||||
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
|
||||
)
|
||||
(requireActivity() as GenericActivity).showRedToast(
|
||||
message,
|
||||
R.drawable.warning_circle
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.e(
|
||||
"$TAG Failed to get FileModel at index [$currentItem], only [${list.size}] items in list"
|
||||
)
|
||||
}
|
||||
exportFile()
|
||||
}
|
||||
|
||||
sharedViewModel.mediaViewerFullScreenMode.observe(viewLifecycleOwner) {
|
||||
|
|
@ -228,6 +192,46 @@ class MediaListViewerFragment : GenericFragment() {
|
|||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun exportFile() {
|
||||
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) {
|
||||
val filePath = model.file
|
||||
lifecycleScope.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
|
||||
)
|
||||
(requireActivity() as GenericActivity).showGreenToast(
|
||||
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
|
||||
)
|
||||
(requireActivity() as GenericActivity).showRedToast(
|
||||
message,
|
||||
R.drawable.warning_circle
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.e(
|
||||
"$TAG Failed to get FileModel at index [$currentItem], only [${list.size}] items in list"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun shareFile() {
|
||||
val list = viewModel.mediaList.value.orEmpty()
|
||||
val currentItem = binding.mediaViewPager.currentItem
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ import org.linphone.databinding.DialogDeleteContactBinding
|
|||
import org.linphone.databinding.DialogKickFromConferenceBinding
|
||||
import org.linphone.databinding.DialogManageAccountInternationalPrefixHelpBinding
|
||||
import org.linphone.databinding.DialogMergeCallsIntoConferenceBinding
|
||||
import org.linphone.databinding.DialogOpenExportFileBinding
|
||||
import org.linphone.databinding.DialogPickNumberOrAddressBinding
|
||||
import org.linphone.databinding.DialogRemoveAccountBinding
|
||||
import org.linphone.databinding.DialogRemoveAllCallLogsBinding
|
||||
|
|
@ -292,6 +293,22 @@ class DialogUtils {
|
|||
return getDialog(context, binding)
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun getOpenOrExportFileDialog(
|
||||
context: Context,
|
||||
viewModel: ConfirmationDialogModel
|
||||
): Dialog {
|
||||
val binding: DialogOpenExportFileBinding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(context),
|
||||
R.layout.dialog_open_export_file,
|
||||
null,
|
||||
false
|
||||
)
|
||||
binding.viewModel = viewModel
|
||||
|
||||
return getDialog(context, binding)
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun getUpdateAvailableDialog(
|
||||
context: Context,
|
||||
|
|
|
|||
104
app/src/main/res/layout/dialog_open_export_file.xml
Normal file
104
app/src/main/res/layout/dialog_open_export_file.xml
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<data>
|
||||
<import type="android.view.View" />
|
||||
<import type="android.graphics.Typeface" />
|
||||
<variable
|
||||
name="viewModel"
|
||||
type="org.linphone.ui.main.history.model.ConfirmationDialogModel" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:onClick="@{() -> viewModel.dismiss()}"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/dialog_background"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:src="@drawable/shape_dialog_background"
|
||||
android:contentDescription="@null"
|
||||
app:layout_constraintWidth_max="@dimen/dialog_max_width"
|
||||
app:layout_constraintBottom_toBottomOf="@id/anchor"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/title" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
style="@style/section_header_style"
|
||||
android:id="@+id/title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="15dp"
|
||||
android:paddingTop="@dimen/dialog_top_bottom_margin"
|
||||
android:text="@string/conversation_dialog_open_or_export_file_title"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
app:layout_constraintBottom_toTopOf="@id/message"
|
||||
app:layout_constraintStart_toStartOf="@id/dialog_background"
|
||||
app:layout_constraintEnd_toEndOf="@id/dialog_background"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
style="@style/default_text_style"
|
||||
android:id="@+id/message"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="15dp"
|
||||
android:layout_marginEnd="15dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:text="@string/conversation_dialog_open_or_export_file_message"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toTopOf="@id/cancel"
|
||||
app:layout_constraintStart_toStartOf="@id/dialog_background"
|
||||
app:layout_constraintEnd_toEndOf="@id/dialog_background"
|
||||
app:layout_constraintTop_toBottomOf="@id/title" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:onClick="@{() -> viewModel.cancel()}"
|
||||
style="@style/primary_button_label_style"
|
||||
android:id="@+id/cancel"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
android:layout_marginStart="15dp"
|
||||
android:layout_marginEnd="15dp"
|
||||
android:text="@string/conversation_dialog_open_file_label"
|
||||
app:layout_constraintStart_toStartOf="@id/dialog_background"
|
||||
app:layout_constraintEnd_toEndOf="@id/dialog_background"
|
||||
app:layout_constraintTop_toBottomOf="@id/message"
|
||||
app:layout_constraintBottom_toTopOf="@id/confirm"/>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:onClick="@{() -> viewModel.confirm()}"
|
||||
style="@style/primary_button_label_style"
|
||||
android:id="@+id/confirm"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginStart="15dp"
|
||||
android:layout_marginEnd="15dp"
|
||||
android:text="@string/conversation_dialog_export_file_label"
|
||||
app:layout_constraintStart_toStartOf="@id/dialog_background"
|
||||
app:layout_constraintEnd_toEndOf="@id/dialog_background"
|
||||
app:layout_constraintTop_toBottomOf="@id/cancel"
|
||||
app:layout_constraintBottom_toTopOf="@id/anchor"/>
|
||||
|
||||
<View
|
||||
android:id="@+id/anchor"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="@dimen/dialog_top_bottom_margin"
|
||||
app:layout_constraintTop_toBottomOf="@id/confirm"
|
||||
app:layout_constraintStart_toStartOf="@id/dialog_background"
|
||||
app:layout_constraintEnd_toEndOf="@id/dialog_background"
|
||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</layout>
|
||||
|
|
@ -437,6 +437,10 @@
|
|||
<string name="conversation_dialog_edit_subject">Renommer la conversation</string>
|
||||
<string name="conversation_dialog_subject_hint">Nom de la conversation</string>
|
||||
<string name="conversation_dialog_edit_subject_confirm_button">Confirmer</string>
|
||||
<string name="conversation_dialog_open_or_export_file_title">Ouvrir ou sauvegarder le fichier ?</string>
|
||||
<string name="conversation_dialog_open_or_export_file_message">&appName; ne peut ouvrir ce fichier.\n\nVoulez-vous l\'ouvrir dans une autre app (si possible), ou le sauvegarder sur votre appareil ?</string>
|
||||
<string name="conversation_dialog_open_file_label">Ouvrir le fichier</string>
|
||||
<string name="conversation_dialog_export_file_label">Sauvegarder le fichier</string>
|
||||
<string name="conversation_message_deleted_toast">Le message a été supprimé</string>
|
||||
|
||||
<string name="conversation_info_participants_list_title">Membres du groupe</string>
|
||||
|
|
|
|||
|
|
@ -473,6 +473,10 @@
|
|||
<string name="conversation_dialog_edit_subject">Edit conversation subject</string>
|
||||
<string name="conversation_dialog_subject_hint">Conversation subject</string>
|
||||
<string name="conversation_dialog_edit_subject_confirm_button">Confirm</string>
|
||||
<string name="conversation_dialog_open_or_export_file_title">Open or export file?</string>
|
||||
<string name="conversation_dialog_open_or_export_file_message">&appName; can\'t open this file.\n\nDo you want to open it in another app (if possible), or export it on your device?</string>
|
||||
<string name="conversation_dialog_open_file_label">Open file</string>
|
||||
<string name="conversation_dialog_export_file_label">Export file</string>
|
||||
<string name="conversation_message_deleted_toast">Message has been deleted</string>
|
||||
|
||||
<string name="conversation_info_participants_list_title">Group members</string>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue