diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4b914d5214..7956eae12b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -151,6 +151,7 @@ dependencies { implementation(libs.androidx.core.splashscreen) implementation(libs.recaptcha) implementation(libs.workmanager) + implementation(libs.splitties.toast) // Test testImplementation(libs.junit) diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt index 12dce4603b..3b975eb077 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportFilesViewModel.kt @@ -51,9 +51,7 @@ import com.infomaniak.swisstransfer.ui.utils.GetSetCallbacks import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject @@ -78,7 +76,6 @@ class ImportFilesViewModel @Inject constructor( initialValue = emptyList(), ) - val failedFiles = importationFilesManager.failedFiles // TODO ? (unused) val filesToImportCount = importationFilesManager.filesToImportCount val currentSessionFilesCount = importationFilesManager.currentSessionFilesCount diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportLocalStorage.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportLocalStorage.kt index 8c5f7adc85..3e62d7c7e2 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportLocalStorage.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportLocalStorage.kt @@ -18,7 +18,6 @@ package com.infomaniak.swisstransfer.ui.screen.newtransfer import android.content.Context -import com.infomaniak.sentry.SentryLog import dagger.hilt.android.qualifiers.ApplicationContext import java.io.File import java.io.InputStream @@ -39,20 +38,12 @@ class ImportLocalStorage @Inject constructor(@ApplicationContext private val app fun getLocalFiles(): Array? = importFolder.listFiles() - fun copyUriDataLocally(inputStream: InputStream, fileName: String): File? { - val file = File(getImportFolderOrCreate(), fileName) - - if (file.exists()) file.delete() - runCatching { file.createNewFile() }.onFailure { return null } - - runCatching { + fun copyUriDataLocally(inputStream: InputStream, fileName: String): Result = runCatching { + File(getImportFolderOrCreate(), fileName).also { file -> + if (file.exists()) file.delete() + file.createNewFile() copyStreams(inputStream, file.outputStream()) - }.onFailure { - SentryLog.w(TAG, "Caught an exception while copying file to local storage: $it") - return null } - - return file } private fun copyStreams(inputStream: InputStream, outputStream: OutputStream): Long { diff --git a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportationFilesManager.kt b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportationFilesManager.kt index 3fd34f0405..1520719577 100644 --- a/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportationFilesManager.kt +++ b/app/src/main/java/com/infomaniak/swisstransfer/ui/screen/newtransfer/ImportationFilesManager.kt @@ -26,14 +26,17 @@ import androidx.core.net.toUri import com.infomaniak.library.filetypes.FileType import com.infomaniak.multiplatform_swisstransfer.common.interfaces.ui.FileUi import com.infomaniak.sentry.SentryLog +import com.infomaniak.swisstransfer.R import com.infomaniak.swisstransfer.ui.utils.FileNameUtils import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.scopes.ViewModelScoped import io.sentry.Sentry import io.sentry.SentryLevel -import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.invoke +import splitties.toast.UnreliableToastApi +import splitties.toast.longToast import java.io.File import java.io.InputStream import javax.inject.Inject @@ -51,9 +54,6 @@ class ImportationFilesManager @Inject constructor( private val _importedFiles = FilesMutableStateFlow() val importedFiles = _importedFiles.flow - private val _failedFiles = MutableSharedFlow() - val failedFiles = _failedFiles.asSharedFlow() - // Importing a file locally can take up time. We can't base the list of already used names on _importedFiles's value because a // new import with the same name could occur while the file is still importing. This would lead to a name collision. // This list needs to mark a name as "taken" as soon as the file is queued to be imported and until the file is removed from @@ -92,12 +92,11 @@ class ImportationFilesManager @Inject constructor( suspend fun continuouslyCopyPickedFilesToLocalStorage() { filesToImportChannel.consume { fileToImport -> SentryLog.i(TAG, "Importing ${fileToImport.uri}") - val copiedFile = copyUriDataLocally(fileToImport.uri, fileToImport.fileName) - - if (copiedFile == null) { - reportFailedImportation(fileToImport) - return@consume - } + val copiedFile = Dispatchers.IO { + copyUriDataLocally(fileToImport.uri, fileToImport.fileName).onFailure { + reportFailedImportation(fileToImport, it) + }.getOrNull() + } ?: return@consume SentryLog.i(TAG, "Successfully imported ${fileToImport.uri}") @@ -115,17 +114,17 @@ class ImportationFilesManager @Inject constructor( } } - private fun copyUriDataLocally(uri: Uri, fileName: String): File? { - val inputStream = openInputStream(uri) ?: return null + private fun copyUriDataLocally( + uri: Uri, + fileName: String + ): Result = openInputStream(uri).mapCatching { inputStream -> return importLocalStorage.copyUriDataLocally(inputStream, fileName) } - private fun openInputStream(uri: Uri): InputStream? { - return runCatching { appContext.contentResolver.openInputStream(uri) } - .onSuccess { - if (it == null) SentryLog.w(ImportLocalStorage.TAG, "During local copy of the file openInputStream returned null") - } - .getOrNull() + private fun openInputStream(uri: Uri): Result = runCatching { + appContext.contentResolver.openInputStream(uri) + }.mapCatching { + it ?: throw NullPointerException("The provider recently crashed") } private fun getRestoredFileUi(localFiles: Array): List { @@ -190,9 +189,13 @@ class ImportationFilesManager @Inject constructor( return getColumnIndex(column).takeIf { it != -1 } } - private suspend fun reportFailedImportation(file: PickedFile) { - SentryLog.e(TAG, "Failed importation of ${file.uri}") - _failedFiles.emit(file) + private fun reportFailedImportation(file: PickedFile, throwable: Throwable) { + SentryLog.e(TAG, "Failed importation of ${file.uri}", throwable) + + //TODO: Make more precise error messages (especially for the low storage issue). + val errorMessage = appContext.getString(R.string.cant_import_file_x, file.fileName) + @OptIn(UnreliableToastApi::class) + longToast(errorMessage) //TODO: Find a better way to show the error in an actionable way. } data class PickedFile(val fileName: String, val fileSizeInBytes: Long, val uri: Uri) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index a32c8b8fc0..20ba580d99 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -26,6 +26,7 @@ Terminer Partager Démarrer + Can\'t import the file %s Retour Fermer Cacher le mot de passe diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 451c12f490..7115d0c9bc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -30,6 +30,7 @@ Finish Share Start + Can\'t import the file %s Back Close Hide password