瀏覽代碼

New upload manager

Signed-off-by: Chris Narkiewicz <hello@ezaquarii.com>
Chris Narkiewicz 4 年之前
父節點
當前提交
ebf63f064b
共有 51 個文件被更改,包括 913 次插入372 次删除
  1. 4 4
      src/androidTest/java/com/nextcloud/client/files/downloader/DownloaderServiceTest.kt
  2. 13 10
      src/androidTest/java/com/nextcloud/client/files/downloader/RegistryTest.kt
  3. 5 5
      src/androidTest/java/com/nextcloud/client/files/downloader/TransferManagerConnectionTest.kt
  4. 16 12
      src/androidTest/java/com/nextcloud/client/files/downloader/TransferManagerTest.kt
  5. 12 11
      src/androidTest/java/com/owncloud/android/AbstractIT.java
  6. 12 11
      src/androidTest/java/com/owncloud/android/AbstractOnServerIT.java
  7. 56 55
      src/androidTest/java/com/owncloud/android/UploadIT.java
  8. 2 2
      src/androidTest/java/com/owncloud/android/datamodel/UploadStorageManagerTest.java
  9. 14 14
      src/androidTest/java/com/owncloud/android/files/services/FileUploaderIT.kt
  10. 1 1
      src/main/AndroidManifest.xml
  11. 2 2
      src/main/java/com/nextcloud/client/di/ComponentsModule.java
  12. 5 5
      src/main/java/com/nextcloud/client/etm/EtmViewModel.kt
  13. 55 27
      src/main/java/com/nextcloud/client/etm/pages/EtmFileTransferFragment.kt
  14. 1 1
      src/main/java/com/nextcloud/client/files/downloader/DownloadTask.kt
  15. 47 22
      src/main/java/com/nextcloud/client/files/downloader/FileTransferService.kt
  16. 29 0
      src/main/java/com/nextcloud/client/files/downloader/PostUploadAction.kt
  17. 156 16
      src/main/java/com/nextcloud/client/files/downloader/Request.kt
  18. 7 1
      src/main/java/com/nextcloud/client/files/downloader/Transfer.kt
  19. 6 3
      src/main/java/com/nextcloud/client/files/downloader/TransferManager.kt
  20. 5 5
      src/main/java/com/nextcloud/client/files/downloader/TransferManagerConnection.kt
  21. 59 13
      src/main/java/com/nextcloud/client/files/downloader/TransferManagerImpl.kt
  22. 93 0
      src/main/java/com/nextcloud/client/files/downloader/UploadTask.kt
  23. 53 0
      src/main/java/com/nextcloud/client/files/downloader/UploadTrigger.kt
  24. 18 15
      src/main/java/com/nextcloud/client/jobs/ContactsBackupWork.kt
  25. 17 5
      src/main/java/com/nextcloud/client/notifications/AppNotificationManager.kt
  26. 23 5
      src/main/java/com/nextcloud/client/notifications/AppNotificationManagerImpl.kt
  27. 3 3
      src/main/java/com/owncloud/android/datamodel/SyncedFolder.java
  28. 2 1
      src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java
  29. 6 5
      src/main/java/com/owncloud/android/db/OCUpload.java
  30. 3 22
      src/main/java/com/owncloud/android/files/services/FileUploader.java
  31. 22 0
      src/main/java/com/owncloud/android/files/services/NameCollisionPolicy.java
  32. 6 5
      src/main/java/com/owncloud/android/operations/SynchronizeFileOperation.java
  33. 4 3
      src/main/java/com/owncloud/android/operations/UploadFileOperation.java
  34. 1 1
      src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.java
  35. 2 2
      src/main/java/com/owncloud/android/providers/FileContentProvider.java
  36. 11 10
      src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.java
  37. 2 1
      src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java
  38. 12 11
      src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java
  39. 17 16
      src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.java
  40. 12 11
      src/main/java/com/owncloud/android/ui/asynctasks/CopyAndUploadContentUrisTask.java
  41. 7 7
      src/main/java/com/owncloud/android/ui/dialog/SyncedFolderPreferencesDialogFragment.java
  42. 6 5
      src/main/java/com/owncloud/android/ui/dialog/parcel/SyncedFolderParcelable.java
  43. 2 1
      src/main/java/com/owncloud/android/ui/fragment/contactsbackup/ContactListFragment.java
  44. 2 1
      src/main/java/com/owncloud/android/ui/helpers/UriUploader.java
  45. 5 0
      src/main/res/drawable/ic_cloud_download.xml
  46. 5 0
      src/main/res/drawable/ic_cloud_upload.xml
  47. 49 11
      src/main/res/layout/etm_transfer_list_item.xml
  48. 1 1
      src/main/res/layout/fragment_etm_downloader.xml
  49. 9 2
      src/main/res/menu/fragment_etm_file_transfer.xml
  50. 11 7
      src/main/res/values/strings.xml
  51. 2 1
      src/test/java/com/owncloud/android/ui/activity/SyncedFoldersActivityTest.java

+ 4 - 4
src/androidTest/java/com/nextcloud/client/files/downloader/DownloaderServiceTest.kt

@@ -44,15 +44,15 @@ class DownloaderServiceTest {
 
     @Test(expected = TimeoutException::class)
     fun cannot_bind_to_service_without_user() {
-        val intent = DownloaderService.createBindIntent(getApplicationContext(), user)
-        intent.removeExtra(DownloaderService.EXTRA_USER)
+        val intent = FileTransferService.createBindIntent(getApplicationContext(), user)
+        intent.removeExtra(FileTransferService.EXTRA_USER)
         service.bindService(intent)
     }
 
     @Test
     fun bind_with_user() {
-        val intent = DownloaderService.createBindIntent(getApplicationContext(), user)
+        val intent = FileTransferService.createBindIntent(getApplicationContext(), user)
         val binder = service.bindService(intent)
-        assertTrue(binder is DownloaderService.Binder)
+        assertTrue(binder is FileTransferService.Binder)
     }
 }

+ 13 - 10
src/androidTest/java/com/nextcloud/client/files/downloader/RegistryTest.kt

@@ -97,7 +97,7 @@ class RegistryTest {
             //      new transfer requests added
             val addedTransfersCount = 10
             for (i in 0 until addedTransfersCount) {
-                val request = Request(user, file)
+                val request = DownloadRequest(user, file)
                 registry.add(request)
             }
 
@@ -116,7 +116,7 @@ class RegistryTest {
         @Before
         fun setUp() {
             for (i in 0 until ENQUEUED_REQUESTS_COUNT) {
-                registry.add(Request(user, file))
+                registry.add(DownloadRequest(user, file))
             }
             assertEquals(ENQUEUED_REQUESTS_COUNT, registry.pending.size)
         }
@@ -180,7 +180,7 @@ class RegistryTest {
 
         @Before
         fun setUp() {
-            val request = Request(user, file)
+            val request = DownloadRequest(user, file)
             uuid = registry.add(request)
             registry.startNext()
             assertEquals(uuid, request.uuid)
@@ -246,7 +246,7 @@ class RegistryTest {
 
         @Before
         fun setUp() {
-            uuid = registry.add(Request(user, file))
+            uuid = registry.add(DownloadRequest(user, file))
             registry.startNext()
             registry.progress(uuid, PROGRESS_FULL)
             resetMocks()
@@ -320,14 +320,14 @@ class RegistryTest {
 
         @Before
         fun setUp() {
-            completedTransferId = registry.add(Request(user, completedTransferFile))
+            completedTransferId = registry.add(DownloadRequest(user, completedTransferFile))
             registry.startNext()
             registry.complete(completedTransferId, true)
 
-            runningTransferId = registry.add(Request(user, runningTransferFile))
+            runningTransferId = registry.add(DownloadRequest(user, runningTransferFile))
             registry.startNext()
 
-            pendingTransferId = registry.add(Request(user, pendingTransferFile))
+            pendingTransferId = registry.add(DownloadRequest(user, pendingTransferFile))
             resetMocks()
 
             assertEquals(1, registry.pending.size)
@@ -475,7 +475,8 @@ class RegistryTest {
         fun request_pending() {
             // WHEN
             //      request is enqueued
-            registry.add(Request(user, OCFile("/path/alpha/1")))
+            val request = DownloadRequest(user, OCFile("/path/alpha/1"))
+            registry.add(request)
             assertEquals(1, registry.pending.size)
             assertEquals(0, registry.running.size)
             assertEquals(0, registry.completed.size)
@@ -489,7 +490,8 @@ class RegistryTest {
         fun request_running() {
             // WHEN
             //      request is running
-            registry.add(Request(user, OCFile("/path/alpha/1")))
+            val request = DownloadRequest(user, OCFile("/path/alpha/1"))
+            registry.add(request)
             registry.startNext()
             assertEquals(0, registry.pending.size)
             assertEquals(1, registry.running.size)
@@ -504,7 +506,8 @@ class RegistryTest {
         fun request_completed() {
             // WHEN
             //      request is running
-            val id = registry.add(Request(user, OCFile("/path/alpha/1")))
+            val request = DownloadRequest(user, OCFile("/path/alpha/1"))
+            val id = registry.add(request)
             registry.startNext()
             registry.complete(id, true)
             assertEquals(0, registry.pending.size)

+ 5 - 5
src/androidTest/java/com/nextcloud/client/files/downloader/TransferManagerConnectionTest.kt

@@ -54,10 +54,10 @@ class TransferManagerConnectionTest {
     lateinit var secondStatusListener: (TransferManager.Status) -> Unit
 
     @MockK
-    lateinit var binder: DownloaderService.Binder
+    lateinit var binder: FileTransferService.Binder
 
     val file get() = OCFile("/path")
-    val componentName = ComponentName("", DownloaderService::class.java.simpleName)
+    val componentName = ComponentName("", FileTransferService::class.java.simpleName)
     val user = MockUser()
 
     @Before
@@ -128,11 +128,11 @@ class TransferManagerConnectionTest {
         connection.registerTransferListener(firstDownloadListener)
         connection.registerTransferListener(secondDownloadListener)
 
-        val request1 = Request(user, file)
+        val request1 = DownloadRequest(user, file)
         connection.enqueue(request1)
         val download1 = Transfer(request1.uuid, TransferState.RUNNING, 50, request1.file, request1)
 
-        val request2 = Request(user, file)
+        val request2 = DownloadRequest(user, file)
         connection.enqueue(request2)
         val download2 = Transfer(request2.uuid, TransferState.RUNNING, 50, request2.file, request1)
 
@@ -223,7 +223,7 @@ class TransferManagerConnectionTest {
         // GIVEN
         //      not bound
         //      some downloads requested without listener
-        val request = Request(user, file)
+        val request = DownloadRequest(user, file)
         connection.enqueue(request)
         val download = Transfer(request.uuid, TransferState.RUNNING, 50, request.file, request)
         connection.registerTransferListener(firstDownloadListener)

+ 16 - 12
src/androidTest/java/com/nextcloud/client/files/downloader/TransferManagerTest.kt

@@ -59,7 +59,10 @@ class TransferManagerTest {
         lateinit var client: OwnCloudClient
 
         @MockK
-        lateinit var mockTaskFactory: DownloadTask.Factory
+        lateinit var mockDownloadTaskFactory: DownloadTask.Factory
+
+        @MockK
+        lateinit var mockUploadTaskFactory: UploadTask.Factory
 
         /**
          * All task mock functions created during test run are
@@ -88,11 +91,12 @@ class TransferManagerTest {
             runner = ManualAsyncRunner()
             transferManager = TransferManagerImpl(
                 runner = runner,
-                taskFactory = mockTaskFactory,
+                downloadTaskFactory = mockDownloadTaskFactory,
+                uploadTaskFactory = mockUploadTaskFactory,
                 threads = MAX_TRANSFER_THREADS
             )
             downloadTaskResult = true
-            every { mockTaskFactory.create() } answers { createMockTask() }
+            every { mockDownloadTaskFactory.create() } answers { createMockTask() }
         }
 
         private fun createMockTask(): DownloadTask {
@@ -119,7 +123,7 @@ class TransferManagerTest {
             // WHEN
             //      download is enqueued
             val file = OCFile("/path")
-            val request = Request(user, file)
+            val request = DownloadRequest(user, file)
             transferManager.enqueue(request)
 
             // THEN
@@ -134,7 +138,7 @@ class TransferManagerTest {
             //      downloader is downloading max simultaneous files
             for (i in 0 until MAX_TRANSFER_THREADS) {
                 val file = OCFile("/running/download/path/$i")
-                val request = Request(user, file)
+                val request = DownloadRequest(user, file)
                 transferManager.enqueue(request)
                 val runningDownload = transferManager.getTransfer(request.uuid)
                 assertEquals(runningDownload?.state, TransferState.RUNNING)
@@ -143,7 +147,7 @@ class TransferManagerTest {
             // WHEN
             //      another download is enqueued
             val file = OCFile("/path")
-            val request = Request(user, file)
+            val request = DownloadRequest(user, file)
             transferManager.enqueue(request)
 
             // THEN
@@ -167,7 +171,7 @@ class TransferManagerTest {
             //      download is being observed
             val downloadUpdates = mutableListOf<Transfer>()
             transferManager.registerTransferListener { downloadUpdates.add(it) }
-            transferManager.enqueue(Request(user, file))
+            transferManager.enqueue(DownloadRequest(user, file))
 
             // WHEN
             //      download task finishes successfully
@@ -186,7 +190,7 @@ class TransferManagerTest {
             //      download is being observed
             val downloadUpdates = mutableListOf<Transfer>()
             transferManager.registerTransferListener { downloadUpdates.add(it) }
-            transferManager.enqueue(Request(user, file))
+            transferManager.enqueue(DownloadRequest(user, file))
 
             // WHEN
             //      download task fails
@@ -205,7 +209,7 @@ class TransferManagerTest {
             //      download is running
             val downloadUpdates = mutableListOf<Transfer>()
             transferManager.registerTransferListener { downloadUpdates.add(it) }
-            transferManager.enqueue(Request(user, file))
+            transferManager.enqueue(DownloadRequest(user, file))
 
             // WHEN
             //      download progress updated 4 times before completion
@@ -233,7 +237,7 @@ class TransferManagerTest {
             // WHEN
             //      multiple downloads are enqueued
             for (i in 0 until MAX_TRANSFER_THREADS * 2) {
-                transferManager.enqueue(Request(user, file))
+                transferManager.enqueue(DownloadRequest(user, file))
             }
 
             // THEN
@@ -252,7 +256,7 @@ class TransferManagerTest {
             // WHEN
             //      download is enqueued
             val file = OCFile("/path/to/file")
-            val request = Request(user, file)
+            val request = DownloadRequest(user, file)
             transferManager.enqueue(request)
 
             // THEN
@@ -265,7 +269,7 @@ class TransferManagerTest {
             // GIVEN
             //      a download is in progress
             val file = OCFile("/path/to/file")
-            val request = Request(user, file)
+            val request = DownloadRequest(user, file)
             transferManager.enqueue(request)
             assertTrue(transferManager.isRunning)
 

+ 12 - 11
src/androidTest/java/com/owncloud/android/AbstractIT.java

@@ -29,6 +29,7 @@ import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.datamodel.UploadsStorageManager;
 import com.owncloud.android.db.OCUpload;
 import com.owncloud.android.files.services.FileUploader;
+import com.owncloud.android.files.services.NameCollisionPolicy;
 import com.owncloud.android.lib.common.OwnCloudClient;
 import com.owncloud.android.lib.common.OwnCloudClientFactory;
 import com.owncloud.android.lib.common.accounts.AccountUtils;
@@ -354,17 +355,17 @@ public abstract class AbstractIT {
                                                                                 targetContext.getContentResolver());
 
         UploadFileOperation newUpload = new UploadFileOperation(
-            uploadsStorageManager,
-            connectivityServiceMock,
-            powerManagementServiceMock,
-            user,
-            null,
-            ocUpload,
-            FileUploader.NameCollisionPolicy.DEFAULT,
-            FileUploader.LOCAL_BEHAVIOUR_COPY,
-            targetContext,
-            false,
-            false
+                uploadsStorageManager,
+                connectivityServiceMock,
+                powerManagementServiceMock,
+                user,
+                null,
+                ocUpload,
+                NameCollisionPolicy.DEFAULT,
+                FileUploader.LOCAL_BEHAVIOUR_COPY,
+                targetContext,
+                false,
+                false
         );
         newUpload.addRenameUploadListener(() -> {
             // dummy

+ 12 - 11
src/androidTest/java/com/owncloud/android/AbstractOnServerIT.java

@@ -20,6 +20,7 @@ import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.datamodel.UploadsStorageManager;
 import com.owncloud.android.db.OCUpload;
 import com.owncloud.android.files.services.FileUploader;
+import com.owncloud.android.files.services.NameCollisionPolicy;
 import com.owncloud.android.lib.common.OwnCloudClient;
 import com.owncloud.android.lib.common.OwnCloudClientFactory;
 import com.owncloud.android.lib.common.accounts.AccountUtils;
@@ -202,17 +203,17 @@ public abstract class AbstractOnServerIT extends AbstractIT {
                                                                                 targetContext.getContentResolver());
 
         UploadFileOperation newUpload = new UploadFileOperation(
-            uploadsStorageManager,
-            connectivityServiceMock,
-            powerManagementServiceMock,
-            user,
-            null,
-            ocUpload,
-            FileUploader.NameCollisionPolicy.DEFAULT,
-            localBehaviour,
-            targetContext,
-            false,
-            false
+                uploadsStorageManager,
+                connectivityServiceMock,
+                powerManagementServiceMock,
+                user,
+                null,
+                ocUpload,
+                NameCollisionPolicy.DEFAULT,
+                localBehaviour,
+                targetContext,
+                false,
+                false
         );
         newUpload.addRenameUploadListener(() -> {
             // dummy

+ 56 - 55
src/androidTest/java/com/owncloud/android/UploadIT.java

@@ -30,6 +30,7 @@ import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.datamodel.UploadsStorageManager;
 import com.owncloud.android.db.OCUpload;
 import com.owncloud.android.files.services.FileUploader;
+import com.owncloud.android.files.services.NameCollisionPolicy;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.operations.RefreshFolderOperation;
 import com.owncloud.android.operations.RemoveFileOperation;
@@ -223,17 +224,17 @@ public class UploadIT extends AbstractOnServerIT {
         ocUpload.setWhileChargingOnly(true);
 
         UploadFileOperation newUpload = new UploadFileOperation(
-            uploadsStorageManager,
-            connectivityServiceMock,
-            powerManagementServiceMock,
-            user,
-            null,
-            ocUpload,
-            FileUploader.NameCollisionPolicy.DEFAULT,
-            FileUploader.LOCAL_BEHAVIOUR_COPY,
-            targetContext,
-            false,
-            true
+                uploadsStorageManager,
+                connectivityServiceMock,
+                powerManagementServiceMock,
+                user,
+                null,
+                ocUpload,
+                NameCollisionPolicy.DEFAULT,
+                FileUploader.LOCAL_BEHAVIOUR_COPY,
+                targetContext,
+                false,
+                true
         );
         newUpload.setRemoteFolderToBeCreated();
         newUpload.addRenameUploadListener(() -> {
@@ -270,17 +271,17 @@ public class UploadIT extends AbstractOnServerIT {
         ocUpload.setWhileChargingOnly(true);
 
         UploadFileOperation newUpload = new UploadFileOperation(
-            uploadsStorageManager,
-            connectivityServiceMock,
-            powerManagementServiceMock,
-            user,
-            null,
-            ocUpload,
-            FileUploader.NameCollisionPolicy.DEFAULT,
-            FileUploader.LOCAL_BEHAVIOUR_COPY,
-            targetContext,
-            false,
-            true
+                uploadsStorageManager,
+                connectivityServiceMock,
+                powerManagementServiceMock,
+                user,
+                null,
+                ocUpload,
+                NameCollisionPolicy.DEFAULT,
+                FileUploader.LOCAL_BEHAVIOUR_COPY,
+                targetContext,
+                false,
+                true
         );
         newUpload.setRemoteFolderToBeCreated();
         newUpload.addRenameUploadListener(() -> {
@@ -309,17 +310,17 @@ public class UploadIT extends AbstractOnServerIT {
         ocUpload.setUseWifiOnly(true);
 
         UploadFileOperation newUpload = new UploadFileOperation(
-            uploadsStorageManager,
-            connectivityServiceMock,
-            powerManagementServiceMock,
-            user,
-            null,
-            ocUpload,
-            FileUploader.NameCollisionPolicy.DEFAULT,
-            FileUploader.LOCAL_BEHAVIOUR_COPY,
-            targetContext,
-            true,
-            false
+                uploadsStorageManager,
+                connectivityServiceMock,
+                powerManagementServiceMock,
+                user,
+                null,
+                ocUpload,
+                NameCollisionPolicy.DEFAULT,
+                FileUploader.LOCAL_BEHAVIOUR_COPY,
+                targetContext,
+                true,
+                false
         );
         newUpload.setRemoteFolderToBeCreated();
         newUpload.addRenameUploadListener(() -> {
@@ -338,17 +339,17 @@ public class UploadIT extends AbstractOnServerIT {
         ocUpload.setWhileChargingOnly(true);
 
         UploadFileOperation newUpload = new UploadFileOperation(
-            uploadsStorageManager,
-            connectivityServiceMock,
-            powerManagementServiceMock,
-            user,
-            null,
-            ocUpload,
-            FileUploader.NameCollisionPolicy.DEFAULT,
-            FileUploader.LOCAL_BEHAVIOUR_COPY,
-            targetContext,
-            true,
-            false
+                uploadsStorageManager,
+                connectivityServiceMock,
+                powerManagementServiceMock,
+                user,
+                null,
+                ocUpload,
+                NameCollisionPolicy.DEFAULT,
+                FileUploader.LOCAL_BEHAVIOUR_COPY,
+                targetContext,
+                true,
+                false
         );
         newUpload.setRemoteFolderToBeCreated();
         newUpload.addRenameUploadListener(() -> {
@@ -386,17 +387,17 @@ public class UploadIT extends AbstractOnServerIT {
         ocUpload.setUseWifiOnly(true);
 
         UploadFileOperation newUpload = new UploadFileOperation(
-            uploadsStorageManager,
-            connectivityServiceMock,
-            powerManagementServiceMock,
-            user,
-            null,
-            ocUpload,
-            FileUploader.NameCollisionPolicy.DEFAULT,
-            FileUploader.LOCAL_BEHAVIOUR_COPY,
-            targetContext,
-            true,
-            false
+                uploadsStorageManager,
+                connectivityServiceMock,
+                powerManagementServiceMock,
+                user,
+                null,
+                ocUpload,
+                NameCollisionPolicy.DEFAULT,
+                FileUploader.LOCAL_BEHAVIOUR_COPY,
+                targetContext,
+                true,
+                false
         );
         newUpload.setRemoteFolderToBeCreated();
         newUpload.addRenameUploadListener(() -> {

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

@@ -13,7 +13,7 @@ import com.owncloud.android.AbstractIT;
 import com.owncloud.android.MainApp;
 import com.owncloud.android.db.OCUpload;
 import com.owncloud.android.db.UploadResult;
-import com.owncloud.android.files.services.FileUploader;
+import com.owncloud.android.files.services.NameCollisionPolicy;
 import com.owncloud.android.lib.common.accounts.AccountUtils;
 import com.owncloud.android.operations.UploadFileOperation;
 
@@ -191,7 +191,7 @@ public class UploadStorageManagerTest extends AbstractIT {
         upload.setFileSize(new Random().nextInt(20000) * 10000);
         upload.setUploadStatus(UploadsStorageManager.UploadStatus.UPLOAD_IN_PROGRESS);
         upload.setLocalAction(2);
-        upload.setNameCollisionPolicy(FileUploader.NameCollisionPolicy.ASK_USER);
+        upload.setNameCollisionPolicy(NameCollisionPolicy.ASK_USER);
         upload.setCreateRemoteFolder(false);
         upload.setUploadEndTimestamp(System.currentTimeMillis());
         upload.setLastResult(UploadResult.DELAYED_FOR_WIFI);

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

@@ -145,7 +145,7 @@ class FileUploaderIT : AbstractOnServerIT() {
             UploadFileOperation.CREATED_BY_USER,
             false,
             false,
-            FileUploader.NameCollisionPolicy.DEFAULT
+            NameCollisionPolicy.DEFAULT
         )
 
         longSleep()
@@ -163,7 +163,7 @@ class FileUploaderIT : AbstractOnServerIT() {
             account,
             ocFile2,
             FileUploader.LOCAL_BEHAVIOUR_COPY,
-            FileUploader.NameCollisionPolicy.OVERWRITE
+            NameCollisionPolicy.OVERWRITE
         )
 
         shortSleep()
@@ -192,7 +192,7 @@ class FileUploaderIT : AbstractOnServerIT() {
                 user,
                 null,
                 ocUpload,
-                FileUploader.NameCollisionPolicy.DEFAULT,
+                NameCollisionPolicy.DEFAULT,
                 FileUploader.LOCAL_BEHAVIOUR_COPY,
                 targetContext,
                 false,
@@ -219,7 +219,7 @@ class FileUploaderIT : AbstractOnServerIT() {
                 user,
                 null,
                 ocUpload2,
-                FileUploader.NameCollisionPolicy.RENAME,
+                NameCollisionPolicy.RENAME,
                 FileUploader.LOCAL_BEHAVIOUR_COPY,
                 targetContext,
                 false,
@@ -262,7 +262,7 @@ class FileUploaderIT : AbstractOnServerIT() {
             UploadFileOperation.CREATED_BY_USER,
             false,
             false,
-            FileUploader.NameCollisionPolicy.DEFAULT
+            NameCollisionPolicy.DEFAULT
         )
 
         longSleep()
@@ -280,7 +280,7 @@ class FileUploaderIT : AbstractOnServerIT() {
             account,
             ocFile2,
             FileUploader.LOCAL_BEHAVIOUR_COPY,
-            FileUploader.NameCollisionPolicy.RENAME
+            NameCollisionPolicy.RENAME
         )
 
         shortSleep()
@@ -312,7 +312,7 @@ class FileUploaderIT : AbstractOnServerIT() {
                 user,
                 null,
                 ocUpload,
-                FileUploader.NameCollisionPolicy.DEFAULT,
+                NameCollisionPolicy.DEFAULT,
                 FileUploader.LOCAL_BEHAVIOUR_COPY,
                 targetContext,
                 false,
@@ -338,7 +338,7 @@ class FileUploaderIT : AbstractOnServerIT() {
                 user,
                 null,
                 ocUpload2,
-                FileUploader.NameCollisionPolicy.CANCEL,
+                NameCollisionPolicy.CANCEL,
                 FileUploader.LOCAL_BEHAVIOUR_COPY,
                 targetContext,
                 false,
@@ -371,7 +371,7 @@ class FileUploaderIT : AbstractOnServerIT() {
             UploadFileOperation.CREATED_BY_USER,
             false,
             false,
-            FileUploader.NameCollisionPolicy.DEFAULT
+            NameCollisionPolicy.DEFAULT
         )
 
         longSleep()
@@ -389,7 +389,7 @@ class FileUploaderIT : AbstractOnServerIT() {
             account,
             ocFile2,
             FileUploader.LOCAL_BEHAVIOUR_COPY,
-            FileUploader.NameCollisionPolicy.CANCEL
+            NameCollisionPolicy.CANCEL
         )
 
         shortSleep()
@@ -416,7 +416,7 @@ class FileUploaderIT : AbstractOnServerIT() {
                 user,
                 null,
                 ocUpload,
-                FileUploader.NameCollisionPolicy.CANCEL,
+                NameCollisionPolicy.CANCEL,
                 FileUploader.LOCAL_BEHAVIOUR_COPY,
                 targetContext,
                 false,
@@ -441,7 +441,7 @@ class FileUploaderIT : AbstractOnServerIT() {
             user,
             null,
             ocUpload2,
-            FileUploader.NameCollisionPolicy.CANCEL,
+            NameCollisionPolicy.CANCEL,
             FileUploader.LOCAL_BEHAVIOUR_COPY,
             targetContext,
             false,
@@ -476,7 +476,7 @@ class FileUploaderIT : AbstractOnServerIT() {
             UploadFileOperation.CREATED_BY_USER,
             false,
             false,
-            FileUploader.NameCollisionPolicy.DEFAULT
+            NameCollisionPolicy.DEFAULT
         )
 
         longSleep()
@@ -494,7 +494,7 @@ class FileUploaderIT : AbstractOnServerIT() {
             account,
             ocFile2,
             FileUploader.LOCAL_BEHAVIOUR_COPY,
-            FileUploader.NameCollisionPolicy.CANCEL
+            NameCollisionPolicy.CANCEL
         )
 
         shortSleep()

+ 1 - 1
src/main/AndroidManifest.xml

@@ -322,7 +322,7 @@
 
         <service android:name=".services.OperationsService" />
         <service android:name=".files.services.FileDownloader" />
-        <service android:name="com.nextcloud.client.files.downloader.DownloaderService" />
+        <service android:name="com.nextcloud.client.files.downloader.FileTransferService" />
         <service android:name=".files.services.FileUploader" />
         <service android:name="com.nextcloud.client.media.PlayerService"/>
 

+ 2 - 2
src/main/java/com/nextcloud/client/di/ComponentsModule.java

@@ -21,7 +21,7 @@
 package com.nextcloud.client.di;
 
 import com.nextcloud.client.etm.EtmActivity;
-import com.nextcloud.client.files.downloader.DownloaderService;
+import com.nextcloud.client.files.downloader.FileTransferService;
 import com.nextcloud.client.jobs.NotificationWork;
 import com.nextcloud.client.logger.ui.LogsActivity;
 import com.nextcloud.client.media.PlayerService;
@@ -207,5 +207,5 @@ abstract class ComponentsModule {
     @ContributesAndroidInjector abstract PlayerService playerService();
 
     @ContributesAndroidInjector
-    abstract DownloaderService fileDownloaderService();
+    abstract FileTransferService fileDownloaderService();
 }

+ 5 - 5
src/main/java/com/nextcloud/client/etm/EtmViewModel.kt

@@ -32,7 +32,7 @@ import com.nextcloud.client.account.User
 import com.nextcloud.client.account.UserAccountManager
 import com.nextcloud.client.etm.pages.EtmAccountsFragment
 import com.nextcloud.client.etm.pages.EtmBackgroundJobsFragment
-import com.nextcloud.client.etm.pages.EtmDownloaderFragment
+import com.nextcloud.client.etm.pages.EtmFileTransferFragment
 import com.nextcloud.client.etm.pages.EtmMigrations
 import com.nextcloud.client.etm.pages.EtmPreferencesFragment
 import com.nextcloud.client.files.downloader.TransferManagerConnection
@@ -104,12 +104,12 @@ class EtmViewModel @Inject constructor(
             pageClass = EtmMigrations::class
         ),
         EtmMenuEntry(
-            iconRes = R.drawable.ic_download_grey600,
-            titleRes = R.string.etm_downloader,
-            pageClass = EtmDownloaderFragment::class
+            iconRes = R.drawable.ic_cloud_download,
+            titleRes = R.string.etm_transfer,
+            pageClass = EtmFileTransferFragment::class
         )
     )
-    val downloaderConnection = TransferManagerConnection(context, accountManager.user)
+    val transferManagerConnection = TransferManagerConnection(context, accountManager.user)
 
     val preferences: Map<String, String> get() {
         return defaultPreferences.all

+ 55 - 27
src/main/java/com/nextcloud/client/etm/pages/EtmDownloaderFragment.kt → src/main/java/com/nextcloud/client/etm/pages/EtmFileTransferFragment.kt

@@ -7,19 +7,21 @@ import android.view.MenuInflater
 import android.view.MenuItem
 import android.view.View
 import android.view.ViewGroup
+import android.widget.ImageView
 import android.widget.TextView
 import androidx.recyclerview.widget.DividerItemDecoration
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
 import com.nextcloud.client.etm.EtmBaseFragment
-import com.nextcloud.client.files.downloader.Direction
+import com.nextcloud.client.files.downloader.DownloadRequest
 import com.nextcloud.client.files.downloader.Transfer
 import com.nextcloud.client.files.downloader.TransferManager
-import com.nextcloud.client.files.downloader.Request
+import com.nextcloud.client.files.downloader.UploadRequest
 import com.owncloud.android.R
 import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.db.OCUpload
 
-class EtmDownloaderFragment : EtmBaseFragment() {
+class EtmFileTransferFragment : EtmBaseFragment() {
 
     companion object {
         private const val TEST_DOWNLOAD_DUMMY_PATH = "/test/dummy_file.txt"
@@ -28,12 +30,14 @@ class EtmDownloaderFragment : EtmBaseFragment() {
     class Adapter(private val inflater: LayoutInflater) : RecyclerView.Adapter<Adapter.ViewHolder>() {
 
         class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
-            val uuid = view.findViewById<TextView>(R.id.etm_download_uuid)
-            val path = view.findViewById<TextView>(R.id.etm_download_path)
-            val user = view.findViewById<TextView>(R.id.etm_download_user)
-            val state = view.findViewById<TextView>(R.id.etm_download_state)
-            val progress = view.findViewById<TextView>(R.id.etm_download_progress)
-            private val progressRow = view.findViewById<View>(R.id.etm_download_progress_row)
+            val type = view.findViewById<TextView>(R.id.etm_transfer_type)
+            val typeIcon = view.findViewById<ImageView>(R.id.etm_transfer_type_icon)
+            val uuid = view.findViewById<TextView>(R.id.etm_transfer_uuid)
+            val path = view.findViewById<TextView>(R.id.etm_transfer_remote_path)
+            val user = view.findViewById<TextView>(R.id.etm_transfer_user)
+            val state = view.findViewById<TextView>(R.id.etm_transfer_state)
+            val progress = view.findViewById<TextView>(R.id.etm_transfer_progress)
+            private val progressRow = view.findViewById<View>(R.id.etm_transfer_progress_row)
 
             var progressEnabled: Boolean = progressRow.visibility == View.VISIBLE
                 get() {
@@ -49,31 +53,44 @@ class EtmDownloaderFragment : EtmBaseFragment() {
                 }
         }
 
-        private var downloads = listOf<Transfer>()
+        private var transfers = listOf<Transfer>()
 
         fun setStatus(status: TransferManager.Status) {
-            downloads = listOf(status.pending, status.running, status.completed).flatten().reversed()
+            transfers = listOf(status.pending, status.running, status.completed).flatten().reversed()
             notifyDataSetChanged()
         }
 
         override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
-            val view = inflater.inflate(R.layout.etm_download_list_item, parent, false)
+            val view = inflater.inflate(R.layout.etm_transfer_list_item, parent, false)
             return ViewHolder(view)
         }
 
         override fun getItemCount(): Int {
-            return downloads.size
+            return transfers.size
         }
 
         override fun onBindViewHolder(vh: ViewHolder, position: Int) {
-            val download = downloads[position]
-            vh.uuid.text = download.uuid.toString()
-            vh.path.text = download.request.file.remotePath
-            vh.user.text = download.request.user.accountName
-            vh.state.text = download.state.toString()
-            if (download.progress >= 0) {
+            val transfer = transfers[position]
+
+            val transferTypeStrId = when (transfer.request) {
+                is DownloadRequest -> R.string.etm_transfer_type_download
+                is UploadRequest -> R.string.etm_transfer_type_upload
+            }
+
+            val transferTypeIconId = when (transfer.request) {
+                is DownloadRequest -> R.drawable.ic_cloud_download
+                is UploadRequest -> R.drawable.ic_cloud_upload
+            }
+
+            vh.type.setText(transferTypeStrId)
+            vh.typeIcon.setImageResource(transferTypeIconId)
+            vh.uuid.text = transfer.uuid.toString()
+            vh.path.text = transfer.request.file.remotePath
+            vh.user.text = transfer.request.user.accountName
+            vh.state.text = transfer.state.toString()
+            if (transfer.progress >= 0) {
                 vh.progressEnabled = true
-                vh.progress.text = download.progress.toString()
+                vh.progress.text = transfer.progress.toString()
             } else {
                 vh.progressEnabled = false
             }
@@ -100,18 +117,18 @@ class EtmDownloaderFragment : EtmBaseFragment() {
 
     override fun onResume() {
         super.onResume()
-        vm.downloaderConnection.bind()
-        vm.downloaderConnection.registerStatusListener(this::onDownloaderStatusChanged)
+        vm.transferManagerConnection.bind()
+        vm.transferManagerConnection.registerStatusListener(this::onDownloaderStatusChanged)
     }
 
     override fun onPause() {
         super.onPause()
-        vm.downloaderConnection.unbind()
+        vm.transferManagerConnection.unbind()
     }
 
     override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
         super.onCreateOptionsMenu(menu, inflater)
-        inflater.inflate(R.menu.fragment_etm_downloader, menu)
+        inflater.inflate(R.menu.fragment_etm_file_transfer, menu)
     }
 
     override fun onOptionsItemSelected(item: MenuItem): Boolean {
@@ -119,18 +136,29 @@ class EtmDownloaderFragment : EtmBaseFragment() {
             R.id.etm_test_download -> {
                 scheduleTestDownload(); true
             }
+            R.id.etm_test_upload -> {
+                scheduleTestUpload(); true
+            }
             else -> super.onOptionsItemSelected(item)
         }
     }
 
     private fun scheduleTestDownload() {
-        val request = Request(
+        val request = DownloadRequest(
             vm.currentUser,
             OCFile(TEST_DOWNLOAD_DUMMY_PATH),
-            Direction.DOWNLOAD,
             true
         )
-        vm.downloaderConnection.enqueue(request)
+        vm.transferManagerConnection.enqueue(request)
+    }
+
+    private fun scheduleTestUpload() {
+        val request = UploadRequest(
+            vm.currentUser,
+            OCUpload(TEST_DOWNLOAD_DUMMY_PATH, TEST_DOWNLOAD_DUMMY_PATH, vm.currentUser.accountName),
+            true
+        )
+        vm.transferManagerConnection.enqueue(request)
     }
 
     private fun onDownloaderStatusChanged(status: TransferManager.Status) {

+ 1 - 1
src/main/java/com/nextcloud/client/files/downloader/DownloadTask.kt

@@ -62,7 +62,7 @@ class DownloadTask(
         }
     }
 
-    fun download(request: Request, progress: (Int) -> Unit, isCancelled: IsCancelled): Result {
+    fun download(request: DownloadRequest, progress: (Int) -> Unit, isCancelled: IsCancelled): Result {
         val op = DownloadFileOperation(request.user.toPlatformAccount(), request.file, context)
         val client = clientProvider.invoke()
         val result = op.execute(client)

+ 47 - 22
src/main/java/com/nextcloud/client/files/downloader/DownloaderService.kt → src/main/java/com/nextcloud/client/files/downloader/FileTransferService.kt

@@ -2,7 +2,7 @@
  * Nextcloud Android client application
  *
  * @author Chris Narkiewicz
- * Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
+ * Copyright (C) 2021 Chris Narkiewicz <hello@ezaquarii.com>
  *
  * 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
@@ -26,30 +26,33 @@ import android.os.IBinder
 import com.nextcloud.client.account.User
 import com.nextcloud.client.core.AsyncRunner
 import com.nextcloud.client.core.LocalBinder
+import com.nextcloud.client.device.PowerManagementService
 import com.nextcloud.client.logger.Logger
 import com.nextcloud.client.network.ClientFactory
+import com.nextcloud.client.network.ConnectivityService
 import com.nextcloud.client.notifications.AppNotificationManager
+import com.owncloud.android.datamodel.UploadsStorageManager
 import dagger.android.AndroidInjection
 import javax.inject.Inject
 import javax.inject.Named
 
-class DownloaderService : Service() {
+class FileTransferService : Service() {
 
     companion object {
         const val TAG = "DownloaderService"
-        const val ACTION_DOWNLOAD = "download"
+        const val ACTION_TRANSFER = "transfer"
         const val EXTRA_REQUEST = "request"
         const val EXTRA_USER = "user"
 
         fun createBindIntent(context: Context, user: User): Intent {
-            return Intent(context, DownloaderService::class.java).apply {
+            return Intent(context, FileTransferService::class.java).apply {
                 putExtra(EXTRA_USER, user)
             }
         }
 
-        fun createDownloadIntent(context: Context, request: Request): Intent {
-            return Intent(context, DownloaderService::class.java).apply {
-                action = ACTION_DOWNLOAD
+        fun createTransferRequestIntent(context: Context, request: Request): Intent {
+            return Intent(context, FileTransferService::class.java).apply {
+                action = ACTION_TRANSFER
                 putExtra(EXTRA_REQUEST, request)
             }
         }
@@ -60,8 +63,8 @@ class DownloaderService : Service() {
      */
     class Binder(
         downloader: TransferManagerImpl,
-        service: DownloaderService
-    ) : LocalBinder<DownloaderService>(service),
+        service: FileTransferService
+    ) : LocalBinder<FileTransferService>(service),
         TransferManager by downloader
 
     @Inject
@@ -77,6 +80,15 @@ class DownloaderService : Service() {
     @Inject
     lateinit var logger: Logger
 
+    @Inject
+    lateinit var uploadsStorageManager: UploadsStorageManager
+
+    @Inject
+    lateinit var connectivityService: ConnectivityService
+
+    @Inject
+    lateinit var powerManagementService: PowerManagementService
+
     val isRunning: Boolean get() = downloaders.any { it.value.isRunning }
 
     private val downloaders: MutableMap<String, TransferManagerImpl> = mutableMapOf()
@@ -86,22 +98,22 @@ class DownloaderService : Service() {
     }
 
     override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
-        if (intent.action != ACTION_DOWNLOAD) {
+        if (intent.action != ACTION_TRANSFER) {
             return START_NOT_STICKY
         }
 
         if (!isRunning) {
             startForeground(
-                AppNotificationManager.DOWNLOAD_NOTIFICATION_ID,
+                AppNotificationManager.TRANSFER_NOTIFICATION_ID,
                 notificationsManager.buildDownloadServiceForegroundNotification()
             )
         }
 
         val request = intent.getParcelableExtra(EXTRA_REQUEST) as Request
-        val downloader = getDownloader(request.user)
-        downloader.enqueue(request)
+        val transferManager = getTransferManager(request.user)
+        transferManager.enqueue(request)
 
-        logger.d(TAG, "Enqueued new download: ${request.uuid} ${request.file.remotePath}")
+        logger.d(TAG, "Enqueued new transfer: ${request.uuid} ${request.file.remotePath}")
 
         return START_NOT_STICKY
     }
@@ -109,25 +121,31 @@ class DownloaderService : Service() {
     override fun onBind(intent: Intent?): IBinder? {
         val user = intent?.getParcelableExtra<User>(EXTRA_USER)
         if (user != null) {
-            return Binder(getDownloader(user), this)
+            return Binder(getTransferManager(user), this)
         } else {
             return null
         }
     }
 
-    private fun onDownloadUpdate(transfer: Transfer) {
+    private fun onTransferUpdate(transfer: Transfer) {
         if (!isRunning) {
             logger.d(TAG, "All downloads completed")
-            notificationsManager.cancelDownloadProgress()
+            notificationsManager.cancelTransferNotification()
             stopForeground(true)
             stopSelf()
-        } else {
-            notificationsManager.postDownloadProgress(
+        } else if (transfer.direction == Direction.DOWNLOAD) {
+            notificationsManager.postDownloadTransferProgress(
                 fileOwner = transfer.request.user,
                 file = transfer.request.file,
                 progress = transfer.progress,
                 allowPreview = !transfer.request.test
             )
+        } else if (transfer.direction == Direction.UPLOAD) {
+            notificationsManager.postUploadTransferProgress(
+                fileOwner = transfer.request.user,
+                file = transfer.request.file,
+                progress = transfer.progress
+            )
         }
     }
 
@@ -136,7 +154,7 @@ class DownloaderService : Service() {
         logger.d(TAG, "Stopping downloader service")
     }
 
-    private fun getDownloader(user: User): TransferManagerImpl {
+    private fun getTransferManager(user: User): TransferManagerImpl {
         val existingDownloader = downloaders[user.accountName]
         return if (existingDownloader != null) {
             existingDownloader
@@ -146,8 +164,15 @@ class DownloaderService : Service() {
                 { clientFactory.create(user) },
                 contentResolver
             )
-            val newDownloader = TransferManagerImpl(runner, downloadTaskFactory)
-            newDownloader.registerTransferListener(this::onDownloadUpdate)
+            val uploadTaskFactory = UploadTask.Factory(
+                applicationContext,
+                uploadsStorageManager,
+                connectivityService,
+                powerManagementService,
+                { clientFactory.create(user) }
+            )
+            val newDownloader = TransferManagerImpl(runner, downloadTaskFactory, uploadTaskFactory)
+            newDownloader.registerTransferListener(this::onTransferUpdate)
             downloaders[user.accountName] = newDownloader
             newDownloader
         }

+ 29 - 0
src/main/java/com/nextcloud/client/files/downloader/PostUploadAction.kt

@@ -0,0 +1,29 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Chris Narkiewicz
+ * Copyright (C) 2021 Chris Narkiewicz <hello@ezaquarii.com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.client.files.downloader
+
+import com.owncloud.android.files.services.FileUploader
+
+enum class PostUploadAction(val value: Int) {
+    NONE(FileUploader.LOCAL_BEHAVIOUR_FORGET),
+    COPY_TO_APP(FileUploader.LOCAL_BEHAVIOUR_COPY),
+    MOVE_TO_APP(FileUploader.LOCAL_BEHAVIOUR_MOVE),
+    DELETE_SOURCE(FileUploader.LOCAL_BEHAVIOUR_DELETE);
+}

+ 156 - 16
src/main/java/com/nextcloud/client/files/downloader/Request.kt

@@ -2,7 +2,7 @@
  * Nextcloud Android client application
  *
  * @author Chris Narkiewicz
- * Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
+ * Copyright (C) 2021 Chris Narkiewicz <hello@ezaquarii.com>
  *
  * 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
@@ -23,8 +23,19 @@ import android.os.Parcel
 import android.os.Parcelable
 import com.nextcloud.client.account.User
 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 java.util.UUID
 
+sealed class Request(
+    val user: User,
+    val file: OCFile,
+    val uuid: UUID,
+    val type: Direction,
+    val test: Boolean
+) : Parcelable
+
 /**
  * Transfer request. This class should collect all information
  * required to trigger transfer operation.
@@ -38,26 +49,24 @@ import java.util.UUID
  * @property uuid Unique request identifier; this identifier can be set in [Transfer]
  * @property dummy if true, this requests a dummy test transfer; no real file transfer will occur
  */
-class Request internal constructor(
-    val user: User,
-    val file: OCFile,
-    val uuid: UUID,
-    val type: Direction = Direction.DOWNLOAD,
-    val test: Boolean = false
-) : Parcelable {
+class DownloadRequest internal constructor(
+    user: User,
+    file: OCFile,
+    uuid: UUID,
+    type: Direction,
+    test: Boolean = false
+) : Request(user, file, uuid, type, test) {
 
     constructor(
         user: User,
         file: OCFile,
-        type: Direction = Direction.DOWNLOAD
-    ) : this(user, file, UUID.randomUUID(), type)
+    ) : this(user, file, UUID.randomUUID(), Direction.DOWNLOAD)
 
     constructor(
         user: User,
         file: OCFile,
-        type: Direction,
         test: Boolean
-    ) : this(user, file, UUID.randomUUID(), type, test)
+    ) : this(user, file, UUID.randomUUID(), Direction.DOWNLOAD, test)
 
     constructor(parcel: Parcel) : this(
         user = parcel.readParcelable<User>(User::class.java.classLoader) as User,
@@ -79,13 +88,144 @@ class Request internal constructor(
         return 0
     }
 
-    companion object CREATOR : Parcelable.Creator<Request> {
-        override fun createFromParcel(parcel: Parcel): Request {
-            return Request(parcel)
+    companion object CREATOR : Parcelable.Creator<DownloadRequest> {
+        override fun createFromParcel(parcel: Parcel): DownloadRequest {
+            return DownloadRequest(parcel)
         }
 
-        override fun newArray(size: Int): Array<Request?> {
+        override fun newArray(size: Int): Array<DownloadRequest?> {
             return arrayOfNulls(size)
         }
     }
 }
+
+@Suppress("LongParameterList")
+class UploadRequest internal constructor(
+    user: User,
+    file: OCFile,
+    val upload: OCUpload,
+    uuid: UUID,
+    type: Direction,
+    test: Boolean,
+) : Request(user, file, uuid, type, test) {
+
+    constructor(
+        user: User,
+        upload: OCUpload,
+        test: Boolean
+    ) : this(
+        user,
+        OCFile(upload.remotePath).apply {
+            storagePath = upload.localPath
+            fileLength = upload.fileSize
+        },
+        upload,
+        UUID.randomUUID(),
+        Direction.UPLOAD,
+        test
+    )
+
+    constructor(
+        user: User,
+        upload: OCUpload
+    ) : this(user, upload, false)
+
+    constructor(parcel: Parcel) : this(
+        user = parcel.readParcelable<User>(User::class.java.classLoader) as User,
+        file = parcel.readParcelable<OCFile>(OCFile::class.java.classLoader) as OCFile,
+        upload = parcel.readParcelable<OCUpload>(OCUpload::class.java.classLoader) as OCUpload,
+        uuid = parcel.readSerializable() as UUID,
+        type = parcel.readSerializable() as Direction,
+        test = parcel.readInt() != 0
+    )
+
+    override fun writeToParcel(parcel: Parcel, flags: Int) {
+        parcel.writeParcelable(user, flags)
+        parcel.writeParcelable(file, flags)
+        parcel.writeParcelable(upload, flags)
+        parcel.writeSerializable(uuid)
+        parcel.writeSerializable(type)
+        parcel.writeInt(if (test) 1 else 0)
+    }
+
+    override fun describeContents(): Int {
+        return 0
+    }
+
+    companion object CREATOR : Parcelable.Creator<UploadRequest> {
+        override fun createFromParcel(parcel: Parcel): UploadRequest {
+            return UploadRequest(parcel)
+        }
+
+        override fun newArray(size: Int): Array<UploadRequest?> {
+            return arrayOfNulls(size)
+        }
+    }
+
+    /**
+     * This class provides a builder pattern with API convenient to be used in Java.
+     */
+    class Builder(private val user: User, private var source: String, private var destination: String) {
+        private var fileSize: Long = 0
+        private var nameConflictPolicy = NameCollisionPolicy.ASK_USER
+        private var createRemoteFolder = true
+        private var trigger = UploadTrigger.USER
+        private var requireWifi = false
+        private var requireCharging = false
+        private var postUploadAction = PostUploadAction.NONE
+
+        fun setPaths(source: String, destination: String): Builder {
+            this.source = source
+            this.destination = destination
+            return this
+        }
+
+        fun setFileSize(fileSize: Long): Builder {
+            this.fileSize = fileSize
+            return this
+        }
+
+        fun setNameConflicPolicy(policy: NameCollisionPolicy): Builder {
+            this.nameConflictPolicy = policy
+            return this
+        }
+
+        fun setCreateRemoteFolder(create: Boolean): Builder {
+            this.createRemoteFolder = create
+            return this
+        }
+
+        fun setTrigger(trigger: UploadTrigger): Builder {
+            this.trigger = trigger
+            return this
+        }
+
+        fun setRequireWifi(require: Boolean): Builder {
+            this.requireWifi = require
+            return this
+        }
+
+        fun setRequireCharging(require: Boolean): Builder {
+            this.requireCharging = require
+            return this
+        }
+
+        fun setPostAction(action: PostUploadAction): Builder {
+            this.postUploadAction = action
+            return this
+        }
+
+        fun build(): Request {
+            val upload = OCUpload(source, destination, user.accountName)
+            upload.fileSize = fileSize
+            upload.nameCollisionPolicy = this.nameConflictPolicy
+            upload.isCreateRemoteFolder = this.createRemoteFolder
+            upload.createdBy = this.trigger.value
+            upload.localAction = this.postUploadAction.value
+            upload.isUseWifiOnly = this.requireWifi
+            upload.isWhileChargingOnly = this.requireCharging
+            upload.uploadStatus = UploadsStorageManager.UploadStatus.UPLOAD_IN_PROGRESS
+            return UploadRequest(user, upload)
+        }
+    }
+}

+ 7 - 1
src/main/java/com/nextcloud/client/files/downloader/Transfer.kt

@@ -2,7 +2,7 @@
  * Nextcloud Android client application
  *
  * @author Chris Narkiewicz
- * Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
+ * Copyright (C) 2021 Chris Narkiewicz <hello@ezaquarii.com>
  *
  * 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
@@ -34,6 +34,7 @@ import java.util.UUID
  * @property progress transfer progress, 0-100 percent
  * @property file transferred file
  * @property request initial transfer request
+ * @property direction transfer direction, download or upload
  */
 data class Transfer(
     val uuid: UUID,
@@ -46,4 +47,9 @@ data class Transfer(
      * True if download is no longer running, false if it is still being processed.
      */
     val isFinished: Boolean get() = state == TransferState.COMPLETED || state == TransferState.FAILED
+
+    val direction: Direction get() = when (request) {
+        is DownloadRequest -> Direction.DOWNLOAD
+        is UploadRequest -> Direction.UPLOAD
+    }
 }

+ 6 - 3
src/main/java/com/nextcloud/client/files/downloader/TransferManager.kt

@@ -2,7 +2,7 @@
  * Nextcloud Android client application
  *
  * @author Chris Narkiewicz
- * Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
+ * Copyright (C) 2021 Chris Narkiewicz <hello@ezaquarii.com>
  *
  * 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
@@ -22,6 +22,9 @@ package com.nextcloud.client.files.downloader
 import com.owncloud.android.datamodel.OCFile
 import java.util.UUID
 
+/**
+ * Transfer manager provides API to upload and download files.
+ */
 interface TransferManager {
 
     /**
@@ -48,7 +51,7 @@ interface TransferManager {
     val status: Status
 
     /**
-     * Register transfer progress listener. Registration is idempotent - listener can be registered only once.
+     * Register transfer progress listener. Registration is idempotent - a listener will be registered only once.
      */
     fun registerTransferListener(listener: (Transfer) -> Unit)
 
@@ -58,7 +61,7 @@ interface TransferManager {
     fun removeTransferListener(listener: (Transfer) -> Unit)
 
     /**
-     * Register transfer manager status listener. Registration is idempotent - listener can be registered only once.
+     * Register transfer manager status listener. Registration is idempotent - a listener will be registered only once.
      */
     fun registerStatusListener(listener: (Status) -> Unit)
 

+ 5 - 5
src/main/java/com/nextcloud/client/files/downloader/TransferManagerConnection.kt

@@ -30,11 +30,11 @@ import java.util.UUID
 class TransferManagerConnection(
     context: Context,
     val user: User
-) : LocalConnection<DownloaderService>(context), TransferManager {
+) : LocalConnection<FileTransferService>(context), TransferManager {
 
     private var transferListeners: MutableSet<(Transfer) -> Unit> = mutableSetOf()
     private var statusListeners: MutableSet<(TransferManager.Status) -> Unit> = mutableSetOf()
-    private var binder: DownloaderService.Binder? = null
+    private var binder: FileTransferService.Binder? = null
     private val transfersRequiringStatusRedelivery: MutableSet<UUID> = mutableSetOf()
 
     override val isRunning: Boolean
@@ -48,7 +48,7 @@ class TransferManagerConnection(
     override fun getTransfer(file: OCFile): Transfer? = binder?.getTransfer(file)
 
     override fun enqueue(request: Request) {
-        val intent = DownloaderService.createDownloadIntent(context, request)
+        val intent = FileTransferService.createTransferRequestIntent(context, request)
         context.startService(intent)
         if (!isConnected && transferListeners.size > 0) {
             transfersRequiringStatusRedelivery.add(request.uuid)
@@ -76,12 +76,12 @@ class TransferManagerConnection(
     }
 
     override fun createBindIntent(): Intent {
-        return DownloaderService.createBindIntent(context, user)
+        return FileTransferService.createBindIntent(context, user)
     }
 
     override fun onBound(binder: IBinder) {
         super.onBound(binder)
-        this.binder = binder as DownloaderService.Binder
+        this.binder = binder as FileTransferService.Binder
         transferListeners.forEach { listener ->
             binder.registerTransferListener(listener)
         }

+ 59 - 13
src/main/java/com/nextcloud/client/files/downloader/TransferManagerImpl.kt

@@ -24,6 +24,7 @@ import com.nextcloud.client.core.IsCancelled
 import com.nextcloud.client.core.OnProgressCallback
 import com.nextcloud.client.core.TaskFunction
 import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.operations.UploadFileOperation
 import java.util.UUID
 
 /**
@@ -33,13 +34,14 @@ import java.util.UUID
  * in the background.
  *
  * @param runner Background task runner. It is important to provide runner that is not shared with UI code.
- * @param taskFactory Download task factory
+ * @param downloadTaskFactory Download task factory
  * @param threads maximum number of concurrent transfer processes
  */
 @Suppress("LongParameterList") // transfer operations requires those resources
 class TransferManagerImpl(
     private val runner: AsyncRunner,
-    private val taskFactory: DownloadTask.Factory,
+    private val downloadTaskFactory: DownloadTask.Factory,
+    private val uploadTaskFactory: UploadTask.Factory,
     threads: Int = 1
 ) : TransferManager {
 
@@ -47,6 +49,7 @@ class TransferManagerImpl(
         const val PROGRESS_PERCENTAGE_MAX = 100
         const val PROGRESS_PERCENTAGE_MIN = 0
         const val TEST_DOWNLOAD_PROGRESS_UPDATE_PERIOD_MS = 200L
+        const val TEST_UPLOAD_PROGRESS_UPDATE_PERIOD_MS = 200L
     }
 
     private val registry = Registry(
@@ -92,25 +95,30 @@ class TransferManagerImpl(
     override fun getTransfer(file: OCFile): Transfer? = registry.getTransfer(file)
 
     private fun onStartTransfer(uuid: UUID, request: Request) {
-        val transferTask = when (request.type) {
-            Direction.DOWNLOAD -> createDownloadTask(request)
-            Direction.UPLOAD -> createDownloadTask(request) // plug a hole for now - uploads are not supported
+        if (request is DownloadRequest) {
+            runner.postTask(
+                task = createDownloadTask(request),
+                onProgress = { progress: Int -> registry.progress(uuid, progress) },
+                onResult = { result -> registry.complete(uuid, result.success, result.file); registry.startNext() },
+                onError = { registry.complete(uuid, false); registry.startNext() }
+            )
+        } else if (request is UploadRequest) {
+            runner.postTask(
+                task = createUploadTask(request),
+                onProgress = { progress: Int -> registry.progress(uuid, progress) },
+                onResult = { result -> registry.complete(uuid, result.success, result.file); registry.startNext() },
+                onError = { registry.complete(uuid, false); registry.startNext() }
+            )
         }
-        runner.postTask(
-            task = transferTask,
-            onProgress = { progress: Int -> registry.progress(uuid, progress) },
-            onResult = { result -> registry.complete(uuid, result.success, result.file); registry.startNext() },
-            onError = { registry.complete(uuid, false); registry.startNext() }
-        )
     }
 
-    private fun createDownloadTask(request: Request): TaskFunction<DownloadTask.Result, Int> {
+    private fun createDownloadTask(request: DownloadRequest): TaskFunction<DownloadTask.Result, Int> {
         return if (request.test) {
             { progress: OnProgressCallback<Int>, isCancelled: IsCancelled ->
                 testDownloadTask(request.file, progress, isCancelled)
             }
         } else {
-            val downloadTask = taskFactory.create()
+            val downloadTask = downloadTaskFactory.create()
             val wrapper: TaskFunction<DownloadTask.Result, Int> = { progress: ((Int) -> Unit), isCancelled ->
                 downloadTask.download(request, progress, isCancelled)
             }
@@ -118,6 +126,25 @@ class TransferManagerImpl(
         }
     }
 
+    private fun createUploadTask(request: UploadRequest): TaskFunction<UploadTask.Result, Int> {
+        return if (request.test) {
+            { progress: OnProgressCallback<Int>, isCancelled: IsCancelled ->
+                val file = UploadFileOperation.obtainNewOCFileToUpload(
+                    request.upload.remotePath,
+                    request.upload.localPath,
+                    request.upload.mimeType
+                )
+                testUploadTask(file, progress, isCancelled)
+            }
+        } else {
+            val uploadTask = uploadTaskFactory.create()
+            val wrapper: TaskFunction<UploadTask.Result, Int> = { progress: ((Int) -> Unit), isCancelled ->
+                uploadTask.upload(request.user, request.upload)
+            }
+            wrapper
+        }
+    }
+
     private fun onTransferUpdate(transfer: Transfer) {
         transferListeners.forEach { it.invoke(transfer) }
         if (statusListeners.isNotEmpty()) {
@@ -144,4 +171,23 @@ class TransferManagerImpl(
         }
         return DownloadTask.Result(file, true)
     }
+
+    /**
+     *  Test upload task is used only to simulate upload process without
+     *  any network traffic. It is used for development.
+     */
+    private fun testUploadTask(
+        file: OCFile,
+        onProgress: OnProgressCallback<Int>,
+        isCancelled: IsCancelled
+    ): UploadTask.Result {
+        for (i in PROGRESS_PERCENTAGE_MIN..PROGRESS_PERCENTAGE_MAX) {
+            onProgress(i)
+            if (isCancelled()) {
+                return UploadTask.Result(file, false)
+            }
+            Thread.sleep(TEST_UPLOAD_PROGRESS_UPDATE_PERIOD_MS)
+        }
+        return UploadTask.Result(file, true)
+    }
 }

+ 93 - 0
src/main/java/com/nextcloud/client/files/downloader/UploadTask.kt

@@ -0,0 +1,93 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Chris Narkiewicz
+ * Copyright (C) 2021 Chris Narkiewicz <hello@ezaquarii.com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.client.files.downloader
+
+import android.content.Context
+import com.nextcloud.client.account.User
+import com.nextcloud.client.device.PowerManagementService
+import com.nextcloud.client.network.ConnectivityService
+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.OwnCloudClient
+import com.owncloud.android.operations.UploadFileOperation
+
+@Suppress("LongParameterList")
+class UploadTask(
+    private val applicationContext: Context,
+    private val uploadsStorageManager: UploadsStorageManager,
+    private val connectivityService: ConnectivityService,
+    private val powerManagementService: PowerManagementService,
+    private val clientProvider: () -> OwnCloudClient
+) {
+
+    data class Result(val file: OCFile, val success: Boolean)
+
+    /**
+     * This class is a helper factory to to keep static dependencies
+     * injection out of the upload task instance.
+     */
+    @Suppress("LongParameterList")
+    class Factory(
+        private val applicationContext: Context,
+        private val uploadsStorageManager: UploadsStorageManager,
+        private val connectivityService: ConnectivityService,
+        private val powerManagementService: PowerManagementService,
+        private val clientProvider: () -> OwnCloudClient
+    ) {
+        fun create(): UploadTask {
+            return UploadTask(
+                applicationContext,
+                uploadsStorageManager,
+                connectivityService,
+                powerManagementService,
+                clientProvider
+            )
+        }
+    }
+
+    fun upload(user: User, upload: OCUpload): Result {
+        val file = UploadFileOperation.obtainNewOCFileToUpload(
+            upload.remotePath,
+            upload.localPath,
+            upload.mimeType
+        )
+        val op = UploadFileOperation(
+            uploadsStorageManager,
+            connectivityService,
+            powerManagementService,
+            user,
+            file,
+            upload,
+            NameCollisionPolicy.ASK_USER,
+            upload.localAction,
+            applicationContext,
+            upload.isUseWifiOnly,
+            upload.isWhileChargingOnly,
+            false
+        )
+        val client = clientProvider()
+        uploadsStorageManager.updateDatabaseUploadStart(op)
+        val result = op.execute(client)
+        uploadsStorageManager.updateDatabaseUploadResult(result, op)
+        return Result(file, result.isSuccess)
+    }
+}

+ 53 - 0
src/main/java/com/nextcloud/client/files/downloader/UploadTrigger.kt

@@ -0,0 +1,53 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Chris Narkiewicz
+ * Copyright (C) 2021 Chris Narkiewicz <hello@ezaquarii.com>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.client.files.downloader
+
+import com.owncloud.android.operations.UploadFileOperation
+
+/**
+ * Upload transfer trigger.
+ */
+enum class UploadTrigger(val value: Int) {
+
+    /**
+     * Transfer triggered manually by the user.
+     */
+    USER(UploadFileOperation.CREATED_BY_USER),
+
+    /**
+     * Transfer triggered automatically by taking a photo.
+     */
+    PHOTO(UploadFileOperation.CREATED_AS_INSTANT_PICTURE),
+
+    /**
+     * Transfer triggered automatically by making a video.
+     */
+    VIDEO(UploadFileOperation.CREATED_AS_INSTANT_VIDEO);
+
+    companion object {
+        @JvmStatic
+        fun fromValue(value: Int) = when (value) {
+            UploadFileOperation.CREATED_BY_USER -> USER
+            UploadFileOperation.CREATED_AS_INSTANT_PICTURE -> PHOTO
+            UploadFileOperation.CREATED_AS_INSTANT_VIDEO -> VIDEO
+            else -> USER
+        }
+    }
+}

+ 18 - 15
src/main/java/com/nextcloud/client/jobs/ContactsBackupWork.kt

@@ -35,13 +35,16 @@ import androidx.work.Worker
 import androidx.work.WorkerParameters
 import com.nextcloud.client.account.User
 import com.nextcloud.client.account.UserAccountManager
+import com.nextcloud.client.files.downloader.PostUploadAction
+import com.nextcloud.client.files.downloader.TransferManagerConnection
+import com.nextcloud.client.files.downloader.UploadRequest
+import com.nextcloud.client.files.downloader.UploadTrigger
 import com.owncloud.android.R
 import com.owncloud.android.datamodel.ArbitraryDataProvider
 import com.owncloud.android.datamodel.FileDataStorageManager
 import com.owncloud.android.datamodel.OCFile
-import com.owncloud.android.files.services.FileUploader
+import com.owncloud.android.files.services.NameCollisionPolicy
 import com.owncloud.android.lib.common.utils.Log_OC
-import com.owncloud.android.operations.UploadFileOperation
 import com.owncloud.android.services.OperationsService
 import com.owncloud.android.services.OperationsService.OperationsServiceBinder
 import com.owncloud.android.ui.activity.ContactsPreferenceActivity
@@ -158,19 +161,19 @@ class ContactsBackupWork(
                 }
             }
         }
-        FileUploader.uploadNewFile(
-            applicationContext,
-            user.toPlatformAccount(),
-            file.absolutePath,
-            backupFolder + filename,
-            FileUploader.LOCAL_BEHAVIOUR_MOVE,
-            null,
-            true,
-            UploadFileOperation.CREATED_BY_USER,
-            false,
-            false,
-            FileUploader.NameCollisionPolicy.RENAME
-        )
+
+        val request = UploadRequest.Builder(user = user, source = file.absolutePath, destination = backupFolder + file)
+            .setFileSize(file.length())
+            .setNameConflicPolicy(NameCollisionPolicy.RENAME)
+            .setCreateRemoteFolder(true)
+            .setTrigger(UploadTrigger.USER)
+            .setPostAction(PostUploadAction.MOVE_TO_APP)
+            .setRequireWifi(false)
+            .setRequireCharging(false)
+            .build()
+
+        val connection = TransferManagerConnection(applicationContext, user)
+        connection.enqueue(request)
     }
 
     private fun expireFiles(daysToExpire: Int, backupFolderString: String, user: User) { // -1 disables expiration

+ 17 - 5
src/main/java/com/nextcloud/client/notifications/AppNotificationManager.kt

@@ -12,7 +12,7 @@ import com.owncloud.android.datamodel.OCFile
 interface AppNotificationManager {
 
     companion object {
-        const val DOWNLOAD_NOTIFICATION_ID = 1_000_000
+        const val TRANSFER_NOTIFICATION_ID = 1_000_000
     }
 
     /**
@@ -23,16 +23,28 @@ interface AppNotificationManager {
     fun buildDownloadServiceForegroundNotification(): Notification
 
     /**
-     * Post download progress notification.
+     * Post download transfer progress notification. Subsequent calls will update
+     * currently displayed transfer notification.
+     *
      * @param fileOwner User owning the downloaded file
      * @param file File being downloaded
      * @param progress Progress as percentage (0-100)
      * @param allowPreview if true, pending intent with preview action is added to the notification
      */
-    fun postDownloadProgress(fileOwner: User, file: OCFile, progress: Int, allowPreview: Boolean = true)
+    fun postDownloadTransferProgress(fileOwner: User, file: OCFile, progress: Int, allowPreview: Boolean = true)
+
+    /**
+     * Post upload transfer progress notification. Subsequent calls will update
+     * currently displayed transfer notification.
+     *
+     * @param fileOwner User owning the downloaded file
+     * @param file File being downloaded
+     * @param progress Progress as percentage (0-100)
+     */
+    fun postUploadTransferProgress(fileOwner: User, file: OCFile, progress: Int)
 
     /**
-     * Removes download progress notification.
+     * Removes download or upload progress notification.
      */
-    fun cancelDownloadProgress()
+    fun cancelTransferNotification()
 }

+ 23 - 5
src/main/java/com/nextcloud/client/notifications/AppNotificationManagerImpl.kt

@@ -49,7 +49,7 @@ class AppNotificationManagerImpl @Inject constructor(
             .build()
     }
 
-    override fun postDownloadProgress(fileOwner: User, file: OCFile, progress: Int, allowPreview: Boolean) {
+    override fun postDownloadTransferProgress(fileOwner: User, file: OCFile, progress: Int, allowPreview: Boolean) {
         val builder = builder(NotificationUtils.NOTIFICATION_CHANNEL_DOWNLOAD)
         val content = resources.getString(
             R.string.downloader_download_in_progress_content,
@@ -57,7 +57,7 @@ class AppNotificationManagerImpl @Inject constructor(
             file.fileName
         )
         builder
-            .setSmallIcon(R.drawable.notification_icon)
+            .setSmallIcon(R.drawable.ic_cloud_download)
             .setTicker(resources.getString(R.string.downloader_download_in_progress_ticker))
             .setContentTitle(resources.getString(R.string.downloader_download_in_progress_ticker))
             .setOngoing(true)
@@ -79,10 +79,28 @@ class AppNotificationManagerImpl @Inject constructor(
             )
             builder.setContentIntent(pendingOpenFileIntent)
         }
-        platformNotificationsManager.notify(AppNotificationManager.DOWNLOAD_NOTIFICATION_ID, builder.build())
+        platformNotificationsManager.notify(AppNotificationManager.TRANSFER_NOTIFICATION_ID, builder.build())
+    }
+
+    override fun postUploadTransferProgress(fileOwner: User, file: OCFile, progress: Int) {
+        val builder = builder(NotificationUtils.NOTIFICATION_CHANNEL_DOWNLOAD)
+        val content = resources.getString(
+            R.string.uploader_upload_in_progress_content,
+            progress,
+            file.fileName
+        )
+        builder
+            .setSmallIcon(R.drawable.ic_cloud_upload)
+            .setTicker(resources.getString(R.string.uploader_upload_in_progress_ticker))
+            .setContentTitle(resources.getString(R.string.uploader_upload_in_progress_ticker))
+            .setOngoing(true)
+            .setProgress(PROGRESS_PERCENTAGE_MAX, progress, progress <= PROGRESS_PERCENTAGE_MIN)
+            .setContentText(content)
+
+        platformNotificationsManager.notify(AppNotificationManager.TRANSFER_NOTIFICATION_ID, builder.build())
     }
 
-    override fun cancelDownloadProgress() {
-        platformNotificationsManager.cancel(AppNotificationManager.DOWNLOAD_NOTIFICATION_ID)
+    override fun cancelTransferNotification() {
+        platformNotificationsManager.cancel(AppNotificationManager.TRANSFER_NOTIFICATION_ID)
     }
 }

+ 3 - 3
src/main/java/com/owncloud/android/datamodel/SyncedFolder.java

@@ -21,7 +21,7 @@
 
 package com.owncloud.android.datamodel;
 
-import com.owncloud.android.files.services.FileUploader;
+import com.owncloud.android.files.services.NameCollisionPolicy;
 
 import java.io.Serializable;
 
@@ -184,8 +184,8 @@ public class SyncedFolder implements Serializable, Cloneable {
         return this.nameCollisionPolicy;
     }
 
-    public FileUploader.NameCollisionPolicy getNameCollisionPolicy() {
-        return FileUploader.NameCollisionPolicy.deserialize(nameCollisionPolicy);
+    public NameCollisionPolicy getNameCollisionPolicy() {
+        return NameCollisionPolicy.deserialize(nameCollisionPolicy);
     }
 
     public boolean isEnabled() {

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

@@ -35,6 +35,7 @@ import com.owncloud.android.db.OCUpload;
 import com.owncloud.android.db.ProviderMeta.ProviderTableMeta;
 import com.owncloud.android.db.UploadResult;
 import com.owncloud.android.files.services.FileUploader;
+import com.owncloud.android.files.services.NameCollisionPolicy;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.operations.UploadFileOperation;
@@ -409,7 +410,7 @@ public class UploadsStorageManager extends Observable {
                     UploadStatus.fromValue(c.getInt(c.getColumnIndex(ProviderTableMeta.UPLOADS_STATUS)))
             );
             upload.setLocalAction(c.getInt(c.getColumnIndex(ProviderTableMeta.UPLOADS_LOCAL_BEHAVIOUR)));
-            upload.setNameCollisionPolicy(FileUploader.NameCollisionPolicy.deserialize(c.getInt(
+            upload.setNameCollisionPolicy(NameCollisionPolicy.deserialize(c.getInt(
                     c.getColumnIndex(ProviderTableMeta.UPLOADS_NAME_COLLISION_POLICY))));
             upload.setCreateRemoteFolder(c.getInt(
                     c.getColumnIndex(ProviderTableMeta.UPLOADS_IS_CREATE_REMOTE_FOLDER)) == 1);

+ 6 - 5
src/main/java/com/owncloud/android/db/OCUpload.java

@@ -32,6 +32,7 @@ import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.datamodel.UploadsStorageManager;
 import com.owncloud.android.datamodel.UploadsStorageManager.UploadStatus;
 import com.owncloud.android.files.services.FileUploader;
+import com.owncloud.android.files.services.NameCollisionPolicy;
 import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.operations.UploadFileOperation;
 import com.owncloud.android.utils.MimeTypeUtil;
@@ -79,7 +80,7 @@ public class OCUpload implements Parcelable {
     /**
      * What to do in case of name collision.
      */
-    private FileUploader.NameCollisionPolicy nameCollisionPolicy;
+    private NameCollisionPolicy nameCollisionPolicy;
 
     /**
      * Create destination folder?
@@ -172,7 +173,7 @@ public class OCUpload implements Parcelable {
         fileSize = -1;
         uploadId = -1;
         localAction = FileUploader.LOCAL_BEHAVIOUR_COPY;
-        nameCollisionPolicy = FileUploader.NameCollisionPolicy.DEFAULT;
+        nameCollisionPolicy = NameCollisionPolicy.DEFAULT;
         createRemoteFolder = false;
         uploadStatus = UploadStatus.UPLOAD_IN_PROGRESS;
         lastResult = UploadResult.UNKNOWN;
@@ -281,7 +282,7 @@ public class OCUpload implements Parcelable {
         remotePath = source.readString();
         accountName = source.readString();
         localAction = source.readInt();
-        nameCollisionPolicy = FileUploader.NameCollisionPolicy.deserialize(source.readInt());
+        nameCollisionPolicy = NameCollisionPolicy.deserialize(source.readInt());
         createRemoteFolder = source.readInt() == 1;
         try {
             uploadStatus = UploadStatus.valueOf(source.readString());
@@ -368,7 +369,7 @@ public class OCUpload implements Parcelable {
         return this.localAction;
     }
 
-    public FileUploader.NameCollisionPolicy getNameCollisionPolicy() {
+    public NameCollisionPolicy getNameCollisionPolicy() {
         return this.nameCollisionPolicy;
     }
 
@@ -424,7 +425,7 @@ public class OCUpload implements Parcelable {
         this.localAction = localAction;
     }
 
-    public void setNameCollisionPolicy(FileUploader.NameCollisionPolicy nameCollisionPolicy) {
+    public void setNameCollisionPolicy(NameCollisionPolicy nameCollisionPolicy) {
         this.nameCollisionPolicy = nameCollisionPolicy;
     }
 

+ 3 - 22
src/main/java/com/owncloud/android/files/services/FileUploader.java

@@ -1090,17 +1090,19 @@ public class FileUploader extends Service
             accountMatch = account == null || account.name.equals(failedUpload.getAccountName());
             resultMatch = uploadResult == null || uploadResult == failedUpload.getLastResult();
             if (accountMatch && resultMatch) {
+                // 1. extract failed upload owner account in efficient name (expensive query)
                 if (currentAccount == null || !currentAccount.name.equals(failedUpload.getAccountName())) {
                     currentAccount = failedUpload.getAccount(accountManager);
                 }
 
                 if (!new File(failedUpload.getLocalPath()).exists()) {
+                    // 2A. for deleted files, mark as permanently failed
                     if (failedUpload.getLastResult() != UploadResult.FILE_NOT_FOUND) {
                         failedUpload.setLastResult(UploadResult.FILE_NOT_FOUND);
                         uploadsStorageManager.updateUpload(failedUpload);
                     }
                 } else {
-
+                    // 2B. for existing local files, try restarting it if possible
                     if (!isPowerSaving && gotNetwork && canUploadBeRetried(failedUpload, gotWifi, charging)) {
                         retryUpload(context, currentAccount, failedUpload);
                     }
@@ -1130,27 +1132,6 @@ public class FileUploader extends Service
     }
 
 
-    /**
-     * Ordinal of enumerated constants is important for old data compatibility.
-     */
-    public enum NameCollisionPolicy {
-        RENAME, // Ordinal corresponds to old forceOverwrite = false (0 in database)
-        OVERWRITE, // Ordinal corresponds to old forceOverwrite = true (1 in database)
-        CANCEL,
-        ASK_USER;
-
-        public static final NameCollisionPolicy DEFAULT = RENAME;
-
-        public static NameCollisionPolicy deserialize(int ordinal) {
-            NameCollisionPolicy[] values = NameCollisionPolicy.values();
-            return ordinal >= 0 && ordinal < values.length ? values[ordinal] : DEFAULT;
-        }
-
-        public int serialize() {
-            return this.ordinal();
-        }
-    }
-
     /**
      * Binder to let client components to perform operations on the queue of uploads.
      *

+ 22 - 0
src/main/java/com/owncloud/android/files/services/NameCollisionPolicy.java

@@ -0,0 +1,22 @@
+package com.owncloud.android.files.services;
+
+/**
+ * Ordinal of enumerated constants is important for old data compatibility.
+ */
+public enum NameCollisionPolicy {
+    RENAME, // Ordinal corresponds to old forceOverwrite = false (0 in database)
+    OVERWRITE, // Ordinal corresponds to old forceOverwrite = true (1 in database)
+    CANCEL,
+    ASK_USER;
+
+    public static final NameCollisionPolicy DEFAULT = RENAME;
+
+    public static NameCollisionPolicy deserialize(int ordinal) {
+        NameCollisionPolicy[] values = NameCollisionPolicy.values();
+        return ordinal >= 0 && ordinal < values.length ? values[ordinal] : DEFAULT;
+    }
+
+    public int serialize() {
+        return this.ordinal();
+    }
+}

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

@@ -29,6 +29,7 @@ import com.nextcloud.client.account.User;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.files.services.FileDownloader;
 import com.owncloud.android.files.services.FileUploader;
+import com.owncloud.android.files.services.NameCollisionPolicy;
 import com.owncloud.android.lib.common.OwnCloudClient;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode;
@@ -292,11 +293,11 @@ public class SynchronizeFileOperation extends SyncOperation {
      */
     private void requestForUpload(OCFile file) {
         FileUploader.uploadUpdateFile(
-            mContext,
-            mUser.toPlatformAccount(),
-            file,
-            FileUploader.LOCAL_BEHAVIOUR_MOVE,
-            FileUploader.NameCollisionPolicy.OVERWRITE
+                mContext,
+                mUser.toPlatformAccount(),
+                file,
+                FileUploader.LOCAL_BEHAVIOUR_MOVE,
+                NameCollisionPolicy.OVERWRITE
         );
 
         mTransferWasRequested = true;

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

@@ -43,6 +43,7 @@ import com.owncloud.android.datamodel.ThumbnailsCacheManager;
 import com.owncloud.android.datamodel.UploadsStorageManager;
 import com.owncloud.android.db.OCUpload;
 import com.owncloud.android.files.services.FileUploader;
+import com.owncloud.android.files.services.NameCollisionPolicy;
 import com.owncloud.android.lib.common.OwnCloudClient;
 import com.owncloud.android.lib.common.network.OnDatatransferProgressListener;
 import com.owncloud.android.lib.common.network.ProgressiveDataTransfer;
@@ -113,7 +114,7 @@ public class UploadFileOperation extends SyncOperation {
     private String mRemotePath;
     private String mFolderUnlockToken;
     private boolean mRemoteFolderToBeCreated;
-    private FileUploader.NameCollisionPolicy mNameCollisionPolicy;
+    private NameCollisionPolicy mNameCollisionPolicy;
     private int mLocalBehaviour;
     private int mCreatedBy;
     private boolean mOnWifiOnly;
@@ -177,7 +178,7 @@ public class UploadFileOperation extends SyncOperation {
                                User user,
                                OCFile file,
                                OCUpload upload,
-                               FileUploader.NameCollisionPolicy nameCollisionPolicy,
+                               NameCollisionPolicy nameCollisionPolicy,
                                int localBehaviour,
                                Context context,
                                boolean onWifiOnly,
@@ -192,7 +193,7 @@ public class UploadFileOperation extends SyncOperation {
                                User user,
                                OCFile file,
                                OCUpload upload,
-                               FileUploader.NameCollisionPolicy nameCollisionPolicy,
+                               NameCollisionPolicy nameCollisionPolicy,
                                int localBehaviour,
                                Context context,
                                boolean onWifiOnly,

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

@@ -54,7 +54,7 @@ import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.datamodel.ThumbnailsCacheManager;
 import com.owncloud.android.files.services.FileDownloader;
 import com.owncloud.android.files.services.FileUploader;
-import com.owncloud.android.files.services.FileUploader.NameCollisionPolicy;
+import com.owncloud.android.files.services.NameCollisionPolicy;
 import com.owncloud.android.lib.common.OwnCloudAccount;
 import com.owncloud.android.lib.common.OwnCloudClient;
 import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;

+ 2 - 2
src/main/java/com/owncloud/android/providers/FileContentProvider.java

@@ -49,7 +49,7 @@ import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.datamodel.SyncedFolder;
 import com.owncloud.android.db.ProviderMeta;
 import com.owncloud.android.db.ProviderMeta.ProviderTableMeta;
-import com.owncloud.android.files.services.FileUploader;
+import com.owncloud.android.files.services.NameCollisionPolicy;
 import com.owncloud.android.lib.common.accounts.AccountUtils;
 import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.lib.resources.shares.ShareType;
@@ -2185,7 +2185,7 @@ public class FileContentProvider extends ContentProvider {
                     // make sure all existing folders set to FileUploader.NameCollisionPolicy.ASK_USER.
                     db.execSQL("UPDATE " + ProviderTableMeta.SYNCED_FOLDERS_TABLE_NAME + " SET " +
                                    ProviderTableMeta.SYNCED_FOLDER_NAME_COLLISION_POLICY + " = " +
-                                   FileUploader.NameCollisionPolicy.ASK_USER.serialize());
+                                   NameCollisionPolicy.ASK_USER.serialize());
                     upgraded = true;
                     db.setTransactionSuccessful();
                 } finally {

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

@@ -31,6 +31,7 @@ import com.owncloud.android.datamodel.UploadsStorageManager;
 import com.owncloud.android.db.OCUpload;
 import com.owncloud.android.files.services.FileDownloader;
 import com.owncloud.android.files.services.FileUploader;
+import com.owncloud.android.files.services.NameCollisionPolicy;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation;
@@ -120,22 +121,22 @@ public class ConflictsResolveActivity extends FileActivity implements OnConflict
                     break;
                 case KEEP_LOCAL: // Upload
                     FileUploader.uploadUpdateFile(
-                        getBaseContext(),
-                        getAccount(),
-                        file,
-                        localBehaviour,
-                        FileUploader.NameCollisionPolicy.OVERWRITE
+                            getBaseContext(),
+                            getAccount(),
+                            file,
+                            localBehaviour,
+                            NameCollisionPolicy.OVERWRITE
                                                  );
 
                     uploadsStorageManager.removeUpload(upload);
                     break;
                 case KEEP_BOTH: // Upload
                     FileUploader.uploadUpdateFile(
-                        getBaseContext(),
-                        getAccount(),
-                        file,
-                        localBehaviour,
-                        FileUploader.NameCollisionPolicy.RENAME
+                            getBaseContext(),
+                            getAccount(),
+                            file,
+                            localBehaviour,
+                            NameCollisionPolicy.RENAME
                                                  );
 
                     uploadsStorageManager.removeUpload(upload);

+ 2 - 1
src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java

@@ -71,6 +71,7 @@ import com.owncloud.android.files.services.FileDownloader;
 import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder;
 import com.owncloud.android.files.services.FileUploader;
 import com.owncloud.android.files.services.FileUploader.FileUploaderBinder;
+import com.owncloud.android.files.services.NameCollisionPolicy;
 import com.owncloud.android.lib.common.accounts.AccountUtils;
 import com.owncloud.android.lib.common.operations.RemoteOperation;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
@@ -1001,7 +1002,7 @@ public class FileDisplayActivity extends FileActivity
                 UploadFileOperation.CREATED_BY_USER,
                 false,
                 false,
-                FileUploader.NameCollisionPolicy.ASK_USER
+                NameCollisionPolicy.ASK_USER
             );
 
         } else {

+ 12 - 11
src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java

@@ -69,6 +69,7 @@ import com.owncloud.android.MainApp;
 import com.owncloud.android.R;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.files.services.FileUploader;
+import com.owncloud.android.files.services.NameCollisionPolicy;
 import com.owncloud.android.lib.common.operations.RemoteOperation;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode;
@@ -896,17 +897,17 @@ public class ReceiveExternalFilesActivity extends FileActivity
 
     public void uploadFile(String tmpName, String filename) {
         FileUploader.uploadNewFile(
-            getBaseContext(),
-            getAccount(),
-            tmpName,
-            mFile.getRemotePath() + filename,
-            FileUploader.LOCAL_BEHAVIOUR_COPY,
-            null,
-            true,
-            UploadFileOperation.CREATED_BY_USER,
-            false,
-            false,
-            FileUploader.NameCollisionPolicy.ASK_USER
+                getBaseContext(),
+                getAccount(),
+                tmpName,
+                mFile.getRemotePath() + filename,
+                FileUploader.LOCAL_BEHAVIOUR_COPY,
+                null,
+                true,
+                UploadFileOperation.CREATED_BY_USER,
+                false,
+                false,
+                NameCollisionPolicy.ASK_USER
         );
         finish();
     }

+ 17 - 16
src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.java

@@ -61,6 +61,7 @@ import com.owncloud.android.datamodel.SyncedFolder;
 import com.owncloud.android.datamodel.SyncedFolderDisplayItem;
 import com.owncloud.android.datamodel.SyncedFolderProvider;
 import com.owncloud.android.files.services.FileUploader;
+import com.owncloud.android.files.services.NameCollisionPolicy;
 import com.owncloud.android.ui.adapter.SyncedFolderAdapter;
 import com.owncloud.android.ui.decoration.MediaGridItemDecoration;
 import com.owncloud.android.ui.dialog.SyncedFolderPreferencesDialogFragment;
@@ -445,7 +446,7 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
                 false,
                 getAccount().name,
                 FileUploader.LOCAL_BEHAVIOUR_FORGET,
-                FileUploader.NameCollisionPolicy.ASK_USER.serialize(),
+                NameCollisionPolicy.ASK_USER.serialize(),
                 false,
                 clock.getCurrentTime(),
                 mediaFolder.filePaths,
@@ -524,21 +525,21 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
         } else if (itemId == R.id.action_create_custom_folder) {
             Log.d(TAG, "Show custom folder dialog");
             SyncedFolderDisplayItem emptyCustomFolder = new SyncedFolderDisplayItem(
-                UNPERSISTED_ID,
-                null,
-                null,
-                true,
-                false,
-                true,
-                false,
-                getAccount().name,
-                FileUploader.LOCAL_BEHAVIOUR_FORGET,
-                FileUploader.NameCollisionPolicy.ASK_USER.serialize(),
-                false,
-                clock.getCurrentTime(),
-                null,
-                MediaFolderType.CUSTOM,
-                false);
+                    UNPERSISTED_ID,
+                    null,
+                    null,
+                    true,
+                    false,
+                    true,
+                    false,
+                    getAccount().name,
+                    FileUploader.LOCAL_BEHAVIOUR_FORGET,
+                    NameCollisionPolicy.ASK_USER.serialize(),
+                    false,
+                    clock.getCurrentTime(),
+                    null,
+                    MediaFolderType.CUSTOM,
+                    false);
             onSyncFolderSettingsClick(0, emptyCustomFolder);
 
             result = super.onOptionsItemSelected(item);

+ 12 - 11
src/main/java/com/owncloud/android/ui/asynctasks/CopyAndUploadContentUrisTask.java

@@ -33,6 +33,7 @@ import android.widget.Toast;
 import com.nextcloud.client.account.User;
 import com.owncloud.android.R;
 import com.owncloud.android.files.services.FileUploader;
+import com.owncloud.android.files.services.NameCollisionPolicy;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode;
 import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.operations.UploadFileOperation;
@@ -250,17 +251,17 @@ public class CopyAndUploadContentUrisTask extends AsyncTask<Object, Void, Result
 
     private void requestUpload(Account account, String localPath, String remotePath, int behaviour, String mimeType) {
         FileUploader.uploadNewFile(
-            mAppContext,
-            account,
-            localPath,
-            remotePath,
-            behaviour,
-            mimeType,
-            false,      // do not create parent folder if not existent
-            UploadFileOperation.CREATED_BY_USER,
-            false,
-            false,
-            FileUploader.NameCollisionPolicy.ASK_USER
+                mAppContext,
+                account,
+                localPath,
+                remotePath,
+                behaviour,
+                mimeType,
+                false,      // do not create parent folder if not existent
+                UploadFileOperation.CREATED_BY_USER,
+                false,
+                false,
+                NameCollisionPolicy.ASK_USER
         );
     }
 

+ 7 - 7
src/main/java/com/owncloud/android/ui/dialog/SyncedFolderPreferencesDialogFragment.java

@@ -39,7 +39,7 @@ import com.google.android.material.button.MaterialButton;
 import com.owncloud.android.R;
 import com.owncloud.android.datamodel.MediaFolderType;
 import com.owncloud.android.datamodel.SyncedFolderDisplayItem;
-import com.owncloud.android.files.services.FileUploader;
+import com.owncloud.android.files.services.NameCollisionPolicy;
 import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.ui.activity.FolderPickerActivity;
 import com.owncloud.android.ui.activity.UploadFilesActivity;
@@ -589,7 +589,7 @@ public class SyncedFolderPreferencesDialogFragment extends DialogFragment {
      * Get index for name collision selection dialog.
      * @return 0 if ASK_USER, 1 if OVERWRITE, 2 if RENAME, 3 if SKIP, Otherwise: 0
      */
-    static private Integer getSelectionIndexForNameCollisionPolicy(FileUploader.NameCollisionPolicy nameCollisionPolicy) {
+    static private Integer getSelectionIndexForNameCollisionPolicy(NameCollisionPolicy nameCollisionPolicy) {
         switch (nameCollisionPolicy) {
             case OVERWRITE:
                 return 1;
@@ -608,17 +608,17 @@ public class SyncedFolderPreferencesDialogFragment extends DialogFragment {
      *
      * @return ASK_USER if 0, OVERWRITE if 1, RENAME if 2, SKIP if 3. Otherwise: ASK_USER
      */
-    static private FileUploader.NameCollisionPolicy getNameCollisionPolicyForSelectionIndex(int index) {
+    static private NameCollisionPolicy getNameCollisionPolicyForSelectionIndex(int index) {
         switch (index) {
             case 1:
-                return FileUploader.NameCollisionPolicy.OVERWRITE;
+                return NameCollisionPolicy.OVERWRITE;
             case 2:
-                return FileUploader.NameCollisionPolicy.RENAME;
+                return NameCollisionPolicy.RENAME;
             case 3:
-                return FileUploader.NameCollisionPolicy.CANCEL;
+                return NameCollisionPolicy.CANCEL;
             case 0:
             default:
-                return FileUploader.NameCollisionPolicy.ASK_USER;
+                return NameCollisionPolicy.ASK_USER;
         }
     }
 }

+ 6 - 5
src/main/java/com/owncloud/android/ui/dialog/parcel/SyncedFolderParcelable.java

@@ -26,6 +26,7 @@ import android.os.Parcelable;
 import com.owncloud.android.datamodel.MediaFolderType;
 import com.owncloud.android.datamodel.SyncedFolderDisplayItem;
 import com.owncloud.android.files.services.FileUploader;
+import com.owncloud.android.files.services.NameCollisionPolicy;
 
 /**
  * Parcelable for {@link SyncedFolderDisplayItem} objects to transport them from/to dialog fragments.
@@ -40,7 +41,7 @@ public class SyncedFolderParcelable implements Parcelable {
     private boolean enabled = false;
     private boolean subfolderByDate = false;
     private Integer uploadAction;
-    private FileUploader.NameCollisionPolicy nameCollisionPolicy = FileUploader.NameCollisionPolicy.ASK_USER;
+    private NameCollisionPolicy nameCollisionPolicy = NameCollisionPolicy.ASK_USER;
     private MediaFolderType type;
     private boolean hidden = false;
     private long id;
@@ -60,7 +61,7 @@ public class SyncedFolderParcelable implements Parcelable {
         type = syncedFolderDisplayItem.getType();
         account = syncedFolderDisplayItem.getAccount();
         uploadAction = syncedFolderDisplayItem.getUploadAction();
-        nameCollisionPolicy = FileUploader.NameCollisionPolicy.deserialize(
+        nameCollisionPolicy = NameCollisionPolicy.deserialize(
             syncedFolderDisplayItem.getNameCollisionPolicyInt());
         this.section = section;
         hidden = syncedFolderDisplayItem.isHidden();
@@ -79,7 +80,7 @@ public class SyncedFolderParcelable implements Parcelable {
         type = MediaFolderType.getById(read.readInt());
         account = read.readString();
         uploadAction = read.readInt();
-        nameCollisionPolicy = FileUploader.NameCollisionPolicy.deserialize(read.readInt());
+        nameCollisionPolicy = NameCollisionPolicy.deserialize(read.readInt());
         section = read.readInt();
         hidden = read.readInt() != 0;
     }
@@ -191,7 +192,7 @@ public class SyncedFolderParcelable implements Parcelable {
         return this.uploadAction;
     }
 
-    public FileUploader.NameCollisionPolicy getNameCollisionPolicy() {
+    public NameCollisionPolicy getNameCollisionPolicy() {
         return this.nameCollisionPolicy;
     }
 
@@ -247,7 +248,7 @@ public class SyncedFolderParcelable implements Parcelable {
         this.subfolderByDate = subfolderByDate;
     }
 
-    public void setNameCollisionPolicy(FileUploader.NameCollisionPolicy nameCollisionPolicy) {
+    public void setNameCollisionPolicy(NameCollisionPolicy nameCollisionPolicy) {
         this.nameCollisionPolicy = nameCollisionPolicy;
     }
 

+ 2 - 1
src/main/java/com/owncloud/android/ui/fragment/contactsbackup/ContactListFragment.java

@@ -53,6 +53,7 @@ import com.nextcloud.client.account.User;
 import com.nextcloud.client.account.UserAccountManager;
 import com.nextcloud.client.di.Injectable;
 import com.nextcloud.client.files.downloader.Direction;
+import com.nextcloud.client.files.downloader.DownloadRequest;
 import com.nextcloud.client.files.downloader.Request;
 import com.nextcloud.client.files.downloader.Transfer;
 import com.nextcloud.client.files.downloader.TransferManagerConnection;
@@ -186,7 +187,7 @@ public class ContactListFragment extends FileFragment implements Injectable {
         fileDownloader.registerTransferListener(this::onDownloadUpdate);
         fileDownloader.bind();
         if (!ocFile.isDown()) {
-            Request request = new Request(user, ocFile, Direction.DOWNLOAD);
+            Request request = new DownloadRequest(user, ocFile);
             fileDownloader.enqueue(request);
         } else {
             loadContactsTask.execute();

+ 2 - 1
src/main/java/com/owncloud/android/ui/helpers/UriUploader.java

@@ -25,6 +25,7 @@ import android.os.Parcelable;
 import com.nextcloud.client.account.User;
 import com.owncloud.android.R;
 import com.owncloud.android.files.services.FileUploader;
+import com.owncloud.android.files.services.NameCollisionPolicy;
 import com.owncloud.android.lib.common.utils.Log_OC;
 import com.owncloud.android.operations.UploadFileOperation;
 import com.owncloud.android.ui.activity.FileActivity;
@@ -165,7 +166,7 @@ public class UriUploader {
             UploadFileOperation.CREATED_BY_USER,
             false,
             false,
-            FileUploader.NameCollisionPolicy.ASK_USER
+            NameCollisionPolicy.ASK_USER
         );
     }
 

+ 5 - 0
src/main/res/drawable/ic_cloud_download.xml

@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#757575"
+    android:viewportHeight="24" android:viewportWidth="24"
+    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@android:color/white" android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4 9.11,4 6.6,5.64 5.35,8.04 2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5 0,-2.64 -2.05,-4.78 -4.65,-4.96zM17,13l-5,5 -5,-5h3V9h4v4h3z"/>
+</vector>

+ 5 - 0
src/main/res/drawable/ic_cloud_upload.xml

@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#757575"
+    android:viewportHeight="24" android:viewportWidth="24"
+    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@android:color/white" android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4 9.11,4 6.6,5.64 5.35,8.04 2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5 0,-2.64 -2.05,-4.78 -4.65,-4.96zM14,13v4h-4v-4H7l5,-5 5,5h-3z"/>
+</vector>

+ 49 - 11
src/main/res/layout/etm_download_list_item.xml → src/main/res/layout/etm_transfer_list_item.xml

@@ -34,10 +34,48 @@
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_marginEnd="20dp"
-            android:text="@string/etm_download_uuid" />
+            android:text="@string/etm_transfer_type" />
+
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:layout_width="match_parent"
+            android:layout_height="match_parent">
+
+            <ImageView
+                android:id="@+id/etm_transfer_type_icon"
+                android:layout_width="wrap_content"
+                android:layout_height="0dip"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="parent"
+                app:layout_constraintBottom_toBottomOf="parent"
+                tools:src="@drawable/ic_cloud_download"
+                tools:ignore="ContentDescription" />
+
+            <TextView
+                android:id="@+id/etm_transfer_type"
+                android:layout_width="0dip"
+                android:layout_height="0dip"
+                android:layout_marginStart="4dp"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toEndOf="@+id/etm_transfer_type_icon"
+                app:layout_constraintTop_toTopOf="parent"
+                app:layout_constraintBottom_toBottomOf="parent"
+                tools:text="@string/etm_transfer_type_download" />
+        </androidx.constraintlayout.widget.ConstraintLayout>
+
+    </TableRow>
+
+    <TableRow
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content">
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginEnd="20dp"
+            android:text="@string/etm_transfer_uuid" />
 
         <TextView
-            android:id="@+id/etm_download_uuid"
+            android:id="@+id/etm_transfer_uuid"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             tools:text="d7edb387-0b61-4e4e-a728-ffab3055d700" />
@@ -51,10 +89,10 @@
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_marginEnd="20dp"
-            android:text="@string/etm_download_path" />
+            android:text="@string/etm_transfer_remote_path" />
 
         <TextView
-            android:id="@+id/etm_download_path"
+            android:id="@+id/etm_transfer_remote_path"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             tools:text="file path" />
@@ -69,10 +107,10 @@
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_marginEnd="20dp"
-            android:text="@string/etm_download_user" />
+            android:text="@string/etm_transfer_user" />
 
         <TextView
-            android:id="@+id/etm_download_user"
+            android:id="@+id/etm_transfer_user"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             tools:text="user@nextcloud.com" />
@@ -87,10 +125,10 @@
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_marginEnd="20dp"
-            android:text="@string/etm_download_state" />
+            android:text="@string/etm_transfer_state" />
 
         <TextView
-            android:id="@+id/etm_download_state"
+            android:id="@+id/etm_transfer_state"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             tools:text="PENDING" />
@@ -98,7 +136,7 @@
     </TableRow>
 
     <TableRow
-        android:id="@+id/etm_download_progress_row"
+        android:id="@+id/etm_transfer_progress_row"
         android:layout_width="match_parent"
         android:layout_height="wrap_content">
 
@@ -106,10 +144,10 @@
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_marginEnd="20dp"
-            android:text="@string/etm_download_progress" />
+            android:text="@string/etm_transfer_progress" />
 
         <TextView
-            android:id="@+id/etm_download_progress"
+            android:id="@+id/etm_transfer_progress"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             tools:text="50%" />

+ 1 - 1
src/main/res/layout/fragment_etm_downloader.xml

@@ -21,7 +21,7 @@
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    tools:context="com.nextcloud.client.etm.pages.EtmDownloaderFragment">
+    tools:context="com.nextcloud.client.etm.pages.EtmFileTransferFragment">
 
     <androidx.recyclerview.widget.RecyclerView
         android:id="@+id/etm_download_list"

+ 9 - 2
src/main/res/menu/fragment_etm_downloader.xml → src/main/res/menu/fragment_etm_file_transfer.xml

@@ -25,9 +25,16 @@
 
     <item
         android:id="@+id/etm_test_download"
-        android:title="@string/etm_download_enqueue_test_download"
+        android:title="@string/etm_transfer_enqueue_test_download"
         app:showAsAction="ifRoom"
         android:showAsAction="ifRoom"
-        android:icon="@drawable/ic_plus" />
+        android:icon="@drawable/ic_cloud_download" />
+
+    <item
+        android:id="@+id/etm_test_upload"
+        android:title="@string/etm_transfer_enqueue_test_upload"
+        app:showAsAction="ifRoom"
+        android:showAsAction="ifRoom"
+        android:icon="@drawable/ic_cloud_upload" />
 
 </menu>

+ 11 - 7
src/main/res/values/strings.xml

@@ -856,13 +856,17 @@
     <string name="etm_background_job_started">Started</string>
     <string name="etm_background_job_progress">Progress</string>
     <string name="etm_migrations">Migrations (app upgrade)</string>
-    <string name="etm_downloader">Downloader</string>
-    <string name="etm_download_path">Remote path</string>
-    <string name="etm_download_enqueue_test_download">Enqueue test download</string>
-    <string name="etm_download_uuid" translatable="false">@string/etm_background_job_uuid</string>
-    <string name="etm_download_user" translatable="false">@string/etm_background_job_user</string>
-    <string name="etm_download_state" translatable="false">@string/etm_background_job_state</string>
-    <string name="etm_download_progress" translatable="false">@string/etm_background_job_progress</string>
+    <string name="etm_transfer">File transfer</string>
+    <string name="etm_transfer_remote_path">Remote path</string>
+    <string name="etm_transfer_enqueue_test_download">Enqueue test download</string>
+    <string name="etm_transfer_enqueue_test_upload">Enqueue test upload</string>
+    <string name="etm_transfer_type">Transfer</string>
+    <string name="etm_transfer_type_upload">upload</string>
+    <string name="etm_transfer_type_download">download</string>
+    <string name="etm_transfer_uuid" translatable="false">@string/etm_background_job_uuid</string>
+    <string name="etm_transfer_user" translatable="false">@string/etm_background_job_user</string>
+    <string name="etm_transfer_state" translatable="false">@string/etm_background_job_state</string>
+    <string name="etm_transfer_progress" translatable="false">@string/etm_background_job_progress</string>
 
     <string name="logs_status_loading">Loading…</string>
     <string name="logs_status_filtered">Logs: %1$d kB, query matched %2$d / %3$d in %4$d ms</string>

+ 2 - 1
src/test/java/com/owncloud/android/ui/activity/SyncedFoldersActivityTest.java

@@ -25,6 +25,7 @@ package com.owncloud.android.ui.activity;
 import com.owncloud.android.datamodel.MediaFolderType;
 import com.owncloud.android.datamodel.SyncedFolderDisplayItem;
 import com.owncloud.android.files.services.FileUploader;
+import com.owncloud.android.files.services.NameCollisionPolicy;
 
 import org.junit.Test;
 
@@ -168,7 +169,7 @@ public class SyncedFoldersActivityTest {
                                            true,
                                            "test@nextcloud.com",
                                            FileUploader.LOCAL_BEHAVIOUR_MOVE,
-                                           FileUploader.NameCollisionPolicy.ASK_USER.serialize(),
+                                           NameCollisionPolicy.ASK_USER.serialize(),
                                            enabled,
                                            System.currentTimeMillis(),
                                            new ArrayList<String>(),