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,11 +149,15 @@ 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 -> {
lifecycleScope.launch {
handleSendImage(intent) handleSendImage(intent)
} }
}
Intent.ACTION_SEND_MULTIPLE -> { Intent.ACTION_SEND_MULTIPLE -> {
lifecycleScope.launch {
handleSendMultipleImages(intent) 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)) {
val contactUri = intent.data val contactUri = intent.data
@ -218,14 +224,19 @@ 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 {
val deferred = async {
FileUtils.getFilePath(this@MainActivity, it)
}
val path = deferred.await()
if (path != null) { if (path != null) {
list.add(path) list.add(path)
Log.i("[Main Activity] Found single file to share: $path") Log.i("[Main Activity] Found single file to share: $path")
} }
}
sharedViewModel.filesToShare.value = list sharedViewModel.filesToShare.value = list
val deepLink = "linphone-android://chat/" val deepLink = "linphone-android://chat/"
@ -234,15 +245,21 @@ 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>()
coroutineScope {
val deferred = arrayListOf<Deferred<String?>>()
for (parcelable in it) { for (parcelable in it) {
val uri = parcelable as Uri val uri = parcelable as Uri
val path = FileUtils.getFilePath(this, uri) 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") Log.i("[Main Activity] Found file to share: $path")
if (path != null) list.add(path) if (path != null) list.add(path)
} }
}
sharedViewModel.filesToShare.value = list sharedViewModel.filesToShare.value = list
val deepLink = "linphone-android://chat/" val deepLink = "linphone-android://chat/"

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,8 +301,8 @@ 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) {
lifecycleScope.launch {
var fileToUploadPath: String? = null var fileToUploadPath: String? = null
val temporaryFileUploadPath = chatSendingViewModel.temporaryFileUploadPath val temporaryFileUploadPath = chatSendingViewModel.temporaryFileUploadPath
if (temporaryFileUploadPath != null) { if (temporaryFileUploadPath != null) {
if (data != null) { if (data != null) {
@ -336,6 +338,7 @@ class DetailChatRoomFragment : MasterFragment() {
} }
} }
} }
}
private fun enterEditionMode() { private fun enterEditionMode() {
listSelectionViewModel.isEditionEnabled.value = true listSelectionViewModel.isEditionEnabled.value = true

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,6 +207,7 @@ class ChatMessageViewModel(
} }
private fun addContentToMediaStore(content: Content) { private fun addContentToMediaStore(content: Content) {
viewModelScope.launch {
when (content.type) { when (content.type) {
"image" -> { "image" -> {
if (Compatibility.addImageToMediaStore(coreContext.context, content)) { if (Compatibility.addImageToMediaStore(coreContext.context, content)) {
@ -232,4 +235,5 @@ class ChatMessageViewModel(
} }
} }
} }
}
} }

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,6 +132,7 @@ 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) {
lifecycleScope.launch {
var fileToUploadPath: String? = null var fileToUploadPath: String? = null
val temporaryFileUploadPath = temporaryPicturePath val temporaryFileUploadPath = temporaryPicturePath
@ -167,6 +170,7 @@ class ContactEditorFragment : Fragment() {
} }
} }
} }
}
private fun pickFile() { private fun pickFile() {
val cameraIntents = ArrayList<Intent>() val cameraIntents = ArrayList<Intent>()

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 {
val deferred = async { copyToFile(remoteFile, localFile) }
if (deferred.await()) {
Log.i("[File Utils] Copy successful") Log.i("[File Utils] Copy successful")
result = localFile.absolutePath result = localFile.absolutePath
} else { } else {
Log.e("[File Utils] Copy failed") 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,12 +178,14 @@ class FileUtils {
} }
try { try {
withContext(Dispatchers.IO) {
val inputStream = FileInputStream(file) val inputStream = FileInputStream(file)
val buffer = ByteArray(4096) val buffer = ByteArray(4096)
var bytesRead: Int var bytesRead: Int
while (inputStream.read(buffer).also { bytesRead = it } >= 0) { while (inputStream.read(buffer).also { bytesRead = it } >= 0) {
outputStream.write(buffer, 0, bytesRead) outputStream.write(buffer, 0, bytesRead)
} }
}
return true return true
} catch (e: IOException) { } catch (e: IOException) {
Log.e("[File Utils] copyFileTo exception: $e") Log.e("[File Utils] copyFileTo exception: $e")
@ -184,9 +193,10 @@ 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 {
withContext(Dispatchers.IO) {
FileOutputStream(destFile).use { out -> FileOutputStream(destFile).use { out ->
val buffer = ByteArray(4096) val buffer = ByteArray(4096)
var bytesRead: Int var bytesRead: Int
@ -194,6 +204,7 @@ class FileUtils {
out.write(buffer, 0, bytesRead) out.write(buffer, 0, bytesRead)
} }
} }
}
return true return true
} catch (e: IOException) { } catch (e: IOException) {
Log.e("[File Utils] copyToFile exception: $e") Log.e("[File Utils] copyToFile exception: $e")