Skip to content

BugFix - Track File Upload Index #14822

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.nextcloud.client.core.ClockImpl
import com.nextcloud.client.database.dao.ArbitraryDataDao
import com.nextcloud.client.database.dao.FileDao
import com.nextcloud.client.database.dao.OfflineOperationDao
import com.nextcloud.client.database.dao.UploadDao
import com.nextcloud.client.database.entity.ArbitraryDataEntity
import com.nextcloud.client.database.entity.CapabilityEntity
import com.nextcloud.client.database.entity.ExternalLinkEntity
Expand Down Expand Up @@ -83,6 +84,7 @@ abstract class NextcloudDatabase : RoomDatabase() {
abstract fun arbitraryDataDao(): ArbitraryDataDao
abstract fun fileDao(): FileDao
abstract fun offlineOperationDao(): OfflineOperationDao
abstract fun uploadDao(): UploadDao

companion object {
const val FIRST_ROOM_DB_VERSION = 65
Expand Down
22 changes: 22 additions & 0 deletions app/src/main/java/com/nextcloud/client/database/dao/UploadDao.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2025 Alper Ozturk <[email protected]>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

package com.nextcloud.client.database.dao

import androidx.room.Dao
import androidx.room.Query
import com.owncloud.android.db.ProviderMeta.ProviderTableMeta

@Dao
interface UploadDao {
@Query(
"SELECT _id FROM " + ProviderTableMeta.UPLOADS_TABLE_NAME +
" WHERE " + ProviderTableMeta.UPLOADS_STATUS + " = :status AND " +
ProviderTableMeta.UPLOADS_ACCOUNT_NAME + " = :accountName AND _id IS NOT NULL"
)
fun getAllIds(status: Int, accountName: String): List<Int>
}
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ interface BackgroundJobManager {

fun startNotificationJob(subject: String, signature: String)
fun startAccountRemovalJob(accountName: String, remoteWipe: Boolean)
fun startFilesUploadJob(user: User)
fun startFilesUploadJob(user: User, uploadIds: LongArray)
fun getFileUploads(user: User): LiveData<List<JobInfo>>
fun cancelFilesUploadJob(user: User)
fun isStartFileUploadJobScheduled(user: User): Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import com.owncloud.android.operations.DownloadType
import java.util.Date
import java.util.UUID
import java.util.concurrent.TimeUnit
import kotlin.random.Random
import kotlin.reflect.KClass

/**
Expand Down Expand Up @@ -579,18 +580,31 @@ internal class BackgroundJobManagerImpl(
return workManager.isWorkScheduled(startFileUploadJobTag(user))
}

override fun startFilesUploadJob(user: User) {
val data = workDataOf(FileUploadWorker.ACCOUNT to user.accountName)

val tag = startFileUploadJobTag(user)
/**
* This method supports initiating uploads for various scenarios, including:
* - New upload batches
* - Failed uploads
* - FilesSyncWork
*
* A unique tag is generated for each upload job. This is intentional because this job may encapsulate an arbitrary
* number of files so it's safer to treat each invocation as an independent job.
*
* @param user The user for whom the upload job is being created.
* @param uploadIds array of upload ids
*/
override fun startFilesUploadJob(user: User, uploadIds: LongArray) {
val tag = startFileUploadJobTag(user) + Random.nextLong()
val dataBuilder = Data.Builder()
.putString(FileUploadWorker.ACCOUNT, user.accountName)
.putLongArray(FileUploadWorker.UPLOAD_IDS, uploadIds)

val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()

val request = oneTimeRequestBuilder(FileUploadWorker::class, JOB_FILES_UPLOAD, user)
.addTag(tag)
.setInputData(data)
.setInputData(dataBuilder.build())
.setConstraints(constraints)
.build()

Expand Down
3 changes: 2 additions & 1 deletion app/src/main/java/com/nextcloud/client/jobs/FilesSyncWork.kt
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ class FilesSyncWork(

val user = userAccountManager.getUser(syncedFolder.account)
if (user.isPresent) {
backgroundJobManager.startFilesUploadJob(user.get())
var uploadIds = uploadsStorageManager.getCurrentUploadIds(user.get().accountName)
backgroundJobManager.startFilesUploadJob(user.get(), uploadIds)
}

// Get changed files from ContentObserverWork (only images and videos) or by scanning filesystem
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.nextcloud.client.jobs.BackgroundJobManager
import com.nextcloud.client.jobs.upload.FileUploadWorker.Companion.currentUploadFileOperation
import com.nextcloud.client.network.Connectivity
import com.nextcloud.client.network.ConnectivityService
import com.nextcloud.utils.extensions.getUploadIds
import com.owncloud.android.MainApp
import com.owncloud.android.datamodel.FileDataStorageManager
import com.owncloud.android.datamodel.OCFile
Expand Down Expand Up @@ -186,7 +187,7 @@ class FileUploadHelper {
accountNames.forEach { accountName ->
val user = accountManager.getUser(accountName)
if (user.isPresent) {
backgroundJobManager.startFilesUploadJob(user.get())
backgroundJobManager.startFilesUploadJob(user.get(), failedUploads.getUploadIds())
}
}

Expand Down Expand Up @@ -217,7 +218,7 @@ class FileUploadHelper {
}
}
uploadsStorageManager.storeUploads(uploads)
backgroundJobManager.startFilesUploadJob(user)
backgroundJobManager.startFilesUploadJob(user, uploads.getUploadIds())
}

fun removeFileUpload(remotePath: String, accountName: String) {
Expand All @@ -227,10 +228,10 @@ class FileUploadHelper {
// need to update now table in mUploadsStorageManager,
// since the operation will not get to be run by FileUploader#uploadFile
uploadsStorageManager.removeUpload(accountName, remotePath)

cancelAndRestartUploadJob(user)
val uploadIds = uploadsStorageManager.getCurrentUploadIds(user.accountName)
cancelAndRestartUploadJob(user, uploadIds)
} catch (e: NoSuchElementException) {
Log_OC.e(TAG, "Error cancelling current upload because user does not exist!")
Log_OC.e(TAG, "Error cancelling current upload because user does not exist!: " + e.message)
}
}

Expand All @@ -253,16 +254,16 @@ class FileUploadHelper {

try {
val user = accountManager.getUser(accountName).get()
cancelAndRestartUploadJob(user)
cancelAndRestartUploadJob(user, uploads.getUploadIds())
} catch (e: NoSuchElementException) {
Log_OC.e(TAG, "Error restarting upload job because user does not exist!")
Log_OC.e(TAG, "Error restarting upload job because user does not exist!: " + e.message)
}
}

fun cancelAndRestartUploadJob(user: User) {
fun cancelAndRestartUploadJob(user: User, uploadIds: LongArray) {
backgroundJobManager.run {
cancelFilesUploadJob(user)
startFilesUploadJob(user)
startFilesUploadJob(user, uploadIds)
}
}

Expand Down Expand Up @@ -377,7 +378,8 @@ class FileUploadHelper {
}
}
uploadsStorageManager.storeUploads(uploads)
backgroundJobManager.startFilesUploadJob(user)
val uploadIds: LongArray = uploads.filterNotNull().map { it.uploadId }.toLongArray()
backgroundJobManager.startFilesUploadJob(user, uploadIds)
}

/**
Expand Down Expand Up @@ -421,12 +423,13 @@ class FileUploadHelper {
upload.uploadStatus = UploadStatus.UPLOAD_IN_PROGRESS
uploadsStorageManager.updateUpload(upload)

backgroundJobManager.startFilesUploadJob(user)
backgroundJobManager.startFilesUploadJob(user, longArrayOf(upload.uploadId))
}

fun cancel(accountName: String) {
uploadsStorageManager.removeUploads(accountName)
cancelAndRestartUploadJob(accountManager.getUser(accountName).get())
val uploadIds = uploadsStorageManager.getCurrentUploadIds(accountName)
cancelAndRestartUploadJob(accountManager.getUser(accountName).get(), uploadIds)
}

fun addUploadTransferProgressListener(listener: OnDatatransferProgressListener, targetKey: String) {
Expand Down
117 changes: 44 additions & 73 deletions app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import com.owncloud.android.operations.UploadFileOperation
import com.owncloud.android.utils.ErrorMessageAdapter
import com.owncloud.android.utils.theme.ViewThemeUtils
import java.io.File
import kotlin.random.Random

@Suppress("LongParameterList")
class FileUploadWorker(
Expand All @@ -56,6 +57,7 @@ class FileUploadWorker(

const val NOTIFICATION_ERROR_ID: Int = 413
const val ACCOUNT = "data_account"
const val UPLOAD_IDS = "uploads_ids"
var currentUploadFileOperation: UploadFileOperation? = null

private const val UPLOADS_ADDED_MESSAGE = "UPLOADS_ADDED"
Expand Down Expand Up @@ -88,17 +90,16 @@ class FileUploadWorker(
}
}

private var currentUploadIndex: Int = 1
private var lastPercent = 0
private val notificationManager = UploadNotificationManager(context, viewThemeUtils)
private val notificationManager = UploadNotificationManager(context, viewThemeUtils, Random.nextInt())
private val intents = FileUploaderIntents(context)
private val fileUploaderDelegate = FileUploaderDelegate()

@Suppress("TooGenericExceptionCaught")
override fun doWork(): Result {
return try {
backgroundJobManager.logStartOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class))
val result = retrievePagesBySortingUploadsByID()
val result = uploadFiles()
backgroundJobManager.logEndOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class), result)
notificationManager.dismissNotification()
if (result == Result.success()) {
Expand All @@ -121,23 +122,22 @@ class FileUploadWorker(
super.onStopped()
}

private fun setWorkerState(user: User?, uploads: List<OCUpload>) {
WorkerStateLiveData.instance().setWorkState(WorkerState.UploadStarted(user, uploads))
private fun setWorkerState(user: User?) {
WorkerStateLiveData.instance().setWorkState(WorkerState.UploadStarted(user))
}

private fun setIdleWorkerState() {
WorkerStateLiveData.instance().setWorkState(WorkerState.UploadFinished(currentUploadFileOperation?.file))
}

@Suppress("ReturnCount")
private fun retrievePagesBySortingUploadsByID(): Result {
private fun uploadFiles(): Result {
val accountName = inputData.getString(ACCOUNT) ?: return Result.failure()
var uploadsPerPage = uploadsStorageManager.getCurrentUploadsForAccountPageAscById(-1, accountName)
val totalUploadSize = uploadsStorageManager.getTotalUploadSize(accountName)
val uploadIds = inputData.getLongArray(UPLOAD_IDS) ?: return Result.success()
val uploads = uploadIds.map { id -> uploadsStorageManager.getUploadById(id) }.filterNotNull()
val totalUploadSize = uploadIds.size

Log_OC.d(TAG, "Total upload size: $totalUploadSize")

while (uploadsPerPage.isNotEmpty() && !isStopped) {
for ((index, upload) in uploads.withIndex()) {
if (preferences.isGlobalUploadPaused) {
Log_OC.d(TAG, "Upload is paused, skip uploading files!")
notificationManager.notifyPaused(
Expand All @@ -151,18 +151,41 @@ class FileUploadWorker(
return Result.failure()
}

Log_OC.d(TAG, "Handling ${uploadsPerPage.size} uploads for account $accountName")
val lastId = uploadsPerPage.last().uploadId
uploadFiles(totalUploadSize, uploadsPerPage, accountName)
uploadsPerPage =
uploadsStorageManager.getCurrentUploadsForAccountPageAscById(lastId, accountName)
}
val user = userAccountManager.getUser(accountName)
if (!user.isPresent) {
uploadsStorageManager.removeUpload(upload.uploadId)
continue
}

if (isStopped) {
Log_OC.d(TAG, "FileUploadWorker for account $accountName was stopped")
} else {
Log_OC.d(TAG, "No more pending uploads for account $accountName, stopping work")
if (isStopped) {
continue
}

setWorkerState(user.get())

val operation = createUploadFileOperation(upload, user.get())
currentUploadFileOperation = operation

notificationManager.prepareForStart(
operation,
cancelPendingIntent = intents.startIntent(operation),
startIntent = intents.notificationStartIntent(operation),
currentUploadIndex = index,
totalUploadSize = totalUploadSize
)

val result = upload(operation, user.get())
currentUploadFileOperation = null

fileUploaderDelegate.sendBroadcastUploadFinished(
operation,
result,
operation.oldFile?.storagePath,
context,
localBroadcastManager
)
}

return Result.success()
}

Expand All @@ -180,58 +203,6 @@ class FileUploadWorker(
return result
}

@Suppress("NestedBlockDepth")
private fun uploadFiles(totalUploadSize: Int, uploadsPerPage: List<OCUpload>, accountName: String) {
val user = userAccountManager.getUser(accountName)
setWorkerState(user.get(), uploadsPerPage)

if (canExitEarly()) {
notificationManager.showConnectionErrorNotification()
return
}

run uploads@{
uploadsPerPage.forEach { upload ->
if (canExitEarly()) {
notificationManager.showConnectionErrorNotification()
return@uploads
}

if (user.isPresent) {
val uploadFileOperation = createUploadFileOperation(upload, user.get())

currentUploadFileOperation = uploadFileOperation

notificationManager.prepareForStart(
uploadFileOperation,
cancelPendingIntent = intents.startIntent(uploadFileOperation),
startIntent = intents.notificationStartIntent(uploadFileOperation),
currentUploadIndex = currentUploadIndex,
totalUploadSize = totalUploadSize
)

val result = upload(uploadFileOperation, user.get())

if (result.isSuccess) {
currentUploadIndex += 1
}

currentUploadFileOperation = null

fileUploaderDelegate.sendBroadcastUploadFinished(
uploadFileOperation,
result,
uploadFileOperation.oldFile?.storagePath,
context,
localBroadcastManager
)
} else {
uploadsStorageManager.removeUpload(upload.uploadId)
}
}
}
}

private fun createUploadFileOperation(upload: OCUpload, user: User): UploadFileOperation {
return UploadFileOperation(
uploadsStorageManager,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,8 @@ import com.owncloud.android.operations.UploadFileOperation
import com.owncloud.android.ui.notifications.NotificationUtils
import com.owncloud.android.utils.theme.ViewThemeUtils

class UploadNotificationManager(private val context: Context, viewThemeUtils: ViewThemeUtils) :
WorkerNotificationManager(ID, context, viewThemeUtils, R.string.foreground_service_upload) {

companion object {
private const val ID = 411
}
class UploadNotificationManager(private val context: Context, viewThemeUtils: ViewThemeUtils, id: Int) :
WorkerNotificationManager(id, context, viewThemeUtils, R.string.foreground_service_upload) {

@Suppress("MagicNumber")
fun prepareForStart(
Expand Down
3 changes: 1 addition & 2 deletions app/src/main/java/com/nextcloud/model/WorkerState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,12 @@ package com.nextcloud.model

import com.nextcloud.client.account.User
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.db.OCUpload
import com.owncloud.android.operations.DownloadFileOperation

sealed class WorkerState {
data class DownloadFinished(var currentFile: OCFile?) : WorkerState()
data class DownloadStarted(var user: User?, var currentDownload: DownloadFileOperation?) : WorkerState()
data class UploadFinished(var currentFile: OCFile?) : WorkerState()
data class UploadStarted(var user: User?, var uploads: List<OCUpload>) : WorkerState()
data class UploadStarted(var user: User?) : WorkerState()
data object OfflineOperationsCompleted : WorkerState()
}
Loading
Loading