diff --git a/app/src/main/java/org/linphone/activities/main/files/fragments/TopBarFragment.kt b/app/src/main/java/org/linphone/activities/main/files/fragments/TopBarFragment.kt index 0699df0a1..dd10adf92 100644 --- a/app/src/main/java/org/linphone/activities/main/files/fragments/TopBarFragment.kt +++ b/app/src/main/java/org/linphone/activities/main/files/fragments/TopBarFragment.kt @@ -21,13 +21,21 @@ package org.linphone.activities.main.files.fragments import android.os.Bundle import android.view.View +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.linphone.R import org.linphone.activities.GenericFragment import org.linphone.activities.SnackBarActivity +import org.linphone.compatibility.Compatibility import org.linphone.core.Content import org.linphone.core.tools.Log import org.linphone.databinding.FileViewerTopBarFragmentBinding +import org.linphone.mediastream.Version import org.linphone.utils.FileUtils +import org.linphone.utils.PermissionHelper class TopBarFragment : GenericFragment() { private var content: Content? = null @@ -46,20 +54,9 @@ class TopBarFragment : GenericFragment() { } binding.setExportClickListener { - if (content != null) { - val filePath = content?.plainFilePath.orEmpty() - plainFilePath = if (filePath.isEmpty()) content?.filePath.orEmpty() else filePath - Log.i("[File Viewer] Plain file path is: $plainFilePath") - if (plainFilePath.isNotEmpty()) { - if (!FileUtils.openFileInThirdPartyApp(requireActivity(), plainFilePath)) { - (requireActivity() as SnackBarActivity).showSnackBar(R.string.chat_message_no_app_found_to_handle_file_mime_type) - if (plainFilePath != content?.filePath.orEmpty()) { - Log.i("[File Viewer] No app to open plain file path: $plainFilePath, destroying it") - FileUtils.deleteFile(plainFilePath) - } - plainFilePath = "" - } - } + val contentToExport = content + if (contentToExport != null) { + exportContent(contentToExport) } else { Log.e("[File Viewer] No Content set!") } @@ -89,4 +86,77 @@ class TopBarFragment : GenericFragment() { content = c binding.fileName.text = c.name } + + private fun exportContent(content: Content) { + lifecycleScope.launch { + var mediaStoreFilePath = "" + if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10) || PermissionHelper.get().hasWriteExternalStoragePermission()) { + Log.i("[File Viewer] Exporting image through Media Store API") + when (content.type) { + "image" -> { + val export = lifecycleScope.async { + Compatibility.addImageToMediaStore(requireContext(), content) + } + if (export.await()) { + Log.i("[File Viewer] Adding image ${content.name} to Media Store terminated: ${content.userData}") + mediaStoreFilePath = content.userData.toString() + } else { + Log.e("[File Viewer] Something went wrong while copying file to Media Store...") + } + } + "video" -> { + val export = lifecycleScope.async { + Compatibility.addVideoToMediaStore(requireContext(), content) + } + if (export.await()) { + Log.i("[File Viewer] Adding video ${content.name} to Media Store terminated: ${content.userData}") + mediaStoreFilePath = content.userData.toString() + } else { + Log.e("[File Viewer] Something went wrong while copying file to Media Store...") + } + } + "audio" -> { + val export = lifecycleScope.async { + Compatibility.addAudioToMediaStore(requireContext(), content) + } + if (export.await()) { + Log.i("[File Viewer] Adding audio ${content.name} to Media Store terminated: ${content.userData}") + mediaStoreFilePath = content.userData.toString() + } else { + Log.e("[File Viewer] Something went wrong while copying file to Media Store...") + } + } + else -> { + Log.w("[File Viewer] File ${content.name} isn't either an image, an audio file or a video, can't add it to the Media Store") + } + } + } else { + Log.w("[File Viewer] Can't export image through Media Store API (requires Android 10 or WRITE_EXTERNAL permission, using fallback method...") + } + + withContext(Dispatchers.Main) { + if (mediaStoreFilePath.isEmpty()) { + Log.w("[File Viewer] Media store file path is empty, media store export failed?") + + val filePath = content.plainFilePath.orEmpty() + plainFilePath = if (filePath.isEmpty()) content.filePath.orEmpty() else filePath + Log.i("[File Viewer] Plain file path is: $plainFilePath") + if (plainFilePath.isNotEmpty()) { + if (!FileUtils.openFileInThirdPartyApp(requireActivity(), plainFilePath)) { + (requireActivity() as SnackBarActivity).showSnackBar(R.string.chat_message_no_app_found_to_handle_file_mime_type) + if (plainFilePath != content.filePath.orEmpty()) { + Log.i("[File Viewer] No app to open plain file path: $plainFilePath, destroying it") + FileUtils.deleteFile(plainFilePath) + } + plainFilePath = "" + } + } + } else { + plainFilePath = "" + Log.i("[File Viewer] Media store file path is: $mediaStoreFilePath") + FileUtils.openMediaStoreFile(requireActivity(), mediaStoreFilePath) + } + } + } + } } diff --git a/app/src/main/java/org/linphone/compatibility/Api21Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api21Compatibility.kt index 807718ba2..278bc66cc 100644 --- a/app/src/main/java/org/linphone/compatibility/Api21Compatibility.kt +++ b/app/src/main/java/org/linphone/compatibility/Api21Compatibility.kt @@ -77,7 +77,9 @@ class Api21Compatibility { return false } - val filePath = content.filePath + val plainFilePath = content.plainFilePath.orEmpty() + val isVfsEncrypted = plainFilePath.isNotEmpty() + val filePath = if (isVfsEncrypted) plainFilePath else content.filePath if (filePath == null) { Log.e("[Media Store] Content doesn't have a file path!") return false @@ -95,6 +97,10 @@ class Api21Compatibility { } val collection = MediaStore.Images.Media.getContentUri("external") val mediaStoreFilePath = addContentValuesToCollection(context, filePath, collection, values) + if (isVfsEncrypted) { + Log.w("[Media Store] Content was encrypted, delete plain version: $plainFilePath") + FileUtils.deleteFile(plainFilePath) + } if (mediaStoreFilePath.isNotEmpty()) { content.userData = mediaStoreFilePath return true @@ -108,7 +114,9 @@ class Api21Compatibility { return false } - val filePath = content.filePath + val plainFilePath = content.plainFilePath.orEmpty() + val isVfsEncrypted = plainFilePath.isNotEmpty() + val filePath = if (isVfsEncrypted) plainFilePath else content.filePath if (filePath == null) { Log.e("[Media Store] Content doesn't have a file path!") return false @@ -127,6 +135,10 @@ class Api21Compatibility { } val collection = MediaStore.Video.Media.getContentUri("external") val mediaStoreFilePath = addContentValuesToCollection(context, filePath, collection, values) + if (isVfsEncrypted) { + Log.w("[Media Store] Content was encrypted, delete plain version: $plainFilePath") + FileUtils.deleteFile(plainFilePath) + } if (mediaStoreFilePath.isNotEmpty()) { content.userData = mediaStoreFilePath return true @@ -140,7 +152,9 @@ class Api21Compatibility { return false } - val filePath = content.filePath + val plainFilePath = content.plainFilePath.orEmpty() + val isVfsEncrypted = plainFilePath.isNotEmpty() + val filePath = if (isVfsEncrypted) plainFilePath else content.filePath if (filePath == null) { Log.e("[Media Store] Content doesn't have a file path!") return false @@ -160,6 +174,10 @@ class Api21Compatibility { val collection = MediaStore.Audio.Media.getContentUri("external") val mediaStoreFilePath = addContentValuesToCollection(context, filePath, collection, values) + if (isVfsEncrypted) { + Log.w("[Media Store] Content was encrypted, delete plain version: $plainFilePath") + FileUtils.deleteFile(plainFilePath) + } if (mediaStoreFilePath.isNotEmpty()) { content.userData = mediaStoreFilePath return true diff --git a/app/src/main/java/org/linphone/compatibility/Api29Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api29Compatibility.kt index 1d5c2831c..0e587f6ef 100644 --- a/app/src/main/java/org/linphone/compatibility/Api29Compatibility.kt +++ b/app/src/main/java/org/linphone/compatibility/Api29Compatibility.kt @@ -99,7 +99,9 @@ class Api29Compatibility { } suspend fun addImageToMediaStore(context: Context, content: Content): Boolean { - val filePath = content.filePath + val plainFilePath = content.plainFilePath.orEmpty() + val isVfsEncrypted = plainFilePath.isNotEmpty() + val filePath = if (isVfsEncrypted) plainFilePath else content.filePath if (filePath == null) { Log.e("[Media Store] Content doesn't have a file path!") return false @@ -119,6 +121,11 @@ class Api29Compatibility { } val collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) val mediaStoreFilePath = addContentValuesToCollection(context, filePath, collection, values, MediaStore.Images.Media.IS_PENDING) + + if (isVfsEncrypted) { + Log.w("[Media Store] Content was encrypted, delete plain version: $plainFilePath") + FileUtils.deleteFile(plainFilePath) + } if (mediaStoreFilePath.isNotEmpty()) { content.userData = mediaStoreFilePath return true @@ -127,7 +134,9 @@ class Api29Compatibility { } suspend fun addVideoToMediaStore(context: Context, content: Content): Boolean { - val filePath = content.filePath + val plainFilePath = content.plainFilePath.orEmpty() + val isVfsEncrypted = plainFilePath.isNotEmpty() + val filePath = if (isVfsEncrypted) plainFilePath else content.filePath if (filePath == null) { Log.e("[Media Store] Content doesn't have a file path!") return false @@ -148,6 +157,11 @@ class Api29Compatibility { } val collection = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) val mediaStoreFilePath = addContentValuesToCollection(context, filePath, collection, values, MediaStore.Video.Media.IS_PENDING) + + if (isVfsEncrypted) { + Log.w("[Media Store] Content was encrypted, delete plain version: $plainFilePath") + FileUtils.deleteFile(plainFilePath) + } if (mediaStoreFilePath.isNotEmpty()) { content.userData = mediaStoreFilePath return true @@ -156,7 +170,9 @@ class Api29Compatibility { } suspend fun addAudioToMediaStore(context: Context, content: Content): Boolean { - val filePath = content.filePath + val plainFilePath = content.plainFilePath.orEmpty() + val isVfsEncrypted = plainFilePath.isNotEmpty() + val filePath = if (isVfsEncrypted) plainFilePath else content.filePath if (filePath == null) { Log.e("[Media Store] Content doesn't have a file path!") return false @@ -178,6 +194,11 @@ class Api29Compatibility { val collection = MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) val mediaStoreFilePath = addContentValuesToCollection(context, filePath, collection, values, MediaStore.Audio.Media.IS_PENDING) + + if (isVfsEncrypted) { + Log.w("[Media Store] Content was encrypted, delete plain version: $plainFilePath") + FileUtils.deleteFile(plainFilePath) + } if (mediaStoreFilePath.isNotEmpty()) { content.userData = mediaStoreFilePath return true diff --git a/app/src/main/java/org/linphone/utils/FileUtils.kt b/app/src/main/java/org/linphone/utils/FileUtils.kt index 0955b39b7..aa3a90f6f 100644 --- a/app/src/main/java/org/linphone/utils/FileUtils.kt +++ b/app/src/main/java/org/linphone/utils/FileUtils.kt @@ -470,5 +470,27 @@ class FileUtils { } return false } + + fun openMediaStoreFile( + activity: Activity, + contentFilePath: String, + newTask: Boolean = false + ): Boolean { + val intent = Intent(Intent.ACTION_VIEW) + val contentUri: Uri = Uri.parse(contentFilePath) + intent.data = contentUri + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + if (newTask) { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + + try { + activity.startActivity(intent) + return true + } catch (anfe: ActivityNotFoundException) { + Log.e("[File Viewer] Can't open media store export in third party app: $anfe") + } + return false + } } }