diff --git a/app/src/main/java/com/infomaniak/drive/data/api/UploadTask.kt b/app/src/main/java/com/infomaniak/drive/data/api/UploadTask.kt index 2e9b7e702a..c4b5396d18 100644 --- a/app/src/main/java/com/infomaniak/drive/data/api/UploadTask.kt +++ b/app/src/main/java/com/infomaniak/drive/data/api/UploadTask.kt @@ -33,7 +33,6 @@ import com.infomaniak.drive.data.models.upload.ValidChunks import com.infomaniak.drive.data.services.UploadWorker import com.infomaniak.drive.data.sync.UploadNotifications import com.infomaniak.drive.ui.MainActivity -import com.infomaniak.drive.utils.KDriveHttpClient import com.infomaniak.drive.utils.NotificationUtils.CURRENT_UPLOAD_ID import com.infomaniak.drive.utils.NotificationUtils.ELAPSED_TIME import com.infomaniak.drive.utils.NotificationUtils.uploadProgressNotification @@ -49,7 +48,6 @@ import kotlinx.coroutines.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withLock -import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody import okhttp3.RequestBody.Companion.toRequestBody @@ -88,7 +86,7 @@ class UploadTask( launchTask(this) return@withContext true } catch (exception: FileNotFoundException) { - UploadFile.deleteIfExists(uploadFile.getUriObject(), keepFile = uploadFile.isSync()) + uploadFile.deleteIfExists(keepFile = uploadFile.isSync()) Sentry.withScope { scope -> scope.level = SentryLevel.WARNING scope.setExtra("data", gson.toJson(uploadFile)) @@ -125,22 +123,21 @@ class UploadTask( private suspend fun launchTask(coroutineScope: CoroutineScope) = withContext(Dispatchers.IO) { val uri = uploadFile.getUriObject() val fileInputStream = context.contentResolver.openInputStream(uploadFile.getOriginalUri(context)) - val okHttpClient = KDriveHttpClient.getHttpClient(userId = uploadFile.userId, timeout = 120) initChunkSize(uploadFile.fileSize) checkLimitParallelRequest() BufferedInputStream(fileInputStream, chunkSize).use { input -> - val waitingCoroutines = arrayListOf() + val waitingCoroutines = mutableListOf() val requestSemaphore = Semaphore(limitParallelRequest) val totalChunks = ceil(uploadFile.fileSize.toDouble() / chunkSize).toInt() if (totalChunks > TOTAL_CHUNKS) throw TotalChunksExceededException() - val uploadedChunks = uploadFile.getValidChunks(okHttpClient) + val uploadedChunks = uploadFile.getValidChunks() val isNewUploadSession = uploadedChunks?.let { needToResetUpload(it) } ?: true - if (isNewUploadSession) uploadFile.prepareUploadSession(totalChunks, okHttpClient) + if (isNewUploadSession) uploadFile.prepareUploadSession(totalChunks) Sentry.addBreadcrumb(Breadcrumb().apply { category = UploadWorker.BREADCRUMB_TAG @@ -176,19 +173,19 @@ class UploadTask( Log.d("kDrive", "Upload > Start upload ${uploadFile.fileName} to $url data size:${data.size}") waitingCoroutines.add( - coroutineScope.uploadChunkRequest(requestSemaphore, data.toRequestBody(), url, okHttpClient) + coroutineScope.uploadChunkRequest(requestSemaphore, data.toRequestBody(), url) ) } waitingCoroutines.joinAll() } coroutineScope.ensureActive() - onFinish(uri, okHttpClient) + onFinish(uri) } - private suspend fun onFinish(uri: Uri, okHttpClient: OkHttpClient) { - with(ApiRepository.finishSession(uploadFile.driveId, uploadFile.uploadToken!!, okHttpClient)) { - if (!isSuccess()) manageUploadErrors(okHttpClient) + private suspend fun onFinish(uri: Uri) = with(uploadFile) { + with(ApiRepository.finishSession(driveId, uploadToken!!, okHttpClient)) { + if (!isSuccess()) manageUploadErrors() } uploadNotification.apply { setOngoing(false) @@ -205,8 +202,7 @@ class UploadTask( private fun CoroutineScope.uploadChunkRequest( requestSemaphore: Semaphore, requestBody: RequestBody, - url: String, - okHttpClient: OkHttpClient + url: String ) = launch(Dispatchers.IO) { val uploadRequestBody = ProgressRequestBody(requestBody) { currentBytes, bytesWritten, contentLength -> launch { @@ -220,8 +216,8 @@ class UploadTask( .headers(HttpUtils.getHeaders(contentType = null)) .post(uploadRequestBody).build() - val response = okHttpClient.newCall(request).execute() - manageApiResponse(response, okHttpClient) + val response = uploadFile.okHttpClient.newCall(request).execute() + manageApiResponse(response) requestSemaphore.release() } @@ -267,7 +263,7 @@ class UploadTask( return false } - private fun manageApiResponse(response: Response, okHttpClient: OkHttpClient) { + private fun manageApiResponse(response: Response) { response.use { val bodyResponse = it.body?.string() Log.i("UploadTask", "response successful ${it.isSuccessful}") @@ -278,7 +274,7 @@ class UploadTask( } catch (e: Exception) { null } - apiResponse.manageUploadErrors(okHttpClient) + apiResponse.manageUploadErrors() } } } @@ -357,11 +353,11 @@ class UploadTask( } } - private fun UploadFile.getValidChunks(okHttpClient: OkHttpClient): ValidChunks? { + private fun UploadFile.getValidChunks(): ValidChunks? { return uploadToken?.let { ApiRepository.getValidChunks(uploadFile.driveId, it, okHttpClient).data } } - private fun UploadFile.prepareUploadSession(totalChunks: Int, okHttpClient: OkHttpClient) { + private fun UploadFile.prepareUploadSession(totalChunks: Int) { val sessionBody = UploadSession.StartSessionBody( conflict = if (replaceOnConflict()) ConflictOption.VERSION else ConflictOption.RENAME, createdAt = if (fileCreatedAt == null) null else fileCreatedAt!!.time / 1000, @@ -375,11 +371,11 @@ class UploadTask( with(ApiRepository.startUploadSession(driveId, sessionBody, okHttpClient)) { if (isSuccess()) data?.token?.let { uploadFile.updateUploadToken(it) } - else manageUploadErrors(okHttpClient) + else manageUploadErrors() } } - private fun ApiResponse?.manageUploadErrors(okHttpClient: OkHttpClient) { + private fun ApiResponse?.manageUploadErrors() { if (this?.translatedError == R.string.connectionError) throw NetworkException() when (this?.error?.code) { "file_already_exists_error" -> Unit @@ -391,9 +387,10 @@ class UploadTask( // Upload finish with 0 chunks uploaded // Upload finish with a different expected number of chunks uploadFile.uploadToken?.let { - ApiRepository.cancelSession(uploadFile.driveId, it, okHttpClient) + with(ApiRepository.cancelSession(uploadFile.driveId, it, uploadFile.okHttpClient)) { + if (data == true) uploadFile.resetUploadToken() + } } - uploadFile.resetUploadToken() throw UploadNotTerminated("Upload finish with 0 chunks uploaded or a different expected number of chunks") } "upload_error" -> { diff --git a/app/src/main/java/com/infomaniak/drive/data/models/UploadFile.kt b/app/src/main/java/com/infomaniak/drive/data/models/UploadFile.kt index b9bb7432f1..86dde01d07 100644 --- a/app/src/main/java/com/infomaniak/drive/data/models/UploadFile.kt +++ b/app/src/main/java/com/infomaniak/drive/data/models/UploadFile.kt @@ -25,14 +25,20 @@ import android.provider.DocumentsContract import android.provider.MediaStore import androidx.core.net.toFile import androidx.core.net.toUri +import com.infomaniak.drive.data.api.ApiRepository +import com.infomaniak.drive.data.api.UploadTask import com.infomaniak.drive.data.cache.DriveInfosController import com.infomaniak.drive.data.sync.UploadMigration import com.infomaniak.drive.utils.AccountUtils +import com.infomaniak.drive.utils.KDriveHttpClient import com.infomaniak.drive.utils.RealmModules +import com.infomaniak.lib.core.R import com.infomaniak.lib.core.utils.format import io.realm.* +import io.realm.annotations.Ignore import io.realm.annotations.PrimaryKey import io.realm.kotlin.oneOf +import okhttp3.OkHttpClient import java.io.File import java.util.* @@ -52,6 +58,14 @@ open class UploadFile( var userId: Int = -1 ) : RealmObject() { + @Ignore + lateinit var okHttpClient: OkHttpClient + private set + + suspend fun initOkHttpClient() { + okHttpClient = KDriveHttpClient.getHttpClient(userId = userId, timeout = 120) + } + fun createSubFolder(parent: String, createDatedSubFolders: Boolean) { remoteSubFolder = parent + if (createDatedSubFolders) "/${fileModifiedAt.format("yyyy/MM")}" else "" } @@ -109,6 +123,25 @@ open class UploadFile( uploadToken = newUploadToken } + fun deleteIfExists(keepFile: Boolean = false) { + getRealmInstance().use { realm -> + syncFileByUriQuery(realm, uri).findFirst()?.let { uploadFileProxy -> + // Cancel session if exists + uploadFileProxy.uploadToken?.let { + with(ApiRepository.cancelSession(uploadFileProxy.driveId, it, okHttpClient)) { + if (translatedError == R.string.connectionError) throw UploadTask.NetworkException() + } + } + // Delete in realm + realm.executeTransaction { + if (uploadFileProxy.isValid) { + if (keepFile) uploadFileProxy.deletedAt = Date() else uploadFileProxy.deleteFromRealm() + } + } + } + } + } + enum class Type { SYNC, UPLOAD, SHARED_FILE, SYNC_OFFLINE, CLOUD_STORAGE } @@ -122,10 +155,12 @@ open class UploadFile( .migration(UploadMigration()) .build() + private inline val Realm.uploadTable get() = where(UploadFile::class.java) + fun getRealmInstance(): Realm = Realm.getInstance(realmConfiguration) private fun syncFileByUriQuery(realm: Realm, uri: String): RealmQuery { - return realm.where(UploadFile::class.java).equalTo(UploadFile::uri.name, uri) + return realm.uploadTable.equalTo(UploadFile::uri.name, uri) } private fun pendingUploadsQuery( @@ -134,7 +169,7 @@ open class UploadFile( onlyCurrentUser: Boolean = false, driveIds: Array? = null ): RealmQuery { - return realm.where(UploadFile::class.java).apply { + return realm.uploadTable.apply { folderId?.let { equalTo(UploadFile::remoteFolder.name, it) } if (onlyCurrentUser) equalTo(UploadFile::userId.name, AccountUtils.currentUserId) driveIds?.let { oneOf(UploadFile::driveId.name, it) } @@ -196,7 +231,7 @@ open class UploadFile( } fun getAllUploadedFiles(type: String = Type.SYNC.name): ArrayList? = getRealmInstance().use { realm -> - realm.where(UploadFile::class.java) + realm.uploadTable .equalTo(UploadFile::type.name, type) .isNull(UploadFile::deletedAt.name) .isNotNull(UploadFile::uploadAt.name) @@ -235,19 +270,7 @@ open class UploadFile( } } - fun deleteIfExists(uri: Uri, keepFile: Boolean = false) { - getRealmInstance().use { realm -> - syncFileByUriQuery(realm, uri.toString()).findFirst()?.let { syncFile -> - realm.executeTransaction { - if (syncFile.isValid) { - if (keepFile) syncFile.deletedAt = Date() else syncFile.deleteFromRealm() - } - } - } - } - } - - fun deleteAll(uploadFiles: ArrayList) { + fun deleteAll(uploadFiles: List) { getRealmInstance().use { it.executeTransaction { realm -> uploadFiles.forEach { uploadFile -> @@ -269,11 +292,26 @@ open class UploadFile( } } + suspend fun cancelAllPendingFilesSessions(folderId: Int) { + getRealmInstance().use { realm -> + realm.uploadTable + .equalTo(UploadFile::remoteFolder.name, folderId) + .isNull(UploadFile::uploadAt.name) + .isNotNull(UploadFile::uploadToken.name) + .findAll()?.onEach { uploadFileProxy -> + with(uploadFileProxy) { + initOkHttpClient() + ApiRepository.cancelSession(driveId, uploadToken!!, okHttpClient) + } + } + } + } + fun deleteAll(folderId: Int?, permanently: Boolean = false) { getRealmInstance().use { it.executeTransaction { realm -> // Delete all data files for all uploads with scheme FILE - realm.where(UploadFile::class.java) + realm.uploadTable .apply { folderId?.let { equalTo(UploadFile::remoteFolder.name, folderId) } } .beginsWith(UploadFile::uri.name, ContentResolver.SCHEME_FILE) .findAll().forEach { uploadFile -> @@ -281,7 +319,7 @@ open class UploadFile( } if (permanently) { - realm.where(UploadFile::class.java) + realm.uploadTable .apply { folderId?.let { equalTo(UploadFile::remoteFolder.name, folderId) } } .isNull(UploadFile::uploadAt.name) .findAll().deleteAllFromRealm() @@ -293,7 +331,7 @@ open class UploadFile( .findAll().forEach { uploadFile -> uploadFile.deletedAt = Date() } // Delete all uploads without type SYNC - realm.where(UploadFile::class.java) + realm.uploadTable .apply { folderId?.let { equalTo(UploadFile::remoteFolder.name, folderId) } } .notEqualTo(UploadFile::type.name, Type.SYNC.name) .findAll().deleteAllFromRealm() @@ -305,7 +343,7 @@ open class UploadFile( fun deleteAllSyncFile() { getRealmInstance().use { realm -> realm.executeTransaction { - it.where(UploadFile::class.java) + it.uploadTable .equalTo(UploadFile::type.name, Type.SYNC.name) .findAll()?.deleteAllFromRealm() } diff --git a/app/src/main/java/com/infomaniak/drive/data/services/UploadWorker.kt b/app/src/main/java/com/infomaniak/drive/data/services/UploadWorker.kt index 02200ec1fe..4d9b5b01be 100644 --- a/app/src/main/java/com/infomaniak/drive/data/services/UploadWorker.kt +++ b/app/src/main/java/com/infomaniak/drive/data/services/UploadWorker.kt @@ -58,11 +58,12 @@ import java.util.* class UploadWorker(appContext: Context, params: WorkerParameters) : CoroutineWorker(appContext, params) { private lateinit var contentResolver: ContentResolver + private val failedNames by lazy { inputData.getStringArray(LAST_FAILED_NAMES)?.toMutableList() ?: mutableListOf() } + private val successNames by lazy { inputData.getStringArray(LAST_SUCCESS_NAMES)?.toMutableList() ?: mutableListOf() } + var currentUploadFile: UploadFile? = null var currentUploadTask: UploadTask? = null var uploadedCount = 0 - private val failedNames by lazy { inputData.getStringArray(LAST_FAILED_NAMES)?.toMutableList() ?: mutableListOf() } - private val successNames by lazy { inputData.getStringArray(LAST_SUCCESS_NAMES)?.toMutableList() ?: mutableListOf() } override suspend fun doWork(): Result { @@ -170,6 +171,7 @@ class UploadWorker(appContext: Context, params: WorkerParameters) : CoroutineWor currentUploadFile = this@initUpload applicationContext.cancelNotification(NotificationUtils.CURRENT_UPLOAD_ID) updateUploadCountNotification(this@initUpload, pendingCount) + initOkHttpClient() try { if (uri.scheme.equals(ContentResolver.SCHEME_FILE)) { @@ -186,15 +188,16 @@ class UploadWorker(appContext: Context, params: WorkerParameters) : CoroutineWor private suspend fun UploadFile.initUploadSchemeFile(uri: Uri): Boolean { val cacheFile = uri.toFile().apply { if (!exists()) { - UploadFile.deleteIfExists(uri) + deleteIfExists() return false } } - return startUploadFile(cacheFile.length()).also { - UploadFile.deleteIfExists(uri) - - if (!isSyncOffline()) cacheFile.delete() + return startUploadFile(cacheFile.length()).also { isUploaded -> + if (isUploaded) { + deleteIfExists() + if (!isSyncOffline()) cacheFile.delete() + } } } @@ -211,7 +214,7 @@ class UploadWorker(appContext: Context, params: WorkerParameters) : CoroutineWor null } } ?: run { - UploadFile.deleteIfExists(uri) + deleteIfExists() false } } @@ -226,7 +229,7 @@ class UploadWorker(appContext: Context, params: WorkerParameters) : CoroutineWor } } else { - UploadFile.deleteIfExists(getUriObject()) + deleteIfExists() Log.d("kDrive", "$TAG > $fileName deleted size:$size") Sentry.withScope { scope -> scope.setExtra("data", ApiController.gson.toJson(this)) @@ -239,7 +242,7 @@ class UploadWorker(appContext: Context, params: WorkerParameters) : CoroutineWor private fun UploadFile.handleException(exception: Exception, uri: Uri) { when (exception) { is SecurityException, is IllegalStateException, is IllegalArgumentException -> { - UploadFile.deleteIfExists(uri) + deleteIfExists() if (exception is IllegalStateException) { Sentry.withScope { scope -> @@ -363,7 +366,6 @@ class UploadWorker(appContext: Context, params: WorkerParameters) : CoroutineWor Log.d(TAG, "getLocalLastMediasAsync > ${mediaFolder.name}/$fileName found") if (fileName != null && UploadFile.canUpload(uri, fileModifiedAt) && fileSize > 0) { - UploadFile.deleteIfExists(uri) UploadFile( uri = uri.toString(), driveId = syncSettings.driveId, @@ -374,6 +376,7 @@ class UploadWorker(appContext: Context, params: WorkerParameters) : CoroutineWor remoteFolder = syncSettings.syncFolder, userId = syncSettings.userId ).apply { + deleteIfExists() createSubFolder(mediaFolder.name, syncSettings.createDatedSubFolders) store() } diff --git a/app/src/main/java/com/infomaniak/drive/ui/fileList/FileAdapter.kt b/app/src/main/java/com/infomaniak/drive/ui/fileList/FileAdapter.kt index 76639d8498..f5c8acc834 100644 --- a/app/src/main/java/com/infomaniak/drive/ui/fileList/FileAdapter.kt +++ b/app/src/main/java/com/infomaniak/drive/ui/fileList/FileAdapter.kt @@ -148,7 +148,7 @@ open class FileAdapter( if (fileList.isManaged) super.updateData(null) } - fun setFiles(newItemList: ArrayList) { + fun setFiles(newItemList: List) { fileList = RealmList(*newItemList.toTypedArray()) hideLoading() notifyDataSetChanged() diff --git a/app/src/main/java/com/infomaniak/drive/ui/fileList/UploadInProgressFragment.kt b/app/src/main/java/com/infomaniak/drive/ui/fileList/UploadInProgressFragment.kt index d8089b757b..b1bfdeaeae 100644 --- a/app/src/main/java/com/infomaniak/drive/ui/fileList/UploadInProgressFragment.kt +++ b/app/src/main/java/com/infomaniak/drive/ui/fileList/UploadInProgressFragment.kt @@ -27,6 +27,7 @@ import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.work.Data import com.infomaniak.drive.R +import com.infomaniak.drive.data.api.ApiRepository import com.infomaniak.drive.data.cache.FileController import com.infomaniak.drive.data.models.File import com.infomaniak.drive.data.models.UploadFile @@ -53,8 +54,8 @@ class UploadInProgressFragment : FileListFragment() { override var hideBackButtonWhenRoot: Boolean = false override var showPendingFiles = false - private var pendingUploadFiles = arrayListOf() - private var pendingFiles = arrayListOf() + private var pendingUploadFiles = mutableListOf() + private var pendingFiles = mutableListOf() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { downloadFiles = DownloadFiles() @@ -189,18 +190,23 @@ class UploadInProgressFragment : FileListFragment() { lifecycleScope.launch(Dispatchers.IO) { var needPopBackStack = false uploadFile?.let { - UploadFile.deleteAll(arrayListOf(it)) + it.initOkHttpClient() + it.uploadToken?.let { token -> + ApiRepository.cancelSession(AccountUtils.currentDriveId, token, it.okHttpClient) + } + UploadFile.deleteAll(listOf(it)) needPopBackStack = true } folderId?.let { + UploadFile.cancelAllPendingFilesSessions(folderId = it) if (isPendingFolders()) UploadFile.deleteAll(null) - else UploadFile.deleteAll(it) + else UploadFile.deleteAll(folderId = it) fileRecyclerView.post { - fileAdapter.setFiles(arrayListOf()) + fileAdapter.setFiles(listOf()) } - needPopBackStack = UploadFile.getCurrentUserPendingUploadsCount(it) == 0 + needPopBackStack = UploadFile.getCurrentUserPendingUploadsCount(folderId = it) == 0 } withContext(Dispatchers.Main) { @@ -247,7 +253,7 @@ class UploadInProgressFragment : FileListFragment() { private inner class DownloadFiles : (Boolean, Boolean) -> Unit { override fun invoke(ignoreCache: Boolean, isNewSort: Boolean) { if (!drivePermissions.checkWriteStoragePermission()) return - if (ignoreCache) fileAdapter.setFiles(arrayListOf()) + if (ignoreCache) fileAdapter.setFiles(listOf()) showLoadingTimer.start() fileAdapter.isComplete = false