Improved file copy code by using coroutines

This commit is contained in:
Sylvain Berfini 2020-04-09 14:57:14 +02:00
parent de389858ce
commit e1dc8ad8c2
8 changed files with 158 additions and 119 deletions

View file

@ -30,12 +30,14 @@ import android.view.inputmethod.InputMethodManager
import androidx.databinding.DataBindingUtil import androidx.databinding.DataBindingUtil
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavDestination import androidx.navigation.NavDestination
import androidx.navigation.findNavController import androidx.navigation.findNavController
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import java.io.UnsupportedEncodingException import java.io.UnsupportedEncodingException
import java.net.URLDecoder import java.net.URLDecoder
import kotlinx.coroutines.*
import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R import org.linphone.R
@ -147,10 +149,14 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
private fun handleIntentParams(intent: Intent) { private fun handleIntentParams(intent: Intent) {
when (intent.action) { when (intent.action) {
Intent.ACTION_SEND -> { Intent.ACTION_SEND -> {
handleSendImage(intent) lifecycleScope.launch {
handleSendImage(intent)
}
} }
Intent.ACTION_SEND_MULTIPLE -> { Intent.ACTION_SEND_MULTIPLE -> {
handleSendMultipleImages(intent) lifecycleScope.launch {
handleSendMultipleImages(intent)
}
} }
Intent.ACTION_VIEW -> { Intent.ACTION_VIEW -> {
if (intent.type == AppUtils.getString(R.string.linphone_address_mime_type)) { if (intent.type == AppUtils.getString(R.string.linphone_address_mime_type)) {
@ -218,13 +224,18 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
} }
} }
private fun handleSendImage(intent: Intent) { private suspend fun handleSendImage(intent: Intent) {
(intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri)?.let { (intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri)?.let {
val list = arrayListOf<String>() val list = arrayListOf<String>()
val path = FileUtils.getFilePath(this, it) coroutineScope {
if (path != null) { val deferred = async {
list.add(path) FileUtils.getFilePath(this@MainActivity, it)
Log.i("[Main Activity] Found single file to share: $path") }
val path = deferred.await()
if (path != null) {
list.add(path)
Log.i("[Main Activity] Found single file to share: $path")
}
} }
sharedViewModel.filesToShare.value = list sharedViewModel.filesToShare.value = list
@ -234,14 +245,20 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
} }
} }
private fun handleSendMultipleImages(intent: Intent) { private suspend fun handleSendMultipleImages(intent: Intent) {
intent.getParcelableArrayListExtra<Parcelable>(Intent.EXTRA_STREAM)?.let { intent.getParcelableArrayListExtra<Parcelable>(Intent.EXTRA_STREAM)?.let {
val list = arrayListOf<String>() val list = arrayListOf<String>()
for (parcelable in it) { coroutineScope {
val uri = parcelable as Uri val deferred = arrayListOf<Deferred<String?>>()
val path = FileUtils.getFilePath(this, uri) for (parcelable in it) {
Log.i("[Main Activity] Found file to share: $path") val uri = parcelable as Uri
if (path != null) list.add(path) deferred.add(async { FileUtils.getFilePath(this@MainActivity, uri) })
}
val paths = deferred.awaitAll()
for (path in paths) {
Log.i("[Main Activity] Found file to share: $path")
if (path != null) list.add(path)
}
} }
sharedViewModel.filesToShare.value = list sharedViewModel.filesToShare.value = list

View file

@ -35,12 +35,14 @@ import androidx.appcompat.view.menu.MenuPopupHelper
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.Navigation import androidx.navigation.Navigation
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import java.io.File import java.io.File
import kotlinx.android.synthetic.main.tabs_fragment.* import kotlinx.android.synthetic.main.tabs_fragment.*
import kotlinx.coroutines.launch
import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R import org.linphone.R
@ -299,40 +301,41 @@ class DetailChatRoomFragment : MasterFragment() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
var fileToUploadPath: String? = null lifecycleScope.launch {
var fileToUploadPath: String? = null
val temporaryFileUploadPath = chatSendingViewModel.temporaryFileUploadPath val temporaryFileUploadPath = chatSendingViewModel.temporaryFileUploadPath
if (temporaryFileUploadPath != null) { if (temporaryFileUploadPath != null) {
if (data != null) { if (data != null) {
val dataUri = data.data val dataUri = data.data
if (dataUri != null) { if (dataUri != null) {
fileToUploadPath = dataUri.toString() fileToUploadPath = dataUri.toString()
Log.i("[Chat Room] Using data URI $fileToUploadPath") Log.i("[Chat Room] Using data URI $fileToUploadPath")
} else if (temporaryFileUploadPath.exists()) {
fileToUploadPath = temporaryFileUploadPath.absolutePath
Log.i("[Chat Room] Data URI is null, using $fileToUploadPath")
}
} else if (temporaryFileUploadPath.exists()) { } else if (temporaryFileUploadPath.exists()) {
fileToUploadPath = temporaryFileUploadPath.absolutePath fileToUploadPath = temporaryFileUploadPath.absolutePath
Log.i("[Chat Room] Data URI is null, using $fileToUploadPath") Log.i("[Chat Room] Data is null, using $fileToUploadPath")
}
} else if (temporaryFileUploadPath.exists()) {
fileToUploadPath = temporaryFileUploadPath.absolutePath
Log.i("[Chat Room] Data is null, using $fileToUploadPath")
}
}
if (fileToUploadPath != null) {
if (fileToUploadPath.startsWith("content://") ||
fileToUploadPath.startsWith("file://")
) {
val uriToParse = Uri.parse(fileToUploadPath)
fileToUploadPath = FileUtils.getFilePath(requireContext(), uriToParse)
Log.i("[Chat] Path was using a content or file scheme, real path is: $fileToUploadPath")
if (fileToUploadPath == null) {
Log.e("[Chat] Failed to get access to file $uriToParse")
} }
} }
}
if (fileToUploadPath != null) { if (fileToUploadPath != null) {
chatSendingViewModel.addAttachment(fileToUploadPath) if (fileToUploadPath.startsWith("content://") ||
fileToUploadPath.startsWith("file://")
) {
val uriToParse = Uri.parse(fileToUploadPath)
fileToUploadPath = FileUtils.getFilePath(requireContext(), uriToParse)
Log.i("[Chat] Path was using a content or file scheme, real path is: $fileToUploadPath")
if (fileToUploadPath == null) {
Log.e("[Chat] Failed to get access to file $uriToParse")
}
}
}
if (fileToUploadPath != null) {
chatSendingViewModel.addAttachment(fileToUploadPath)
}
} }
} }
} }

View file

@ -22,6 +22,8 @@ package org.linphone.activities.main.chat.viewmodels
import android.os.CountDownTimer import android.os.CountDownTimer
import android.text.Spanned import android.text.Spanned
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R import org.linphone.R
@ -205,30 +207,32 @@ class ChatMessageViewModel(
} }
private fun addContentToMediaStore(content: Content) { private fun addContentToMediaStore(content: Content) {
when (content.type) { viewModelScope.launch {
"image" -> { when (content.type) {
if (Compatibility.addImageToMediaStore(coreContext.context, content)) { "image" -> {
Log.i("[Chat Message] Adding image ${content.name} terminated") if (Compatibility.addImageToMediaStore(coreContext.context, content)) {
} else { Log.i("[Chat Message] Adding image ${content.name} terminated")
Log.e("[Chat Message] Something went wrong while copying file...") } else {
Log.e("[Chat Message] Something went wrong while copying file...")
}
} }
} "video" -> {
"video" -> { if (Compatibility.addVideoToMediaStore(coreContext.context, content)) {
if (Compatibility.addVideoToMediaStore(coreContext.context, content)) { Log.i("[Chat Message] Adding video ${content.name} terminated")
Log.i("[Chat Message] Adding video ${content.name} terminated") } else {
} else { Log.e("[Chat Message] Something went wrong while copying file...")
Log.e("[Chat Message] Something went wrong while copying file...") }
} }
} "audio" -> {
"audio" -> { if (Compatibility.addAudioToMediaStore(coreContext.context, content)) {
if (Compatibility.addAudioToMediaStore(coreContext.context, content)) { Log.i("[Chat Message] Adding audio ${content.name} terminated")
Log.i("[Chat Message] Adding audio ${content.name} terminated") } else {
} else { Log.e("[Chat Message] Something went wrong while copying file...")
Log.e("[Chat Message] Something went wrong while copying file...") }
}
else -> {
Log.w("[Chat Message] 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("[Chat Message] File ${content.name} isn't either an image, an audio file or a video, can't add it to the Media Store")
} }
} }
} }

View file

@ -31,8 +31,10 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import java.io.File import java.io.File
import kotlinx.coroutines.launch
import org.linphone.R import org.linphone.R
import org.linphone.activities.main.MainActivity import org.linphone.activities.main.MainActivity
import org.linphone.activities.main.contact.viewmodels.* import org.linphone.activities.main.contact.viewmodels.*
@ -130,40 +132,42 @@ class ContactEditorFragment : Fragment() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
var fileToUploadPath: String? = null lifecycleScope.launch {
var fileToUploadPath: String? = null
val temporaryFileUploadPath = temporaryPicturePath val temporaryFileUploadPath = temporaryPicturePath
if (temporaryFileUploadPath != null) { if (temporaryFileUploadPath != null) {
if (data != null) { if (data != null) {
val dataUri = data.data val dataUri = data.data
if (dataUri != null) { if (dataUri != null) {
fileToUploadPath = dataUri.toString() fileToUploadPath = dataUri.toString()
Log.i("[Chat Room] Using data URI $fileToUploadPath") Log.i("[Chat Room] Using data URI $fileToUploadPath")
} else if (temporaryFileUploadPath.exists()) {
fileToUploadPath = temporaryFileUploadPath.absolutePath
Log.i("[Chat Room] Data URI is null, using $fileToUploadPath")
}
} else if (temporaryFileUploadPath.exists()) { } else if (temporaryFileUploadPath.exists()) {
fileToUploadPath = temporaryFileUploadPath.absolutePath fileToUploadPath = temporaryFileUploadPath.absolutePath
Log.i("[Chat Room] Data URI is null, using $fileToUploadPath") Log.i("[Chat Room] Data is null, using $fileToUploadPath")
}
} else if (temporaryFileUploadPath.exists()) {
fileToUploadPath = temporaryFileUploadPath.absolutePath
Log.i("[Chat Room] Data is null, using $fileToUploadPath")
}
}
if (fileToUploadPath != null) {
if (fileToUploadPath.startsWith("content://") ||
fileToUploadPath.startsWith("file://")
) {
val uriToParse = Uri.parse(fileToUploadPath)
fileToUploadPath = FileUtils.getFilePath(requireContext(), uriToParse)
Log.i("[Chat] Path was using a content or file scheme, real path is: $fileToUploadPath")
if (fileToUploadPath == null) {
Log.e("[Chat] Failed to get access to file $uriToParse")
} }
} }
}
if (fileToUploadPath != null) { if (fileToUploadPath != null) {
viewModel.setPictureFromPath(fileToUploadPath) if (fileToUploadPath.startsWith("content://") ||
fileToUploadPath.startsWith("file://")
) {
val uriToParse = Uri.parse(fileToUploadPath)
fileToUploadPath = FileUtils.getFilePath(requireContext(), uriToParse)
Log.i("[Chat] Path was using a content or file scheme, real path is: $fileToUploadPath")
if (fileToUploadPath == null) {
Log.e("[Chat] Failed to get access to file $uriToParse")
}
}
}
if (fileToUploadPath != null) {
viewModel.setPictureFromPath(fileToUploadPath)
}
} }
} }
} }

View file

@ -57,7 +57,7 @@ class Api21Compatibility {
vibrator.vibrate(pattern, 1) vibrator.vibrate(pattern, 1)
} }
fun addImageToMediaStore(context: Context, content: Content): Boolean { suspend fun addImageToMediaStore(context: Context, content: Content): Boolean {
val filePath = content.filePath val filePath = content.filePath
val appName = AppUtils.getString(R.string.app_name) val appName = AppUtils.getString(R.string.app_name)
val relativePath = "${Environment.DIRECTORY_PICTURES}/$appName" val relativePath = "${Environment.DIRECTORY_PICTURES}/$appName"
@ -88,7 +88,7 @@ class Api21Compatibility {
return copyOk return copyOk
} }
fun addVideoToMediaStore(context: Context, content: Content): Boolean { suspend fun addVideoToMediaStore(context: Context, content: Content): Boolean {
val filePath = content.filePath val filePath = content.filePath
val appName = AppUtils.getString(R.string.app_name) val appName = AppUtils.getString(R.string.app_name)
val relativePath = "${Environment.DIRECTORY_MOVIES}/$appName" val relativePath = "${Environment.DIRECTORY_MOVIES}/$appName"
@ -120,7 +120,7 @@ class Api21Compatibility {
return copyOk return copyOk
} }
fun addAudioToMediaStore(context: Context, content: Content): Boolean { suspend fun addAudioToMediaStore(context: Context, content: Content): Boolean {
val filePath = content.filePath val filePath = content.filePath
val appName = AppUtils.getString(R.string.app_name) val appName = AppUtils.getString(R.string.app_name)
val relativePath = "${Environment.DIRECTORY_MUSIC}/$appName" val relativePath = "${Environment.DIRECTORY_MUSIC}/$appName"

View file

@ -33,7 +33,7 @@ import org.linphone.utils.FileUtils
@TargetApi(29) @TargetApi(29)
class Api29Compatibility { class Api29Compatibility {
companion object { companion object {
fun addImageToMediaStore(context: Context, content: Content): Boolean { suspend fun addImageToMediaStore(context: Context, content: Content): Boolean {
val filePath = content.filePath val filePath = content.filePath
val appName = AppUtils.getString(R.string.app_name) val appName = AppUtils.getString(R.string.app_name)
val relativePath = "${Environment.DIRECTORY_PICTURES}/$appName" val relativePath = "${Environment.DIRECTORY_PICTURES}/$appName"
@ -70,7 +70,7 @@ class Api29Compatibility {
return copyOk return copyOk
} }
fun addVideoToMediaStore(context: Context, content: Content): Boolean { suspend fun addVideoToMediaStore(context: Context, content: Content): Boolean {
val filePath = content.filePath val filePath = content.filePath
val appName = AppUtils.getString(R.string.app_name) val appName = AppUtils.getString(R.string.app_name)
val relativePath = "${Environment.DIRECTORY_MOVIES}/$appName" val relativePath = "${Environment.DIRECTORY_MOVIES}/$appName"
@ -108,7 +108,7 @@ class Api29Compatibility {
return copyOk return copyOk
} }
fun addAudioToMediaStore(context: Context, content: Content): Boolean { suspend fun addAudioToMediaStore(context: Context, content: Content): Boolean {
val filePath = content.filePath val filePath = content.filePath
val appName = AppUtils.getString(R.string.app_name) val appName = AppUtils.getString(R.string.app_name)
val relativePath = "${Environment.DIRECTORY_MUSIC}/$appName" val relativePath = "${Environment.DIRECTORY_MUSIC}/$appName"

View file

@ -115,21 +115,21 @@ class Compatibility {
// TODO Use removeLongLivedShortcuts() starting Android R (API 30) // TODO Use removeLongLivedShortcuts() starting Android R (API 30)
} }
fun addImageToMediaStore(context: Context, content: Content): Boolean { suspend fun addImageToMediaStore(context: Context, content: Content): Boolean {
if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10)) { if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10)) {
return Api29Compatibility.addImageToMediaStore(context, content) return Api29Compatibility.addImageToMediaStore(context, content)
} }
return Api21Compatibility.addImageToMediaStore(context, content) return Api21Compatibility.addImageToMediaStore(context, content)
} }
fun addVideoToMediaStore(context: Context, content: Content): Boolean { suspend fun addVideoToMediaStore(context: Context, content: Content): Boolean {
if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10)) { if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10)) {
return Api29Compatibility.addVideoToMediaStore(context, content) return Api29Compatibility.addVideoToMediaStore(context, content)
} }
return Api21Compatibility.addVideoToMediaStore(context, content) return Api21Compatibility.addVideoToMediaStore(context, content)
} }
fun addAudioToMediaStore(context: Context, content: Content): Boolean { suspend fun addAudioToMediaStore(context: Context, content: Content): Boolean {
if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10)) { if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10)) {
return Api29Compatibility.addAudioToMediaStore(context, content) return Api29Compatibility.addAudioToMediaStore(context, content)
} }

View file

@ -26,6 +26,10 @@ import android.provider.OpenableColumns
import java.io.* import java.io.*
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.withContext
import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
@ -109,7 +113,7 @@ class FileUtils {
} }
} }
fun getFilePath(context: Context, uri: Uri): String? { suspend fun getFilePath(context: Context, uri: Uri): String? {
var result: String? = null var result: String? = null
val name: String = getNameFromUri(uri, context) val name: String = getNameFromUri(uri, context)
@ -123,13 +127,16 @@ class FileUtils {
" to local file " + " to local file " +
localFile.absolutePath localFile.absolutePath
) )
if (copyToFile(remoteFile, localFile)) { coroutineScope {
Log.i("[File Utils] Copy successful") val deferred = async { copyToFile(remoteFile, localFile) }
result = localFile.absolutePath if (deferred.await()) {
} else { Log.i("[File Utils] Copy successful")
Log.e("[File Utils] Copy failed") result = localFile.absolutePath
} else {
Log.e("[File Utils] Copy failed")
}
remoteFile?.close()
} }
remoteFile?.close()
} catch (e: IOException) { } catch (e: IOException) {
Log.e("[File Utils] getFilePath exception: ", e) Log.e("[File Utils] getFilePath exception: ", e)
} }
@ -158,7 +165,7 @@ class FileUtils {
return name return name
} }
fun copyFileTo(filePath: String, outputStream: OutputStream?): Boolean { suspend fun copyFileTo(filePath: String, outputStream: OutputStream?): Boolean {
if (outputStream == null) { if (outputStream == null) {
Log.e("[File Utils] Can't copy file $filePath to given null output stream") Log.e("[File Utils] Can't copy file $filePath to given null output stream")
return false return false
@ -171,11 +178,13 @@ class FileUtils {
} }
try { try {
val inputStream = FileInputStream(file) withContext(Dispatchers.IO) {
val buffer = ByteArray(4096) val inputStream = FileInputStream(file)
var bytesRead: Int val buffer = ByteArray(4096)
while (inputStream.read(buffer).also { bytesRead = it } >= 0) { var bytesRead: Int
outputStream.write(buffer, 0, bytesRead) while (inputStream.read(buffer).also { bytesRead = it } >= 0) {
outputStream.write(buffer, 0, bytesRead)
}
} }
return true return true
} catch (e: IOException) { } catch (e: IOException) {
@ -184,14 +193,16 @@ class FileUtils {
return false return false
} }
private fun copyToFile(inputStream: InputStream?, destFile: File?): Boolean { private suspend fun copyToFile(inputStream: InputStream?, destFile: File?): Boolean {
if (inputStream == null || destFile == null) return false if (inputStream == null || destFile == null) return false
try { try {
FileOutputStream(destFile).use { out -> withContext(Dispatchers.IO) {
val buffer = ByteArray(4096) FileOutputStream(destFile).use { out ->
var bytesRead: Int val buffer = ByteArray(4096)
while (inputStream.read(buffer).also { bytesRead = it } >= 0) { var bytesRead: Int
out.write(buffer, 0, bytesRead) while (inputStream.read(buffer).also { bytesRead = it } >= 0) {
out.write(buffer, 0, bytesRead)
}
} }
} }
return true return true