diff --git a/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt b/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt index 859df7d714..005ea362a6 100644 --- a/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt +++ b/vector/src/main/java/im/vector/app/core/utils/ExternalApplicationsUtil.kt @@ -43,8 +43,7 @@ import im.vector.app.R import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.themes.ThemeUtils import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import okio.buffer import okio.sink import okio.source @@ -57,6 +56,7 @@ import timber.log.Timber import java.io.File import java.io.FileInputStream import java.io.FileOutputStream +import java.lang.IllegalStateException import java.text.SimpleDateFormat import java.util.Date import java.util.Locale @@ -344,90 +344,93 @@ private fun appendTimeToFilename(name: String): String { return """${filename}_$dateExtension.$fileExtension""" } -fun saveMedia(context: Context, file: File, title: String, mediaMimeType: String?, notificationUtils: NotificationUtils) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - val filename = appendTimeToFilename(title) +suspend fun saveMedia(context: Context, file: File, title: String, mediaMimeType: String?, notificationUtils: NotificationUtils) { + withContext(Dispatchers.IO) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val filename = appendTimeToFilename(title) - val values = ContentValues().apply { - put(MediaStore.Images.Media.TITLE, filename) - put(MediaStore.Images.Media.DISPLAY_NAME, filename) - put(MediaStore.Images.Media.MIME_TYPE, mediaMimeType) - put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis()) - put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis()) - } - val externalContentUri = when { - mediaMimeType?.isMimeTypeImage() == true -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI - mediaMimeType?.isMimeTypeVideo() == true -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI - mediaMimeType?.isMimeTypeAudio() == true -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI - else -> MediaStore.Downloads.EXTERNAL_CONTENT_URI - } + val values = ContentValues().apply { + put(MediaStore.Images.Media.TITLE, filename) + put(MediaStore.Images.Media.DISPLAY_NAME, filename) + put(MediaStore.Images.Media.MIME_TYPE, mediaMimeType) + put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis()) + put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis()) + } + val externalContentUri = when { + mediaMimeType?.isMimeTypeImage() == true -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI + mediaMimeType?.isMimeTypeVideo() == true -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI + mediaMimeType?.isMimeTypeAudio() == true -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + else -> MediaStore.Downloads.EXTERNAL_CONTENT_URI + } - val uri = context.contentResolver.insert(externalContentUri, values) - if (uri == null) { - Toast.makeText(context, R.string.error_saving_media_file, Toast.LENGTH_LONG).show() - } else { - val source = file.inputStream().source().buffer() - context.contentResolver.openOutputStream(uri)?.sink()?.buffer()?.let { sink -> - source.use { input -> - sink.use { output -> - output.writeAll(input) + val uri = context.contentResolver.insert(externalContentUri, values) + if (uri == null) { + Toast.makeText(context, R.string.error_saving_media_file, Toast.LENGTH_LONG).show() + throw IllegalStateException(context.getString(R.string.error_saving_media_file)) + } else { + val source = file.inputStream().source().buffer() + context.contentResolver.openOutputStream(uri)?.sink()?.buffer()?.let { sink -> + source.use { input -> + sink.use { output -> + output.writeAll(input) + } } } + notificationUtils.buildDownloadFileNotification( + uri, + filename, + mediaMimeType ?: MimeTypes.OctetStream + ).let { notification -> + notificationUtils.showNotificationMessage("DL", uri.hashCode(), notification) + } } - notificationUtils.buildDownloadFileNotification( - uri, - filename, - mediaMimeType ?: MimeTypes.OctetStream - ).let { notification -> - notificationUtils.showNotificationMessage("DL", uri.hashCode(), notification) - } + } else { + saveMediaLegacy(context, mediaMimeType, title, file) } - } else { - saveMediaLegacy(context, mediaMimeType, title, file) } } @Suppress("DEPRECATION") -private fun saveMediaLegacy(context: Context, mediaMimeType: String?, title: String, file: File) { +private fun saveMediaLegacy(context: Context, + mediaMimeType: String?, + title: String, + file: File) { val state = Environment.getExternalStorageState() if (Environment.MEDIA_MOUNTED != state) { context.toast(context.getString(R.string.error_saving_media_file)) - return + throw IllegalStateException(context.getString(R.string.error_saving_media_file)) } - GlobalScope.launch(Dispatchers.IO) { - val dest = when { - mediaMimeType?.isMimeTypeImage() == true -> Environment.DIRECTORY_PICTURES - mediaMimeType?.isMimeTypeVideo() == true -> Environment.DIRECTORY_MOVIES - mediaMimeType?.isMimeTypeAudio() == true -> Environment.DIRECTORY_MUSIC - else -> Environment.DIRECTORY_DOWNLOADS + val dest = when { + mediaMimeType?.isMimeTypeImage() == true -> Environment.DIRECTORY_PICTURES + mediaMimeType?.isMimeTypeVideo() == true -> Environment.DIRECTORY_MOVIES + mediaMimeType?.isMimeTypeAudio() == true -> Environment.DIRECTORY_MUSIC + else -> Environment.DIRECTORY_DOWNLOADS + } + val downloadDir = Environment.getExternalStoragePublicDirectory(dest) + try { + val outputFilename = if (title.substringAfterLast('.', "").isEmpty()) { + val extension = mediaMimeType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } + "$title.$extension" + } else { + title } - val downloadDir = Environment.getExternalStoragePublicDirectory(dest) - try { - val outputFilename = if (title.substringAfterLast('.', "").isEmpty()) { - val extension = mediaMimeType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) } - "$title.$extension" - } else { - title - } - val savedFile = saveFileIntoLegacy(file, downloadDir, outputFilename) - if (savedFile != null) { - val downloadManager = context.getSystemService() - downloadManager?.addCompletedDownload( - savedFile.name, - title, - true, - mediaMimeType ?: MimeTypes.OctetStream, - savedFile.absolutePath, - savedFile.length(), - true) - addToGallery(savedFile, mediaMimeType, context) - } - } catch (error: Throwable) { - GlobalScope.launch(Dispatchers.Main) { - context.toast(context.getString(R.string.error_saving_media_file)) - } + val savedFile = saveFileIntoLegacy(file, downloadDir, outputFilename) + if (savedFile != null) { + val downloadManager = context.getSystemService() + downloadManager?.addCompletedDownload( + savedFile.name, + title, + true, + mediaMimeType ?: MimeTypes.OctetStream, + savedFile.absolutePath, + savedFile.length(), + true) + addToGallery(savedFile, mediaMimeType, context) } + } catch (error: Throwable) { + context.toast(context.getString(R.string.error_saving_media_file)) + throw error } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index cabd69ecf9..534bf55b33 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -1745,20 +1745,19 @@ class RoomDetailFragment @Inject constructor( session.coroutineScope.launch { val result = runCatching { session.fileService().downloadFile(messageContent = action.messageContent) } if (!isAdded) return@launch - result.fold( - { - saveMedia( - context = requireContext(), - file = it, - title = action.messageContent.body, - mediaMimeType = action.messageContent.mimeType ?: getMimeTypeFromUri(requireContext(), it.toUri()), - notificationUtils = notificationUtils - ) - }, - { + result.mapCatching { + saveMedia( + context = requireContext(), + file = it, + title = action.messageContent.body, + mediaMimeType = action.messageContent.mimeType ?: getMimeTypeFromUri(requireContext(), it.toUri()), + notificationUtils = notificationUtils + ) + } + .onFailure { + if (!isAdded) return@onFailure showErrorInSnackbar(it) } - ) } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt index 3867485e6f..dde71d75ad 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/RoomUploadsFragment.kt @@ -21,6 +21,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.net.toUri +import androidx.lifecycle.lifecycleScope import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState @@ -36,6 +37,7 @@ import im.vector.app.databinding.FragmentRoomUploadsBinding import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.roomprofile.RoomProfileArgs +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject @@ -76,13 +78,22 @@ class RoomUploadsFragment @Inject constructor( shareMedia(requireContext(), it.file, getMimeTypeFromUri(requireContext(), it.file.toUri())) } is RoomUploadsViewEvents.FileReadyForSaving -> { - saveMedia( - context = requireContext(), - file = it.file, - title = it.title, - mediaMimeType = getMimeTypeFromUri(requireContext(), it.file.toUri()), - notificationUtils = notificationUtils - ) + lifecycleScope.launch { + runCatching { + saveMedia( + context = requireContext(), + file = it.file, + title = it.title, + mediaMimeType = getMimeTypeFromUri(requireContext(), it.file.toUri()), + notificationUtils = notificationUtils + ) + }.onFailure { failure -> + if (!isAdded) return@onFailure + showErrorInSnackbar(failure) + } + + } + Unit } is RoomUploadsViewEvents.Failure -> showFailure(it.throwable) }.exhaustive