Selaa lähdekoodia

Upload in background with WorkManager

Signed-off-by: tobiasKaminsky <tobias@kaminsky.me>
Signed-off-by: Álvaro Brey <alvaro.brey@nextcloud.com>
tobiasKaminsky 2 vuotta sitten
vanhempi
commit
54c6d519ff
28 muutettua tiedostoa jossa 682 lisäystä ja 165 poistoa
  1. 1 1
      app/src/androidTest/java/com/owncloud/android/UploadIT.java
  2. 2 2
      app/src/androidTest/java/com/owncloud/android/datamodel/FileDataStorageManagerIT.java
  3. 32 0
      app/src/androidTest/java/com/owncloud/android/files/services/FileUploadWorkerIT.kt
  4. 16 16
      app/src/androidTest/java/com/owncloud/android/files/services/FileUploaderIT.kt
  5. 25 0
      app/src/androidTest/java/com/owncloud/android/files/services/LegacyFileUploaderIT.kt
  6. 1 1
      app/src/androidTest/java/com/owncloud/android/operations/RemoveFileOperationIT.java
  7. 3 0
      app/src/main/java/com/nextcloud/client/di/AppComponent.java
  8. 4 0
      app/src/main/java/com/nextcloud/client/di/ComponentsModule.java
  9. 17 1
      app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt
  10. 2 0
      app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt
  11. 15 0
      app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt
  12. 226 0
      app/src/main/java/com/nextcloud/client/jobs/FilesUploadWorker.kt
  13. 96 0
      app/src/main/java/com/nextcloud/client/utils/FileUploaderDelegate.kt
  14. 1 1
      app/src/main/java/com/owncloud/android/MainApp.java
  15. 2 1
      app/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java
  16. 81 93
      app/src/main/java/com/owncloud/android/files/services/FileUploader.java
  17. 6 6
      app/src/main/java/com/owncloud/android/operations/SynchronizeFileOperation.java
  18. 5 3
      app/src/main/java/com/owncloud/android/operations/SynchronizeFolderOperation.java
  19. 3 2
      app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java
  20. 1 2
      app/src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.java
  21. 10 10
      app/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.java
  22. 1 1
      app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.java
  23. 1 1
      app/src/main/java/com/owncloud/android/ui/adapter/UploadListAdapter.java
  24. 18 20
      app/src/main/java/com/owncloud/android/ui/asynctasks/CopyAndUploadContentUrisTask.java
  25. 1 1
      app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.java
  26. 104 0
      app/src/main/java/com/owncloud/android/utils/FilesUploadHelper.kt
  27. 2 2
      app/src/main/java/com/owncloud/android/utils/ReceiversHelper.java
  28. 6 1
      app/src/test/java/com/nextcloud/client/jobs/BackgroundJobFactoryTest.kt

+ 1 - 1
app/src/androidTest/java/com/owncloud/android/UploadIT.java

@@ -422,7 +422,7 @@ public class UploadIT extends AbstractOnServerIT {
 
     @Test
     public void testCreationAndUploadTimestamp() throws IOException {
-        File file = getDummyFile("/empty.txt");
+        File file = getDummyFile("empty.txt");
         String remotePath = "/testFile.txt";
         OCUpload ocUpload = new OCUpload(file.getAbsolutePath(), remotePath, account.name);
 

+ 2 - 2
app/src/androidTest/java/com/owncloud/android/datamodel/FileDataStorageManagerIT.java

@@ -101,13 +101,13 @@ abstract public class FileDataStorageManagerIT extends AbstractOnServerIT {
 
         assertTrue(new CreateFolderRemoteOperation("/1/2/", true).execute(client).isSuccess());
 
-        assertTrue(new UploadFileRemoteOperation(getDummyFile("/chunkedFile.txt").getAbsolutePath(),
+        assertTrue(new UploadFileRemoteOperation(getDummyFile("chunkedFile.txt").getAbsolutePath(),
                                                  "/1/1/chunkedFile.txt",
                                                  "text/plain",
                                                  String.valueOf(System.currentTimeMillis() / 1000))
                        .execute(client).isSuccess());
 
-        assertTrue(new UploadFileRemoteOperation(getDummyFile("/chunkedFile.txt").getAbsolutePath(),
+        assertTrue(new UploadFileRemoteOperation(getDummyFile("chunkedFile.txt").getAbsolutePath(),
                                                  "/1/1/chunkedFile2.txt",
                                                  "text/plain",
                                                  String.valueOf(System.currentTimeMillis() / 1000))

+ 32 - 0
app/src/androidTest/java/com/owncloud/android/files/services/FileUploadWorkerIT.kt

@@ -0,0 +1,32 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2022 Tobias Kaminsky
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.files.services
+
+import org.junit.Before
+
+class FileUploadWorkerIT : FileUploaderIT() {
+    @Before
+    fun enableWorker() {
+        FileUploader.setForceNewUploadWorker(true)
+    }
+}

+ 16 - 16
app/src/androidTest/java/com/owncloud/android/files/services/FileUploaderIT.kt

@@ -42,7 +42,7 @@ import junit.framework.Assert.assertTrue
 import org.junit.Before
 import org.junit.Test
 
-class FileUploaderIT : AbstractOnServerIT() {
+abstract class FileUploaderIT : AbstractOnServerIT() {
     var uploadsStorageManager: UploadsStorageManager? = null
 
     val connectivityServiceMock: ConnectivityService = object : ConnectivityService {
@@ -74,7 +74,7 @@ class FileUploaderIT : AbstractOnServerIT() {
     // disabled, flaky test
     // @Test
     // fun testKeepLocalAndOverwriteRemote() {
-    //     val file = getDummyFile("/chunkedFile.txt")
+    //     val file = getDummyFile("chunkedFile.txt")
     //     val ocUpload = OCUpload(file.absolutePath, "/testFile.txt", account.name)
     //
     //     assertTrue(
@@ -101,7 +101,7 @@ class FileUploaderIT : AbstractOnServerIT() {
     //
     //     assertEquals(file.length(), (result.data[0] as RemoteFile).length)
     //
-    //     val ocUpload2 = OCUpload(getDummyFile("/empty.txt").absolutePath, "/testFile.txt", account.name)
+    //     val ocUpload2 = OCUpload(getDummyFile("empty.txt").absolutePath, "/testFile.txt", account.name)
     //
     //     assertTrue(
     //         UploadFileOperation(
@@ -132,7 +132,7 @@ class FileUploaderIT : AbstractOnServerIT() {
      */
     @Test
     fun testKeepLocalAndOverwriteRemoteStatic() {
-        val file = getDummyFile("/chunkedFile.txt")
+        val file = getDummyFile("chunkedFile.txt")
 
         FileUploader.uploadNewFile(
             targetContext,
@@ -156,7 +156,7 @@ class FileUploaderIT : AbstractOnServerIT() {
         assertEquals(file.length(), (result.data[0] as RemoteFile).length)
 
         val ocFile2 = OCFile("/testFile.txt")
-        ocFile2.storagePath = getDummyFile("/empty.txt").absolutePath
+        ocFile2.storagePath = getDummyFile("empty.txt").absolutePath
 
         FileUploader.uploadUpdateFile(
             targetContext,
@@ -181,7 +181,7 @@ class FileUploaderIT : AbstractOnServerIT() {
     fun testKeepBoth() {
         var renameListenerWasTriggered = false
 
-        val file = getDummyFile("/chunkedFile.txt")
+        val file = getDummyFile("chunkedFile.txt")
         val ocUpload = OCUpload(file.absolutePath, "/testFile.txt", account.name)
 
         assertTrue(
@@ -251,7 +251,7 @@ class FileUploaderIT : AbstractOnServerIT() {
      */
     @Test
     fun testKeepBothStatic() {
-        val file = getDummyFile("/chunkedFile.txt")
+        val file = getDummyFile("nonEmpty.txt")
 
         FileUploader.uploadNewFile(
             targetContext,
@@ -275,7 +275,7 @@ class FileUploaderIT : AbstractOnServerIT() {
         assertEquals(file.length(), (result.data[0] as RemoteFile).length)
 
         val ocFile2 = OCFile("/testFile.txt")
-        ocFile2.storagePath = getDummyFile("/empty.txt").absolutePath
+        ocFile2.storagePath = getDummyFile("empty.txt").absolutePath
 
         FileUploader.uploadUpdateFile(
             targetContext,
@@ -303,7 +303,7 @@ class FileUploaderIT : AbstractOnServerIT() {
      */
     @Test
     fun testKeepServer() {
-        val file = getDummyFile("/chunkedFile.txt")
+        val file = getDummyFile("chunkedFile.txt")
         val ocUpload = OCUpload(file.absolutePath, "/testFile.txt", account.name)
 
         assertTrue(
@@ -331,7 +331,7 @@ class FileUploaderIT : AbstractOnServerIT() {
 
         assertEquals(file.length(), (result.data[0] as RemoteFile).length)
 
-        val ocUpload2 = OCUpload(getDummyFile("/empty.txt").absolutePath, "/testFile.txt", account.name)
+        val ocUpload2 = OCUpload(getDummyFile("empty.txt").absolutePath, "/testFile.txt", account.name)
 
         assertFalse(
             UploadFileOperation(
@@ -362,7 +362,7 @@ class FileUploaderIT : AbstractOnServerIT() {
      */
     @Test
     fun testKeepServerStatic() {
-        val file = getDummyFile("/chunkedFile.txt")
+        val file = getDummyFile("chunkedFile.txt")
 
         FileUploader.uploadNewFile(
             targetContext,
@@ -386,7 +386,7 @@ class FileUploaderIT : AbstractOnServerIT() {
         assertEquals(file.length(), (result.data[0] as RemoteFile).length)
 
         val ocFile2 = OCFile("/testFile.txt")
-        ocFile2.storagePath = getDummyFile("/empty.txt").absolutePath
+        ocFile2.storagePath = getDummyFile("empty.txt").absolutePath
 
         FileUploader.uploadUpdateFile(
             targetContext,
@@ -409,7 +409,7 @@ class FileUploaderIT : AbstractOnServerIT() {
      */
     @Test
     fun testCancelServer() {
-        val file = getDummyFile("/chunkedFile.txt")
+        val file = getDummyFile("chunkedFile.txt")
         val ocUpload = OCUpload(file.absolutePath, "/testFile.txt", account.name)
 
         assertTrue(
@@ -437,7 +437,7 @@ class FileUploaderIT : AbstractOnServerIT() {
 
         assertEquals(file.length(), (result.data[0] as RemoteFile).length)
 
-        val ocUpload2 = OCUpload(getDummyFile("/empty.txt").absolutePath, "/testFile.txt", account.name)
+        val ocUpload2 = OCUpload(getDummyFile("empty.txt").absolutePath, "/testFile.txt", account.name)
 
         val uploadResult = UploadFileOperation(
             uploadsStorageManager,
@@ -469,7 +469,7 @@ class FileUploaderIT : AbstractOnServerIT() {
      */
     @Test
     fun testKeepCancelStatic() {
-        val file = getDummyFile("/chunkedFile.txt")
+        val file = getDummyFile("chunkedFile.txt")
 
         FileUploader.uploadNewFile(
             targetContext,
@@ -493,7 +493,7 @@ class FileUploaderIT : AbstractOnServerIT() {
         assertEquals(file.length(), (result.data[0] as RemoteFile).length)
 
         val ocFile2 = OCFile("/testFile.txt")
-        ocFile2.storagePath = getDummyFile("/empty.txt").absolutePath
+        ocFile2.storagePath = getDummyFile("empty.txt").absolutePath
 
         FileUploader.uploadUpdateFile(
             targetContext,

+ 25 - 0
app/src/androidTest/java/com/owncloud/android/files/services/LegacyFileUploaderIT.kt

@@ -0,0 +1,25 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2022 Tobias Kaminsky
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.files.services
+
+class LegacyFileUploaderIT : FileUploaderIT()

+ 1 - 1
app/src/androidTest/java/com/owncloud/android/operations/RemoveFileOperationIT.java

@@ -71,7 +71,7 @@ public class RemoveFileOperationIT extends AbstractOnServerIT {
     public void deleteFile() throws IOException {
         String parent = "/test/";
         String path = parent + "empty.txt";
-        OCUpload ocUpload = new OCUpload(getDummyFile("/empty.txt").getAbsolutePath(), path, account.name);
+        OCUpload ocUpload = new OCUpload(getDummyFile("empty.txt").getAbsolutePath(), path, account.name);
 
         uploadOCUpload(ocUpload);
 

+ 3 - 0
app/src/main/java/com/nextcloud/client/di/AppComponent.java

@@ -32,6 +32,7 @@ import com.nextcloud.client.preferences.PreferencesModule;
 import com.owncloud.android.MainApp;
 import com.owncloud.android.media.MediaControlView;
 import com.owncloud.android.ui.ThemeableSwitchPreference;
+import com.owncloud.android.utils.FilesUploadHelper;
 
 import javax.inject.Singleton;
 
@@ -61,6 +62,8 @@ public interface AppComponent {
 
     void inject(ThemeableSwitchPreference switchPreference);
 
+    void inject(FilesUploadHelper filesUploadHelper);
+
     @Component.Builder
     interface Builder {
         @BindsInstance

+ 4 - 0
app/src/main/java/com/nextcloud/client/di/ComponentsModule.java

@@ -125,6 +125,7 @@ import com.owncloud.android.ui.preview.PreviewTextStringFragment;
 import com.owncloud.android.ui.preview.PreviewVideoActivity;
 import com.owncloud.android.ui.preview.pdf.PreviewPdfFragment;
 import com.owncloud.android.ui.trashbin.TrashbinActivity;
+import com.owncloud.android.utils.FilesUploadHelper;
 
 import dagger.Module;
 import dagger.android.ContributesAndroidInjector;
@@ -454,4 +455,7 @@ abstract class ComponentsModule {
 
     @ContributesAndroidInjector
     abstract PreviewBitmapActivity previewBitmapActivity();
+
+    @ContributesAndroidInjector
+    abstract FilesUploadHelper filesUploadHelper();
 }

+ 17 - 1
app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt

@@ -26,6 +26,7 @@ import android.content.Context
 import android.content.res.Resources
 import android.os.Build
 import androidx.annotation.RequiresApi
+import androidx.localbroadcastmanager.content.LocalBroadcastManager
 import androidx.work.ListenableWorker
 import androidx.work.WorkerFactory
 import androidx.work.WorkerParameters
@@ -65,7 +66,8 @@ class BackgroundJobFactory @Inject constructor(
     private val notificationManager: NotificationManager,
     private val eventBus: EventBus,
     private val deckApi: DeckApi,
-    private val viewThemeUtils: Provider<ViewThemeUtils>
+    private val viewThemeUtils: Provider<ViewThemeUtils>,
+    private val localBroadcastManager: Provider<LocalBroadcastManager>
 ) : WorkerFactory() {
 
     @SuppressLint("NewApi")
@@ -96,6 +98,7 @@ class BackgroundJobFactory @Inject constructor(
                 CalendarBackupWork::class -> createCalendarBackupWork(context, workerParameters)
                 CalendarImportWork::class -> createCalendarImportWork(context, workerParameters)
                 FilesExportWork::class -> createFilesExportWork(context, workerParameters)
+                FilesUploadWorker::class -> createFilesUploadWorker(context, workerParameters)
                 else -> null // caller falls back to default factory
             }
         }
@@ -235,4 +238,17 @@ class BackgroundJobFactory @Inject constructor(
             preferences
         )
     }
+
+    private fun createFilesUploadWorker(context: Context, params: WorkerParameters): FilesUploadWorker {
+        return FilesUploadWorker(
+            uploadsStorageManager,
+            connectivityService,
+            powerManagementService,
+            accountManager,
+            themeColorUtils,
+            localBroadcastManager.get(),
+            context,
+            params
+        )
+    }
 }

+ 2 - 0
app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt

@@ -138,6 +138,8 @@ interface BackgroundJobManager {
 
     fun startNotificationJob(subject: String, signature: String)
     fun startAccountRemovalJob(accountName: String, remoteWipe: Boolean)
+    fun startFilesUploadJob(user: User)
+    fun getFileUploads(user: User): LiveData<List<JobInfo>>
 
     fun scheduleTestJob()
     fun startImmediateTestJob()

+ 15 - 0
app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt

@@ -78,6 +78,7 @@ internal class BackgroundJobManagerImpl(
         const val JOB_IMMEDIATE_MEDIA_FOLDER_DETECTION = "immediate_media_folder_detection"
         const val JOB_NOTIFICATION = "notification"
         const val JOB_ACCOUNT_REMOVAL = "account_removal"
+        const val JOB_FILES_UPLOAD = "files_upload"
         const val JOB_IMMEDIATE_CALENDAR_BACKUP = "immediate_calendar_backup"
         const val JOB_IMMEDIATE_FILES_EXPORT = "immediate_files_export"
 
@@ -446,6 +447,20 @@ internal class BackgroundJobManagerImpl(
         workManager.enqueue(request)
     }
 
+    override fun startFilesUploadJob(user: User) {
+        val request = oneTimeRequestBuilder(FilesUploadWorker::class, JOB_FILES_UPLOAD, user)
+            .build()
+
+        workManager.enqueue(request)
+    }
+
+    override fun getFileUploads(user: User): LiveData<List<JobInfo>> {
+        val workInfo = workManager.getWorkInfosByTagLiveData(formatNameTag(JOB_FILES_UPLOAD, user))
+        return Transformations.map(workInfo) {
+            it.map { fromWorkInfo(it) ?: JobInfo() }
+        }
+    }
+
     override fun scheduleTestJob() {
         val request = periodicRequestBuilder(TestJob::class, JOB_TEST)
             .setInitialDelay(DEFAULT_IMMEDIATE_JOB_DELAY_SEC, TimeUnit.SECONDS)

+ 226 - 0
app/src/main/java/com/nextcloud/client/jobs/FilesUploadWorker.kt

@@ -0,0 +1,226 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2022 Tobias Kaminsky
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.client.jobs
+
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import androidx.core.app.NotificationCompat
+import androidx.localbroadcastmanager.content.LocalBroadcastManager
+import androidx.work.Worker
+import androidx.work.WorkerParameters
+import com.nextcloud.client.account.User
+import com.nextcloud.client.account.UserAccountManager
+import com.nextcloud.client.device.PowerManagementService
+import com.nextcloud.client.network.ConnectivityService
+import com.nextcloud.client.utils.FileUploaderDelegate
+import com.owncloud.android.R
+import com.owncloud.android.datamodel.FileDataStorageManager
+import com.owncloud.android.datamodel.ThumbnailsCacheManager
+import com.owncloud.android.datamodel.UploadsStorageManager
+import com.owncloud.android.db.OCUpload
+import com.owncloud.android.lib.common.OwnCloudAccount
+import com.owncloud.android.lib.common.OwnCloudClientManagerFactory
+import com.owncloud.android.lib.common.network.OnDatatransferProgressListener
+import com.owncloud.android.lib.common.operations.RemoteOperationResult
+import com.owncloud.android.lib.common.utils.Log_OC
+import com.owncloud.android.lib.resources.files.FileUtils
+import com.owncloud.android.operations.UploadFileOperation
+import com.owncloud.android.ui.activity.UploadListActivity
+import com.owncloud.android.ui.notifications.NotificationUtils
+import com.owncloud.android.utils.theme.ThemeColorUtils
+import java.io.File
+
+@Suppress("LongParameterList")
+class FilesUploadWorker(
+    val uploadsStorageManager: UploadsStorageManager,
+    val connectivityService: ConnectivityService,
+    val powerManagementService: PowerManagementService,
+    val userAccountManager: UserAccountManager,
+    val themeColorUtils: ThemeColorUtils,
+    val localBroadcastManager: LocalBroadcastManager,
+    val context: Context,
+    params: WorkerParameters
+) : Worker(context, params), OnDatatransferProgressListener {
+    var lastPercent = 0
+    val notificationBuilder: NotificationCompat.Builder =
+        NotificationUtils.newNotificationBuilder(context, themeColorUtils)
+    val notificationManager: NotificationManager =
+        context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+    val fileUploaderDelegate = FileUploaderDelegate()
+
+    override fun doWork(): Result {
+        // get all pending uploads
+        for (upload in uploadsStorageManager.currentAndPendingUploadsForCurrentAccount) {
+            // create upload file operation
+            val user = userAccountManager.getUser(upload.accountName)
+            if (user.isPresent) {
+                val uploadFileOperation = createUploadFileOperation(upload, user.get())
+
+                val result = upload(uploadFileOperation, user.get())
+
+                fileUploaderDelegate.sendBroadcastUploadFinished(
+                    uploadFileOperation,
+                    result,
+                    uploadFileOperation.oldFile?.storagePath,
+                    context,
+                    localBroadcastManager
+                )
+            } else {
+                uploadsStorageManager.removeUpload(upload.uploadId)
+            }
+        }
+
+        return Result.success()
+    }
+
+    /**
+     * from @{link FileUploader#retryUploads()}
+     */
+    private fun createUploadFileOperation(upload: OCUpload, user: User): UploadFileOperation {
+        return UploadFileOperation(
+            uploadsStorageManager,
+            connectivityService,
+            powerManagementService,
+            user,
+            null,
+            upload,
+            upload.nameCollisionPolicy,
+            upload.localAction,
+            context,
+            upload.isUseWifiOnly,
+            upload.isWhileChargingOnly,
+            true,
+            FileDataStorageManager(user, context.contentResolver)
+        ).apply {
+            addDataTransferProgressListener(this@FilesUploadWorker)
+        }
+    }
+
+    @Suppress("TooGenericExceptionCaught")
+    private fun upload(uploadFileOperation: UploadFileOperation, user: User): RemoteOperationResult<Any?> {
+        lateinit var uploadResult: RemoteOperationResult<Any?>
+
+        // start notification
+        createNotification(uploadFileOperation)
+
+        try {
+            val storageManager = uploadFileOperation.storageManager
+
+            // always get client from client manager, to get fresh credentials in case of update
+            val ocAccount = OwnCloudAccount(user.toPlatformAccount(), context)
+            val uploadClient = OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(ocAccount, context)
+            uploadResult = uploadFileOperation.execute(uploadClient)
+
+            // generate new Thumbnail
+            val task = ThumbnailsCacheManager.ThumbnailGenerationTask(storageManager, user)
+            val file = File(uploadFileOperation.originalStoragePath)
+            val remoteId: String? = uploadFileOperation.file.remoteId
+            task.execute(ThumbnailsCacheManager.ThumbnailGenerationTaskObject(file, remoteId))
+        } catch (e: Exception) {
+            Log_OC.e(TAG, "Error uploading", e)
+            uploadResult = RemoteOperationResult<Any?>(e)
+        } finally {
+            uploadsStorageManager.updateDatabaseUploadResult(uploadResult, uploadFileOperation)
+
+            // cancel notification
+            notificationManager.cancel(FOREGROUND_SERVICE_ID)
+        }
+
+        return uploadResult
+    }
+
+    /**
+     * adapted from [com.owncloud.android.files.services.FileUploader.notifyUploadStart]
+     */
+    private fun createNotification(uploadFileOperation: UploadFileOperation) {
+        notificationBuilder
+            .setOngoing(true)
+            .setSmallIcon(R.drawable.notification_icon)
+            .setTicker(context.getString(R.string.uploader_upload_in_progress_ticker))
+            .setProgress(MAX_PROGRESS, 0, false)
+            .setContentText(
+                String.format(
+                    context.getString(R.string.uploader_upload_in_progress_content),
+                    0,
+                    uploadFileOperation.fileName
+                )
+            )
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            notificationBuilder.setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_UPLOAD)
+        }
+
+        // includes a pending intent in the notification showing the details
+        val intent = UploadListActivity.createIntent(
+            uploadFileOperation.file,
+            uploadFileOperation.user,
+            Intent.FLAG_ACTIVITY_CLEAR_TOP,
+            context
+        )
+        notificationBuilder.setContentIntent(
+            PendingIntent.getActivity(
+                context,
+                System.currentTimeMillis().toInt(),
+                intent,
+                PendingIntent.FLAG_IMMUTABLE
+            )
+        )
+
+        if (!uploadFileOperation.isInstantPicture && !uploadFileOperation.isInstantVideo) {
+            notificationManager.notify(FOREGROUND_SERVICE_ID, notificationBuilder.build())
+        } // else wait until the upload really start (onTransferProgress is called), so that if it's discarded
+
+        // due to lack of Wifi, no notification is shown
+        // TODO generalize for automated uploads
+    }
+
+    companion object {
+        val TAG: String = FilesUploadWorker::class.java.simpleName
+        const val FOREGROUND_SERVICE_ID: Int = 412
+        const val MAX_PROGRESS: Int = 100
+    }
+
+    /**
+     * see [com.owncloud.android.files.services.FileUploader.onTransferProgress]
+     */
+    override fun onTransferProgress(
+        progressRate: Long,
+        totalTransferredSoFar: Long,
+        totalToTransfer: Long,
+        fileAbsoluteName: String
+    ) {
+        val percent = (MAX_PROGRESS * totalTransferredSoFar.toDouble() / totalToTransfer.toDouble()).toInt()
+        if (percent != lastPercent) {
+            notificationBuilder.setProgress(MAX_PROGRESS, percent, false)
+            val fileName: String =
+                fileAbsoluteName.substring(fileAbsoluteName.lastIndexOf(FileUtils.PATH_SEPARATOR) + 1)
+            val text = String.format(context.getString(R.string.uploader_upload_in_progress_content), percent, fileName)
+            notificationBuilder.setContentText(text)
+            notificationManager.notify(FOREGROUND_SERVICE_ID, notificationBuilder.build())
+        }
+        lastPercent = percent
+    }
+}

+ 96 - 0
app/src/main/java/com/nextcloud/client/utils/FileUploaderDelegate.kt

@@ -0,0 +1,96 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2022 Tobias Kaminsky
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.client.utils
+
+import android.content.Context
+import android.content.Intent
+import androidx.localbroadcastmanager.content.LocalBroadcastManager
+import com.owncloud.android.files.services.FileUploader
+import com.owncloud.android.lib.common.operations.RemoteOperationResult
+import com.owncloud.android.operations.UploadFileOperation
+
+class FileUploaderDelegate {
+    /**
+     * Sends a broadcast in order to the interested activities can update their view
+     *
+     * TODO - no more broadcasts, replace with a callback to subscribed listeners once we drop FileUploader
+     */
+    fun sendBroadcastUploadsAdded(context: Context, localBroadcastManager: LocalBroadcastManager) {
+        val start = Intent(FileUploader.getUploadsAddedMessage())
+        // nothing else needed right now
+        start.setPackage(context.packageName)
+        localBroadcastManager.sendBroadcast(start)
+    }
+
+    /**
+     * Sends a broadcast in order to the interested activities can update their view
+     *
+     * TODO - no more broadcasts, replace with a callback to subscribed listeners once we drop FileUploader
+     *
+     * @param upload Finished upload operation
+     */
+    fun sendBroadcastUploadStarted(
+        upload: UploadFileOperation,
+        context: Context,
+        localBroadcastManager: LocalBroadcastManager
+    ) {
+        val start = Intent(FileUploader.getUploadStartMessage())
+        start.putExtra(FileUploader.EXTRA_REMOTE_PATH, upload.remotePath) // real remote
+        start.putExtra(FileUploader.EXTRA_OLD_FILE_PATH, upload.originalStoragePath)
+        start.putExtra(FileUploader.ACCOUNT_NAME, upload.user.accountName)
+        start.setPackage(context.packageName)
+        localBroadcastManager.sendBroadcast(start)
+    }
+
+    /**
+     * Sends a broadcast in order to the interested activities can update their view
+     *
+     * TODO - no more broadcasts, replace with a callback to subscribed listeners once we drop FileUploader
+     *
+     * @param upload                 Finished upload operation
+     * @param uploadResult           Result of the upload operation
+     * @param unlinkedFromRemotePath Path in the uploads tree where the upload was unlinked from
+     */
+    fun sendBroadcastUploadFinished(
+        upload: UploadFileOperation,
+        uploadResult: RemoteOperationResult<*>,
+        unlinkedFromRemotePath: String?,
+        context: Context,
+        localBroadcastManager: LocalBroadcastManager
+    ) {
+        val end = Intent(FileUploader.getUploadFinishMessage())
+        // real remote path, after possible automatic renaming
+        end.putExtra(FileUploader.EXTRA_REMOTE_PATH, upload.remotePath)
+        if (upload.wasRenamed()) {
+            end.putExtra(FileUploader.EXTRA_OLD_REMOTE_PATH, upload.oldFile!!.remotePath)
+        }
+        end.putExtra(FileUploader.EXTRA_OLD_FILE_PATH, upload.originalStoragePath)
+        end.putExtra(FileUploader.ACCOUNT_NAME, upload.user.accountName)
+        end.putExtra(FileUploader.EXTRA_UPLOAD_RESULT, uploadResult.isSuccess)
+        if (unlinkedFromRemotePath != null) {
+            end.putExtra(FileUploader.EXTRA_LINKED_TO_PATH, unlinkedFromRemotePath)
+        }
+        end.setPackage(context.packageName)
+        localBroadcastManager.sendBroadcast(end)
+    }
+}

+ 1 - 1
app/src/main/java/com/owncloud/android/MainApp.java

@@ -276,7 +276,7 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector {
     @SuppressFBWarnings("ST")
     @Override
     public void onCreate() {
-        enableStrictMode();
+         enableStrictMode();
 
         viewThemeUtils = viewThemeUtilsProvider.get();
 

+ 2 - 1
app/src/main/java/com/owncloud/android/datamodel/ThumbnailsCacheManager.java

@@ -81,6 +81,7 @@ import java.util.Locale;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
+import androidx.annotation.Nullable;
 import androidx.core.content.res.ResourcesCompat;
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 
@@ -502,7 +503,7 @@ public final class ThumbnailsCacheManager {
         private final Object file;
         private final String imageKey;
 
-        public ThumbnailGenerationTaskObject(Object file, String imageKey) {
+        public ThumbnailGenerationTaskObject(Object file, @Nullable String imageKey) {
             this.file = file;
             this.imageKey = imageKey;
         }

+ 81 - 93
app/src/main/java/com/owncloud/android/files/services/FileUploader.java

@@ -52,8 +52,10 @@ import com.nextcloud.client.account.User;
 import com.nextcloud.client.account.UserAccountManager;
 import com.nextcloud.client.device.BatteryStatus;
 import com.nextcloud.client.device.PowerManagementService;
+import com.nextcloud.client.jobs.FilesUploadWorker;
 import com.nextcloud.client.network.Connectivity;
 import com.nextcloud.client.network.ConnectivityService;
+import com.nextcloud.client.utils.FileUploaderDelegate;
 import com.nextcloud.java.util.Optional;
 import com.owncloud.android.MainApp;
 import com.owncloud.android.R;
@@ -78,6 +80,7 @@ import com.owncloud.android.ui.activity.ConflictsResolveActivity;
 import com.owncloud.android.ui.activity.UploadListActivity;
 import com.owncloud.android.ui.notifications.NotificationUtils;
 import com.owncloud.android.utils.ErrorMessageAdapter;
+import com.owncloud.android.utils.FilesUploadHelper;
 import com.owncloud.android.utils.theme.ViewThemeUtils;
 
 import java.io.File;
@@ -91,6 +94,7 @@ import javax.annotation.Nullable;
 import javax.inject.Inject;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
 import androidx.core.app.NotificationCompat;
 import androidx.localbroadcastmanager.content.LocalBroadcastManager;
 import dagger.android.AndroidInjection;
@@ -187,6 +191,7 @@ public class FileUploader extends Service
     public static final int LOCAL_BEHAVIOUR_FORGET = 2;
     public static final int LOCAL_BEHAVIOUR_DELETE = 3;
 
+    private static boolean forceNewUploadWorker = false;
 
     private Notification mNotification;
     private Looper mServiceLooper;
@@ -214,12 +219,13 @@ public class FileUploader extends Service
     private NotificationManager mNotificationManager;
     private NotificationCompat.Builder mNotificationBuilder;
     private int mLastPercent;
+    private FileUploaderDelegate fileUploaderDelegate;
 
 
     @Override
     public void onRenameUpload() {
         mUploadsStorageManager.updateDatabaseUploadStart(mCurrentUpload);
-        sendBroadcastUploadStarted(mCurrentUpload);
+        fileUploaderDelegate.sendBroadcastUploadStarted(mCurrentUpload, this, localBroadcastManager);
     }
 
     /**
@@ -236,6 +242,7 @@ public class FileUploader extends Service
         mServiceLooper = thread.getLooper();
         mServiceHandler = new ServiceHandler(mServiceLooper, this);
         mBinder = new FileUploaderBinder();
+        fileUploaderDelegate = new FileUploaderDelegate();
 
         NotificationCompat.Builder builder = NotificationUtils.newNotificationBuilder(this, viewThemeUtils).setContentTitle(
                 getApplicationContext().getResources().getString(R.string.app_name))
@@ -325,7 +332,13 @@ public class FileUploader extends Service
         boolean onWifiOnly = intent.getBooleanExtra(KEY_WHILE_ON_WIFI_ONLY, false);
         boolean whileChargingOnly = intent.getBooleanExtra(KEY_WHILE_CHARGING_ONLY, false);
 
-        if (!retry) { // Start new uploads
+        if (retry) { // Retry uploads
+            if (!intent.hasExtra(KEY_ACCOUNT) || !intent.hasExtra(KEY_RETRY_UPLOAD)) {
+                Log_OC.e(TAG, "Not enough information provided in intent: no KEY_RETRY_UPLOAD_KEY");
+                return START_NOT_STICKY;
+            }
+            retryUploads(intent, user, requestedUploads);
+        } else { // Start new uploads
             if (!(intent.hasExtra(KEY_LOCAL_FILE) || intent.hasExtra(KEY_FILE))) {
                 Log_OC.e(TAG, "Not enough information provided in intent");
                 return Service.START_NOT_STICKY;
@@ -335,12 +348,6 @@ public class FileUploader extends Service
             if (error != null) {
                 return error;
             }
-        } else { // Retry uploads
-            if (!intent.hasExtra(KEY_ACCOUNT) || !intent.hasExtra(KEY_RETRY_UPLOAD)) {
-                Log_OC.e(TAG, "Not enough information provided in intent: no KEY_RETRY_UPLOAD_KEY");
-                return START_NOT_STICKY;
-            }
-            retryUploads(intent, user, requestedUploads);
         }
 
         if (requestedUploads.size() > 0) {
@@ -348,7 +355,7 @@ public class FileUploader extends Service
             msg.arg1 = startId;
             msg.obj = requestedUploads;
             mServiceHandler.sendMessage(msg);
-            sendBroadcastUploadsAdded();
+            fileUploaderDelegate.sendBroadcastUploadsAdded(this, localBroadcastManager);
         }
         return Service.START_NOT_STICKY;
     }
@@ -592,7 +599,8 @@ public class FileUploader extends Service
     }
 
     /**
-     * Core upload method: sends the file(s) to upload
+     * Core upload method: sends the file(s) to upload WARNING: legacy code, must be in sync with @{{@link
+     * FilesUploadWorker#upload(UploadFileOperation, User)}
      *
      * @param uploadKey Key to access the upload to perform, contained in mPendingUploads
      */
@@ -613,7 +621,7 @@ public class FileUploader extends Service
 
             notifyUploadStart(mCurrentUpload);
 
-            sendBroadcastUploadStarted(mCurrentUpload);
+            fileUploaderDelegate.sendBroadcastUploadStarted(mCurrentUpload, this, localBroadcastManager);
 
             RemoteOperationResult uploadResult = null;
 
@@ -633,12 +641,13 @@ public class FileUploader extends Service
             } finally {
                 Pair<UploadFileOperation, String> removeResult;
                 if (mCurrentUpload.wasRenamed()) {
-                    removeResult = mPendingUploads.removePayload(
-                        mCurrentAccount.name,
-                        mCurrentUpload.getOldFile().getRemotePath()
-                    );
+                    OCFile oldFile = mCurrentUpload.getOldFile();
+                    String oldRemotePath = "";
+                    if (oldFile != null) {
+                        oldRemotePath = oldFile.getRemotePath();
+                    }
+                    removeResult = mPendingUploads.removePayload(mCurrentAccount.name, oldRemotePath);
                     // TODO: grant that name is also updated for mCurrentUpload.getOCUploadId
-
                 } else {
                     removeResult = mPendingUploads.removePayload(mCurrentAccount.name,
                                                                  mCurrentUpload.getDecryptedRemotePath());
@@ -649,7 +658,11 @@ public class FileUploader extends Service
                 /// notify result
                 notifyUploadResult(mCurrentUpload, uploadResult);
 
-                sendBroadcastUploadFinished(mCurrentUpload, uploadResult, removeResult.second);
+                fileUploaderDelegate.sendBroadcastUploadFinished(mCurrentUpload,
+                                                                 uploadResult,
+                                                                 removeResult.second,
+                                                                 this,
+                                                                 localBroadcastManager);
             }
 
             // generate new Thumbnail
@@ -842,68 +855,6 @@ public class FileUploader extends Service
         }
     }
 
-    /**
-     * Sends a broadcast in order to the interested activities can update their view
-     *
-     * TODO - no more broadcasts, replace with a callback to subscribed listeners
-     */
-    private void sendBroadcastUploadsAdded() {
-        Intent start = new Intent(getUploadsAddedMessage());
-        // nothing else needed right now
-        start.setPackage(getPackageName());
-        localBroadcastManager.sendBroadcast(start);
-    }
-
-    /**
-     * Sends a broadcast in order to the interested activities can update their view
-     *
-     * TODO - no more broadcasts, replace with a callback to subscribed listeners
-     *
-     * @param upload Finished upload operation
-     */
-    private void sendBroadcastUploadStarted(UploadFileOperation upload) {
-        Intent start = new Intent(getUploadStartMessage());
-        start.putExtra(EXTRA_REMOTE_PATH, upload.getRemotePath()); // real remote
-        start.putExtra(EXTRA_OLD_FILE_PATH, upload.getOriginalStoragePath());
-        start.putExtra(ACCOUNT_NAME, upload.getUser().getAccountName());
-
-        start.setPackage(getPackageName());
-        localBroadcastManager.sendBroadcast(start);
-    }
-
-    /**
-     * Sends a broadcast in order to the interested activities can update their view
-     *
-     * TODO - no more broadcasts, replace with a callback to subscribed listeners
-     *
-     * @param upload                 Finished upload operation
-     * @param uploadResult           Result of the upload operation
-     * @param unlinkedFromRemotePath Path in the uploads tree where the upload was unlinked from
-     */
-    private void sendBroadcastUploadFinished(
-        UploadFileOperation upload,
-        RemoteOperationResult uploadResult,
-        String unlinkedFromRemotePath
-    ) {
-        Intent end = new Intent(getUploadFinishMessage());
-        end.putExtra(EXTRA_REMOTE_PATH, upload.getRemotePath()); // real remote
-        // path, after
-        // possible
-        // automatic
-        // renaming
-        if (upload.wasRenamed()) {
-            end.putExtra(EXTRA_OLD_REMOTE_PATH, upload.getOldFile().getRemotePath());
-        }
-        end.putExtra(EXTRA_OLD_FILE_PATH, upload.getOriginalStoragePath());
-        end.putExtra(ACCOUNT_NAME, upload.getUser().getAccountName());
-        end.putExtra(EXTRA_UPLOAD_RESULT, uploadResult.isSuccess());
-        if (unlinkedFromRemotePath != null) {
-            end.putExtra(EXTRA_LINKED_TO_PATH, unlinkedFromRemotePath);
-        }
-        end.setPackage(getPackageName());
-        localBroadcastManager.sendBroadcast(end);
-    }
-
     /**
      * Remove and 'forgets' pending uploads of a user.
      *
@@ -929,7 +880,7 @@ public class FileUploader extends Service
         boolean requiresWifi,
         boolean requiresCharging,
         NameCollisionPolicy nameCollisionPolicy
-    ) {
+                                    ) {
         uploadNewFile(
             context,
             user,
@@ -942,7 +893,7 @@ public class FileUploader extends Service
             requiresWifi,
             requiresCharging,
             nameCollisionPolicy
-        );
+                     );
     }
 
     /**
@@ -960,7 +911,7 @@ public class FileUploader extends Service
         boolean requiresWifi,
         boolean requiresCharging,
         NameCollisionPolicy nameCollisionPolicy
-    ) {
+                                    ) {
         Intent intent = new Intent(context, FileUploader.class);
 
         intent.putExtra(FileUploader.KEY_ACCOUNT, user.toPlatformAccount());
@@ -975,7 +926,16 @@ public class FileUploader extends Service
         intent.putExtra(FileUploader.KEY_WHILE_CHARGING_ONLY, requiresCharging);
         intent.putExtra(FileUploader.KEY_NAME_COLLISION_POLICY, nameCollisionPolicy);
 
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+        if (useFilesUploadWorker(context)) {
+            new FilesUploadHelper().uploadNewFiles(user,
+                                                   localPaths,
+                                                   remotePaths,
+                                                   createRemoteFolder,
+                                                   createdBy,
+                                                   requiresWifi,
+                                                   requiresCharging,
+                                                   nameCollisionPolicy);
+        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
             context.startForegroundService(intent);
         } else {
             context.startService(intent);
@@ -990,9 +950,13 @@ public class FileUploader extends Service
         User user,
         OCFile existingFile,
         Integer behaviour,
-        NameCollisionPolicy nameCollisionPolicy
-    ) {
-        uploadUpdateFile(context, user, new OCFile[]{existingFile}, behaviour, nameCollisionPolicy, true);
+        NameCollisionPolicy nameCollisionPolicy) {
+        uploadUpdateFile(context,
+                         user,
+                         new OCFile[]{existingFile},
+                         behaviour,
+                         nameCollisionPolicy,
+                         true);
     }
 
     /**
@@ -1006,7 +970,12 @@ public class FileUploader extends Service
         NameCollisionPolicy nameCollisionPolicy,
         boolean disableRetries
                                        ) {
-        uploadUpdateFile(context, user, new OCFile[]{existingFile}, behaviour, nameCollisionPolicy, disableRetries);
+        uploadUpdateFile(context,
+                         user,
+                         new OCFile[]{existingFile},
+                         behaviour,
+                         nameCollisionPolicy,
+                         disableRetries);
     }
 
     /**
@@ -1029,7 +998,9 @@ public class FileUploader extends Service
         intent.putExtra(FileUploader.KEY_NAME_COLLISION_POLICY, nameCollisionPolicy);
         intent.putExtra(FileUploader.KEY_DISABLE_RETRIES, disableRetries);
 
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+        if (useFilesUploadWorker(context)) {
+            new FilesUploadHelper().uploadUpdatedFile(user, existingFiles, behaviour, nameCollisionPolicy);
+        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
             context.startForegroundService(intent);
         } else {
             context.startService(intent);
@@ -1039,14 +1010,18 @@ public class FileUploader extends Service
     /**
      * Retry a failed {@link OCUpload} identified by {@link OCUpload#getRemotePath()}
      */
-    public static void retryUpload(@NonNull Context context, @NonNull User user, @NonNull OCUpload upload) {
+    public static void retryUpload(@NonNull Context context,
+                                   @NonNull User user,
+                                   @NonNull OCUpload upload) {
         Intent i = new Intent(context, FileUploader.class);
         i.putExtra(FileUploader.KEY_RETRY, true);
         i.putExtra(FileUploader.KEY_USER, user);
         i.putExtra(FileUploader.KEY_ACCOUNT, user.toPlatformAccount());
         i.putExtra(FileUploader.KEY_RETRY_UPLOAD, upload);
 
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+        if (useFilesUploadWorker(context)) {
+            new FilesUploadHelper().retryUpload(upload, user);
+        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
             context.startForegroundService(i);
         } else {
             context.startService(i);
@@ -1062,9 +1037,9 @@ public class FileUploader extends Service
         @NonNull final ConnectivityService connectivityService,
         @NonNull final UserAccountManager accountManager,
         @NonNull final PowerManagementService powerManagementService
-    ) {
+                                         ) {
         OCUpload[] failedUploads = uploadsStorageManager.getFailedUploads();
-        if(failedUploads == null || failedUploads.length == 0) {
+        if (failedUploads == null || failedUploads.length == 0) {
             return; //nothing to do
         }
 
@@ -1117,9 +1092,22 @@ public class FileUploader extends Service
     }
 
 
+    private static boolean useFilesUploadWorker(Context context) {
+        if (forceNewUploadWorker) {
+            return true;
+        }
+
+        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S || context.getResources().getBoolean(R.bool.is_beta);
+    }
+
+    @VisibleForTesting
+    public static void setForceNewUploadWorker(final Boolean value) {
+        forceNewUploadWorker = value;
+    }
+
     /**
      * Binder to let client components to perform operations on the queue of uploads.
-     *
+     * <p>
      * It provides by itself the available operations.
      */
     public class FileUploaderBinder extends Binder implements OnDatatransferProgressListener {

+ 6 - 6
app/src/main/java/com/owncloud/android/operations/SynchronizeFileOperation.java

@@ -299,12 +299,12 @@ public class SynchronizeFileOperation extends SyncOperation {
      */
     private void requestForUpload(OCFile file) {
         FileUploader.uploadUpdateFile(
-                mContext,
-                mUser,
-                file,
-                FileUploader.LOCAL_BEHAVIOUR_MOVE,
-                NameCollisionPolicy.OVERWRITE
-        );
+            mContext,
+            mUser,
+            file,
+            FileUploader.LOCAL_BEHAVIOUR_MOVE,
+            NameCollisionPolicy.OVERWRITE
+                                     );
 
         mTransferWasRequested = true;
     }

+ 5 - 3
app/src/main/java/com/owncloud/android/operations/SynchronizeFolderOperation.java

@@ -85,14 +85,16 @@ public class SynchronizeFolderOperation extends SyncOperation {
     /** Counter of failed operations in synchronization of kept-in-sync files */
     private int mFailsInFileSyncsFound;
 
-    /** 'True' means that the remote folder changed and should be fetched */
+    /**
+     * 'True' means that the remote folder changed and should be fetched
+     */
     private boolean mRemoteFolderChanged;
 
     private List<OCFile> mFilesForDirectDownload;
-        // to avoid extra PROPFINDs when there was no change in the folder
+    // to avoid extra PROPFINDs when there was no change in the folder
 
     private List<SyncOperation> mFilesToSyncContents;
-        // this will be used for every file when 'folder synchronization' replaces 'folder download'
+    // this will be used for every file when 'folder synchronization' replaces 'folder download'
 
     private final AtomicBoolean mCancellationRequested;
 

+ 3 - 2
app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java

@@ -86,6 +86,7 @@ import java.util.UUID;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 import androidx.annotation.CheckResult;
+import androidx.annotation.Nullable;
 
 
 /**
@@ -275,9 +276,9 @@ public class UploadFileOperation extends SyncOperation {
     }
 
     /**
-     * If remote file was renamed, return original OCFile which was uploaded. Is
-     * null is file was not renamed.
+     * If remote file was renamed, return original OCFile which was uploaded. Is null is file was not renamed.
      */
+    @Nullable
     public OCFile getOldFile() {
         return mOldFile;
     }

+ 1 - 2
app/src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.java

@@ -108,8 +108,7 @@ public class DocumentsStorageProvider extends DocumentsProvider {
 
     private static final long CACHE_EXPIRATION = TimeUnit.MILLISECONDS.convert(1, TimeUnit.MINUTES);
 
-    @Inject
-    UserAccountManager accountManager;
+    @Inject UserAccountManager accountManager;
 
     @VisibleForTesting
     static final String DOCUMENTID_SEPARATOR = "/";

+ 10 - 10
app/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.java

@@ -120,22 +120,22 @@ public class ConflictsResolveActivity extends FileActivity implements OnConflict
                     break;
                 case KEEP_LOCAL: // Upload
                     FileUploader.uploadUpdateFile(
-                            getBaseContext(),
-                            user,
-                            file,
-                            localBehaviour,
-                            NameCollisionPolicy.OVERWRITE
+                        getBaseContext(),
+                        user,
+                        file,
+                        localBehaviour,
+                        NameCollisionPolicy.OVERWRITE
                                                  );
 
                     uploadsStorageManager.removeUpload(upload);
                     break;
                 case KEEP_BOTH: // Upload
                     FileUploader.uploadUpdateFile(
-                            getBaseContext(),
-                            user,
-                            file,
-                            localBehaviour,
-                            NameCollisionPolicy.RENAME
+                        getBaseContext(),
+                        user,
+                        file,
+                        localBehaviour,
+                        NameCollisionPolicy.RENAME
                                                  );
 
                     uploadsStorageManager.removeUpload(upload);

+ 1 - 1
app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.java

@@ -196,7 +196,7 @@ public class UploadListActivity extends FileActivity {
             connectivityService,
             userAccountManager,
             powerManagementService
-        )).start();
+                                                        )).start();
 
         // update UI
         uploadListAdapter.loadUploadItemsFromDb();

+ 1 - 1
app/src/main/java/com/owncloud/android/ui/adapter/UploadListAdapter.java

@@ -143,7 +143,7 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter<SectionedVie
                         connectivityService,
                         accountManager,
                         powerManagementService
-                    )).start();
+                                                                    )).start();
                     break;
 
                 default:

+ 18 - 20
app/src/main/java/com/owncloud/android/ui/asynctasks/CopyAndUploadContentUrisTask.java

@@ -21,7 +21,6 @@
  */
 package com.owncloud.android.ui.asynctasks;
 
-import android.accounts.Account;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.database.Cursor;
@@ -60,9 +59,8 @@ public class CopyAndUploadContentUrisTask extends AsyncTask<Object, Void, Result
     private WeakReference<OnCopyTmpFilesTaskListener> mListener;
 
     /**
-     * Reference to application context, used to access app resources. Holding it should not be a problem,
-     * since it needs to exist until the end of the AsyncTask although the caller Activity were finished
-     * before.
+     * Reference to application context, used to access app resources. Holding it should not be a problem, since it
+     * needs to exist until the end of the AsyncTask although the caller Activity were finished before.
      */
     private final Context mAppContext;
 
@@ -92,7 +90,7 @@ public class CopyAndUploadContentUrisTask extends AsyncTask<Object, Void, Result
      *
      * Any idea to prevent this while keeping the functionality will be welcome.
      *
-     * @return  Correct array of parameters to be passed to {@link #execute(Object[])}
+     * @return Correct array of parameters to be passed to {@link #execute(Object[])}
      */
     public static Object[] makeParamsToExecute(
         User user,
@@ -100,9 +98,9 @@ public class CopyAndUploadContentUrisTask extends AsyncTask<Object, Void, Result
         String[] remotePaths,
         int behaviour,
         ContentResolver contentResolver
-    ) {
+                                              ) {
 
-        return new Object[] {
+        return new Object[]{
             user,
             sourceUris,
             remotePaths,
@@ -114,7 +112,7 @@ public class CopyAndUploadContentUrisTask extends AsyncTask<Object, Void, Result
     public CopyAndUploadContentUrisTask(
         OnCopyTmpFilesTaskListener listener,
         Context context
-    ) {
+                                       ) {
         mListener = new WeakReference<>(listener);
         mAppContext = context.getApplicationContext();
     }
@@ -251,18 +249,18 @@ public class CopyAndUploadContentUrisTask extends AsyncTask<Object, Void, Result
 
     private void requestUpload(User user, String localPath, String remotePath, int behaviour, String mimeType) {
         FileUploader.uploadNewFile(
-                mAppContext,
-                user,
-                localPath,
-                remotePath,
-                behaviour,
-                mimeType,
-                false,      // do not create parent folder if not existent
-                UploadFileOperation.CREATED_BY_USER,
-                false,
-                false,
-                NameCollisionPolicy.ASK_USER
-        );
+            mAppContext,
+            user,
+            localPath,
+            remotePath,
+            behaviour,
+            mimeType,
+            false,      // do not create parent folder if not existent
+            UploadFileOperation.CREATED_BY_USER,
+            false,
+            false,
+            NameCollisionPolicy.ASK_USER
+                                  );
     }
 
     @Override

+ 1 - 1
app/src/main/java/com/owncloud/android/utils/FilesSyncHelper.java

@@ -243,7 +243,7 @@ public final class FilesSyncHelper {
                     connectivityService,
                     accountManager,
                     powerManagementService
-                );
+                                               );
             }
         }).start();
     }

+ 104 - 0
app/src/main/java/com/owncloud/android/utils/FilesUploadHelper.kt

@@ -0,0 +1,104 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2022 Tobias Kaminsky
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.utils
+
+import com.nextcloud.client.account.User
+import com.nextcloud.client.jobs.BackgroundJobManager
+import com.owncloud.android.MainApp
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.datamodel.UploadsStorageManager
+import com.owncloud.android.db.OCUpload
+import com.owncloud.android.files.services.NameCollisionPolicy
+import com.owncloud.android.lib.common.utils.Log_OC
+import javax.inject.Inject
+
+class FilesUploadHelper {
+    @Inject
+    lateinit var backgroundJobManager: BackgroundJobManager
+
+    @Inject
+    lateinit var uploadsStorageManager: UploadsStorageManager
+
+    init {
+        MainApp.getAppComponent().inject(this)
+    }
+
+    @Suppress("LongParameterList")
+    fun uploadNewFiles(
+        user: User,
+        localPaths: Array<String>,
+        remotePaths: Array<String>,
+        createRemoteFolder: Boolean,
+        createdBy: Int,
+        requiresWifi: Boolean,
+        requiresCharging: Boolean,
+        nameCollisionPolicy: NameCollisionPolicy
+    ) {
+        for (i in localPaths.indices) {
+            OCUpload(localPaths[i], remotePaths[i], user.accountName).apply {
+                this.nameCollisionPolicy = nameCollisionPolicy
+                isUseWifiOnly = requiresWifi
+                isWhileChargingOnly = requiresCharging
+                uploadStatus = UploadsStorageManager.UploadStatus.UPLOAD_IN_PROGRESS
+                this.createdBy = createdBy
+                isCreateRemoteFolder = createRemoteFolder
+
+                uploadsStorageManager.storeUpload(this)
+                backgroundJobManager.startFilesUploadJob(user)
+            }
+        }
+    }
+
+    fun uploadUpdatedFile(
+        user: User,
+        existingFiles: Array<OCFile>,
+        behaviour: Int,
+        nameCollisionPolicy: NameCollisionPolicy
+    ) {
+        Log_OC.d(this, "upload updated file")
+
+        for (file in existingFiles) {
+            OCUpload(file, user).apply {
+                fileSize = file.fileLength
+                this.nameCollisionPolicy = nameCollisionPolicy
+                isCreateRemoteFolder = true
+                this.localAction = behaviour
+                isUseWifiOnly = false
+                isWhileChargingOnly = false
+                uploadStatus = UploadsStorageManager.UploadStatus.UPLOAD_IN_PROGRESS
+
+                uploadsStorageManager.storeUpload(this)
+                backgroundJobManager.startFilesUploadJob(user)
+            }
+        }
+    }
+
+    fun retryUpload(upload: OCUpload, user: User) {
+        Log_OC.d(this, "retry upload")
+
+        upload.uploadStatus = UploadsStorageManager.UploadStatus.UPLOAD_IN_PROGRESS
+        uploadsStorageManager.updateUpload(upload)
+
+        backgroundJobManager.startFilesUploadJob(user)
+    }
+}

+ 2 - 2
app/src/main/java/com/owncloud/android/utils/ReceiversHelper.java

@@ -74,7 +74,7 @@ public final class ReceiversHelper {
         final UserAccountManager accountManager,
         final ConnectivityService connectivityService,
         final PowerManagementService powerManagementService
-    ) {
+                                                  ) {
         Context context = MainApp.getAppContext();
 
         IntentFilter intentFilter = new IntentFilter();
@@ -101,7 +101,7 @@ public final class ReceiversHelper {
         final UserAccountManager accountManager,
         final ConnectivityService connectivityService,
         final PowerManagementService powerManagementService
-        ) {
+                                                ) {
         Context context = MainApp.getAppContext();
 
         IntentFilter intentFilter = new IntentFilter();

+ 6 - 1
app/src/test/java/com/nextcloud/client/jobs/BackgroundJobFactoryTest.kt

@@ -24,6 +24,7 @@ import android.content.ContentResolver
 import android.content.Context
 import android.content.res.Resources
 import android.os.Build
+import androidx.localbroadcastmanager.content.LocalBroadcastManager
 import androidx.work.WorkerParameters
 import com.nextcloud.client.account.UserAccountManager
 import com.nextcloud.client.core.Clock
@@ -101,6 +102,9 @@ class BackgroundJobFactoryTest {
     @Mock
     private lateinit var viewThemeUtils: ViewThemeUtils
 
+    @Mock
+    private lateinit var localBroadcastManager: LocalBroadcastManager
+
     private lateinit var factory: BackgroundJobFactory
 
     @Before
@@ -122,7 +126,8 @@ class BackgroundJobFactoryTest {
             notificationManager,
             eventBus,
             deckApi,
-            { viewThemeUtils }
+            { viewThemeUtils },
+            { localBroadcastManager }
         )
     }