Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions opencloudApp/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />

<application
android:name=".MainApp"
Expand Down Expand Up @@ -178,6 +179,11 @@
</provider>

<service android:name=".services.OperationsService" />
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"
android:foregroundServiceType="dataSync"
android:exported="false"
tools:replace="android:foregroundServiceType" />
<service
android:name=".media.MediaService"
android:foregroundServiceType="mediaPlayback"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@
package eu.opencloud.android.usecases.transfers.uploads

import android.net.Uri
import androidx.work.BackoffPolicy
import androidx.work.Constraints
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import java.util.concurrent.TimeUnit
import eu.opencloud.android.domain.BaseUseCase
import eu.opencloud.android.domain.automaticuploads.model.UploadBehavior
import eu.opencloud.android.workers.RemoveSourceFileWorker
Expand Down Expand Up @@ -65,6 +67,11 @@ class UploadFileFromContentUriUseCase(
val uploadFileFromContentUriWorker = OneTimeWorkRequestBuilder<UploadFileFromContentUriWorker>()
.setInputData(inputDataUploadFileFromContentUriWorker)
.setConstraints(constraints)
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
10,
TimeUnit.SECONDS
)
.addTag(params.accountName)
.addTag(params.uploadIdInStorageManager.toString())
.build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@

package eu.opencloud.android.usecases.transfers.uploads

import androidx.work.BackoffPolicy
import androidx.work.Constraints
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import java.util.concurrent.TimeUnit
import eu.opencloud.android.domain.BaseUseCase
import eu.opencloud.android.domain.automaticuploads.model.UploadBehavior
import eu.opencloud.android.workers.RemoveSourceFileWorker
Expand Down Expand Up @@ -64,6 +66,11 @@ class UploadFileFromSystemUseCase(
val uploadFileFromSystemWorker = OneTimeWorkRequestBuilder<UploadFileFromFileSystemWorker>()
.setInputData(inputDataUploadFileFromFileSystemWorker)
.setConstraints(constraints)
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
10,
TimeUnit.SECONDS
)
.addTag(params.accountName)
.addTag(params.uploadIdInStorageManager.toString())
.build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class TusUploadHelper(

var tusUrl = transfer.tusUploadUrl
val checksumHex = transfer.tusUploadChecksum?.substringAfter("sha256:")
var createdOffset: Long? = null

if (tusUrl.isNullOrBlank()) {
val fileName = File(remotePath).name
Expand All @@ -69,7 +70,7 @@ class TusUploadHelper(

// Use creation-with-upload like the browser does for OpenCloud compatibility
val firstChunkSize = minOf(CreateTusUploadRemoteOperation.DEFAULT_FIRST_CHUNK, fileSize)
val createdLocation = executeRemoteOperation {
val creationResult = executeRemoteOperation {
CreateTusUploadRemoteOperation(
file = File(localPath),
remotePath = remotePath,
Expand All @@ -82,11 +83,12 @@ class TusUploadHelper(
).execute(client)
}

if (createdLocation.isNullOrBlank()) {
throw IllegalStateException("TUS: unable to create upload resource for $remotePath")
if (creationResult == null) {
throw java.io.IOException("TUS: unable to create upload resource for $remotePath")
}

tusUrl = createdLocation
tusUrl = creationResult.uploadUrl
createdOffset = creationResult.uploadOffset
val metadataString = metadata.entries.joinToString(";") { (key, value) -> "$key=$value" }

transferRepository.updateTusState(
Expand All @@ -103,16 +105,20 @@ class TusUploadHelper(

val resolvedTusUrl = tusUrl ?: throw IllegalStateException("TUS: missing upload URL for $remotePath")

var offset = try {
executeRemoteOperation {
GetTusUploadOffsetRemoteOperation(resolvedTusUrl).execute(client)
var offset = if (createdOffset != null) {
createdOffset
} else {
try {
executeRemoteOperation {
GetTusUploadOffsetRemoteOperation(resolvedTusUrl).execute(client)
}
} catch (e: java.io.IOException) {
Timber.w(e, "TUS: failed to fetch current offset")
throw e
} catch (e: Throwable) {
Timber.w(e, "TUS: failed to fetch current offset")
0L
}
} catch (e: java.io.IOException) {
Timber.w(e, "TUS: failed to fetch current offset")
throw e
} catch (e: Throwable) {
Timber.w(e, "TUS: failed to fetch current offset")
0L
}.coerceAtLeast(0L)
Timber.d("TUS: resume offset %d / %d", offset, fileSize)
progressCallback?.invoke(offset, fileSize)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,27 @@
package eu.opencloud.android.workers

import android.accounts.Account
import android.app.Notification
import android.content.Context
import android.content.pm.ServiceInfo
import android.net.Uri
import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile
import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import eu.opencloud.android.R
import eu.opencloud.android.data.executeRemoteOperation
import eu.opencloud.android.data.providers.LocalStorageProvider
import eu.opencloud.android.domain.automaticuploads.model.UploadBehavior
import eu.opencloud.android.domain.exceptions.LocalFileNotFoundException
import eu.opencloud.android.domain.exceptions.NetworkErrorException
import eu.opencloud.android.domain.exceptions.NoConnectionWithServerException
import eu.opencloud.android.domain.exceptions.NoNetworkConnectionException
import eu.opencloud.android.domain.exceptions.ServerConnectionTimeoutException
import eu.opencloud.android.domain.exceptions.ServerNotReachableException
import eu.opencloud.android.domain.exceptions.ServerResponseTimeoutException
import eu.opencloud.android.domain.exceptions.UnauthorizedException
import eu.opencloud.android.domain.files.usecases.GetWebDavUrlForSpaceUseCase
import eu.opencloud.android.domain.transfers.TransferRepository
Expand All @@ -51,11 +60,11 @@ import eu.opencloud.android.lib.common.network.OnDatatransferProgressListener
import eu.opencloud.android.lib.common.operations.RemoteOperationResult.ResultCode
import eu.opencloud.android.lib.resources.files.CheckPathExistenceRemoteOperation
import eu.opencloud.android.lib.resources.files.CreateRemoteFolderOperation
import eu.opencloud.android.lib.resources.files.UploadFileFromFileSystemOperation
import eu.opencloud.android.presentation.authentication.AccountUtils
import eu.opencloud.android.utils.NotificationUtils
import eu.opencloud.android.utils.UPLOAD_NOTIFICATION_CHANNEL_ID
import eu.opencloud.android.utils.RemoteFileUtils.getAvailableRemotePath
import eu.opencloud.android.lib.resources.files.UploadFileFromFileSystemOperation
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
Expand All @@ -65,7 +74,6 @@ import timber.log.Timber
import java.io.File
import java.io.FileOutputStream
import java.io.IOException

import kotlin.coroutines.cancellation.CancellationException

class UploadFileFromContentUriWorker(
Expand All @@ -91,14 +99,19 @@ class UploadFileFromContentUriWorker(
private lateinit var uploadFileOperation: UploadFileFromFileSystemOperation
private val tusUploadHelper by lazy { TusUploadHelper(transferRepository) }

private var lastPercent = 0
private var lastPercent = -1

private var foregroundInitialized = false
private var currentForegroundProgress = -1
private val foregroundScope = CoroutineScope(Dispatchers.IO)

private val transferRepository: TransferRepository by inject()
private val getWebdavUrlForSpaceUseCase: GetWebDavUrlForSpaceUseCase by inject()
private val getStoredCapabilitiesUseCase: GetStoredCapabilitiesUseCase by inject()

override suspend fun doWork(): Result = try {
prepareFile()
startForeground()
val clientForThisUpload = getClientForThisUpload()
checkParentFolderExistence(clientForThisUpload)
checkNameCollisionAndGetAnAvailableOneInCase(clientForThisUpload)
Expand All @@ -118,7 +131,6 @@ class UploadFileFromContentUriWorker(
}
}


private fun prepareFile() {
if (!areParametersValid()) return

Expand Down Expand Up @@ -324,12 +336,9 @@ class UploadFileFromContentUriWorker(
)
}

if (attemptedTus) {
clearTusState()
}

Timber.d("Falling back to single PUT upload for %s", uploadPath)
uploadPlainFile(client)
clearTusState()
removeCacheFile()
}

Expand All @@ -345,7 +354,10 @@ class UploadFileFromContentUriWorker(
addDataTransferProgressListener(this@UploadFileFromContentUriWorker)
}

executeRemoteOperation { uploadFileOperation.execute(client) }
val result = executeRemoteOperation { uploadFileOperation.execute(client) }
if (result == Unit) {
clearTusState()
}
}

private fun updateProgressFromTus(offset: Long, totalSize: Long) {
Expand All @@ -357,6 +369,7 @@ class UploadFileFromContentUriWorker(
val progress = workDataOf(DownloadFileWorker.WORKER_KEY_PROGRESS to percent)
setProgress(progress)
}
scheduleForegroundUpdate(percent)
lastPercent = percent
}

Expand All @@ -383,6 +396,13 @@ class UploadFileFromContentUriWorker(
if (throwable is UnauthorizedException || throwable is LocalFileNotFoundException) return false
if (throwable is CancellationException) return true
if (throwable is IOException) return true
// Retry on network-related exceptions
if (throwable is NoConnectionWithServerException) return true
if (throwable is NoNetworkConnectionException) return true
if (throwable is ServerNotReachableException) return true
if (throwable is ServerConnectionTimeoutException) return true
if (throwable is ServerResponseTimeoutException) return true
if (throwable is NetworkErrorException) return true
return shouldRetry(throwable.cause)
}

Expand Down Expand Up @@ -442,9 +462,78 @@ class UploadFileFromContentUriWorker(
setProgress(progress)
}

scheduleForegroundUpdate(percent)
lastPercent = percent
}

private suspend fun startForeground() {
if (foregroundInitialized) return
foregroundInitialized = true
currentForegroundProgress = Int.MIN_VALUE
try {
setForeground(createForegroundInfo(-1))
} catch (e: Exception) {
Timber.w(e, "Failed to set foreground for upload worker")
}
currentForegroundProgress = -1
}

private fun scheduleForegroundUpdate(progress: Int) {
if (!foregroundInitialized) return
if (progress == currentForegroundProgress) return
currentForegroundProgress = progress
foregroundScope.launch {
try {
setForeground(createForegroundInfo(progress))
} catch (e: Exception) {
Timber.w(e, "Failed to update foreground notification")
}
}
}

private fun createForegroundInfo(progress: Int): ForegroundInfo =
ForegroundInfo(
computeNotificationId(),
buildForegroundNotification(progress),
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
)

private fun buildForegroundNotification(progress: Int): Notification {
val fileName = File(uploadPath).name
val builder = NotificationUtils
.newNotificationBuilder(appContext, UPLOAD_NOTIFICATION_CHANNEL_ID)
.setContentTitle(appContext.getString(R.string.uploader_upload_in_progress_ticker))
.setContentIntent(NotificationUtils.composePendingIntentToUploadList(appContext))
.setOnlyAlertOnce(true)
.setOngoing(true)
.setSubText(fileName)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder how this behaves if there is multiple (big) uploads going on at same time. But fine for now, can solve this later if there is an issue.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

Do u mean that?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe. Are those two differently named files? At least on server one of them should be (2) if it's two concurrent uploads?
How does it look in the uploads tab in the app?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No they have same file name, wait. Let me reproduce it to u with real different file names.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And i just uploaded the same file twice. We need to see how it behaves at end! If it does rename automatically to (2) !! :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really good catch from u. If it has the same file name, the second upload gets cancelled basically. Thats not a good result. Can we merge the PRs first, then we go for the fix in a seperate PR? It is really a chaotic situation otherwise. We will find a good solution, pretty sure :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can also do it in this PR if u dont mind...


if (progress in 0..100) {
builder.setContentText(
appContext.getString(
R.string.uploader_upload_in_progress_content,
progress,
fileName
)
)
builder.setProgress(100, progress, false)
} else {
builder.setContentText(appContext.getString(R.string.uploader_upload_in_progress_ticker))
builder.setProgress(0, 0, true)
}

return builder.build()
}

private fun computeNotificationId(): Int {
val id = uploadIdInStorageManager
return if (id in Int.MIN_VALUE.toLong()..Int.MAX_VALUE.toLong()) {
id.toInt()
} else {
id.hashCode()
}
}

companion object {
const val KEY_PARAM_ACCOUNT_NAME = "KEY_PARAM_ACCOUNT_NAME"
const val KEY_PARAM_BEHAVIOR = "KEY_PARAM_BEHAVIOR"
Expand Down
Loading
Loading