Reworked & improved export when usig VFS feature

This commit is contained in:
Sylvain Berfini 2022-01-03 17:00:49 +01:00
parent a2ac7e9f37
commit 0404777c32
4 changed files with 151 additions and 20 deletions

View file

@ -21,13 +21,21 @@ package org.linphone.activities.main.files.fragments
import android.os.Bundle import android.os.Bundle
import android.view.View 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.R
import org.linphone.activities.GenericFragment import org.linphone.activities.GenericFragment
import org.linphone.activities.SnackBarActivity import org.linphone.activities.SnackBarActivity
import org.linphone.compatibility.Compatibility
import org.linphone.core.Content import org.linphone.core.Content
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
import org.linphone.databinding.FileViewerTopBarFragmentBinding import org.linphone.databinding.FileViewerTopBarFragmentBinding
import org.linphone.mediastream.Version
import org.linphone.utils.FileUtils import org.linphone.utils.FileUtils
import org.linphone.utils.PermissionHelper
class TopBarFragment : GenericFragment<FileViewerTopBarFragmentBinding>() { class TopBarFragment : GenericFragment<FileViewerTopBarFragmentBinding>() {
private var content: Content? = null private var content: Content? = null
@ -46,20 +54,9 @@ class TopBarFragment : GenericFragment<FileViewerTopBarFragmentBinding>() {
} }
binding.setExportClickListener { binding.setExportClickListener {
if (content != null) { val contentToExport = content
val filePath = content?.plainFilePath.orEmpty() if (contentToExport != null) {
plainFilePath = if (filePath.isEmpty()) content?.filePath.orEmpty() else filePath exportContent(contentToExport)
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 { } else {
Log.e("[File Viewer] No Content set!") Log.e("[File Viewer] No Content set!")
} }
@ -89,4 +86,77 @@ class TopBarFragment : GenericFragment<FileViewerTopBarFragmentBinding>() {
content = c content = c
binding.fileName.text = c.name 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)
}
}
}
}
} }

View file

@ -77,7 +77,9 @@ class Api21Compatibility {
return false 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) { if (filePath == null) {
Log.e("[Media Store] Content doesn't have a file path!") Log.e("[Media Store] Content doesn't have a file path!")
return false return false
@ -95,6 +97,10 @@ class Api21Compatibility {
} }
val collection = MediaStore.Images.Media.getContentUri("external") val collection = MediaStore.Images.Media.getContentUri("external")
val mediaStoreFilePath = addContentValuesToCollection(context, filePath, collection, values) 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()) { if (mediaStoreFilePath.isNotEmpty()) {
content.userData = mediaStoreFilePath content.userData = mediaStoreFilePath
return true return true
@ -108,7 +114,9 @@ class Api21Compatibility {
return false 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) { if (filePath == null) {
Log.e("[Media Store] Content doesn't have a file path!") Log.e("[Media Store] Content doesn't have a file path!")
return false return false
@ -127,6 +135,10 @@ class Api21Compatibility {
} }
val collection = MediaStore.Video.Media.getContentUri("external") val collection = MediaStore.Video.Media.getContentUri("external")
val mediaStoreFilePath = addContentValuesToCollection(context, filePath, collection, values) 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()) { if (mediaStoreFilePath.isNotEmpty()) {
content.userData = mediaStoreFilePath content.userData = mediaStoreFilePath
return true return true
@ -140,7 +152,9 @@ class Api21Compatibility {
return false 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) { if (filePath == null) {
Log.e("[Media Store] Content doesn't have a file path!") Log.e("[Media Store] Content doesn't have a file path!")
return false return false
@ -160,6 +174,10 @@ class Api21Compatibility {
val collection = MediaStore.Audio.Media.getContentUri("external") val collection = MediaStore.Audio.Media.getContentUri("external")
val mediaStoreFilePath = addContentValuesToCollection(context, filePath, collection, values) 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()) { if (mediaStoreFilePath.isNotEmpty()) {
content.userData = mediaStoreFilePath content.userData = mediaStoreFilePath
return true return true

View file

@ -99,7 +99,9 @@ class Api29Compatibility {
} }
suspend fun addImageToMediaStore(context: Context, content: Content): Boolean { 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) { if (filePath == null) {
Log.e("[Media Store] Content doesn't have a file path!") Log.e("[Media Store] Content doesn't have a file path!")
return false return false
@ -119,6 +121,11 @@ class Api29Compatibility {
} }
val collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) val collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val mediaStoreFilePath = addContentValuesToCollection(context, filePath, collection, values, MediaStore.Images.Media.IS_PENDING) 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()) { if (mediaStoreFilePath.isNotEmpty()) {
content.userData = mediaStoreFilePath content.userData = mediaStoreFilePath
return true return true
@ -127,7 +134,9 @@ class Api29Compatibility {
} }
suspend fun addVideoToMediaStore(context: Context, content: Content): Boolean { 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) { if (filePath == null) {
Log.e("[Media Store] Content doesn't have a file path!") Log.e("[Media Store] Content doesn't have a file path!")
return false return false
@ -148,6 +157,11 @@ class Api29Compatibility {
} }
val collection = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) val collection = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val mediaStoreFilePath = addContentValuesToCollection(context, filePath, collection, values, MediaStore.Video.Media.IS_PENDING) 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()) { if (mediaStoreFilePath.isNotEmpty()) {
content.userData = mediaStoreFilePath content.userData = mediaStoreFilePath
return true return true
@ -156,7 +170,9 @@ class Api29Compatibility {
} }
suspend fun addAudioToMediaStore(context: Context, content: Content): Boolean { 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) { if (filePath == null) {
Log.e("[Media Store] Content doesn't have a file path!") Log.e("[Media Store] Content doesn't have a file path!")
return false return false
@ -178,6 +194,11 @@ class Api29Compatibility {
val collection = MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) val collection = MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val mediaStoreFilePath = addContentValuesToCollection(context, filePath, collection, values, MediaStore.Audio.Media.IS_PENDING) 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()) { if (mediaStoreFilePath.isNotEmpty()) {
content.userData = mediaStoreFilePath content.userData = mediaStoreFilePath
return true return true

View file

@ -470,5 +470,27 @@ class FileUtils {
} }
return false 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
}
} }
} }