浏览代码

Merge pull request #12308 from nextcloud/refactor/use-work-manager-file-download

Use Work Manager For File Download
Alper Öztürk 1 年之前
父节点
当前提交
ed88a4764c
共有 68 个文件被更改,包括 1592 次插入1166 次删除
  1. 1 0
      app/src/androidTest/java/com/nextcloud/client/files/downloader/DownloaderServiceTest.kt
  2. 5 0
      app/src/androidTest/java/com/nextcloud/client/files/downloader/RegistryTest.kt
  3. 6 0
      app/src/androidTest/java/com/nextcloud/client/files/downloader/TransferManagerConnectionTest.kt
  4. 6 0
      app/src/androidTest/java/com/nextcloud/client/files/downloader/TransferManagerTest.kt
  5. 4 4
      app/src/androidTest/java/com/owncloud/android/files/FileMenuFilterIT.kt
  6. 0 1
      app/src/androidTest/java/com/owncloud/android/ui/activity/FolderPickerActivityIT.java
  7. 2 2
      app/src/debug/java/com/nextcloud/test/TestActivity.kt
  8. 7 5
      app/src/main/AndroidManifest.xml
  9. 3 0
      app/src/main/java/com/nextcloud/client/di/AppComponent.java
  10. 1 5
      app/src/main/java/com/nextcloud/client/di/ComponentsModule.java
  11. 1 1
      app/src/main/java/com/nextcloud/client/etm/EtmViewModel.kt
  12. 4 4
      app/src/main/java/com/nextcloud/client/etm/pages/EtmFileTransferFragment.kt
  13. 1 1
      app/src/main/java/com/nextcloud/client/files/Direction.kt
  14. 7 4
      app/src/main/java/com/nextcloud/client/files/Registry.kt
  15. 7 4
      app/src/main/java/com/nextcloud/client/files/Request.kt
  16. 159 0
      app/src/main/java/com/nextcloud/client/files/downloader/DownloadNotificationManager.kt
  17. 28 19
      app/src/main/java/com/nextcloud/client/files/downloader/DownloadTask.kt
  18. 26 0
      app/src/main/java/com/nextcloud/client/files/downloader/FileDownloadError.kt
  19. 162 0
      app/src/main/java/com/nextcloud/client/files/downloader/FileDownloadHelper.kt
  20. 98 0
      app/src/main/java/com/nextcloud/client/files/downloader/FileDownloadIntents.kt
  21. 477 0
      app/src/main/java/com/nextcloud/client/files/downloader/FileDownloadWorker.kt
  22. 9 4
      app/src/main/java/com/nextcloud/client/files/transfer/FileTransferService.kt
  23. 15 9
      app/src/main/java/com/nextcloud/client/files/transfer/Transfer.kt
  24. 7 5
      app/src/main/java/com/nextcloud/client/files/transfer/TransferManager.kt
  25. 7 5
      app/src/main/java/com/nextcloud/client/files/transfer/TransferManagerConnection.kt
  26. 12 5
      app/src/main/java/com/nextcloud/client/files/transfer/TransferManagerImpl.kt
  27. 1 1
      app/src/main/java/com/nextcloud/client/files/transfer/TransferState.kt
  28. 1 1
      app/src/main/java/com/nextcloud/client/files/upload/PostUploadAction.kt
  29. 5 4
      app/src/main/java/com/nextcloud/client/files/upload/UploadTask.kt
  30. 5 4
      app/src/main/java/com/nextcloud/client/files/upload/UploadTrigger.kt
  31. 12 0
      app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt
  32. 16 0
      app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt
  33. 47 1
      app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt
  34. 4 4
      app/src/main/java/com/nextcloud/client/jobs/ContactsBackupWork.kt
  35. 6 9
      app/src/main/java/com/nextcloud/client/jobs/FilesExportWork.kt
  36. 1 1
      app/src/main/java/com/nextcloud/client/notifications/AppNotificationManagerImpl.kt
  37. 30 0
      app/src/main/java/com/nextcloud/model/WorkerState.kt
  38. 41 0
      app/src/main/java/com/nextcloud/model/WorkerStateLiveData.kt
  39. 13 0
      app/src/main/java/com/nextcloud/utils/ForegroundServiceHelper.kt
  40. 49 0
      app/src/main/java/com/nextcloud/utils/extensions/WorkManagerExtensions.kt
  41. 44 1
      app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java
  42. 8 9
      app/src/main/java/com/owncloud/android/files/FileMenuFilter.java
  43. 0 744
      app/src/main/java/com/owncloud/android/files/services/FileDownloader.java
  44. 5 1
      app/src/main/java/com/owncloud/android/files/services/IndexedForest.java
  45. 28 8
      app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.java
  46. 7 3
      app/src/main/java/com/owncloud/android/operations/DownloadType.kt
  47. 4 19
      app/src/main/java/com/owncloud/android/operations/SynchronizeFileOperation.java
  48. 6 44
      app/src/main/java/com/owncloud/android/operations/SynchronizeFolderOperation.java
  49. 1 2
      app/src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.java
  50. 8 8
      app/src/main/java/com/owncloud/android/services/SyncFolderHandler.java
  51. 4 4
      app/src/main/java/com/owncloud/android/ui/activity/ComponentsGetter.java
  52. 8 6
      app/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.kt
  53. 8 17
      app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java
  54. 42 41
      app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java
  55. 24 26
      app/src/main/java/com/owncloud/android/ui/activity/ManageAccountsActivity.java
  56. 20 11
      app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt
  57. 10 13
      app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java
  58. 2 4
      app/src/main/java/com/owncloud/android/ui/fragment/FileFragment.java
  59. 5 5
      app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupListFragment.java
  60. 5 5
      app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java
  61. 5 5
      app/src/main/java/com/owncloud/android/ui/preview/FileDownloadFragment.java
  62. 36 36
      app/src/main/java/com/owncloud/android/ui/preview/PreviewImageActivity.java
  63. 0 1
      app/src/main/java/com/owncloud/android/ui/preview/PreviewImagePagerAdapter.java
  64. 13 36
      app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaActivity.kt
  65. 2 7
      app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.java
  66. 5 5
      app/src/main/java/third_parties/sufficientlysecure/SaveCalendar.java
  67. 5 1
      app/src/main/res/values/strings.xml
  68. 1 1
      build.gradle

+ 1 - 0
app/src/androidTest/java/com/nextcloud/client/files/downloader/DownloaderServiceTest.kt

@@ -22,6 +22,7 @@ package com.nextcloud.client.files.downloader
 import androidx.test.core.app.ApplicationProvider.getApplicationContext
 import androidx.test.rule.ServiceTestRule
 import com.nextcloud.client.account.MockUser
+import com.nextcloud.client.files.transfer.FileTransferService
 import io.mockk.MockKAnnotations
 import org.junit.Assert.assertTrue
 import org.junit.Before

+ 5 - 0
app/src/androidTest/java/com/nextcloud/client/files/downloader/RegistryTest.kt

@@ -20,6 +20,11 @@
 package com.nextcloud.client.files.downloader
 
 import com.nextcloud.client.account.User
+import com.nextcloud.client.files.DownloadRequest
+import com.nextcloud.client.files.Registry
+import com.nextcloud.client.files.Request
+import com.nextcloud.client.files.transfer.Transfer
+import com.nextcloud.client.files.transfer.TransferState
 import com.owncloud.android.datamodel.OCFile
 import io.mockk.CapturingSlot
 import io.mockk.MockKAnnotations

+ 6 - 0
app/src/androidTest/java/com/nextcloud/client/files/downloader/TransferManagerConnectionTest.kt

@@ -22,6 +22,12 @@ package com.nextcloud.client.files.downloader
 import android.content.ComponentName
 import android.content.Context
 import com.nextcloud.client.account.MockUser
+import com.nextcloud.client.files.DownloadRequest
+import com.nextcloud.client.files.transfer.FileTransferService
+import com.nextcloud.client.files.transfer.Transfer
+import com.nextcloud.client.files.transfer.TransferManager
+import com.nextcloud.client.files.transfer.TransferManagerConnection
+import com.nextcloud.client.files.transfer.TransferState
 import com.owncloud.android.datamodel.OCFile
 import io.mockk.MockKAnnotations
 import io.mockk.every

+ 6 - 0
app/src/androidTest/java/com/nextcloud/client/files/downloader/TransferManagerTest.kt

@@ -23,6 +23,12 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule
 import com.nextcloud.client.account.User
 import com.nextcloud.client.core.ManualAsyncRunner
 import com.nextcloud.client.core.OnProgressCallback
+import com.nextcloud.client.files.DownloadRequest
+import com.nextcloud.client.files.Request
+import com.nextcloud.client.files.transfer.Transfer
+import com.nextcloud.client.files.transfer.TransferManagerImpl
+import com.nextcloud.client.files.transfer.TransferState
+import com.nextcloud.client.files.upload.UploadTask
 import com.owncloud.android.datamodel.OCFile
 import com.owncloud.android.lib.common.OwnCloudClient
 import io.mockk.MockKAnnotations

+ 4 - 4
app/src/androidTest/java/com/owncloud/android/files/FileMenuFilterIT.kt

@@ -23,6 +23,7 @@ package com.owncloud.android.files
 import androidx.test.core.app.launchActivity
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.nextcloud.client.account.User
+import com.nextcloud.client.files.downloader.FileDownloadWorker
 import com.nextcloud.test.TestActivity
 import com.nextcloud.utils.EditorUtils
 import com.owncloud.android.AbstractIT
@@ -30,7 +31,6 @@ 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.FileDownloader
 import com.owncloud.android.files.services.FileUploader
 import com.owncloud.android.lib.resources.files.model.FileLockType
 import com.owncloud.android.lib.resources.status.CapabilityBooleanType
@@ -62,7 +62,7 @@ class FileMenuFilterIT : AbstractIT() {
     private lateinit var mockFileUploaderBinder: FileUploader.FileUploaderBinder
 
     @MockK
-    private lateinit var mockFileDownloaderBinder: FileDownloader.FileDownloaderBinder
+    private lateinit var mockFileDownloadProgressListener: FileDownloadWorker.FileDownloadProgressListener
 
     @MockK
     private lateinit var mockOperationsServiceBinder: OperationsService.OperationsServiceBinder
@@ -77,8 +77,8 @@ class FileMenuFilterIT : AbstractIT() {
         MockKAnnotations.init(this)
         every { mockFileUploaderBinder.isUploading(any(), any()) } returns false
         every { mockComponentsGetter.fileUploaderBinder } returns mockFileUploaderBinder
-        every { mockFileDownloaderBinder.isDownloading(any(), any()) } returns false
-        every { mockComponentsGetter.fileDownloaderBinder } returns mockFileDownloaderBinder
+        every { mockFileDownloadProgressListener.isDownloading(any(), any()) } returns false
+        every { mockComponentsGetter.fileDownloadProgressListener } returns mockFileDownloadProgressListener
         every { mockOperationsServiceBinder.isSynchronizing(any(), any()) } returns false
         every { mockComponentsGetter.operationsServiceBinder } returns mockOperationsServiceBinder
         every { mockStorageManager.getFileById(any()) } returns OCFile("/")

+ 0 - 1
app/src/androidTest/java/com/owncloud/android/ui/activity/FolderPickerActivityIT.java

@@ -22,7 +22,6 @@ package com.owncloud.android.ui.activity;
  */
 
 import android.content.Intent;
-import android.view.View;
 
 import com.owncloud.android.AbstractIT;
 import com.owncloud.android.R;

+ 2 - 2
app/src/debug/java/com/nextcloud/test/TestActivity.kt

@@ -25,6 +25,7 @@ import android.os.Bundle
 import android.view.View
 import androidx.fragment.app.Fragment
 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
+import com.nextcloud.client.files.downloader.FileDownloadWorker
 import com.nextcloud.client.network.Connectivity
 import com.nextcloud.client.network.ConnectivityService
 import com.nextcloud.utils.EditorUtils
@@ -33,7 +34,6 @@ import com.owncloud.android.databinding.TestLayoutBinding
 import com.owncloud.android.datamodel.ArbitraryDataProviderImpl
 import com.owncloud.android.datamodel.FileDataStorageManager
 import com.owncloud.android.datamodel.OCFile
-import com.owncloud.android.files.services.FileDownloader
 import com.owncloud.android.files.services.FileUploader
 import com.owncloud.android.lib.resources.status.OCCapability
 import com.owncloud.android.lib.resources.status.OwnCloudVersion
@@ -130,7 +130,7 @@ class TestActivity :
         return null
     }
 
-    override fun getFileDownloaderBinder(): FileDownloader.FileDownloaderBinder? {
+    override fun getFileDownloadProgressListener(): FileDownloadWorker.FileDownloadProgressListener? {
         return null
     }
 

+ 7 - 5
app/src/main/AndroidManifest.xml

@@ -239,6 +239,12 @@
             android:exported="false"
             android:configChanges="orientation|screenLayout|screenSize|keyboardHidden"
             android:theme="@style/Theme.ownCloud.Media" />
+        <service
+            android:name="androidx.work.impl.foreground.SystemForegroundService"
+            android:directBootAware="false"
+            android:enabled="@bool/enable_system_foreground_service_default"
+            android:exported="false"
+            android:foregroundServiceType="dataSync" />
         <service
             android:name=".authentication.AccountAuthenticatorService"
             android:exported="false">
@@ -394,11 +400,7 @@
             android:name=".services.OperationsService"
             android:exported="false" />
         <service
-            android:name=".files.services.FileDownloader"
-            android:foregroundServiceType="dataSync"
-            android:exported="false" />
-        <service
-            android:name="com.nextcloud.client.files.downloader.FileTransferService"
+            android:name="com.nextcloud.client.files.transfer.FileTransferService"
             android:foregroundServiceType="dataSync"
             android:exported="false" />
         <service

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

@@ -26,6 +26,7 @@ import com.nextcloud.appReview.InAppReviewModule;
 import com.nextcloud.client.appinfo.AppInfoModule;
 import com.nextcloud.client.database.DatabaseModule;
 import com.nextcloud.client.device.DeviceModule;
+import com.nextcloud.client.files.downloader.FileDownloadHelper;
 import com.nextcloud.client.integrations.IntegrationsModule;
 import com.nextcloud.client.jobs.JobsModule;
 import com.nextcloud.client.network.NetworkModule;
@@ -71,6 +72,8 @@ public interface AppComponent {
 
     void inject(FilesUploadHelper filesUploadHelper);
 
+    void inject(FileDownloadHelper fileDownloadHelper);
+
     void inject(ProgressIndicator progressIndicator);
 
     @Component.Builder

+ 1 - 5
app/src/main/java/com/nextcloud/client/di/ComponentsModule.java

@@ -24,7 +24,7 @@ import com.nextcloud.client.documentscan.DocumentScanActivity;
 import com.nextcloud.client.editimage.EditImageActivity;
 import com.nextcloud.client.etm.EtmActivity;
 import com.nextcloud.client.etm.pages.EtmBackgroundJobsFragment;
-import com.nextcloud.client.files.downloader.FileTransferService;
+import com.nextcloud.client.files.transfer.FileTransferService;
 import com.nextcloud.client.jobs.BackgroundJobManagerImpl;
 import com.nextcloud.client.jobs.NotificationWork;
 import com.nextcloud.client.jobs.TestJob;
@@ -46,7 +46,6 @@ import com.owncloud.android.MainApp;
 import com.owncloud.android.authentication.AuthenticatorActivity;
 import com.owncloud.android.authentication.DeepLinkLoginActivity;
 import com.owncloud.android.files.BootupBroadcastReceiver;
-import com.owncloud.android.files.services.FileDownloader;
 import com.owncloud.android.files.services.FileUploader;
 import com.owncloud.android.providers.DiskLruImageCacheFileProvider;
 import com.owncloud.android.providers.DocumentsStorageProvider;
@@ -324,9 +323,6 @@ abstract class ComponentsModule {
     @ContributesAndroidInjector
     abstract FileUploader fileUploader();
 
-    @ContributesAndroidInjector
-    abstract FileDownloader fileDownloader();
-
     @ContributesAndroidInjector
     abstract BootupBroadcastReceiver bootupBroadcastReceiver();
 

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

@@ -35,7 +35,7 @@ import com.nextcloud.client.etm.pages.EtmBackgroundJobsFragment
 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
+import com.nextcloud.client.files.transfer.TransferManagerConnection
 import com.nextcloud.client.jobs.BackgroundJobManager
 import com.nextcloud.client.jobs.JobInfo
 import com.nextcloud.client.migrations.MigrationInfo

+ 4 - 4
app/src/main/java/com/nextcloud/client/etm/pages/EtmFileTransferFragment.kt

@@ -13,10 +13,10 @@ 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.DownloadRequest
-import com.nextcloud.client.files.downloader.Transfer
-import com.nextcloud.client.files.downloader.TransferManager
-import com.nextcloud.client.files.downloader.UploadRequest
+import com.nextcloud.client.files.DownloadRequest
+import com.nextcloud.client.files.UploadRequest
+import com.nextcloud.client.files.transfer.Transfer
+import com.nextcloud.client.files.transfer.TransferManager
 import com.owncloud.android.R
 import com.owncloud.android.datamodel.OCFile
 import com.owncloud.android.db.OCUpload

+ 1 - 1
app/src/main/java/com/nextcloud/client/files/downloader/Direction.kt → app/src/main/java/com/nextcloud/client/files/Direction.kt

@@ -17,7 +17,7 @@
  * 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
+package com.nextcloud.client.files
 
 enum class Direction {
     DOWNLOAD,

+ 7 - 4
app/src/main/java/com/nextcloud/client/files/downloader/Registry.kt → app/src/main/java/com/nextcloud/client/files/Registry.kt

@@ -1,8 +1,9 @@
 /*
  * Nextcloud Android client application
  *
- * @author Chris Narkiewicz
- * Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
+ * @author Alper Ozturk
+ * Copyright (C) 2023 Alper Ozturk
+ * Copyright (C) 2023 Nextcloud GmbH
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as published by
@@ -15,10 +16,12 @@
  * 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
  */
-package com.nextcloud.client.files.downloader
+package com.nextcloud.client.files
 
+import com.nextcloud.client.files.transfer.Transfer
+import com.nextcloud.client.files.transfer.TransferState
 import com.owncloud.android.datamodel.OCFile
 import java.util.UUID
 import kotlin.math.max

+ 7 - 4
app/src/main/java/com/nextcloud/client/files/downloader/Request.kt → app/src/main/java/com/nextcloud/client/files/Request.kt

@@ -1,8 +1,9 @@
 /*
  * Nextcloud Android client application
  *
- * @author Chris Narkiewicz
- * Copyright (C) 2021 Chris Narkiewicz <hello@ezaquarii.com>
+ * @author Alper Ozturk
+ * Copyright (C) 2023 Alper Ozturk
+ * Copyright (C) 2023 Nextcloud GmbH
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as published by
@@ -15,13 +16,15 @@
  * 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
  */
-package com.nextcloud.client.files.downloader
+package com.nextcloud.client.files
 
 import android.os.Parcel
 import android.os.Parcelable
 import com.nextcloud.client.account.User
+import com.nextcloud.client.files.upload.PostUploadAction
+import com.nextcloud.client.files.upload.UploadTrigger
 import com.owncloud.android.datamodel.OCFile
 import com.owncloud.android.datamodel.UploadsStorageManager
 import com.owncloud.android.db.OCUpload

+ 159 - 0
app/src/main/java/com/nextcloud/client/files/downloader/DownloadNotificationManager.kt

@@ -0,0 +1,159 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Alper Ozturk
+ * Copyright (C) 2023 Alper Ozturk
+ * Copyright (C) 2023 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.client.files.downloader
+
+import android.app.Notification
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.graphics.BitmapFactory
+import android.os.Build
+import android.os.Handler
+import android.os.Looper
+import androidx.core.app.NotificationCompat
+import com.owncloud.android.R
+import com.owncloud.android.lib.resources.files.FileUtils
+import com.owncloud.android.operations.DownloadFileOperation
+import com.owncloud.android.ui.notifications.NotificationUtils
+import com.owncloud.android.utils.theme.ViewThemeUtils
+import java.io.File
+import java.security.SecureRandom
+
+@Suppress("TooManyFunctions")
+class DownloadNotificationManager(
+    private val id: Int,
+    private val context: Context,
+    private val viewThemeUtils: ViewThemeUtils
+) {
+
+    private var notification: Notification
+    private var notificationBuilder: NotificationCompat.Builder
+    private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+
+    init {
+        notificationBuilder = NotificationUtils.newNotificationBuilder(context, viewThemeUtils).apply {
+            setContentTitle(context.getString(R.string.downloader_download_in_progress_ticker))
+            setSmallIcon(R.drawable.notification_icon)
+            setLargeIcon(BitmapFactory.decodeResource(context.resources, R.drawable.notification_icon))
+
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+                setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_DOWNLOAD)
+            }
+        }
+
+        notification = notificationBuilder.build()
+    }
+
+    @Suppress("MagicNumber")
+    fun prepareForStart(operation: DownloadFileOperation) {
+        notificationBuilder = NotificationUtils.newNotificationBuilder(context, viewThemeUtils).apply {
+            setSmallIcon(R.drawable.notification_icon)
+            setOngoing(true)
+            setProgress(100, 0, operation.size < 0)
+            setContentText(
+                String.format(
+                    context.getString(R.string.downloader_download_in_progress), 0,
+                    File(operation.savePath).name
+                )
+            )
+
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+                setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_DOWNLOAD)
+            }
+
+            notificationManager.notify(
+                id,
+                this.build()
+            )
+        }
+    }
+
+    fun prepareForResult() {
+        notificationBuilder
+            .setAutoCancel(true)
+            .setOngoing(false)
+            .setProgress(0, 0, false)
+    }
+
+    @Suppress("MagicNumber")
+    fun updateDownloadProgress(filePath: String, percent: Int, totalToTransfer: Long) {
+        notificationBuilder.run {
+            setProgress(100, percent, totalToTransfer < 0)
+            val fileName: String = filePath.substring(filePath.lastIndexOf(FileUtils.PATH_SEPARATOR) + 1)
+            val text =
+                String.format(context.getString(R.string.downloader_download_in_progress), percent, fileName)
+            val title =
+                context.getString(R.string.downloader_download_in_progress_ticker)
+            updateNotificationText(title, text)
+        }
+    }
+
+    @Suppress("MagicNumber")
+    fun dismissNotification() {
+        Handler(Looper.getMainLooper()).postDelayed({
+            notificationManager.cancel(id)
+        }, 2000)
+    }
+
+    fun showNewNotification(text: String) {
+        val notifyId = SecureRandom().nextInt()
+
+        notificationBuilder.run {
+            setProgress(0, 0, false)
+            setContentTitle(null)
+            setContentText(text)
+            setOngoing(false)
+            notificationManager.notify(notifyId, this.build())
+        }
+    }
+
+    private fun updateNotificationText(title: String?, text: String) {
+        notificationBuilder.run {
+            title?.let {
+                setContentTitle(title)
+            }
+
+            setContentText(text)
+            notificationManager.notify(id, this.build())
+        }
+    }
+
+    fun setContentIntent(intent: Intent, flag: Int) {
+        notificationBuilder.setContentIntent(
+            PendingIntent.getActivity(
+                context,
+                System.currentTimeMillis().toInt(),
+                intent,
+                flag
+            )
+        )
+    }
+
+    fun getId(): Int {
+        return id
+    }
+
+    fun getNotification(): Notification {
+        return notificationBuilder.build()
+    }
+}

+ 28 - 19
app/src/main/java/com/nextcloud/client/files/downloader/DownloadTask.kt

@@ -22,6 +22,7 @@ package com.nextcloud.client.files.downloader
 import android.content.ContentResolver
 import android.content.Context
 import com.nextcloud.client.core.IsCancelled
+import com.nextcloud.client.files.DownloadRequest
 import com.owncloud.android.datamodel.FileDataStorageManager
 import com.owncloud.android.datamodel.OCFile
 import com.owncloud.android.lib.common.OwnCloudClient
@@ -37,9 +38,9 @@ import java.io.File
  * This design can be regarded as intermediary refactoring step.
  */
 class DownloadTask(
-    val context: Context,
-    val contentResolver: ContentResolver,
-    val clientProvider: () -> OwnCloudClient
+    private val context: Context,
+    private val contentResolver: ContentResolver,
+    private val clientProvider: () -> OwnCloudClient
 ) {
 
     data class Result(val file: OCFile, val success: Boolean)
@@ -62,39 +63,47 @@ class DownloadTask(
         }
     }
 
+    // Unused progress, isCancelled arguments needed for TransferManagerTest
     fun download(request: DownloadRequest, progress: (Int) -> Unit, isCancelled: IsCancelled): Result {
         val op = DownloadFileOperation(request.user, request.file, context)
         val client = clientProvider.invoke()
         val result = op.execute(client)
-        if (result.isSuccess) {
+
+        return if (result.isSuccess) {
             val storageManager = FileDataStorageManager(
                 request.user,
                 contentResolver
             )
             val file = saveDownloadedFile(op, storageManager)
-            return Result(file, true)
+            Result(file, true)
         } else {
-            return Result(request.file, false)
+            Result(request.file, false)
         }
     }
 
     private fun saveDownloadedFile(op: DownloadFileOperation, storageManager: FileDataStorageManager): OCFile {
-        val file = storageManager.getFileById(op.getFile().getFileId()) as OCFile
-        val syncDate = System.currentTimeMillis()
-        file.lastSyncDateForProperties = syncDate
-        file.lastSyncDateForData = syncDate
-        file.isUpdateThumbnailNeeded = true
-        file.modificationTimestamp = op.getModificationTimestamp()
-        file.modificationTimestampAtLastSyncForData = op.getModificationTimestamp()
-        file.etag = op.getEtag()
-        file.mimeType = op.getMimeType()
-        file.storagePath = op.getSavePath()
-        file.fileLength = File(op.getSavePath()).length()
-        file.remoteId = op.getFile().getRemoteId()
+        val file = storageManager.getFileById(op.file.fileId) as OCFile
+
+        file.apply {
+            val syncDate = System.currentTimeMillis()
+            lastSyncDateForProperties = syncDate
+            lastSyncDateForData = syncDate
+            isUpdateThumbnailNeeded = true
+            modificationTimestamp = op.modificationTimestamp
+            modificationTimestampAtLastSyncForData = op.modificationTimestamp
+            etag = op.etag
+            mimeType = op.mimeType
+            storagePath = op.savePath
+            fileLength = File(op.savePath).length()
+            remoteId = op.file.remoteId
+        }
+
         storageManager.saveFile(file)
-        if (MimeTypeUtil.isMedia(op.getMimeType())) {
+
+        if (MimeTypeUtil.isMedia(op.mimeType)) {
             FileDataStorageManager.triggerMediaScan(file.storagePath)
         }
+
         return file
     }
 }

+ 26 - 0
app/src/main/java/com/nextcloud/client/files/downloader/FileDownloadError.kt

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

+ 162 - 0
app/src/main/java/com/nextcloud/client/files/downloader/FileDownloadHelper.kt

@@ -0,0 +1,162 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Alper Ozturk
+ * Copyright (C) 2023 Alper Ozturk
+ * Copyright (C) 2023 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.client.files.downloader
+
+import com.nextcloud.client.account.User
+import com.nextcloud.client.jobs.BackgroundJobManager
+import com.owncloud.android.MainApp
+import com.owncloud.android.datamodel.FileDataStorageManager
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.datamodel.UploadsStorageManager
+import com.owncloud.android.operations.DownloadFileOperation
+import com.owncloud.android.operations.DownloadType
+import com.owncloud.android.utils.MimeTypeUtil
+import java.io.File
+import javax.inject.Inject
+
+class FileDownloadHelper {
+
+    @Inject
+    lateinit var backgroundJobManager: BackgroundJobManager
+
+    @Inject
+    lateinit var uploadsStorageManager: UploadsStorageManager
+
+    companion object {
+        private var instance: FileDownloadHelper? = null
+
+        fun instance(): FileDownloadHelper {
+            return instance ?: synchronized(this) {
+                instance ?: FileDownloadHelper().also { instance = it }
+            }
+        }
+    }
+
+    init {
+        MainApp.getAppComponent().inject(this)
+    }
+
+    fun isDownloading(user: User?, file: OCFile?): Boolean {
+        if (user == null || file == null) {
+            return false
+        }
+
+        val fileStorageManager = FileDataStorageManager(user, MainApp.getAppContext().contentResolver)
+        val topParentId = fileStorageManager.getTopParentId(file)
+
+        return if (file.isFolder) {
+            backgroundJobManager.isStartFileDownloadJobScheduled(user, file.fileId) ||
+                backgroundJobManager.isStartFileDownloadJobScheduled(user, topParentId)
+        } else {
+            FileDownloadWorker.isDownloading(user.accountName, file.fileId)
+        }
+    }
+
+    fun cancelPendingOrCurrentDownloads(user: User?, files: List<OCFile>?) {
+        if (user == null || files == null) return
+
+        files.forEach { file ->
+            FileDownloadWorker.cancelOperation(user.accountName, file.fileId)
+            backgroundJobManager.cancelFilesDownloadJob(user, file.fileId)
+        }
+    }
+
+    fun cancelAllDownloadsForAccount(accountName: String?, currentDownload: DownloadFileOperation?) {
+        if (accountName == null || currentDownload == null) return
+
+        val currentUser = currentDownload.user
+        val currentFile = currentDownload.file
+
+        if (!currentUser.nameEquals(accountName)) {
+            return
+        }
+
+        currentDownload.cancel()
+        FileDownloadWorker.cancelOperation(currentUser.accountName, currentFile.fileId)
+        backgroundJobManager.cancelFilesDownloadJob(currentUser, currentFile.fileId)
+    }
+
+    fun saveFile(
+        file: OCFile,
+        currentDownload: DownloadFileOperation?,
+        storageManager: FileDataStorageManager?
+    ) {
+        val syncDate = System.currentTimeMillis()
+
+        file.apply {
+            lastSyncDateForProperties = syncDate
+            lastSyncDateForData = syncDate
+            isUpdateThumbnailNeeded = true
+            modificationTimestamp = currentDownload?.modificationTimestamp ?: 0L
+            modificationTimestampAtLastSyncForData = currentDownload?.modificationTimestamp ?: 0L
+            etag = currentDownload?.etag
+            mimeType = currentDownload?.mimeType
+            storagePath = currentDownload?.savePath
+
+            val savePathFile = currentDownload?.savePath?.let { File(it) }
+            savePathFile?.let {
+                fileLength = savePathFile.length()
+            }
+
+            remoteId = currentDownload?.file?.remoteId
+        }
+
+        storageManager?.saveFile(file)
+
+        if (MimeTypeUtil.isMedia(currentDownload?.mimeType)) {
+            FileDataStorageManager.triggerMediaScan(file.storagePath, file)
+        }
+
+        storageManager?.saveConflict(file, null)
+    }
+
+    fun downloadFileIfNotStartedBefore(user: User, file: OCFile) {
+        if (!isDownloading(user, file)) {
+            downloadFile(user, file, downloadType = DownloadType.DOWNLOAD)
+        }
+    }
+
+    fun downloadFile(user: User, file: OCFile) {
+        downloadFile(user, file, downloadType = DownloadType.DOWNLOAD)
+    }
+
+    @Suppress("LongParameterList")
+    fun downloadFile(
+        user: User,
+        ocFile: OCFile,
+        behaviour: String = "",
+        downloadType: DownloadType? = DownloadType.DOWNLOAD,
+        activityName: String = "",
+        packageName: String = "",
+        conflictUploadId: Long? = null
+    ) {
+        backgroundJobManager.startFileDownloadJob(
+            user,
+            ocFile,
+            behaviour,
+            downloadType,
+            activityName,
+            packageName,
+            conflictUploadId
+        )
+    }
+}

+ 98 - 0
app/src/main/java/com/nextcloud/client/files/downloader/FileDownloadIntents.kt

@@ -0,0 +1,98 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Alper Ozturk
+ * Copyright (C) 2023 Alper Ozturk
+ * Copyright (C) 2023 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.client.files.downloader
+
+import android.content.Context
+import android.content.Intent
+import com.nextcloud.client.account.User
+import com.owncloud.android.authentication.AuthenticatorActivity
+import com.owncloud.android.lib.common.operations.RemoteOperationResult
+import com.owncloud.android.operations.DownloadFileOperation
+import com.owncloud.android.ui.activity.FileActivity
+import com.owncloud.android.ui.activity.FileDisplayActivity
+import com.owncloud.android.ui.dialog.SendShareDialog
+import com.owncloud.android.ui.fragment.OCFileListFragment
+import com.owncloud.android.ui.preview.PreviewImageActivity
+import com.owncloud.android.ui.preview.PreviewImageFragment
+
+class FileDownloadIntents(private val context: Context) {
+
+    fun newDownloadIntent(
+        download: DownloadFileOperation,
+        linkedToRemotePath: String
+    ): Intent {
+        return Intent(FileDownloadWorker.getDownloadAddedMessage()).apply {
+            putExtra(FileDownloadWorker.EXTRA_ACCOUNT_NAME, download.user.accountName)
+            putExtra(FileDownloadWorker.EXTRA_REMOTE_PATH, download.remotePath)
+            putExtra(FileDownloadWorker.EXTRA_LINKED_TO_PATH, linkedToRemotePath)
+            setPackage(context.packageName)
+        }
+    }
+
+    fun downloadFinishedIntent(
+        download: DownloadFileOperation,
+        downloadResult: RemoteOperationResult<*>,
+        unlinkedFromRemotePath: String?
+    ): Intent {
+        return Intent(FileDownloadWorker.getDownloadFinishMessage()).apply {
+            putExtra(FileDownloadWorker.EXTRA_DOWNLOAD_RESULT, downloadResult.isSuccess)
+            putExtra(FileDownloadWorker.EXTRA_ACCOUNT_NAME, download.user.accountName)
+            putExtra(FileDownloadWorker.EXTRA_REMOTE_PATH, download.remotePath)
+            putExtra(OCFileListFragment.DOWNLOAD_BEHAVIOUR, download.behaviour)
+            putExtra(SendShareDialog.ACTIVITY_NAME, download.activityName)
+            putExtra(SendShareDialog.PACKAGE_NAME, download.packageName)
+            if (unlinkedFromRemotePath != null) {
+                putExtra(FileDownloadWorker.EXTRA_LINKED_TO_PATH, unlinkedFromRemotePath)
+            }
+            setPackage(context.packageName)
+        }
+    }
+
+    fun credentialContentIntent(user: User): Intent {
+        return Intent(context, AuthenticatorActivity::class.java).apply {
+            putExtra(AuthenticatorActivity.EXTRA_ACCOUNT, user.toPlatformAccount())
+            putExtra(
+                AuthenticatorActivity.EXTRA_ACTION,
+                AuthenticatorActivity.ACTION_UPDATE_EXPIRED_TOKEN
+            )
+            addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+            addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
+            addFlags(Intent.FLAG_FROM_BACKGROUND)
+        }
+    }
+
+    fun detailsIntent(operation: DownloadFileOperation?): Intent {
+        return if (operation != null) {
+            if (PreviewImageFragment.canBePreviewed(operation.file)) {
+                Intent(context, PreviewImageActivity::class.java)
+            } else {
+                Intent(context, FileDisplayActivity::class.java)
+            }.apply {
+                putExtra(FileActivity.EXTRA_FILE, operation.file)
+                putExtra(FileActivity.EXTRA_USER, operation.user)
+                flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
+            }
+        } else {
+            Intent()
+        }
+    }
+}

+ 477 - 0
app/src/main/java/com/nextcloud/client/files/downloader/FileDownloadWorker.kt

@@ -0,0 +1,477 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Alper Ozturk
+ * Copyright (C) 2023 Alper Ozturk
+ * Copyright (C) 2023 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.client.files.downloader
+
+import android.accounts.Account
+import android.accounts.AccountManager
+import android.accounts.OnAccountsUpdateListener
+import android.app.PendingIntent
+import android.content.Context
+import androidx.core.util.component1
+import androidx.core.util.component2
+import androidx.localbroadcastmanager.content.LocalBroadcastManager
+import androidx.work.Worker
+import androidx.work.WorkerParameters
+import com.nextcloud.client.account.User
+import com.nextcloud.client.account.UserAccountManager
+import com.nextcloud.java.util.Optional
+import com.nextcloud.model.WorkerState
+import com.nextcloud.model.WorkerStateLiveData
+import com.nextcloud.utils.ForegroundServiceHelper
+import com.owncloud.android.R
+import com.owncloud.android.datamodel.FileDataStorageManager
+import com.owncloud.android.datamodel.ForegroundServiceType
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.files.services.IndexedForest
+import com.owncloud.android.lib.common.OwnCloudAccount
+import com.owncloud.android.lib.common.OwnCloudClientManagerFactory
+import com.owncloud.android.lib.common.network.OnDatatransferProgressListener
+import com.owncloud.android.lib.common.operations.RemoteOperationResult
+import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode
+import com.owncloud.android.lib.common.utils.Log_OC
+import com.owncloud.android.operations.DownloadFileOperation
+import com.owncloud.android.operations.DownloadType
+import com.owncloud.android.utils.theme.ViewThemeUtils
+import java.security.SecureRandom
+import java.util.AbstractList
+import java.util.Vector
+
+@Suppress("LongParameterList", "TooManyFunctions")
+class FileDownloadWorker(
+    private val viewThemeUtils: ViewThemeUtils,
+    private val accountManager: UserAccountManager,
+    private var localBroadcastManager: LocalBroadcastManager,
+    private val context: Context,
+    params: WorkerParameters
+) : Worker(context, params), OnAccountsUpdateListener, OnDatatransferProgressListener {
+
+    companion object {
+        private val TAG = FileDownloadWorker::class.java.simpleName
+
+        private val pendingDownloads = IndexedForest<DownloadFileOperation>()
+
+        fun cancelOperation(accountName: String, fileId: Long) {
+            pendingDownloads.all.forEach {
+                it.value?.payload?.cancelMatchingOperation(accountName, fileId)
+            }
+        }
+
+        fun isDownloading(accountName: String, fileId: Long): Boolean {
+            return pendingDownloads.all.any { it.value?.payload?.isMatching(accountName, fileId) == true }
+        }
+
+        const val WORKER_ID = "WORKER_ID"
+        const val FILE_REMOTE_PATH = "FILE_REMOTE_PATH"
+        const val ACCOUNT_NAME = "ACCOUNT_NAME"
+        const val BEHAVIOUR = "BEHAVIOUR"
+        const val DOWNLOAD_TYPE = "DOWNLOAD_TYPE"
+        const val ACTIVITY_NAME = "ACTIVITY_NAME"
+        const val PACKAGE_NAME = "PACKAGE_NAME"
+        const val CONFLICT_UPLOAD_ID = "CONFLICT_UPLOAD_ID"
+
+        const val EXTRA_DOWNLOAD_RESULT = "EXTRA_DOWNLOAD_RESULT"
+        const val EXTRA_REMOTE_PATH = "EXTRA_REMOTE_PATH"
+        const val EXTRA_LINKED_TO_PATH = "EXTRA_LINKED_TO_PATH"
+        const val EXTRA_ACCOUNT_NAME = "EXTRA_ACCOUNT_NAME"
+
+        fun getDownloadAddedMessage(): String {
+            return FileDownloadWorker::class.java.name + "DOWNLOAD_ADDED"
+        }
+
+        fun getDownloadFinishMessage(): String {
+            return FileDownloadWorker::class.java.name + "DOWNLOAD_FINISH"
+        }
+    }
+
+    private var currentDownload: DownloadFileOperation? = null
+
+    private var conflictUploadId: Long? = null
+    private var lastPercent = 0
+
+    private val intents = FileDownloadIntents(context)
+    private lateinit var notificationManager: DownloadNotificationManager
+    private var downloadProgressListener = FileDownloadProgressListener()
+
+    private var user: User? = null
+    private var currentUser = Optional.empty<User>()
+
+    private var currentUserFileStorageManager: FileDataStorageManager? = null
+    private var fileDataStorageManager: FileDataStorageManager? = null
+
+    private var workerId: Int? = null
+    private var downloadError: FileDownloadError? = null
+
+    @Suppress("TooGenericExceptionCaught")
+    override fun doWork(): Result {
+        return try {
+            val requestDownloads = getRequestDownloads()
+
+            notificationManager =
+                DownloadNotificationManager(workerId ?: SecureRandom().nextInt(), context, viewThemeUtils)
+            addAccountUpdateListener()
+
+            val foregroundInfo = ForegroundServiceHelper.createWorkerForegroundInfo(
+                notificationManager.getId(),
+                notificationManager.getNotification(),
+                ForegroundServiceType.DataSync
+            )
+            setForegroundAsync(foregroundInfo)
+
+            requestDownloads.forEach {
+                downloadFile(it)
+            }
+
+            downloadError?.let {
+                showDownloadErrorNotification(it)
+                notificationManager.dismissNotification()
+            }
+
+            setIdleWorkerState()
+
+            Log_OC.e(TAG, "FilesDownloadWorker successfully completed")
+            Result.success()
+        } catch (t: Throwable) {
+            notificationManager.dismissNotification()
+            notificationManager.showNewNotification(context.getString(R.string.downloader_unexpected_error))
+            Log_OC.e(TAG, "Error caught at FilesDownloadWorker(): " + t.localizedMessage)
+            setIdleWorkerState()
+            Result.failure()
+        }
+    }
+
+    override fun onStopped() {
+        Log_OC.e(TAG, "FilesDownloadWorker stopped")
+
+        notificationManager.dismissNotification()
+        setIdleWorkerState()
+
+        super.onStopped()
+    }
+
+    private fun setWorkerState(user: User?) {
+        WorkerStateLiveData.instance().setWorkState(WorkerState.Download(user, currentDownload))
+    }
+
+    private fun setIdleWorkerState() {
+        WorkerStateLiveData.instance().setWorkState(WorkerState.Idle)
+    }
+
+    private fun removePendingDownload(accountName: String?) {
+        pendingDownloads.remove(accountName)
+    }
+
+    private fun getRequestDownloads(): AbstractList<String> {
+        workerId = inputData.keyValueMap[WORKER_ID] as Int
+        Log_OC.e(TAG, "FilesDownloadWorker started for $workerId")
+
+        setUser()
+        val files = getFiles()
+        val downloadType = getDownloadType()
+
+        conflictUploadId = inputData.keyValueMap[CONFLICT_UPLOAD_ID] as Long?
+
+        val behaviour = inputData.keyValueMap[BEHAVIOUR] as String? ?: ""
+        val activityName = inputData.keyValueMap[ACTIVITY_NAME] as String? ?: ""
+        val packageName = inputData.keyValueMap[PACKAGE_NAME] as String? ?: ""
+
+        val requestedDownloads: AbstractList<String> = Vector()
+
+        return try {
+            files.forEach { file ->
+                val operation = DownloadFileOperation(
+                    user,
+                    file,
+                    behaviour,
+                    activityName,
+                    packageName,
+                    context,
+                    downloadType
+                )
+
+                operation.addDownloadDataTransferProgressListener(this)
+                operation.addDownloadDataTransferProgressListener(downloadProgressListener)
+                val (downloadKey, linkedToRemotePath) = pendingDownloads.putIfAbsent(
+                    user?.accountName,
+                    file.remotePath,
+                    operation
+                )
+
+                if (downloadKey != null) {
+                    requestedDownloads.add(downloadKey)
+                    localBroadcastManager.sendBroadcast(intents.newDownloadIntent(operation, linkedToRemotePath))
+                }
+            }
+
+            requestedDownloads
+        } catch (e: IllegalArgumentException) {
+            Log_OC.e(TAG, "Not enough information provided in intent: " + e.message)
+            requestedDownloads
+        }
+    }
+
+    private fun setUser() {
+        val accountName = inputData.keyValueMap[ACCOUNT_NAME] as String
+        user = accountManager.getUser(accountName).get()
+        fileDataStorageManager = FileDataStorageManager(user, context.contentResolver)
+    }
+
+    private fun getFiles(): List<OCFile> {
+        val remotePath = inputData.keyValueMap[FILE_REMOTE_PATH] as String?
+        val file = fileDataStorageManager?.getFileByEncryptedRemotePath(remotePath) ?: return listOf()
+
+        return if (file.isFolder) {
+            fileDataStorageManager?.getAllFilesRecursivelyInsideFolder(file) ?: listOf()
+        } else {
+            listOf(file)
+        }
+    }
+
+    private fun getDownloadType(): DownloadType? {
+        val typeAsString = inputData.keyValueMap[DOWNLOAD_TYPE] as String?
+        return if (typeAsString != null) {
+            if (typeAsString == DownloadType.DOWNLOAD.toString()) {
+                DownloadType.DOWNLOAD
+            } else {
+                DownloadType.EXPORT
+            }
+        } else {
+            null
+        }
+    }
+
+    private fun addAccountUpdateListener() {
+        val am = AccountManager.get(context)
+        am.addOnAccountsUpdatedListener(this, null, false)
+    }
+
+    @Suppress("TooGenericExceptionCaught")
+    private fun downloadFile(downloadKey: String) {
+        currentDownload = pendingDownloads.get(downloadKey)
+
+        if (currentDownload == null) {
+            return
+        }
+
+        setWorkerState(user)
+        Log_OC.e(TAG, "FilesDownloadWorker downloading: $downloadKey")
+
+        val isAccountExist = accountManager.exists(currentDownload?.user?.toPlatformAccount())
+        if (!isAccountExist) {
+            removePendingDownload(currentDownload?.user?.accountName)
+            return
+        }
+
+        notifyDownloadStart(currentDownload!!)
+        var downloadResult: RemoteOperationResult<*>? = null
+        try {
+            val ocAccount = getOCAccountForDownload()
+            val downloadClient =
+                OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(ocAccount, context)
+
+            downloadResult = currentDownload?.execute(downloadClient)
+            if (downloadResult?.isSuccess == true && currentDownload?.downloadType === DownloadType.DOWNLOAD) {
+                getCurrentFile()?.let {
+                    FileDownloadHelper.instance().saveFile(it, currentDownload, currentUserFileStorageManager)
+                }
+            }
+        } catch (e: Exception) {
+            Log_OC.e(TAG, "Error downloading", e)
+            downloadResult = RemoteOperationResult<Any?>(e)
+        } finally {
+            cleanupDownloadProcess(downloadResult)
+        }
+    }
+
+    private fun notifyDownloadStart(download: DownloadFileOperation) {
+        lastPercent = 0
+
+        notificationManager.run {
+            prepareForStart(download)
+            setContentIntent(intents.detailsIntent(download), PendingIntent.FLAG_IMMUTABLE)
+        }
+    }
+
+    private fun getOCAccountForDownload(): OwnCloudAccount {
+        val currentDownloadAccount = currentDownload?.user?.toPlatformAccount()
+        val currentDownloadUser = accountManager.getUser(currentDownloadAccount?.name)
+        if (currentUser != currentDownloadUser) {
+            currentUser = currentDownloadUser
+            currentUserFileStorageManager = FileDataStorageManager(currentUser.get(), context.contentResolver)
+        }
+        return currentDownloadUser.get().toOwnCloudAccount()
+    }
+
+    private fun getCurrentFile(): OCFile? {
+        var file: OCFile? = currentDownload?.file?.fileId?.let { currentUserFileStorageManager?.getFileById(it) }
+
+        if (file == null) {
+            file = currentUserFileStorageManager?.getFileByDecryptedRemotePath(currentDownload?.file?.remotePath)
+        }
+
+        if (file == null) {
+            Log_OC.e(this, "Could not save " + currentDownload?.file?.remotePath)
+            return null
+        }
+
+        return file
+    }
+
+    private fun cleanupDownloadProcess(result: RemoteOperationResult<*>?) {
+        result?.let {
+            checkDownloadError(it)
+        }
+
+        val removeResult = pendingDownloads.removePayload(
+            currentDownload?.user?.accountName,
+            currentDownload?.remotePath
+        )
+
+        val downloadResult = result ?: RemoteOperationResult<Any?>(RuntimeException("Error downloading…"))
+
+        currentDownload?.run {
+            notifyDownloadResult(this, downloadResult)
+
+            val downloadFinishedIntent = intents.downloadFinishedIntent(
+                this,
+                downloadResult,
+                removeResult.second
+            )
+
+            localBroadcastManager.sendBroadcast(downloadFinishedIntent)
+        }
+    }
+
+    private fun checkDownloadError(result: RemoteOperationResult<*>) {
+        if (result.isSuccess || downloadError != null) {
+            return
+        }
+
+        downloadError = if (result.isCancelled) {
+            FileDownloadError.Cancelled
+        } else {
+            FileDownloadError.Failed
+        }
+    }
+
+    private fun showDownloadErrorNotification(downloadError: FileDownloadError) {
+        val text = when (downloadError) {
+            FileDownloadError.Cancelled -> {
+                context.getString(R.string.downloader_file_download_cancelled)
+            }
+            FileDownloadError.Failed -> {
+                context.getString(R.string.downloader_file_download_failed)
+            }
+        }
+
+        notificationManager.showNewNotification(text)
+    }
+
+    private fun notifyDownloadResult(
+        download: DownloadFileOperation,
+        downloadResult: RemoteOperationResult<*>
+    ) {
+        if (downloadResult.isCancelled) {
+            return
+        }
+
+        val needsToUpdateCredentials = (ResultCode.UNAUTHORIZED == downloadResult.code)
+        notificationManager.run {
+            prepareForResult()
+
+            if (needsToUpdateCredentials) {
+                showNewNotification(context.getString(R.string.downloader_download_failed_credentials_error))
+                setContentIntent(
+                    intents.credentialContentIntent(download.user),
+                    PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
+                )
+            } else {
+                setContentIntent(intents.detailsIntent(null), PendingIntent.FLAG_IMMUTABLE)
+            }
+        }
+    }
+
+    override fun onAccountsUpdated(accounts: Array<out Account>?) {
+        if (!accountManager.exists(currentDownload?.user?.toPlatformAccount())) {
+            currentDownload?.cancel()
+        }
+    }
+
+    @Suppress("MagicNumber")
+    override fun onTransferProgress(
+        progressRate: Long,
+        totalTransferredSoFar: Long,
+        totalToTransfer: Long,
+        filePath: String
+    ) {
+        val percent: Int = (100.0 * totalTransferredSoFar.toDouble() / totalToTransfer.toDouble()).toInt()
+
+        if (percent != lastPercent) {
+            notificationManager.run {
+                updateDownloadProgress(filePath, percent, totalToTransfer)
+            }
+        }
+
+        lastPercent = percent
+    }
+
+    inner class FileDownloadProgressListener : OnDatatransferProgressListener {
+        private val boundListeners: MutableMap<Long, OnDatatransferProgressListener> = HashMap()
+
+        fun isDownloading(user: User?, file: OCFile?): Boolean {
+            return FileDownloadHelper.instance().isDownloading(user, file)
+        }
+
+        fun addDataTransferProgressListener(listener: OnDatatransferProgressListener?, file: OCFile?) {
+            if (file == null || listener == null) {
+                return
+            }
+
+            boundListeners[file.fileId] = listener
+        }
+
+        fun removeDataTransferProgressListener(listener: OnDatatransferProgressListener?, file: OCFile?) {
+            if (file == null || listener == null) {
+                return
+            }
+
+            val fileId = file.fileId
+            if (boundListeners[fileId] === listener) {
+                boundListeners.remove(fileId)
+            }
+        }
+
+        override fun onTransferProgress(
+            progressRate: Long,
+            totalTransferredSoFar: Long,
+            totalToTransfer: Long,
+            fileName: String
+        ) {
+            val listener = boundListeners[currentDownload?.file?.fileId]
+            listener?.onTransferProgress(
+                progressRate,
+                totalTransferredSoFar,
+                totalToTransfer,
+                fileName
+            )
+        }
+    }
+}

+ 9 - 4
app/src/main/java/com/nextcloud/client/files/downloader/FileTransferService.kt → app/src/main/java/com/nextcloud/client/files/transfer/FileTransferService.kt

@@ -1,8 +1,9 @@
 /*
  * Nextcloud Android client application
  *
- * @author Chris Narkiewicz
- * Copyright (C) 2021 Chris Narkiewicz <hello@ezaquarii.com>
+ * @author Alper Ozturk
+ * Copyright (C) 2023 Alper Ozturk
+ * Copyright (C) 2023 Nextcloud GmbH
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as published by
@@ -15,9 +16,9 @@
  * 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
  */
-package com.nextcloud.client.files.downloader
+package com.nextcloud.client.files.transfer
 
 import android.app.Service
 import android.content.Context
@@ -27,6 +28,10 @@ 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.files.Direction
+import com.nextcloud.client.files.Request
+import com.nextcloud.client.files.downloader.DownloadTask
+import com.nextcloud.client.files.upload.UploadTask
 import com.nextcloud.client.logger.Logger
 import com.nextcloud.client.network.ClientFactory
 import com.nextcloud.client.network.ConnectivityService

+ 15 - 9
app/src/main/java/com/nextcloud/client/files/downloader/Transfer.kt → app/src/main/java/com/nextcloud/client/files/transfer/Transfer.kt

@@ -1,8 +1,9 @@
-/**
+/*
  * Nextcloud Android client application
  *
- * @author Chris Narkiewicz
- * Copyright (C) 2021 Chris Narkiewicz <hello@ezaquarii.com>
+ * @author Alper Ozturk
+ * Copyright (C) 2023 Alper Ozturk
+ * Copyright (C) 2023 Nextcloud GmbH
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as published by
@@ -15,10 +16,14 @@
  * 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
  */
-package com.nextcloud.client.files.downloader
+package com.nextcloud.client.files.transfer
 
+import com.nextcloud.client.files.Direction
+import com.nextcloud.client.files.DownloadRequest
+import com.nextcloud.client.files.Request
+import com.nextcloud.client.files.UploadRequest
 import com.owncloud.android.datamodel.OCFile
 import java.util.UUID
 
@@ -48,8 +53,9 @@ data class Transfer(
      */
     val isFinished: Boolean get() = state == TransferState.COMPLETED || state == TransferState.FAILED
 
-    val direction: Direction get() = when (request) {
-        is DownloadRequest -> Direction.DOWNLOAD
-        is UploadRequest -> Direction.UPLOAD
-    }
+    val direction: Direction
+        get() = when (request) {
+            is DownloadRequest -> Direction.DOWNLOAD
+            is UploadRequest -> Direction.UPLOAD
+        }
 }

+ 7 - 5
app/src/main/java/com/nextcloud/client/files/downloader/TransferManager.kt → app/src/main/java/com/nextcloud/client/files/transfer/TransferManager.kt

@@ -1,8 +1,9 @@
-/**
+/*
  * Nextcloud Android client application
  *
- * @author Chris Narkiewicz
- * Copyright (C) 2021 Chris Narkiewicz <hello@ezaquarii.com>
+ * @author Alper Ozturk
+ * Copyright (C) 2023 Alper Ozturk
+ * Copyright (C) 2023 Nextcloud GmbH
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as published by
@@ -15,10 +16,11 @@
  * 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
  */
-package com.nextcloud.client.files.downloader
+package com.nextcloud.client.files.transfer
 
+import com.nextcloud.client.files.Request
 import com.owncloud.android.datamodel.OCFile
 import java.util.UUID
 

+ 7 - 5
app/src/main/java/com/nextcloud/client/files/downloader/TransferManagerConnection.kt → app/src/main/java/com/nextcloud/client/files/transfer/TransferManagerConnection.kt

@@ -1,8 +1,9 @@
-/**
+/*
  * Nextcloud Android client application
  *
- * @author Chris Narkiewicz
- * Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
+ * @author Alper Ozturk
+ * Copyright (C) 2023 Alper Ozturk
+ * Copyright (C) 2023 Nextcloud GmbH
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as published by
@@ -15,15 +16,16 @@
  * 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
  */
-package com.nextcloud.client.files.downloader
+package com.nextcloud.client.files.transfer
 
 import android.content.Context
 import android.content.Intent
 import android.os.IBinder
 import com.nextcloud.client.account.User
 import com.nextcloud.client.core.LocalConnection
+import com.nextcloud.client.files.Request
 import com.owncloud.android.datamodel.OCFile
 import java.util.UUID
 

+ 12 - 5
app/src/main/java/com/nextcloud/client/files/downloader/TransferManagerImpl.kt → app/src/main/java/com/nextcloud/client/files/transfer/TransferManagerImpl.kt

@@ -1,8 +1,9 @@
 /*
  * Nextcloud Android client application
  *
- * @author Chris Narkiewicz
- * Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
+ * @author Alper Ozturk
+ * Copyright (C) 2023 Alper Ozturk
+ * Copyright (C) 2023 Nextcloud GmbH
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as published by
@@ -15,14 +16,20 @@
  * 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
  */
-package com.nextcloud.client.files.downloader
+package com.nextcloud.client.files.transfer
 
 import com.nextcloud.client.core.AsyncRunner
 import com.nextcloud.client.core.IsCancelled
 import com.nextcloud.client.core.OnProgressCallback
 import com.nextcloud.client.core.TaskFunction
+import com.nextcloud.client.files.DownloadRequest
+import com.nextcloud.client.files.Registry
+import com.nextcloud.client.files.Request
+import com.nextcloud.client.files.UploadRequest
+import com.nextcloud.client.files.downloader.DownloadTask
+import com.nextcloud.client.files.upload.UploadTask
 import com.owncloud.android.datamodel.OCFile
 import com.owncloud.android.operations.UploadFileOperation
 import java.util.UUID
@@ -150,7 +157,7 @@ class TransferManagerImpl(
             }
         } else {
             val uploadTask = uploadTaskFactory.create()
-            val wrapper: TaskFunction<UploadTask.Result, Int> = { progress: ((Int) -> Unit), isCancelled ->
+            val wrapper: TaskFunction<UploadTask.Result, Int> = { _: ((Int) -> Unit), _ ->
                 uploadTask.upload(request.user, request.upload)
             }
             wrapper

+ 1 - 1
app/src/main/java/com/nextcloud/client/files/downloader/TransferState.kt → app/src/main/java/com/nextcloud/client/files/transfer/TransferState.kt

@@ -17,7 +17,7 @@
  * 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
+package com.nextcloud.client.files.transfer
 
 enum class TransferState {
     PENDING,

+ 1 - 1
app/src/main/java/com/nextcloud/client/files/downloader/PostUploadAction.kt → app/src/main/java/com/nextcloud/client/files/upload/PostUploadAction.kt

@@ -17,7 +17,7 @@
  * 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
+package com.nextcloud.client.files.upload
 
 import com.owncloud.android.files.services.FileUploader
 

+ 5 - 4
app/src/main/java/com/nextcloud/client/files/downloader/UploadTask.kt → app/src/main/java/com/nextcloud/client/files/upload/UploadTask.kt

@@ -1,8 +1,9 @@
 /*
  * Nextcloud Android client application
  *
- * @author Chris Narkiewicz
- * Copyright (C) 2021 Chris Narkiewicz <hello@ezaquarii.com>
+ * @author Alper Ozturk
+ * Copyright (C) 2023 Alper Ozturk
+ * Copyright (C) 2023 Nextcloud GmbH
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as published by
@@ -15,9 +16,9 @@
  * 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
  */
-package com.nextcloud.client.files.downloader
+package com.nextcloud.client.files.upload
 
 import android.content.Context
 import com.nextcloud.client.account.User

+ 5 - 4
app/src/main/java/com/nextcloud/client/files/downloader/UploadTrigger.kt → app/src/main/java/com/nextcloud/client/files/upload/UploadTrigger.kt

@@ -1,8 +1,9 @@
 /*
  * Nextcloud Android client application
  *
- * @author Chris Narkiewicz
- * Copyright (C) 2021 Chris Narkiewicz <hello@ezaquarii.com>
+ * @author Alper Ozturk
+ * Copyright (C) 2023 Alper Ozturk
+ * Copyright (C) 2023 Nextcloud GmbH
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as published by
@@ -15,9 +16,9 @@
  * 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/>.
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
  */
-package com.nextcloud.client.files.downloader
+package com.nextcloud.client.files.upload
 
 import com.owncloud.android.operations.UploadFileOperation
 

+ 12 - 0
app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt

@@ -34,6 +34,7 @@ import com.nextcloud.client.device.DeviceInfo
 import com.nextcloud.client.device.PowerManagementService
 import com.nextcloud.client.documentscan.GeneratePDFUseCase
 import com.nextcloud.client.documentscan.GeneratePdfFromImagesWork
+import com.nextcloud.client.files.downloader.FileDownloadWorker
 import com.nextcloud.client.integrations.deck.DeckApi
 import com.nextcloud.client.logger.Logger
 import com.nextcloud.client.network.ConnectivityService
@@ -102,6 +103,7 @@ class BackgroundJobFactory @Inject constructor(
                 CalendarImportWork::class -> createCalendarImportWork(context, workerParameters)
                 FilesExportWork::class -> createFilesExportWork(context, workerParameters)
                 FilesUploadWorker::class -> createFilesUploadWorker(context, workerParameters)
+                FileDownloadWorker::class -> createFilesDownloadWorker(context, workerParameters)
                 GeneratePdfFromImagesWork::class -> createPDFGenerateWork(context, workerParameters)
                 HealthStatusWork::class -> createHealthStatusWork(context, workerParameters)
                 TestJob::class -> createTestJob(context, workerParameters)
@@ -253,6 +255,16 @@ class BackgroundJobFactory @Inject constructor(
         )
     }
 
+    private fun createFilesDownloadWorker(context: Context, params: WorkerParameters): FileDownloadWorker {
+        return FileDownloadWorker(
+            viewThemeUtils.get(),
+            accountManager,
+            localBroadcastManager.get(),
+            context,
+            params
+        )
+    }
+
     private fun createPDFGenerateWork(context: Context, params: WorkerParameters): GeneratePdfFromImagesWork {
         return GeneratePdfFromImagesWork(
             appContext = context,

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

@@ -23,6 +23,7 @@ import androidx.lifecycle.LiveData
 import androidx.work.ListenableWorker
 import com.nextcloud.client.account.User
 import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.operations.DownloadType
 
 /**
  * This interface allows to control, schedule and monitor all application
@@ -144,6 +145,21 @@ interface BackgroundJobManager {
     fun getFileUploads(user: User): LiveData<List<JobInfo>>
     fun cancelFilesUploadJob(user: User)
 
+    fun cancelFilesDownloadJob(user: User, fileId: Long)
+
+    fun isStartFileDownloadJobScheduled(user: User, fileId: Long): Boolean
+
+    @Suppress("LongParameterList")
+    fun startFileDownloadJob(
+        user: User,
+        file: OCFile,
+        behaviour: String,
+        downloadType: DownloadType?,
+        activityName: String,
+        packageName: String,
+        conflictUploadId: Long?
+    )
+
     fun startPdfGenerateAndUploadWork(user: User, uploadFolder: String, imagePaths: List<String>, pdfPath: String)
 
     fun scheduleTestJob()

+ 47 - 1
app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt

@@ -38,8 +38,11 @@ import com.nextcloud.client.account.User
 import com.nextcloud.client.core.Clock
 import com.nextcloud.client.di.Injectable
 import com.nextcloud.client.documentscan.GeneratePdfFromImagesWork
+import com.nextcloud.client.files.downloader.FileDownloadWorker
 import com.nextcloud.client.preferences.AppPreferences
+import com.nextcloud.utils.extensions.isWorkScheduled
 import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.operations.DownloadType
 import java.util.Date
 import java.util.UUID
 import java.util.concurrent.TimeUnit
@@ -83,6 +86,8 @@ internal class BackgroundJobManagerImpl(
         const val JOB_NOTIFICATION = "notification"
         const val JOB_ACCOUNT_REMOVAL = "account_removal"
         const val JOB_FILES_UPLOAD = "files_upload"
+        const val JOB_FOLDER_DOWNLOAD = "folder_download"
+        const val JOB_FILES_DOWNLOAD = "files_download"
         const val JOB_PDF_GENERATION = "pdf_generation"
         const val JOB_IMMEDIATE_CALENDAR_BACKUP = "immediate_calendar_backup"
         const val JOB_IMMEDIATE_FILES_EXPORT = "immediate_files_export"
@@ -494,7 +499,6 @@ internal class BackgroundJobManagerImpl(
 
         workManager.enqueue(request)
     }
-
     override fun startFilesUploadJob(user: User) {
         val data = workDataOf(FilesUploadWorker.ACCOUNT to user.accountName)
 
@@ -505,6 +509,44 @@ internal class BackgroundJobManagerImpl(
         workManager.enqueueUniqueWork(JOB_FILES_UPLOAD + user.accountName, ExistingWorkPolicy.KEEP, request)
     }
 
+    private fun startFileDownloadJobTag(user: User, fileId: Long): String {
+        return JOB_FOLDER_DOWNLOAD + user.accountName + fileId
+    }
+
+    override fun isStartFileDownloadJobScheduled(user: User, fileId: Long): Boolean {
+        return workManager.isWorkScheduled(startFileDownloadJobTag(user, fileId))
+    }
+
+    override fun startFileDownloadJob(
+        user: User,
+        file: OCFile,
+        behaviour: String,
+        downloadType: DownloadType?,
+        activityName: String,
+        packageName: String,
+        conflictUploadId: Long?
+    ) {
+        val tag = startFileDownloadJobTag(user, file.fileId)
+
+        val data = workDataOf(
+            FileDownloadWorker.WORKER_ID to file.fileId.toInt(),
+            FileDownloadWorker.ACCOUNT_NAME to user.accountName,
+            FileDownloadWorker.FILE_REMOTE_PATH to file.remotePath,
+            FileDownloadWorker.BEHAVIOUR to behaviour,
+            FileDownloadWorker.DOWNLOAD_TYPE to downloadType.toString(),
+            FileDownloadWorker.ACTIVITY_NAME to activityName,
+            FileDownloadWorker.PACKAGE_NAME to packageName,
+            FileDownloadWorker.CONFLICT_UPLOAD_ID to conflictUploadId
+        )
+
+        val request = oneTimeRequestBuilder(FileDownloadWorker::class, JOB_FILES_DOWNLOAD, user)
+            .addTag(tag)
+            .setInputData(data)
+            .build()
+
+        workManager.enqueueUniqueWork(tag, ExistingWorkPolicy.REPLACE, request)
+    }
+
     override fun getFileUploads(user: User): LiveData<List<JobInfo>> {
         val workInfo = workManager.getWorkInfosByTagLiveData(formatNameTag(JOB_FILES_UPLOAD, user))
         return workInfo.map { it -> it.map { fromWorkInfo(it) ?: JobInfo() } }
@@ -514,6 +556,10 @@ internal class BackgroundJobManagerImpl(
         workManager.cancelJob(JOB_FILES_UPLOAD, user)
     }
 
+    override fun cancelFilesDownloadJob(user: User, fileId: Long) {
+        workManager.cancelAllWorkByTag(startFileDownloadJobTag(user, fileId))
+    }
+
     override fun startPdfGenerateAndUploadWork(
         user: User,
         uploadFolder: String,

+ 4 - 4
app/src/main/java/com/nextcloud/client/jobs/ContactsBackupWork.kt

@@ -35,10 +35,10 @@ 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.nextcloud.client.files.UploadRequest
+import com.nextcloud.client.files.transfer.TransferManagerConnection
+import com.nextcloud.client.files.upload.PostUploadAction
+import com.nextcloud.client.files.upload.UploadTrigger
 import com.owncloud.android.R
 import com.owncloud.android.datamodel.ArbitraryDataProvider
 import com.owncloud.android.datamodel.FileDataStorageManager

+ 6 - 9
app/src/main/java/com/nextcloud/client/jobs/FilesExportWork.kt

@@ -33,13 +33,12 @@ import androidx.core.app.NotificationCompat
 import androidx.work.Worker
 import androidx.work.WorkerParameters
 import com.nextcloud.client.account.User
+import com.nextcloud.client.files.downloader.FileDownloadHelper
 import com.owncloud.android.R
 import com.owncloud.android.datamodel.FileDataStorageManager
 import com.owncloud.android.datamodel.OCFile
-import com.owncloud.android.files.services.FileDownloader
 import com.owncloud.android.lib.common.utils.Log_OC
 import com.owncloud.android.operations.DownloadType
-import com.owncloud.android.ui.dialog.SendShareDialog
 import com.owncloud.android.ui.notifications.NotificationUtils
 import com.owncloud.android.utils.FileExportUtils
 import com.owncloud.android.utils.FileStorageUtils
@@ -112,13 +111,11 @@ class FilesExportWork(
     }
 
     private fun downloadFile(ocFile: OCFile) {
-        val i = Intent(appContext, FileDownloader::class.java)
-        i.putExtra(FileDownloader.EXTRA_USER, user)
-        i.putExtra(FileDownloader.EXTRA_FILE, ocFile)
-        i.putExtra(SendShareDialog.PACKAGE_NAME, "")
-        i.putExtra(SendShareDialog.ACTIVITY_NAME, "")
-        i.putExtra(FileDownloader.DOWNLOAD_TYPE, DownloadType.EXPORT)
-        appContext.startService(i)
+        FileDownloadHelper.instance().downloadFile(
+            user,
+            ocFile,
+            downloadType = DownloadType.EXPORT
+        )
     }
 
     private fun showErrorNotification(successfulExports: Int) {

+ 1 - 1
app/src/main/java/com/nextcloud/client/notifications/AppNotificationManagerImpl.kt

@@ -45,7 +45,7 @@ class AppNotificationManagerImpl @Inject constructor(
         val icon = BitmapFactory.decodeResource(resources, R.drawable.notification_icon)
         return builder(NotificationUtils.NOTIFICATION_CHANNEL_DOWNLOAD)
             .setContentTitle(resources.getString(R.string.app_name))
-            .setContentText(resources.getString(R.string.foreground_service_download))
+            .setContentText(resources.getString(R.string.worker_download))
             .setSmallIcon(R.drawable.notification_icon)
             .setLargeIcon(icon)
             .build()

+ 30 - 0
app/src/main/java/com/nextcloud/model/WorkerState.kt

@@ -0,0 +1,30 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Alper Ozturk
+ * Copyright (C) 2023 Alper Ozturk
+ * Copyright (C) 2023 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.model
+
+import com.nextcloud.client.account.User
+import com.owncloud.android.operations.DownloadFileOperation
+
+sealed class WorkerState {
+    object Idle : WorkerState()
+    class Download(var user: User?, var currentDownload: DownloadFileOperation?) : WorkerState()
+}

+ 41 - 0
app/src/main/java/com/nextcloud/model/WorkerStateLiveData.kt

@@ -0,0 +1,41 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Alper Ozturk
+ * Copyright (C) 2023 Alper Ozturk
+ * Copyright (C) 2023 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.model
+
+import androidx.lifecycle.LiveData
+
+class WorkerStateLiveData private constructor() : LiveData<WorkerState>() {
+
+    fun setWorkState(state: WorkerState) {
+        postValue(state)
+    }
+
+    companion object {
+        private var instance: WorkerStateLiveData? = null
+
+        fun instance(): WorkerStateLiveData {
+            return instance ?: synchronized(this) {
+                instance ?: WorkerStateLiveData().also { instance = it }
+            }
+        }
+    }
+}

+ 13 - 0
app/src/main/java/com/nextcloud/utils/ForegroundServiceHelper.kt

@@ -25,6 +25,7 @@ import android.app.Notification
 import android.app.Service
 import android.os.Build
 import androidx.core.app.ServiceCompat
+import androidx.work.ForegroundInfo
 import com.owncloud.android.datamodel.ForegroundServiceType
 
 object ForegroundServiceHelper {
@@ -45,4 +46,16 @@ object ForegroundServiceHelper {
             service.startForeground(id, notification)
         }
     }
+
+    fun createWorkerForegroundInfo(
+        id: Int,
+        notification: Notification,
+        foregroundServiceType: ForegroundServiceType
+    ): ForegroundInfo {
+        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+            ForegroundInfo(id, notification, foregroundServiceType.getId())
+        } else {
+            ForegroundInfo(id, notification)
+        }
+    }
 }

+ 49 - 0
app/src/main/java/com/nextcloud/utils/extensions/WorkManagerExtensions.kt

@@ -0,0 +1,49 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Alper Ozturk
+ * Copyright (C) 2023 Alper Ozturk
+ * Copyright (C) 2023 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.utils.extensions
+
+import androidx.work.WorkInfo
+import androidx.work.WorkManager
+import com.google.common.util.concurrent.ListenableFuture
+import com.owncloud.android.lib.common.utils.Log_OC
+import java.util.concurrent.ExecutionException
+
+fun WorkManager.isWorkScheduled(tag: String): Boolean {
+    val statuses: ListenableFuture<List<WorkInfo>> = this.getWorkInfosByTag(tag)
+    var running = false
+    var workInfoList: List<WorkInfo> = emptyList()
+
+    try {
+        workInfoList = statuses.get()
+    } catch (e: ExecutionException) {
+        Log_OC.d("Worker", "ExecutionException in isWorkScheduled: $e")
+    } catch (e: InterruptedException) {
+        Log_OC.d("Worker", "InterruptedException in isWorkScheduled: $e")
+    }
+
+    for (workInfo in workInfoList) {
+        val state = workInfo.state
+        running = running || (state == WorkInfo.State.RUNNING || state == WorkInfo.State.ENQUEUED)
+    }
+
+    return running
+}

+ 44 - 1
app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java

@@ -180,6 +180,50 @@ public class FileDataStorageManager {
         return fileDao.getFileByEncryptedRemotePath(path, user.getAccountName()) != null;
     }
 
+    public long getTopParentId(OCFile file) {
+        if (file.getParentId() == 1) {
+            return file.getFileId();
+        }
+
+        return getTopParentIdRecursive(file);
+    }
+
+    private long getTopParentIdRecursive(OCFile file) {
+        if (file.getParentId() == 1) {
+            return file.getFileId();
+        }
+
+        OCFile parentFile = getFileById(file.getParentId());
+        if (parentFile != null) {
+            return getTopParentId(parentFile);
+        }
+
+        return file.getFileId();
+    }
+
+    public List<OCFile> getAllFilesRecursivelyInsideFolder(OCFile file) {
+        ArrayList<OCFile> result = new ArrayList<>();
+
+        if (file == null || !file.fileExists()) {
+            return result;
+        }
+
+        if (!file.isFolder()) {
+            result.add(file);
+            return result;
+        }
+
+        List<OCFile> filesInsideFolder = getFolderContent(file.getFileId(), false);
+        for (OCFile item: filesInsideFolder) {
+            if (!item.isFolder()) {
+                result.add(item);
+            } else {
+                result.addAll(getAllFilesRecursivelyInsideFolder(item));
+            }
+        }
+
+        return result;
+    }
 
     public List<OCFile> getFolderContent(OCFile ocFile, boolean onlyOnDevice) {
         if (ocFile != null && ocFile.isFolder() && ocFile.fileExists()) {
@@ -189,7 +233,6 @@ public class FileDataStorageManager {
         }
     }
 
-
     public List<OCFile> getFolderImages(OCFile folder, boolean onlyOnDevice) {
         List<OCFile> imageList = new ArrayList<>();
 

+ 8 - 9
app/src/main/java/com/owncloud/android/files/FileMenuFilter.java

@@ -31,11 +31,11 @@ import android.view.Menu;
 import com.nextcloud.android.files.FileLockingHelper;
 import com.nextcloud.client.account.User;
 import com.nextcloud.client.editimage.EditImageActivity;
+import com.nextcloud.client.files.downloader.FileDownloadHelper;
 import com.nextcloud.utils.EditorUtils;
 import com.owncloud.android.R;
 import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
-import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder;
 import com.owncloud.android.files.services.FileUploader.FileUploaderBinder;
 import com.owncloud.android.lib.resources.status.OCCapability;
 import com.owncloud.android.services.OperationsService.OperationsServiceBinder;
@@ -380,9 +380,8 @@ public class FileMenuFilter {
         if (componentsGetter != null && !files.isEmpty() && user != null) {
             OperationsServiceBinder opsBinder = componentsGetter.getOperationsServiceBinder();
             FileUploaderBinder uploaderBinder = componentsGetter.getFileUploaderBinder();
-            FileDownloaderBinder downloaderBinder = componentsGetter.getFileDownloaderBinder();
             synchronizing = anyFileSynchronizing(opsBinder) ||      // comparing local and remote
-                            anyFileDownloading(downloaderBinder) ||
+                            anyFileDownloading() ||
                             anyFileUploading(uploaderBinder);
         }
         return synchronizing;
@@ -398,14 +397,14 @@ public class FileMenuFilter {
         return synchronizing;
     }
 
-    private boolean anyFileDownloading(FileDownloaderBinder downloaderBinder) {
-        boolean downloading = false;
-        if (downloaderBinder != null) {
-            for (Iterator<OCFile> iterator = files.iterator(); !downloading && iterator.hasNext(); ) {
-                downloading = downloaderBinder.isDownloading(user, iterator.next());
+    private boolean anyFileDownloading() {
+        for (OCFile file : files) {
+            if (FileDownloadHelper.Companion.instance().isDownloading(user, file)) {
+                return true;
             }
         }
-        return downloading;
+
+        return false;
     }
 
     private boolean anyFileUploading(FileUploaderBinder uploaderBinder) {

+ 0 - 744
app/src/main/java/com/owncloud/android/files/services/FileDownloader.java

@@ -1,744 +0,0 @@
-/*
- *   ownCloud Android client application
- *
- *   Copyright (C) 2012 Bartek Przybylski
- *   Copyright (C) 2012-2016 ownCloud Inc.
- *
- *   This program is free software: you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License version 2,
- *   as published by the Free Software Foundation.
- *
- *   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 General Public License for more details.
- *
- *   You should have received a copy of the GNU General Public License
- *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
- *
- */
-
-package com.owncloud.android.files.services;
-
-import android.accounts.Account;
-import android.accounts.AccountManager;
-import android.accounts.OnAccountsUpdateListener;
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.app.Service;
-import android.content.Intent;
-import android.graphics.BitmapFactory;
-import android.os.Binder;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.IBinder;
-import android.os.Looper;
-import android.os.Message;
-import android.os.Process;
-import android.util.Pair;
-
-import com.nextcloud.client.account.User;
-import com.nextcloud.client.account.UserAccountManager;
-import com.nextcloud.client.files.downloader.DownloadTask;
-import com.nextcloud.java.util.Optional;
-import com.nextcloud.utils.ForegroundServiceHelper;
-import com.nextcloud.utils.extensions.IntentExtensionsKt;
-import com.owncloud.android.R;
-import com.owncloud.android.authentication.AuthenticatorActivity;
-import com.owncloud.android.datamodel.FileDataStorageManager;
-import com.owncloud.android.datamodel.ForegroundServiceType;
-import com.owncloud.android.datamodel.OCFile;
-import com.owncloud.android.datamodel.UploadsStorageManager;
-import com.owncloud.android.lib.common.OwnCloudAccount;
-import com.owncloud.android.lib.common.OwnCloudClient;
-import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
-import com.owncloud.android.lib.common.network.OnDatatransferProgressListener;
-import com.owncloud.android.lib.common.operations.RemoteOperationResult;
-import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode;
-import com.owncloud.android.lib.common.utils.Log_OC;
-import com.owncloud.android.lib.resources.files.FileUtils;
-import com.owncloud.android.operations.DownloadFileOperation;
-import com.owncloud.android.operations.DownloadType;
-import com.owncloud.android.providers.DocumentsStorageProvider;
-import com.owncloud.android.ui.activity.ConflictsResolveActivity;
-import com.owncloud.android.ui.activity.FileActivity;
-import com.owncloud.android.ui.activity.FileDisplayActivity;
-import com.owncloud.android.ui.dialog.SendShareDialog;
-import com.owncloud.android.ui.fragment.OCFileListFragment;
-import com.owncloud.android.ui.notifications.NotificationUtils;
-import com.owncloud.android.ui.preview.PreviewImageActivity;
-import com.owncloud.android.ui.preview.PreviewImageFragment;
-import com.owncloud.android.utils.ErrorMessageAdapter;
-import com.owncloud.android.utils.MimeTypeUtil;
-import com.owncloud.android.utils.theme.ViewThemeUtils;
-
-import java.io.File;
-import java.security.SecureRandom;
-import java.util.AbstractList;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.Vector;
-
-import javax.inject.Inject;
-
-import androidx.core.app.NotificationCompat;
-import androidx.localbroadcastmanager.content.LocalBroadcastManager;
-import dagger.android.AndroidInjection;
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-
-public class FileDownloader extends Service
-        implements OnDatatransferProgressListener, OnAccountsUpdateListener {
-
-    public static final String EXTRA_USER = "USER";
-    public static final String EXTRA_FILE = "FILE";
-
-    private static final String DOWNLOAD_ADDED_MESSAGE = "DOWNLOAD_ADDED";
-    private static final String DOWNLOAD_FINISH_MESSAGE = "DOWNLOAD_FINISH";
-    public static final String EXTRA_DOWNLOAD_RESULT = "RESULT";
-    public static final String EXTRA_REMOTE_PATH = "REMOTE_PATH";
-    public static final String EXTRA_LINKED_TO_PATH = "LINKED_TO";
-    public static final String ACCOUNT_NAME = "ACCOUNT_NAME";
-    public static final String DOWNLOAD_TYPE = "DOWNLOAD_TYPE";
-
-    private static final int FOREGROUND_SERVICE_ID = 412;
-
-    private static final String TAG = FileDownloader.class.getSimpleName();
-
-    private Looper mServiceLooper;
-    private ServiceHandler mServiceHandler;
-    private IBinder mBinder;
-    private OwnCloudClient mDownloadClient;
-    private Optional<User> currentUser = Optional.empty();
-    private FileDataStorageManager mStorageManager;
-
-    private IndexedForest<DownloadFileOperation> mPendingDownloads = new IndexedForest<>();
-
-    private DownloadFileOperation mCurrentDownload;
-
-    private NotificationManager mNotificationManager;
-    private NotificationCompat.Builder mNotificationBuilder;
-    private int mLastPercent;
-
-    private Notification mNotification;
-
-    private long conflictUploadId;
-
-    public boolean mStartedDownload = false;
-
-    @Inject UserAccountManager accountManager;
-    @Inject UploadsStorageManager uploadsStorageManager;
-    @Inject LocalBroadcastManager localBroadcastManager;
-    @Inject ViewThemeUtils viewThemeUtils;
-
-    public static String getDownloadAddedMessage() {
-        return FileDownloader.class.getName() + DOWNLOAD_ADDED_MESSAGE;
-    }
-
-    public static String getDownloadFinishMessage() {
-        return FileDownloader.class.getName() + DOWNLOAD_FINISH_MESSAGE;
-    }
-
-    /**
-     * Service initialization
-     */
-    @Override
-    public void onCreate() {
-        super.onCreate();
-        AndroidInjection.inject(this);
-        Log_OC.d(TAG, "Creating service");
-        mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
-        HandlerThread thread = new HandlerThread("FileDownloaderThread", Process.THREAD_PRIORITY_BACKGROUND);
-        thread.start();
-        mServiceLooper = thread.getLooper();
-        mServiceHandler = new ServiceHandler(mServiceLooper, this);
-        mBinder = new FileDownloaderBinder();
-
-        NotificationCompat.Builder builder = NotificationUtils.newNotificationBuilder(this, viewThemeUtils).setContentTitle(
-                getApplicationContext().getResources().getString(R.string.app_name))
-            .setContentText(getApplicationContext().getResources().getString(R.string.foreground_service_download))
-            .setSmallIcon(R.drawable.notification_icon)
-            .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.notification_icon));
-
-
-        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
-            builder.setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_DOWNLOAD);
-        }
-
-        mNotification = builder.build();
-
-        // add AccountsUpdatedListener
-        AccountManager am = AccountManager.get(getApplicationContext());
-        am.addOnAccountsUpdatedListener(this, null, false);
-    }
-
-
-    /**
-     * Service clean up
-     */
-    @Override
-    public void onDestroy() {
-        Log_OC.v(TAG, "Destroying service");
-        mBinder = null;
-        mServiceHandler = null;
-        mServiceLooper.quit();
-        mServiceLooper = null;
-        mNotificationManager = null;
-
-        // remove AccountsUpdatedListener
-        AccountManager am = AccountManager.get(getApplicationContext());
-        am.removeOnAccountsUpdatedListener(this);
-        super.onDestroy();
-    }
-
-
-    /**
-     * Entry point to add one or several files to the queue of downloads.
-     * <p>
-     * New downloads are added calling to startService(), resulting in a call to this method. This ensures the service
-     * will keep on working although the caller activity goes away.
-     */
-    @Override
-    public int onStartCommand(Intent intent, int flags, int startId) {
-        Log_OC.d(TAG, "Starting command with id " + startId);
-
-        ForegroundServiceHelper.INSTANCE.startService(this, FOREGROUND_SERVICE_ID, mNotification, ForegroundServiceType.DataSync);
-
-        if (intent == null || !intent.hasExtra(EXTRA_USER) || !intent.hasExtra(EXTRA_FILE)) {
-            Log_OC.e(TAG, "Not enough information provided in intent");
-            return START_NOT_STICKY;
-        } else {
-            final User user = IntentExtensionsKt.getParcelableArgument(intent, EXTRA_USER, User.class);
-            final OCFile file = IntentExtensionsKt.getParcelableArgument(intent, EXTRA_FILE, OCFile.class);
-            final String behaviour = intent.getStringExtra(OCFileListFragment.DOWNLOAD_BEHAVIOUR);
-
-            DownloadType downloadType = DownloadType.DOWNLOAD;
-            if (intent.hasExtra(DOWNLOAD_TYPE)) {
-                downloadType = IntentExtensionsKt.getSerializableArgument(intent, DOWNLOAD_TYPE, DownloadType.class);
-            }
-            String activityName = intent.getStringExtra(SendShareDialog.ACTIVITY_NAME);
-            String packageName = intent.getStringExtra(SendShareDialog.PACKAGE_NAME);
-            conflictUploadId = intent.getLongExtra(ConflictsResolveActivity.EXTRA_CONFLICT_UPLOAD_ID, -1);
-            AbstractList<String> requestedDownloads = new Vector<String>();
-            try {
-                DownloadFileOperation newDownload = new DownloadFileOperation(user,
-                                                                              file,
-                                                                              behaviour,
-                                                                              activityName,
-                                                                              packageName,
-                                                                              getBaseContext(),
-                                                                              downloadType);
-                newDownload.addDatatransferProgressListener(this);
-                newDownload.addDatatransferProgressListener((FileDownloaderBinder) mBinder);
-                Pair<String, String> putResult = mPendingDownloads.putIfAbsent(user.getAccountName(),
-                                                                               file.getRemotePath(),
-                                                                               newDownload);
-                if (putResult != null) {
-                    String downloadKey = putResult.first;
-                    requestedDownloads.add(downloadKey);
-                    sendBroadcastNewDownload(newDownload, putResult.second);
-                }   // else, file already in the queue of downloads; don't repeat the request
-
-            } catch (IllegalArgumentException e) {
-                Log_OC.e(TAG, "Not enough information provided in intent: " + e.getMessage());
-                return START_NOT_STICKY;
-            }
-
-            if (requestedDownloads.size() > 0) {
-                Message msg = mServiceHandler.obtainMessage();
-                msg.arg1 = startId;
-                msg.obj = requestedDownloads;
-                mServiceHandler.sendMessage(msg);
-            }
-        }
-
-        return START_NOT_STICKY;
-    }
-
-    /**
-     * Provides a binder object that clients can use to perform operations on the queue of downloads,
-     * excepting the addition of new files.
-     *
-     * Implemented to perform cancellation, pause and resume of existing downloads.
-     */
-    @Override
-    public IBinder onBind(Intent intent) {
-        return mBinder;
-    }
-
-
-    /**
-     * Called when ALL the bound clients were onbound.
-     */
-    @Override
-    public boolean onUnbind(Intent intent) {
-        ((FileDownloaderBinder) mBinder).clearListeners();
-        return false;   // not accepting rebinding (default behaviour)
-    }
-
-    @Override
-    public void onAccountsUpdated(Account[] accounts) {
-         //review the current download and cancel it if its account doesn't exist
-        if (mCurrentDownload != null && !accountManager.exists(mCurrentDownload.getUser().toPlatformAccount())) {
-            mCurrentDownload.cancel();
-        }
-        // The rest of downloads are cancelled when they try to start
-    }
-
-
-    /**
-     * Binder to let client components to perform operations on the queue of downloads.
-     * <p/>
-     * It provides by itself the available operations.
-     */
-    public class FileDownloaderBinder extends Binder implements OnDatatransferProgressListener {
-
-        /**
-         * Map of listeners that will be reported about progress of downloads from a
-         * {@link FileDownloaderBinder}
-         * instance.
-         */
-        private Map<Long, OnDatatransferProgressListener> mBoundListeners =
-                new HashMap<Long, OnDatatransferProgressListener>();
-
-
-        /**
-         * Cancels a pending or current download of a remote file.
-         *
-         * @param account ownCloud account where the remote file is stored.
-         * @param file    A file in the queue of pending downloads
-         */
-        public void cancel(Account account, OCFile file) {
-            Pair<DownloadFileOperation, String> removeResult =
-                mPendingDownloads.remove(account.name, file.getRemotePath());
-            DownloadFileOperation download = removeResult.first;
-            if (download != null) {
-                download.cancel();
-            } else {
-                if (mCurrentDownload != null && currentUser.isPresent() &&
-                    mCurrentDownload.getRemotePath().startsWith(file.getRemotePath()) &&
-                        account.name.equals(currentUser.get().getAccountName())) {
-                    mCurrentDownload.cancel();
-                }
-            }
-        }
-
-        /**
-         * Cancels all the downloads for an account
-         */
-        public void cancel(String accountName) {
-            if (mCurrentDownload != null && mCurrentDownload.getUser().nameEquals(accountName)) {
-                mCurrentDownload.cancel();
-            }
-            // Cancel pending downloads
-            cancelPendingDownloads(accountName);
-        }
-
-        public void clearListeners() {
-            mBoundListeners.clear();
-        }
-
-
-        /**
-         * Returns True when the file described by 'file' in the ownCloud account 'account'
-         * is downloading or waiting to download.
-         *
-         * If 'file' is a directory, returns 'true' if any of its descendant files is downloading or
-         * waiting to download.
-         *
-         * @param user    user where the remote file is stored.
-         * @param file    A file that could be in the queue of downloads.
-         */
-        public boolean isDownloading(User user, OCFile file) {
-            return user != null && file != null && mPendingDownloads.contains(user.getAccountName(), file.getRemotePath());
-        }
-
-
-        /**
-         * Adds a listener interested in the progress of the download for a concrete file.
-         *
-         * @param listener Object to notify about progress of transfer.
-         * @param file     {@link OCFile} of interest for listener.
-         */
-        public void addDatatransferProgressListener(OnDatatransferProgressListener listener, OCFile file) {
-            if (file == null || listener == null) {
-                return;
-            }
-            mBoundListeners.put(file.getFileId(), listener);
-        }
-
-
-        /**
-         * Removes a listener interested in the progress of the download for a concrete file.
-         *
-         * @param listener      Object to notify about progress of transfer.
-         * @param file          {@link OCFile} of interest for listener.
-         */
-        public void removeDatatransferProgressListener(OnDatatransferProgressListener listener, OCFile file) {
-            if (file == null || listener == null) {
-                return;
-            }
-            Long fileId = file.getFileId();
-            if (mBoundListeners.get(fileId) == listener) {
-                mBoundListeners.remove(fileId);
-            }
-        }
-
-        @Override
-        public void onTransferProgress(long progressRate, long totalTransferredSoFar,
-                                       long totalToTransfer, String fileName) {
-            OnDatatransferProgressListener boundListener =
-                    mBoundListeners.get(mCurrentDownload.getFile().getFileId());
-            if (boundListener != null) {
-                boundListener.onTransferProgress(progressRate, totalTransferredSoFar,
-                                                 totalToTransfer, fileName);
-            }
-        }
-
-    }
-
-    /**
-     * Download worker. Performs the pending downloads in the order they were requested.
-
-     * Created with the Looper of a new thread, started in {@link FileUploader#onCreate()}.
-     */
-    private static class ServiceHandler extends Handler {
-        // don't make it a final class, and don't remove the static ; lint will warn about a
-        // possible memory leak
-        FileDownloader mService;
-
-        public ServiceHandler(Looper looper, FileDownloader service) {
-            super(looper);
-            if (service == null) {
-                throw new IllegalArgumentException("Received invalid NULL in parameter 'service'");
-            }
-            mService = service;
-        }
-
-        @Override
-        public void handleMessage(Message msg) {
-            @SuppressWarnings("unchecked")
-            AbstractList<String> requestedDownloads = (AbstractList<String>) msg.obj;
-            if (msg.obj != null) {
-                Iterator<String> it = requestedDownloads.iterator();
-                while (it.hasNext()) {
-                    String next = it.next();
-                    mService.downloadFile(next);
-                }
-            }
-            mService.mStartedDownload=false;
-
-            (new Handler()).postDelayed(() -> {
-                if(!mService.mStartedDownload){
-                    mService.mNotificationManager.cancel(R.string.downloader_download_in_progress_ticker);
-                }
-                Log_OC.d(TAG, "Stopping after command with id " + msg.arg1);
-                mService.mNotificationManager.cancel(FOREGROUND_SERVICE_ID);
-                mService.stopForeground(true);
-                mService.stopSelf(msg.arg1);
-            }, 2000);
-        }
-    }
-
-
-    /**
-     * Core download method: requests a file to download and stores it.
-     *
-     * @param downloadKey Key to access the download to perform, contained in mPendingDownloads
-     */
-    private void downloadFile(String downloadKey) {
-
-        mStartedDownload = true;
-        mCurrentDownload = mPendingDownloads.get(downloadKey);
-
-        if (mCurrentDownload != null) {
-            // Detect if the account exists
-            if (accountManager.exists(mCurrentDownload.getUser().toPlatformAccount())) {
-                notifyDownloadStart(mCurrentDownload);
-                RemoteOperationResult downloadResult = null;
-                try {
-                    /// prepare client object to send the request to the ownCloud server
-                    Account currentDownloadAccount = mCurrentDownload.getUser().toPlatformAccount();
-                    Optional<User> currentDownloadUser = accountManager.getUser(currentDownloadAccount.name);
-                    if (!currentUser.equals(currentDownloadUser)) {
-                        currentUser = currentDownloadUser;
-                        mStorageManager = new FileDataStorageManager(currentUser.get(), getContentResolver());
-                    }   // else, reuse storage manager from previous operation
-
-                    // always get client from client manager, to get fresh credentials in case
-                    // of update
-                    OwnCloudAccount ocAccount = currentDownloadUser.get().toOwnCloudAccount();
-                    mDownloadClient = OwnCloudClientManagerFactory.getDefaultSingleton().
-                            getClientFor(ocAccount, this);
-
-
-                    /// perform the download
-                    downloadResult = mCurrentDownload.execute(mDownloadClient);
-                    if (downloadResult.isSuccess() && mCurrentDownload.getDownloadType() == DownloadType.DOWNLOAD) {
-                        saveDownloadedFile();
-                    }
-
-                } catch (Exception e) {
-                    Log_OC.e(TAG, "Error downloading", e);
-                    downloadResult = new RemoteOperationResult(e);
-
-                } finally {
-                    Pair<DownloadFileOperation, String> removeResult = mPendingDownloads.removePayload(
-                        mCurrentDownload.getUser().getAccountName(), mCurrentDownload.getRemotePath());
-
-                    if (downloadResult == null) {
-                        downloadResult = new RemoteOperationResult(new RuntimeException("Error downloading…"));
-                    }
-
-                    /// notify result
-                    notifyDownloadResult(mCurrentDownload, downloadResult);
-                    sendBroadcastDownloadFinished(mCurrentDownload, downloadResult, removeResult.second);
-                }
-            } else {
-                cancelPendingDownloads(mCurrentDownload.getUser().getAccountName());
-            }
-        }
-    }
-
-
-    /**
-     * Updates the OC File after a successful download.
-     *
-     * TODO move to DownloadFileOperation
-     *  unify with code from {@link DocumentsStorageProvider} and {@link DownloadTask}.
-     */
-    private void saveDownloadedFile() {
-        OCFile file = mStorageManager.getFileById(mCurrentDownload.getFile().getFileId());
-
-        if (file == null) {
-            // try to get file via path, needed for overwriting existing files on conflict dialog
-            file = mStorageManager.getFileByDecryptedRemotePath(mCurrentDownload.getFile().getRemotePath());
-        }
-
-        if (file == null) {
-            Log_OC.e(this, "Could not save " + mCurrentDownload.getFile().getRemotePath());
-            return;
-        }
-
-        long syncDate = System.currentTimeMillis();
-        file.setLastSyncDateForProperties(syncDate);
-        file.setLastSyncDateForData(syncDate);
-        file.setUpdateThumbnailNeeded(true);
-        file.setModificationTimestamp(mCurrentDownload.getModificationTimestamp());
-        file.setModificationTimestampAtLastSyncForData(mCurrentDownload.getModificationTimestamp());
-        file.setEtag(mCurrentDownload.getEtag());
-        file.setMimeType(mCurrentDownload.getMimeType());
-        file.setStoragePath(mCurrentDownload.getSavePath());
-        file.setFileLength(new File(mCurrentDownload.getSavePath()).length());
-        file.setRemoteId(mCurrentDownload.getFile().getRemoteId());
-        mStorageManager.saveFile(file);
-        if (MimeTypeUtil.isMedia(mCurrentDownload.getMimeType())) {
-            FileDataStorageManager.triggerMediaScan(file.getStoragePath(), file);
-        }
-        mStorageManager.saveConflict(file, null);
-    }
-
-    /**
-     * Creates a status notification to show the download progress
-     *
-     * @param download Download operation starting.
-     */
-    private void notifyDownloadStart(DownloadFileOperation download) {
-        /// create status notification with a progress bar
-        mLastPercent = 0;
-        mNotificationBuilder = NotificationUtils.newNotificationBuilder(this, viewThemeUtils);
-        mNotificationBuilder
-            .setSmallIcon(R.drawable.notification_icon)
-            .setTicker(getString(R.string.downloader_download_in_progress_ticker))
-            .setContentTitle(getString(R.string.downloader_download_in_progress_ticker))
-            .setOngoing(true)
-            .setProgress(100, 0, download.getSize() < 0)
-            .setContentText(
-                String.format(getString(R.string.downloader_download_in_progress_content), 0,
-                              new File(download.getSavePath()).getName())
-                           );
-
-        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
-            mNotificationBuilder.setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_DOWNLOAD);
-        }
-
-        /// includes a pending intent in the notification showing the details view of the file
-        Intent showDetailsIntent = null;
-        if (PreviewImageFragment.canBePreviewed(download.getFile())) {
-            showDetailsIntent = new Intent(this, PreviewImageActivity.class);
-        } else {
-            showDetailsIntent = new Intent(this, FileDisplayActivity.class);
-        }
-        showDetailsIntent.putExtra(FileActivity.EXTRA_FILE, download.getFile());
-        showDetailsIntent.putExtra(FileActivity.EXTRA_USER, download.getUser());
-        showDetailsIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
-
-        mNotificationBuilder.setContentIntent(PendingIntent.getActivity(this, (int) System.currentTimeMillis(),
-                                                                        showDetailsIntent, PendingIntent.FLAG_IMMUTABLE));
-
-
-        if (mNotificationManager == null) {
-            mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
-        }
-        if (mNotificationManager != null) {
-            mNotificationManager.notify(R.string.downloader_download_in_progress_ticker, mNotificationBuilder.build());
-        }
-    }
-
-
-    /**
-     * Callback method to update the progress bar in the status notification.
-     */
-    @Override
-    public void onTransferProgress(long progressRate, long totalTransferredSoFar,
-                                   long totalToTransfer, String filePath) {
-        int percent = (int) (100.0 * ((double) totalTransferredSoFar) / ((double) totalToTransfer));
-        if (percent != mLastPercent) {
-            mNotificationBuilder.setProgress(100, percent, totalToTransfer < 0);
-            String fileName = filePath.substring(filePath.lastIndexOf(FileUtils.PATH_SEPARATOR) + 1);
-            String text = String.format(getString(R.string.downloader_download_in_progress_content), percent, fileName);
-            mNotificationBuilder.setContentText(text);
-
-            if (mNotificationManager == null) {
-                mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
-            }
-
-            if (mNotificationManager != null) {
-                mNotificationManager.notify(R.string.downloader_download_in_progress_ticker,
-                        mNotificationBuilder.build());
-            }
-        }
-        mLastPercent = percent;
-    }
-
-
-    /**
-     * Updates the status notification with the result of a download operation.
-     *
-     * @param downloadResult Result of the download operation.
-     * @param download       Finished download operation
-     */
-    @SuppressFBWarnings("DMI")
-    private void notifyDownloadResult(DownloadFileOperation download,
-                                      RemoteOperationResult downloadResult) {
-        if (mNotificationManager == null) {
-            mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
-        }
-
-        if (!downloadResult.isCancelled()) {
-            if (downloadResult.isSuccess()) {
-                if (conflictUploadId > 0) {
-                    uploadsStorageManager.removeUpload(conflictUploadId);
-                }
-                // Dont show notification except an error has occured.
-                return;
-            }
-            int tickerId = downloadResult.isSuccess() ?
-                    R.string.downloader_download_succeeded_ticker : R.string.downloader_download_failed_ticker;
-
-            boolean needsToUpdateCredentials = ResultCode.UNAUTHORIZED == downloadResult.getCode();
-            tickerId = needsToUpdateCredentials ?
-                    R.string.downloader_download_failed_credentials_error : tickerId;
-
-            mNotificationBuilder
-                    .setTicker(getString(tickerId))
-                    .setContentTitle(getString(tickerId))
-                    .setAutoCancel(true)
-                    .setOngoing(false)
-                    .setProgress(0, 0, false);
-
-            if (needsToUpdateCredentials) {
-                configureUpdateCredentialsNotification(download.getUser());
-
-            } else {
-                // TODO put something smart in showDetailsIntent
-                Intent showDetailsIntent = new Intent();
-                mNotificationBuilder.setContentIntent(PendingIntent.getActivity(this, (int) System.currentTimeMillis(),
-                                                                                showDetailsIntent, PendingIntent.FLAG_IMMUTABLE));
-            }
-
-            mNotificationBuilder.setContentText(ErrorMessageAdapter.getErrorCauseMessage(downloadResult,
-                    download, getResources()));
-
-            if (mNotificationManager != null) {
-                mNotificationManager.notify((new SecureRandom()).nextInt(), mNotificationBuilder.build());
-
-                // Remove success notification
-                if (downloadResult.isSuccess()) {
-                    // Sleep 2 seconds, so show the notification before remove it
-                    NotificationUtils.cancelWithDelay(mNotificationManager,
-                                                      R.string.downloader_download_succeeded_ticker, 2000);
-                }
-            }
-        }
-    }
-
-    private void configureUpdateCredentialsNotification(User user) {
-        // let the user update credentials with one click
-        Intent updateAccountCredentials = new Intent(this, AuthenticatorActivity.class);
-        updateAccountCredentials.putExtra(AuthenticatorActivity.EXTRA_ACCOUNT, user.toPlatformAccount());
-        updateAccountCredentials.putExtra(
-                AuthenticatorActivity.EXTRA_ACTION,
-                AuthenticatorActivity.ACTION_UPDATE_EXPIRED_TOKEN
-        );
-        updateAccountCredentials.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        updateAccountCredentials.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
-        updateAccountCredentials.addFlags(Intent.FLAG_FROM_BACKGROUND);
-        mNotificationBuilder.setContentIntent(
-            PendingIntent.getActivity(this,
-                                      (int) System.currentTimeMillis(),
-                                      updateAccountCredentials,
-                                      PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE)
-                                             );
-    }
-
-
-    /**
-     * Sends a broadcast when a download finishes in order to the interested activities can
-     * update their view
-     *
-     * @param download               Finished download operation
-     * @param downloadResult         Result of the download operation
-     * @param unlinkedFromRemotePath Path in the downloads tree where the download was unlinked from
-     */
-    private void sendBroadcastDownloadFinished(
-            DownloadFileOperation download,
-            RemoteOperationResult downloadResult,
-            String unlinkedFromRemotePath) {
-
-        Intent end = new Intent(getDownloadFinishMessage());
-        end.putExtra(EXTRA_DOWNLOAD_RESULT, downloadResult.isSuccess());
-        end.putExtra(ACCOUNT_NAME, download.getUser().getAccountName());
-        end.putExtra(EXTRA_REMOTE_PATH, download.getRemotePath());
-        end.putExtra(OCFileListFragment.DOWNLOAD_BEHAVIOUR, download.getBehaviour());
-        end.putExtra(SendShareDialog.ACTIVITY_NAME, download.getActivityName());
-        end.putExtra(SendShareDialog.PACKAGE_NAME, download.getPackageName());
-        if (unlinkedFromRemotePath != null) {
-            end.putExtra(EXTRA_LINKED_TO_PATH, unlinkedFromRemotePath);
-        }
-        end.setPackage(getPackageName());
-        localBroadcastManager.sendBroadcast(end);
-    }
-
-
-    /**
-     * Sends a broadcast when a new download is added to the queue.
-     *
-     * @param download           Added download operation
-     * @param linkedToRemotePath Path in the downloads tree where the download was linked to
-     */
-    private void sendBroadcastNewDownload(DownloadFileOperation download,
-                                          String linkedToRemotePath) {
-        Intent added = new Intent(getDownloadAddedMessage());
-        added.putExtra(ACCOUNT_NAME, download.getUser().getAccountName());
-        added.putExtra(EXTRA_REMOTE_PATH, download.getRemotePath());
-        added.putExtra(EXTRA_LINKED_TO_PATH, linkedToRemotePath);
-        added.setPackage(getPackageName());
-        localBroadcastManager.sendBroadcast(added);
-    }
-
-    private void cancelPendingDownloads(String accountName) {
-        mPendingDownloads.remove(accountName);
-    }
-}

+ 5 - 1
app/src/main/java/com/owncloud/android/files/services/IndexedForest.java

@@ -43,8 +43,12 @@ public class IndexedForest<V> {
 
     private ConcurrentMap<String, Node<V>> mMap = new ConcurrentHashMap<>();
 
+    public ConcurrentMap<String, Node<V>> getAll() {
+        return mMap;
+    }
+
     @SuppressWarnings("PMD.ShortClassName")
-    private class Node<V> {
+    public class Node<V> {
         private String mKey;
         private Node<V> mParent;
         private Set<Node<V>> mChildren = new HashSet<>();    // TODO be careful with hash()

+ 28 - 8
app/src/main/java/com/owncloud/android/operations/DownloadFileOperation.java

@@ -23,6 +23,7 @@ package com.owncloud.android.operations;
 
 import android.content.Context;
 import android.text.TextUtils;
+import android.util.Log;
 import android.webkit.MimeTypeMap;
 
 import com.nextcloud.client.account.User;
@@ -43,6 +44,7 @@ import com.owncloud.android.utils.FileStorageUtils;
 
 import java.io.File;
 import java.io.FileOutputStream;
+import java.lang.ref.WeakReference;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.Set;
@@ -62,7 +64,7 @@ public class DownloadFileOperation extends RemoteOperation {
     private String packageName;
     private DownloadType downloadType;
 
-    private Context context;
+    private final WeakReference<Context> context;
     private Set<OnDatatransferProgressListener> dataTransferListeners = new HashSet<>();
     private long modificationTimestamp;
     private DownloadFileRemoteOperation downloadOperation;
@@ -90,7 +92,7 @@ public class DownloadFileOperation extends RemoteOperation {
         this.behaviour = behaviour;
         this.activityName = activityName;
         this.packageName = packageName;
-        this.context = context;
+        this.context = new WeakReference<>(context);
         this.downloadType = downloadType;
     }
 
@@ -98,6 +100,16 @@ public class DownloadFileOperation extends RemoteOperation {
         this(user, file, null, null, null, context, DownloadType.DOWNLOAD);
     }
 
+    public boolean isMatching(String accountName, long fileId) {
+        return getFile().getFileId() == fileId && getUser().getAccountName().equals(accountName);
+    }
+
+    public void cancelMatchingOperation(String accountName, long fileId) {
+        if (isMatching(accountName, fileId)) {
+            cancel();
+        }
+    }
+
     public String getSavePath() {
         if (file.getStoragePath() != null) {
             File parentFile = new File(file.getStoragePath()).getParentFile();
@@ -160,6 +172,11 @@ public class DownloadFileOperation extends RemoteOperation {
             }
         }
 
+        Context operationContext = context.get();
+        if (operationContext == null) {
+            return new RemoteOperationResult(RemoteOperationResult.ResultCode.UNKNOWN_ERROR);
+        }
+
         RemoteOperationResult result;
         File newFile = null;
         boolean moved;
@@ -180,6 +197,8 @@ public class DownloadFileOperation extends RemoteOperation {
 
         result = downloadOperation.execute(client);
 
+
+
         if (result.isSuccess()) {
             modificationTimestamp = downloadOperation.getModificationTimestamp();
             etag = downloadOperation.getEtag();
@@ -194,13 +213,13 @@ public class DownloadFileOperation extends RemoteOperation {
 
             // decrypt file
             if (file.isEncrypted()) {
-                FileDataStorageManager fileDataStorageManager = new FileDataStorageManager(user, context.getContentResolver());
+                FileDataStorageManager fileDataStorageManager = new FileDataStorageManager(user, operationContext.getContentResolver());
 
-                OCFile parent = fileDataStorageManager.getFileByPath(file.getParentRemotePath());
+                OCFile parent = fileDataStorageManager.getFileByEncryptedRemotePath(file.getParentRemotePath());
 
                 DecryptedFolderMetadata metadata = EncryptionUtils.downloadFolderMetadata(parent,
                                                                                           client,
-                                                                                          context,
+                                                                                          operationContext,
                                                                                           user);
 
                 if (metadata == null) {
@@ -218,7 +237,7 @@ public class DownloadFileOperation extends RemoteOperation {
                                                                         key,
                                                                         iv,
                                                                         authenticationTag,
-                                                                        new ArbitraryDataProviderImpl(context),
+                                                                        new ArbitraryDataProviderImpl(operationContext),
                                                                         user);
 
                     try (FileOutputStream fileOutputStream = new FileOutputStream(tmpFile)) {
@@ -238,7 +257,7 @@ public class DownloadFileOperation extends RemoteOperation {
             } else if (downloadType == DownloadType.EXPORT) {
                 new FileExportUtils().exportFile(file.getFileName(),
                                                  file.getMimeType(),
-                                                 context.getContentResolver(),
+                                                 operationContext.getContentResolver(),
                                                  null,
                                                  tmpFile);
                 if (!tmpFile.delete()) {
@@ -246,6 +265,7 @@ public class DownloadFileOperation extends RemoteOperation {
                 }
             }
         }
+
         Log_OC.i(TAG, "Download of " + file.getRemotePath() + " to " + getSavePath() + ": " +
                 result.getLogMessage());
 
@@ -260,7 +280,7 @@ public class DownloadFileOperation extends RemoteOperation {
     }
 
 
-    public void addDatatransferProgressListener (OnDatatransferProgressListener listener) {
+    public void addDownloadDataTransferProgressListener(OnDatatransferProgressListener listener) {
         synchronized (dataTransferListeners) {
             dataTransferListeners.add(listener);
         }

+ 7 - 3
app/src/main/java/com/owncloud/android/operations/DownloadType.kt

@@ -22,7 +22,11 @@
 
 package com.owncloud.android.operations
 
-enum class DownloadType {
-    DOWNLOAD,
-    EXPORT
+enum class DownloadType(var type: String) {
+    DOWNLOAD("DOWNLOAD"),
+    EXPORT("EXPORT");
+
+    override fun toString(): String {
+        return type
+    }
 }

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

@@ -22,13 +22,12 @@
 package com.owncloud.android.operations;
 
 import android.content.Context;
-import android.content.Intent;
 import android.text.TextUtils;
 
 import com.nextcloud.client.account.User;
+import com.nextcloud.client.files.downloader.FileDownloadHelper;
 import com.owncloud.android.datamodel.FileDataStorageManager;
 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;
@@ -310,33 +309,19 @@ public class SynchronizeFileOperation extends SyncOperation {
         mTransferWasRequested = true;
     }
 
-
-    /**
-     * Requests for a download to the FileDownloader service
-     *
-     * @param file OCFile object representing the file to download
-     */
     private void requestForDownload(OCFile file) {
-        Intent i = new Intent(mContext, FileDownloader.class);
-        i.putExtra(FileDownloader.EXTRA_USER, mUser);
-        i.putExtra(FileDownloader.EXTRA_FILE, file);
-        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
-            mContext.startForegroundService(i);
-        } else {
-            mContext.startService(i);
-        }
+        FileDownloadHelper.Companion.instance().downloadFile(
+            mUser,
+            file);
 
         mTransferWasRequested = true;
     }
 
-
     public boolean transferWasRequested() {
         return mTransferWasRequested;
     }
 
-
     public OCFile getLocalFile() {
         return mLocalFile;
     }
-
 }

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

@@ -25,10 +25,10 @@ import android.content.Intent;
 import android.text.TextUtils;
 
 import com.nextcloud.client.account.User;
+import com.nextcloud.client.files.downloader.FileDownloadHelper;
 import com.owncloud.android.datamodel.DecryptedFolderMetadata;
 import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
-import com.owncloud.android.files.services.FileDownloader;
 import com.owncloud.android.lib.common.OwnCloudClient;
 import com.owncloud.android.lib.common.operations.OperationCancelledException;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
@@ -376,19 +376,8 @@ public class SynchronizeFolderOperation extends SyncOperation {
         }
     }
 
-    private void classifyFileForLaterSyncOrDownload(OCFile remoteFile, OCFile localFile)
-            throws OperationCancelledException {
-        if (remoteFile.isFolder()) {
-            /// to download children files recursively
-            synchronized (mCancellationRequested) {
-                if (mCancellationRequested.get()) {
-                    throw new OperationCancelledException();
-                }
-                startSyncFolderOperation(remoteFile.getRemotePath());
-            }
-
-        } else {
-            /// prepare content synchronization for files (any file, not just favorites)
+    private void classifyFileForLaterSyncOrDownload(OCFile remoteFile, OCFile localFile) {
+        if (!remoteFile.isFolder()) {
             SynchronizeFileOperation operation = new SynchronizeFileOperation(
                 localFile,
                 remoteFile,
@@ -405,18 +394,7 @@ public class SynchronizeFolderOperation extends SyncOperation {
     private void prepareOpsFromLocalKnowledge() throws OperationCancelledException {
         List<OCFile> children = getStorageManager().getFolderContent(mLocalFolder, false);
         for (OCFile child : children) {
-            /// classify file to sync/download contents later
-            if (child.isFolder()) {
-                /// to download children files recursively
-                synchronized(mCancellationRequested) {
-                    if (mCancellationRequested.get()) {
-                        throw new OperationCancelledException();
-                    }
-                    startSyncFolderOperation(child.getRemotePath());
-                }
-
-            } else {
-                /// synchronization for regular files
+            if (!child.isFolder()) {
                 if (!child.isDown()) {
                     mFilesForDirectDownload.add(child);
 
@@ -433,34 +411,18 @@ public class SynchronizeFolderOperation extends SyncOperation {
                     mFilesToSyncContents.add(operation);
 
                 }
-
             }
         }
     }
 
-
     private void syncContents() throws OperationCancelledException {
         startDirectDownloads();
         startContentSynchronizations(mFilesToSyncContents);
     }
 
 
-    private void startDirectDownloads() throws OperationCancelledException {
-        for (OCFile file : mFilesForDirectDownload) {
-            synchronized(mCancellationRequested) {
-                if (mCancellationRequested.get()) {
-                    throw new OperationCancelledException();
-                }
-                Intent i = new Intent(mContext, FileDownloader.class);
-                i.putExtra(FileDownloader.EXTRA_USER, user);
-                i.putExtra(FileDownloader.EXTRA_FILE, file);
-                if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
-                    mContext.startForegroundService(i);
-                } else {
-                    mContext.startService(i);
-                }
-            }
-        }
+    private void startDirectDownloads() {
+        FileDownloadHelper.Companion.instance().downloadFile(user, mLocalFolder);
     }
 
     /**

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

@@ -52,7 +52,6 @@ import com.owncloud.android.R;
 import com.owncloud.android.datamodel.FileDataStorageManager;
 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.NameCollisionPolicy;
 import com.owncloud.android.lib.common.OwnCloudAccount;
@@ -308,7 +307,7 @@ public class DocumentsStorageProvider extends DocumentsProvider {
     /**
      * Updates the OC File after a successful download.
      *
-     * TODO unify with code from {@link FileDownloader} and {@link DownloadTask}.
+     * TODO unify with code from {@link com.nextcloud.client.files.downloader.FileDownloadWorker} and {@link DownloadTask}.
      */
     private void saveDownloadedFile(FileDataStorageManager storageManager, DownloadFileOperation dfo, OCFile file) {
         long syncDate = System.currentTimeMillis();

+ 8 - 8
app/src/main/java/com/owncloud/android/services/SyncFolderHandler.java

@@ -28,8 +28,8 @@ import android.os.Message;
 import android.util.Pair;
 
 import com.nextcloud.client.account.User;
+import com.nextcloud.client.files.downloader.FileDownloadWorker;
 import com.owncloud.android.datamodel.OCFile;
-import com.owncloud.android.files.services.FileDownloader;
 import com.owncloud.android.files.services.IndexedForest;
 import com.owncloud.android.lib.common.OwnCloudAccount;
 import com.owncloud.android.lib.common.OwnCloudClient;
@@ -169,9 +169,9 @@ class SyncFolderHandler extends Handler {
      * this is a fast and ugly patch.
      */
     private void sendBroadcastNewSyncFolder(Account account, String remotePath) {
-        Intent added = new Intent(FileDownloader.getDownloadAddedMessage());
-        added.putExtra(FileDownloader.ACCOUNT_NAME, account.name);
-        added.putExtra(FileDownloader.EXTRA_REMOTE_PATH, remotePath);
+        Intent added = new Intent(FileDownloadWorker.Companion.getDownloadAddedMessage());
+        added.putExtra(FileDownloadWorker.EXTRA_ACCOUNT_NAME, account.name);
+        added.putExtra(FileDownloadWorker.EXTRA_REMOTE_PATH, remotePath);
         added.setPackage(mService.getPackageName());
         LocalBroadcastManager.getInstance(mService.getApplicationContext()).sendBroadcast(added);
     }
@@ -182,10 +182,10 @@ class SyncFolderHandler extends Handler {
      */
     private void sendBroadcastFinishedSyncFolder(Account account, String remotePath,
                                                  boolean success) {
-        Intent finished = new Intent(FileDownloader.getDownloadFinishMessage());
-        finished.putExtra(FileDownloader.ACCOUNT_NAME, account.name);
-        finished.putExtra(FileDownloader.EXTRA_REMOTE_PATH, remotePath);
-        finished.putExtra(FileDownloader.EXTRA_DOWNLOAD_RESULT, success);
+        Intent finished = new Intent(FileDownloadWorker.Companion.getDownloadFinishMessage());
+        finished.putExtra(FileDownloadWorker.EXTRA_ACCOUNT_NAME, account.name);
+        finished.putExtra(FileDownloadWorker.EXTRA_REMOTE_PATH, remotePath);
+        finished.putExtra(FileDownloadWorker.EXTRA_DOWNLOAD_RESULT, success);
         finished.setPackage(mService.getPackageName());
         LocalBroadcastManager.getInstance(mService.getApplicationContext()).sendBroadcast(finished);
     }

+ 4 - 4
app/src/main/java/com/owncloud/android/ui/activity/ComponentsGetter.java

@@ -20,8 +20,8 @@
 
 package com.owncloud.android.ui.activity;
 
+import com.nextcloud.client.files.downloader.FileDownloadWorker;
 import com.owncloud.android.datamodel.FileDataStorageManager;
-import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder;
 import com.owncloud.android.files.services.FileUploader.FileUploaderBinder;
 import com.owncloud.android.services.OperationsService.OperationsServiceBinder;
 import com.owncloud.android.ui.helpers.FileOperationsHelper;
@@ -30,11 +30,11 @@ public interface ComponentsGetter {
 
     /**
      * To be invoked when the parent activity is fully created to get a reference
-     * to the FileDownloader service API.
+     * to the FileDownloadWorker.
      */
-    public FileDownloaderBinder getFileDownloaderBinder();
+    public FileDownloadWorker.FileDownloadProgressListener getFileDownloadProgressListener();
+
 
-    
     /**
      * To be invoked when the parent activity is fully created to get a reference
      * to the FileUploader service API.

+ 8 - 6
app/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.kt

@@ -21,13 +21,13 @@ import android.content.Intent
 import android.os.Bundle
 import android.widget.Toast
 import com.nextcloud.client.account.User
+import com.nextcloud.client.files.downloader.FileDownloadHelper
 import com.nextcloud.model.HTTPStatusCodes
 import com.nextcloud.utils.extensions.getParcelableArgument
 import com.owncloud.android.R
 import com.owncloud.android.datamodel.OCFile
 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.utils.Log_OC
@@ -114,11 +114,13 @@ class ConflictsResolveActivity : FileActivity(), OnConflictDecisionMadeListener
 
                 Decision.KEEP_SERVER -> if (!shouldDeleteLocal()) {
                     // Overwrite local file
-                    val intent = Intent(baseContext, FileDownloader::class.java)
-                    intent.putExtra(FileDownloader.EXTRA_USER, getUser().orElseThrow { RuntimeException() })
-                    intent.putExtra(FileDownloader.EXTRA_FILE, file)
-                    intent.putExtra(EXTRA_CONFLICT_UPLOAD_ID, conflictUploadId)
-                    startService(intent)
+                    file?.let {
+                        FileDownloadHelper.instance().downloadFile(
+                            getUser().orElseThrow { RuntimeException() },
+                            file,
+                            conflictUploadId = conflictUploadId
+                        )
+                    }
                 } else {
                     uploadsStorageManager!!.removeUpload(upload)
                 }

+ 8 - 17
app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java

@@ -43,6 +43,8 @@ import android.text.TextUtils;
 import com.google.android.material.snackbar.Snackbar;
 import com.nextcloud.client.account.User;
 import com.nextcloud.client.account.UserAccountManager;
+import com.nextcloud.client.files.downloader.FileDownloadHelper;
+import com.nextcloud.client.files.downloader.FileDownloadWorker;
 import com.nextcloud.client.jobs.BackgroundJobManager;
 import com.nextcloud.client.network.ConnectivityService;
 import com.nextcloud.utils.EditorUtils;
@@ -54,8 +56,6 @@ import com.owncloud.android.authentication.AuthenticatorActivity;
 import com.owncloud.android.datamodel.ArbitraryDataProvider;
 import com.owncloud.android.datamodel.ArbitraryDataProviderImpl;
 import com.owncloud.android.datamodel.OCFile;
-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.lib.common.OwnCloudAccount;
@@ -166,9 +166,8 @@ public abstract class FileActivity extends DrawerActivity
 
     private boolean mResumed;
 
-    protected FileDownloaderBinder mDownloaderBinder;
+    protected FileDownloadWorker.FileDownloadProgressListener fileDownloadProgressListener;
     protected FileUploaderBinder mUploaderBinder;
-    private ServiceConnection mDownloadServiceConnection;
     private ServiceConnection mUploadServiceConnection;
 
     @Inject
@@ -206,6 +205,7 @@ public abstract class FileActivity extends DrawerActivity
         super.onCreate(savedInstanceState);
         mHandler = new Handler();
         mFileOperationsHelper = new FileOperationsHelper(this, getUserAccountManager(), connectivityService, editorUtils);
+        User user = null;
 
         if (savedInstanceState != null) {
             mFile = BundleExtensionsKt.getParcelableArgument(savedInstanceState, FileActivity.EXTRA_FILE, OCFile.class);
@@ -218,25 +218,20 @@ public abstract class FileActivity extends DrawerActivity
                 viewThemeUtils.files.themeActionBar(this, actionBar, savedInstanceState.getString(KEY_ACTION_BAR_TITLE));
             }
         } else {
-            User user = IntentExtensionsKt.getParcelableArgument(getIntent(), FileActivity.EXTRA_USER, User.class);
+            user = IntentExtensionsKt.getParcelableArgument(getIntent(), FileActivity.EXTRA_USER, User.class);
             mFile = IntentExtensionsKt.getParcelableArgument(getIntent(), FileActivity.EXTRA_FILE, OCFile.class);
             mFromNotification = getIntent().getBooleanExtra(FileActivity.EXTRA_FROM_NOTIFICATION,
                     false);
+
             if (user != null) {
                 setUser(user);
             }
         }
 
-
         mOperationsServiceConnection = new OperationsServiceConnection();
         bindService(new Intent(this, OperationsService.class), mOperationsServiceConnection,
                 Context.BIND_AUTO_CREATE);
 
-        mDownloadServiceConnection = newTransferenceServiceConnection();
-        if (mDownloadServiceConnection != null) {
-            bindService(new Intent(this, FileDownloader.class), mDownloadServiceConnection,
-                    Context.BIND_AUTO_CREATE);
-        }
         mUploadServiceConnection = newTransferenceServiceConnection();
         if (mUploadServiceConnection != null) {
             bindService(new Intent(this, FileUploader.class), mUploadServiceConnection,
@@ -280,10 +275,6 @@ public abstract class FileActivity extends DrawerActivity
             unbindService(mOperationsServiceConnection);
             mOperationsServiceBinder = null;
         }
-        if (mDownloadServiceConnection != null) {
-            unbindService(mDownloadServiceConnection);
-            mDownloadServiceConnection = null;
-        }
         if (mUploadServiceConnection != null) {
             unbindService(mUploadServiceConnection);
             mUploadServiceConnection = null;
@@ -616,8 +607,8 @@ public abstract class FileActivity extends DrawerActivity
     }
 
     @Override
-    public FileDownloaderBinder getFileDownloaderBinder() {
-        return mDownloaderBinder;
+    public FileDownloadWorker.FileDownloadProgressListener getFileDownloadProgressListener() {
+        return fileDownloadProgressListener;
     }
 
     @Override

+ 42 - 41
app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java

@@ -65,12 +65,16 @@ import com.nextcloud.client.core.AsyncRunner;
 import com.nextcloud.client.di.Injectable;
 import com.nextcloud.client.editimage.EditImageActivity;
 import com.nextcloud.client.files.DeepLinkHandler;
+import com.nextcloud.client.files.downloader.FileDownloadHelper;
+import com.nextcloud.client.files.downloader.FileDownloadWorker;
 import com.nextcloud.client.media.PlayerServiceConnection;
 import com.nextcloud.client.network.ClientFactory;
 import com.nextcloud.client.network.ConnectivityService;
 import com.nextcloud.client.preferences.AppPreferences;
 import com.nextcloud.client.utils.IntentUtil;
 import com.nextcloud.java.util.Optional;
+import com.nextcloud.model.WorkerState;
+import com.nextcloud.model.WorkerStateLiveData;
 import com.nextcloud.utils.extensions.BundleExtensionsKt;
 import com.nextcloud.utils.extensions.IntentExtensionsKt;
 import com.nextcloud.utils.view.FastScrollUtils;
@@ -80,8 +84,6 @@ import com.owncloud.android.databinding.FilesBinding;
 import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.datamodel.VirtualFolderType;
-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;
@@ -94,6 +96,7 @@ import com.owncloud.android.lib.resources.files.RestoreFileVersionRemoteOperatio
 import com.owncloud.android.lib.resources.files.SearchRemoteOperation;
 import com.owncloud.android.operations.CopyFileOperation;
 import com.owncloud.android.operations.CreateFolderOperation;
+import com.owncloud.android.operations.DownloadType;
 import com.owncloud.android.operations.MoveFileOperation;
 import com.owncloud.android.operations.RefreshFolderOperation;
 import com.owncloud.android.operations.RemoveFileOperation;
@@ -284,6 +287,7 @@ public class FileDisplayActivity extends FileActivity
         checkStoragePath();
 
         initSyncBroadcastReceiver();
+        observeWorkerState();
     }
 
     @SuppressWarnings("unchecked")
@@ -684,12 +688,12 @@ public class FileDisplayActivity extends FileActivity
                 // the user browsed to other file ; forget the automatic preview
                 mWaitingToPreview = null;
 
-            } else if (downloadEvent.equals(FileDownloader.getDownloadAddedMessage())) {
+            } else if (downloadEvent.equals(FileDownloadWorker.Companion.getDownloadAddedMessage())) {
                 // grant that the details fragment updates the progress bar
                 detailsFragment.listenForTransferProgress();
                 detailsFragment.updateFileDetails(true, false);
 
-            } else if (downloadEvent.equals(FileDownloader.getDownloadFinishMessage())) {
+            } else if (downloadEvent.equals(FileDownloadWorker.Companion.getDownloadFinishMessage())) {
                 //  update the details panel
                 boolean detailsFragmentChanged = false;
                 if (waitedPreview) {
@@ -1115,8 +1119,8 @@ public class FileDisplayActivity extends FileActivity
         localBroadcastManager.registerReceiver(mUploadFinishReceiver, uploadIntentFilter);
 
         // Listen for download messages
-        IntentFilter downloadIntentFilter = new IntentFilter(FileDownloader.getDownloadAddedMessage());
-        downloadIntentFilter.addAction(FileDownloader.getDownloadFinishMessage());
+        IntentFilter downloadIntentFilter = new IntentFilter(FileDownloadWorker.Companion.getDownloadAddedMessage());
+        downloadIntentFilter.addAction(FileDownloadWorker.Companion.getDownloadFinishMessage());
         mDownloadFinishReceiver = new DownloadFinishReceiver();
         localBroadcastManager.registerReceiver(mDownloadFinishReceiver, downloadIntentFilter);
 
@@ -1417,7 +1421,7 @@ public class FileDisplayActivity extends FileActivity
 
 
     /**
-     * Class waiting for broadcast events from the {@link FileDownloader} service.
+     * Class waiting for broadcast events from the {@link FileDownloadWorker} service.
      * <p>
      * Updates the UI when a download is started or finished, provided that it is relevant for the current folder.
      */
@@ -1426,16 +1430,16 @@ public class FileDisplayActivity extends FileActivity
         @Override
         public void onReceive(Context context, Intent intent) {
             boolean sameAccount = isSameAccount(intent);
-            String downloadedRemotePath = intent.getStringExtra(FileDownloader.EXTRA_REMOTE_PATH);
+            String downloadedRemotePath = intent.getStringExtra(FileDownloadWorker.EXTRA_REMOTE_PATH);
             String downloadBehaviour = intent.getStringExtra(OCFileListFragment.DOWNLOAD_BEHAVIOUR);
             boolean isDescendant = isDescendant(downloadedRemotePath);
 
             if (sameAccount && isDescendant) {
-                String linkedToRemotePath = intent.getStringExtra(FileDownloader.EXTRA_LINKED_TO_PATH);
+                String linkedToRemotePath = intent.getStringExtra(FileDownloadWorker.EXTRA_LINKED_TO_PATH);
                 if (linkedToRemotePath == null || isAscendant(linkedToRemotePath)) {
                     updateListOfFilesFragment(false);
                 }
-                refreshDetailsFragmentIfVisible(intent.getAction(), downloadedRemotePath, intent.getBooleanExtra(FileDownloader.EXTRA_DOWNLOAD_RESULT, false));
+                refreshDetailsFragmentIfVisible(intent.getAction(), downloadedRemotePath, intent.getBooleanExtra(FileDownloadWorker.EXTRA_DOWNLOAD_RESULT, false));
             }
 
             if (mWaitingToSend != null) {
@@ -1468,7 +1472,7 @@ public class FileDisplayActivity extends FileActivity
         }
 
         private boolean isSameAccount(Intent intent) {
-            String accountName = intent.getStringExtra(FileDownloader.ACCOUNT_NAME);
+            String accountName = intent.getStringExtra(FileDownloadWorker.EXTRA_ACCOUNT_NAME);
             return accountName != null && getAccount() != null && accountName.equals(getAccount().name);
         }
     }
@@ -1558,6 +1562,24 @@ public class FileDisplayActivity extends FileActivity
         return isRoot(getCurrentDir());
     }
 
+    private void observeWorkerState() {
+        WorkerStateLiveData.Companion.instance().observe(this, state -> {
+            if (state instanceof WorkerState.Download) {
+                Log_OC.d(TAG, "Download worker started");
+                handleDownloadWorkerState();
+            }
+        });
+    }
+
+    private void handleDownloadWorkerState() {
+        if (mWaitingToPreview != null && getStorageManager() != null) {
+            mWaitingToPreview = getStorageManager().getFileById(mWaitingToPreview.getFileId());
+            if (mWaitingToPreview != null && !mWaitingToPreview.isDown()) {
+                requestForDownload();
+            }
+        }
+    }
+
     @Override
     protected ServiceConnection newTransferenceServiceConnection() {
         return new ListServiceConnection();
@@ -1570,22 +1592,13 @@ public class FileDisplayActivity extends FileActivity
 
         @Override
         public void onServiceConnected(ComponentName component, IBinder service) {
-            if (component.equals(new ComponentName(FileDisplayActivity.this, FileDownloader.class))) {
-                Log_OC.d(TAG, "Download service connected");
-                mDownloaderBinder = (FileDownloaderBinder) service;
-                if (mWaitingToPreview != null && getStorageManager() != null) {
-                    // update the file
-                    mWaitingToPreview = getStorageManager().getFileById(mWaitingToPreview.getFileId());
-                    if (mWaitingToPreview != null && !mWaitingToPreview.isDown()) {
-                        requestForDownload();
-                    }
-                }
-            } else if (component.equals(new ComponentName(FileDisplayActivity.this, FileUploader.class))) {
+            if (component.equals(new ComponentName(FileDisplayActivity.this, FileUploader.class))) {
                 Log_OC.d(TAG, "Upload service connected");
                 mUploaderBinder = (FileUploaderBinder) service;
             } else {
                 return;
             }
+
             // a new chance to get the mDownloadBinder through
             // getFileDownloadBinder() - THIS IS A MESS
             OCFileListFragment listOfFiles = getListOfFilesFragment();
@@ -1593,9 +1606,9 @@ public class FileDisplayActivity extends FileActivity
                 IntentExtensionsKt.getParcelableArgument(getIntent(), EXTRA_FILE, OCFile.class) == null))) {
                 listOfFiles.listDirectory(MainApp.isOnlyOnDevice(), false);
             }
+
             Fragment leftFragment = getLeftFragment();
-            if (leftFragment instanceof FileDetailFragment) {
-                FileDetailFragment detailFragment = (FileDetailFragment) leftFragment;
+            if (leftFragment instanceof FileDetailFragment detailFragment) {
                 detailFragment.listenForTransferProgress();
                 detailFragment.updateFileDetails(false, false);
             }
@@ -1603,9 +1616,9 @@ public class FileDisplayActivity extends FileActivity
 
         @Override
         public void onServiceDisconnected(ComponentName component) {
-            if (component.equals(new ComponentName(FileDisplayActivity.this, FileDownloader.class))) {
+            if (component.equals(new ComponentName(FileDisplayActivity.this, FileDownloadWorker.class))) {
                 Log_OC.d(TAG, "Download service disconnected");
-                mDownloaderBinder = null;
+                fileDownloadProgressListener = null;
             } else if (component.equals(new ComponentName(FileDisplayActivity.this, FileUploader.class))) {
                 Log_OC.d(TAG, "Upload service disconnected");
                 mUploaderBinder = null;
@@ -1883,13 +1896,7 @@ public class FileDisplayActivity extends FileActivity
 
     private void requestForDownload() {
         User user = getUser().orElseThrow(RuntimeException::new);
-        //if (!mWaitingToPreview.isDownloading()) {
-        if (!mDownloaderBinder.isDownloading(user, mWaitingToPreview)) {
-            Intent i = new Intent(this, FileDownloader.class);
-            i.putExtra(FileDownloader.EXTRA_USER, user);
-            i.putExtra(FileDownloader.EXTRA_FILE, mWaitingToPreview);
-            startService(i);
-        }
+        FileDownloadHelper.Companion.instance().downloadFileIfNotStartedBefore(user, mWaitingToPreview);
     }
 
     @Override
@@ -1959,14 +1966,8 @@ public class FileDisplayActivity extends FileActivity
 
     private void requestForDownload(OCFile file, String downloadBehaviour, String packageName, String activityName) {
         final User currentUser = getUser().orElseThrow(RuntimeException::new);
-        if (!mDownloaderBinder.isDownloading(currentUser, mWaitingToPreview)) {
-            Intent i = new Intent(this, FileDownloader.class);
-            i.putExtra(FileDownloader.EXTRA_USER, currentUser);
-            i.putExtra(FileDownloader.EXTRA_FILE, file);
-            i.putExtra(SendShareDialog.PACKAGE_NAME, packageName);
-            i.putExtra(SendShareDialog.ACTIVITY_NAME, activityName);
-            i.putExtra(OCFileListFragment.DOWNLOAD_BEHAVIOUR, downloadBehaviour);
-            startService(i);
+        if (!FileDownloadHelper.Companion.instance().isDownloading(currentUser, file)) {
+            FileDownloadHelper.Companion.instance().downloadFile(currentUser, file, downloadBehaviour, DownloadType.DOWNLOAD, activityName, packageName, null);
         }
     }
 

+ 24 - 26
app/src/main/java/com/owncloud/android/ui/activity/ManageAccountsActivity.java

@@ -41,9 +41,12 @@ import android.view.View;
 import com.google.common.collect.Sets;
 import com.nextcloud.client.account.User;
 import com.nextcloud.client.account.UserAccountManager;
+import com.nextcloud.client.files.downloader.FileDownloadHelper;
 import com.nextcloud.client.jobs.BackgroundJobManager;
 import com.nextcloud.client.onboarding.FirstRunActivity;
 import com.nextcloud.java.util.Optional;
+import com.nextcloud.model.WorkerState;
+import com.nextcloud.model.WorkerStateLiveData;
 import com.nextcloud.utils.extensions.BundleExtensionsKt;
 import com.owncloud.android.MainApp;
 import com.owncloud.android.R;
@@ -51,11 +54,11 @@ import com.owncloud.android.authentication.AuthenticatorActivity;
 import com.owncloud.android.datamodel.ArbitraryDataProvider;
 import com.owncloud.android.datamodel.ArbitraryDataProviderImpl;
 import com.owncloud.android.datamodel.FileDataStorageManager;
-import com.owncloud.android.files.services.FileDownloader;
 import com.owncloud.android.files.services.FileUploader;
 import com.owncloud.android.lib.common.OwnCloudAccount;
 import com.owncloud.android.lib.common.UserInfo;
 import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.operations.DownloadFileOperation;
 import com.owncloud.android.services.OperationsService;
 import com.owncloud.android.ui.adapter.UserListAdapter;
 import com.owncloud.android.ui.adapter.UserListItem;
@@ -105,7 +108,6 @@ public class ManageAccountsActivity extends FileActivity implements UserListAdap
     private final Handler handler = new Handler();
     private String accountName;
     private UserListAdapter userListAdapter;
-    private ServiceConnection downloadServiceConnection;
     private ServiceConnection uploadServiceConnection;
     private Set<String> originalUsers;
     private String originalCurrentUser;
@@ -113,6 +115,9 @@ public class ManageAccountsActivity extends FileActivity implements UserListAdap
     private ArbitraryDataProvider arbitraryDataProvider;
     private boolean multipleAccountsSupported;
 
+    private String workerAccountName;
+    private DownloadFileOperation workerCurrentDownload;
+
     @Inject BackgroundJobManager backgroundJobManager;
     @Inject UserAccountManager accountManager;
 
@@ -160,6 +165,7 @@ public class ManageAccountsActivity extends FileActivity implements UserListAdap
         recyclerView.setAdapter(userListAdapter);
         recyclerView.setLayoutManager(new LinearLayoutManager(this));
         initializeComponentGetters();
+        observeWorkerState();
     }
 
 
@@ -241,11 +247,6 @@ public class ManageAccountsActivity extends FileActivity implements UserListAdap
      * Initialize ComponentsGetters.
      */
     private void initializeComponentGetters() {
-        downloadServiceConnection = newTransferenceServiceConnection();
-        if (downloadServiceConnection != null) {
-            bindService(new Intent(this, FileDownloader.class), downloadServiceConnection,
-                        Context.BIND_AUTO_CREATE);
-        }
         uploadServiceConnection = newTransferenceServiceConnection();
         if (uploadServiceConnection != null) {
             bindService(new Intent(this, FileUploader.class), uploadServiceConnection,
@@ -340,9 +341,8 @@ public class ManageAccountsActivity extends FileActivity implements UserListAdap
                 if (mUploaderBinder != null) {
                     mUploaderBinder.cancel(accountName);
                 }
-                if (mDownloaderBinder != null) {
-                    mDownloaderBinder.cancel(accountName);
-                }
+
+                FileDownloadHelper.Companion.instance().cancelAllDownloadsForAccount(workerAccountName, workerCurrentDownload);
             }
 
             User currentUser = getUserAccountManager().getUser();
@@ -374,10 +374,6 @@ public class ManageAccountsActivity extends FileActivity implements UserListAdap
 
     @Override
     protected void onDestroy() {
-        if (downloadServiceConnection != null) {
-            unbindService(downloadServiceConnection);
-            downloadServiceConnection = null;
-        }
         if (uploadServiceConnection != null) {
             unbindService(uploadServiceConnection);
             uploadServiceConnection = null;
@@ -435,9 +431,8 @@ public class ManageAccountsActivity extends FileActivity implements UserListAdap
         if (mUploaderBinder != null) {
             mUploaderBinder.cancel(user);
         }
-        if (mDownloaderBinder != null) {
-            mDownloaderBinder.cancel(user.getAccountName());
-        }
+
+        FileDownloadHelper.Companion.instance().cancelAllDownloadsForAccount(workerAccountName, workerCurrentDownload);
 
         backgroundJobManager.startAccountRemovalJob(user.getAccountName(), false);
 
@@ -522,6 +517,16 @@ public class ManageAccountsActivity extends FileActivity implements UserListAdap
         }
     }
 
+    private void observeWorkerState() {
+        WorkerStateLiveData.Companion.instance().observe(this, state -> {
+            if (state instanceof WorkerState.Download) {
+                Log_OC.d(TAG, "Download worker started");
+                workerAccountName = ((WorkerState.Download) state).getUser().getAccountName();
+                workerCurrentDownload = ((WorkerState.Download) state).getCurrentDownload();
+            }
+        });
+    }
+
     @Override
     public void onAccountClicked(User user) {
         openAccount(user);
@@ -534,11 +539,7 @@ public class ManageAccountsActivity extends FileActivity implements UserListAdap
 
         @Override
         public void onServiceConnected(ComponentName component, IBinder service) {
-
-            if (component.equals(new ComponentName(ManageAccountsActivity.this, FileDownloader.class))) {
-                mDownloaderBinder = (FileDownloader.FileDownloaderBinder) service;
-
-            } else if (component.equals(new ComponentName(ManageAccountsActivity.this, FileUploader.class))) {
+            if (component.equals(new ComponentName(ManageAccountsActivity.this, FileUploader.class))) {
                 Log_OC.d(TAG, "Upload service connected");
                 mUploaderBinder = (FileUploader.FileUploaderBinder) service;
             }
@@ -546,10 +547,7 @@ public class ManageAccountsActivity extends FileActivity implements UserListAdap
 
         @Override
         public void onServiceDisconnected(ComponentName component) {
-            if (component.equals(new ComponentName(ManageAccountsActivity.this, FileDownloader.class))) {
-                Log_OC.d(TAG, "Download service suddenly disconnected");
-                mDownloaderBinder = null;
-            } else if (component.equals(new ComponentName(ManageAccountsActivity.this, FileUploader.class))) {
+            if (component.equals(new ComponentName(ManageAccountsActivity.this, FileUploader.class))) {
                 Log_OC.d(TAG, "Upload service suddenly disconnected");
                 mUploaderBinder = null;
             }

+ 20 - 11
app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt

@@ -32,6 +32,7 @@ import androidx.core.content.res.ResourcesCompat
 import com.elyeproj.loaderviewlibrary.LoaderImageView
 import com.nextcloud.android.common.ui.theme.utils.ColorRole
 import com.nextcloud.client.account.User
+import com.nextcloud.client.files.downloader.FileDownloadHelper
 import com.nextcloud.client.preferences.AppPreferences
 import com.nextcloud.utils.extensions.createRoundedOutline
 import com.owncloud.android.R
@@ -340,25 +341,33 @@ class OCFileListDelegate(
 
     private fun showLocalFileIndicator(file: OCFile, gridViewHolder: ListGridImageViewHolder) {
         val operationsServiceBinder = transferServiceGetter.operationsServiceBinder
-        val fileDownloaderBinder = transferServiceGetter.fileDownloaderBinder
         val fileUploaderBinder = transferServiceGetter.fileUploaderBinder
-        when {
+
+        val icon: Int? = when {
             operationsServiceBinder?.isSynchronizing(user, file) == true ||
-                fileDownloaderBinder?.isDownloading(user, file) == true ||
+                FileDownloadHelper.instance().isDownloading(user, file) ||
                 fileUploaderBinder?.isUploading(user, file) == true -> {
                 // synchronizing, downloading or uploading
-                gridViewHolder.localFileIndicator.setImageResource(R.drawable.ic_synchronizing)
-                gridViewHolder.localFileIndicator.visibility = View.VISIBLE
+                R.drawable.ic_synchronizing
             }
+
             file.etagInConflict != null -> {
-                // conflict
-                gridViewHolder.localFileIndicator.setImageResource(R.drawable.ic_synchronizing_error)
-                gridViewHolder.localFileIndicator.visibility = View.VISIBLE
+                R.drawable.ic_synchronizing_error
             }
+
             file.isDown -> {
-                // downloaded
-                gridViewHolder.localFileIndicator.setImageResource(R.drawable.ic_synced)
-                gridViewHolder.localFileIndicator.visibility = View.VISIBLE
+                R.drawable.ic_synced
+            }
+
+            else -> {
+                null
+            }
+        }
+
+        gridViewHolder.localFileIndicator.run {
+            icon?.let {
+                setImageResource(icon)
+                visibility = View.VISIBLE
             }
         }
     }

+ 10 - 13
app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java

@@ -44,6 +44,7 @@ import com.google.android.material.tabs.TabLayout;
 import com.nextcloud.client.account.User;
 import com.nextcloud.client.account.UserAccountManager;
 import com.nextcloud.client.di.Injectable;
+import com.nextcloud.client.files.downloader.FileDownloadHelper;
 import com.nextcloud.client.jobs.BackgroundJobManager;
 import com.nextcloud.client.network.ClientFactory;
 import com.nextcloud.client.network.ConnectivityService;
@@ -57,7 +58,6 @@ import com.owncloud.android.databinding.FileDetailsFragmentBinding;
 import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.datamodel.ThumbnailsCacheManager;
-import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder;
 import com.owncloud.android.files.services.FileUploader.FileUploaderBinder;
 import com.owncloud.android.lib.common.OwnCloudClient;
 import com.owncloud.android.lib.common.network.OnDatatransferProgressListener;
@@ -502,7 +502,7 @@ public class FileDetailFragment extends FileFragment implements OnClickListener,
      * TODO Remove parameter when the transferring state of files is kept in database.
      *
      * @param transferring Flag signaling if the file should be considered as downloading or uploading, although
-     *                     {@link FileDownloaderBinder#isDownloading(User, OCFile)}  and
+     *                     {@link FileDownloadHelper#isDownloading(User, OCFile)}  and
      *                     {@link FileUploaderBinder#isUploading(User, OCFile)} return false.
      * @param refresh      If 'true', try to refresh the whole file from the database
      */
@@ -534,10 +534,9 @@ public class FileDetailFragment extends FileFragment implements OnClickListener,
             setFavoriteIconStatus(file.isFavorite());
 
             // configure UI for depending upon local state of the file
-            FileDownloaderBinder downloaderBinder = containerActivity.getFileDownloaderBinder();
             FileUploaderBinder uploaderBinder = containerActivity.getFileUploaderBinder();
             if (transferring
-                || (downloaderBinder != null && downloaderBinder.isDownloading(user, file))
+                || (FileDownloadHelper.Companion.instance().isDownloading(user, file))
                 || (uploaderBinder != null && uploaderBinder.isUploading(user, file))) {
                 setButtonsForTransferring();
 
@@ -659,10 +658,8 @@ public class FileDetailFragment extends FileFragment implements OnClickListener,
             // show the progress bar for the transfer
             binding.progressBlock.setVisibility(View.VISIBLE);
             binding.progressText.setVisibility(View.VISIBLE);
-            FileDownloaderBinder downloaderBinder = containerActivity.getFileDownloaderBinder();
             FileUploaderBinder uploaderBinder = containerActivity.getFileUploaderBinder();
-            //if (getFile().isDownloading()) {
-            if (downloaderBinder != null && downloaderBinder.isDownloading(user, getFile())) {
+            if (FileDownloadHelper.Companion.instance().isDownloading(user, getFile())) {
                 binding.progressText.setText(R.string.downloader_download_in_progress_ticker);
             } else {
                 if (uploaderBinder != null && uploaderBinder.isUploading(user, getFile())) {
@@ -694,9 +691,9 @@ public class FileDetailFragment extends FileFragment implements OnClickListener,
 
     public void listenForTransferProgress() {
         if (progressListener != null) {
-            if (containerActivity.getFileDownloaderBinder() != null) {
-                containerActivity.getFileDownloaderBinder().
-                    addDatatransferProgressListener(progressListener, getFile());
+            if (containerActivity.getFileDownloadProgressListener() != null) {
+                containerActivity.getFileDownloadProgressListener().
+                    addDataTransferProgressListener(progressListener, getFile());
             }
             if (containerActivity.getFileUploaderBinder() != null) {
                 containerActivity.getFileUploaderBinder().
@@ -709,9 +706,9 @@ public class FileDetailFragment extends FileFragment implements OnClickListener,
 
     private void leaveTransferProgress() {
         if (progressListener != null) {
-            if (containerActivity.getFileDownloaderBinder() != null) {
-                containerActivity.getFileDownloaderBinder().
-                    removeDatatransferProgressListener(progressListener, getFile());
+            if (containerActivity.getFileDownloadProgressListener() != null) {
+                containerActivity.getFileDownloadProgressListener().
+                    removeDataTransferProgressListener(progressListener, getFile());
             }
             if (containerActivity.getFileUploaderBinder() != null) {
                 containerActivity.getFileUploaderBinder().

+ 2 - 4
app/src/main/java/com/owncloud/android/ui/fragment/FileFragment.java

@@ -21,13 +21,11 @@
 
 package com.owncloud.android.ui.fragment;
 
-import android.accounts.Account;
 import android.app.Activity;
 import android.os.Bundle;
 
 import com.nextcloud.utils.extensions.BundleExtensionsKt;
 import com.owncloud.android.datamodel.OCFile;
-import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder;
 import com.owncloud.android.files.services.FileUploader.FileUploaderBinder;
 import com.owncloud.android.ui.activity.ComponentsGetter;
 
@@ -161,8 +159,8 @@ public class FileFragment extends Fragment {
          * This happens when a download or upload is started or ended for a file.
          *
          * This method is necessary by now to update the user interface of the double-pane layout
-         * in tablets because methods {@link FileDownloaderBinder#isDownloading(Account, OCFile)}
-         * and {@link FileUploaderBinder#isUploading(Account, OCFile)}
+         * in tablets because methods {@link //FileDownloaderBinder # isDownloading(Account, OCFile)}
+         * and {@link FileUploaderBinder# isUploading(Account, OCFile)}
          * won't provide the needed response before the method where this is called finishes.
          *
          * TODO Remove this when the transfer state of a file is kept in the database

+ 5 - 5
app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupListFragment.java

@@ -38,11 +38,11 @@ import com.google.android.material.snackbar.Snackbar;
 import com.nextcloud.client.account.User;
 import com.nextcloud.client.account.UserAccountManager;
 import com.nextcloud.client.di.Injectable;
-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;
-import com.nextcloud.client.files.downloader.TransferState;
+import com.nextcloud.client.files.DownloadRequest;
+import com.nextcloud.client.files.Request;
+import com.nextcloud.client.files.transfer.Transfer;
+import com.nextcloud.client.files.transfer.TransferManagerConnection;
+import com.nextcloud.client.files.transfer.TransferState;
 import com.nextcloud.client.jobs.BackgroundJobManager;
 import com.nextcloud.client.network.ClientFactory;
 import com.nextcloud.utils.extensions.BundleExtensionsKt;

+ 5 - 5
app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java

@@ -47,6 +47,7 @@ import android.webkit.MimeTypeMap;
 
 import com.nextcloud.client.account.CurrentAccountProvider;
 import com.nextcloud.client.account.User;
+import com.nextcloud.client.files.downloader.FileDownloadHelper;
 import com.nextcloud.client.jobs.BackgroundJobManager;
 import com.nextcloud.client.network.ConnectivityService;
 import com.nextcloud.java.util.Optional;
@@ -58,7 +59,6 @@ import com.owncloud.android.datamodel.ArbitraryDataProviderImpl;
 import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.files.StreamMediaFileOperation;
-import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder;
 import com.owncloud.android.files.services.FileUploader.FileUploaderBinder;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.lib.common.utils.Log_OC;
@@ -996,11 +996,11 @@ public class FileOperationsHelper {
             }
         }
 
-        // for both files and folders
-        FileDownloaderBinder downloaderBinder = fileActivity.getFileDownloaderBinder();
-        if (downloaderBinder != null && downloaderBinder.isDownloading(currentUser, file)) {
-            downloaderBinder.cancel(currentUser.toPlatformAccount(), file);
+        if (FileDownloadHelper.Companion.instance().isDownloading(currentUser, file)) {
+            List<OCFile> files = fileActivity.getStorageManager().getAllFilesRecursivelyInsideFolder(file);
+            FileDownloadHelper.Companion.instance().cancelPendingOrCurrentDownloads(currentUser, files);
         }
+
         FileUploaderBinder uploaderBinder = fileActivity.getFileUploaderBinder();
         if (uploaderBinder != null && uploaderBinder.isUploading(currentUser, file)) {
             uploaderBinder.cancel(currentUser.toPlatformAccount(), file);

+ 5 - 5
app/src/main/java/com/owncloud/android/ui/preview/FileDownloadFragment.java

@@ -261,8 +261,8 @@ public class FileDownloadFragment extends FileFragment implements OnClickListene
 
 
     public void listenForTransferProgress() {
-        if (mProgressListener != null && !mListening && containerActivity.getFileDownloaderBinder() != null) {
-            containerActivity.getFileDownloaderBinder().addDatatransferProgressListener(mProgressListener, getFile());
+        if (mProgressListener != null && !mListening && containerActivity.getFileDownloadProgressListener() != null) {
+            containerActivity.getFileDownloadProgressListener().addDataTransferProgressListener(mProgressListener, getFile());
             mListening = true;
             setButtonsForTransferring();
         }
@@ -270,9 +270,9 @@ public class FileDownloadFragment extends FileFragment implements OnClickListene
 
 
     public void leaveTransferProgress() {
-        if (mProgressListener != null && containerActivity.getFileDownloaderBinder() != null) {
-            containerActivity.getFileDownloaderBinder()
-                .removeDatatransferProgressListener(mProgressListener, getFile());
+        if (mProgressListener != null && containerActivity.getFileDownloadProgressListener() != null) {
+            containerActivity.getFileDownloadProgressListener()
+                .removeDataTransferProgressListener(mProgressListener, getFile());
             mListening = false;
         }
     }

+ 36 - 36
app/src/main/java/com/owncloud/android/ui/preview/PreviewImageActivity.java

@@ -37,22 +37,25 @@ import android.view.View;
 import com.nextcloud.client.account.User;
 import com.nextcloud.client.di.Injectable;
 import com.nextcloud.client.editimage.EditImageActivity;
+import com.nextcloud.client.files.downloader.FileDownloadHelper;
+import com.nextcloud.client.files.downloader.FileDownloadWorker;
 import com.nextcloud.client.preferences.AppPreferences;
 import com.nextcloud.java.util.Optional;
+import com.nextcloud.model.WorkerState;
+import com.nextcloud.model.WorkerStateLiveData;
 import com.nextcloud.utils.extensions.IntentExtensionsKt;
 import com.owncloud.android.MainApp;
 import com.owncloud.android.R;
 import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.datamodel.VirtualFolderType;
-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.lib.common.operations.OnRemoteOperationListener;
 import com.owncloud.android.lib.common.operations.RemoteOperation;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.operations.DownloadType;
 import com.owncloud.android.operations.RemoveFileOperation;
 import com.owncloud.android.operations.SynchronizeFileOperation;
 import com.owncloud.android.ui.activity.FileActivity;
@@ -99,6 +102,8 @@ public class PreviewImageActivity extends FileActivity implements
     private DownloadFinishReceiver mDownloadFinishReceiver;
     private UploadFinishReceiver mUploadFinishReceiver;
     private View mFullScreenAnchorView;
+    private boolean isDownloadWorkStarted = false;
+
     @Inject AppPreferences preferences;
     @Inject LocalBroadcastManager localBroadcastManager;
 
@@ -146,6 +151,8 @@ public class PreviewImageActivity extends FileActivity implements
         } else {
             mRequestWaitingForBinder = false;
         }
+
+        observeWorkerState();
     }
 
     public void toggleActionBarVisibility(boolean hide) {
@@ -299,6 +306,25 @@ public class PreviewImageActivity extends FileActivity implements
         }
     }
 
+    private void observeWorkerState() {
+       WorkerStateLiveData.Companion.instance().observe(this, state -> {
+            if (state instanceof WorkerState.Download) {
+                Log_OC.d(TAG, "Download worker started");
+                isDownloadWorkStarted = true;
+
+                if (mRequestWaitingForBinder) {
+                    mRequestWaitingForBinder = false;
+                    Log_OC.d(TAG, "Simulating reselection of current page after connection " +
+                        "of download binder");
+                    onPageSelected(mViewPager.getCurrentItem());
+                }
+            } else {
+                Log_OC.d(TAG, "Download worker stopped");
+                isDownloadWorkStarted = false;
+            }
+        });
+    }
+
     @Override
     protected ServiceConnection newTransferenceServiceConnection() {
         return new PreviewImageServiceConnection();
@@ -309,18 +335,7 @@ public class PreviewImageActivity extends FileActivity implements
 
         @Override
         public void onServiceConnected(ComponentName component, IBinder service) {
-
             if (component.equals(new ComponentName(PreviewImageActivity.this,
-                    FileDownloader.class))) {
-                mDownloaderBinder = (FileDownloaderBinder) service;
-                if (mRequestWaitingForBinder) {
-                    mRequestWaitingForBinder = false;
-                    Log_OC.d(TAG, "Simulating reselection of current page after connection " +
-                            "of download binder");
-                    onPageSelected(mViewPager.getCurrentItem());
-                }
-
-            } else if (component.equals(new ComponentName(PreviewImageActivity.this,
                     FileUploader.class))) {
                 Log_OC.d(TAG, "Upload service connected");
                 mUploaderBinder = (FileUploaderBinder) service;
@@ -331,10 +346,6 @@ public class PreviewImageActivity extends FileActivity implements
         @Override
         public void onServiceDisconnected(ComponentName component) {
             if (component.equals(new ComponentName(PreviewImageActivity.this,
-                    FileDownloader.class))) {
-                Log_OC.d(TAG, "Download service suddenly disconnected");
-                mDownloaderBinder = null;
-            } else if (component.equals(new ComponentName(PreviewImageActivity.this,
                     FileUploader.class))) {
                 Log_OC.d(TAG, "Upload service suddenly disconnected");
                 mUploaderBinder = null;
@@ -359,7 +370,7 @@ public class PreviewImageActivity extends FileActivity implements
         super.onResume();
 
         mDownloadFinishReceiver = new DownloadFinishReceiver();
-        IntentFilter downloadIntentFilter = new IntentFilter(FileDownloader.getDownloadFinishMessage());
+        IntentFilter downloadIntentFilter = new IntentFilter(FileDownloadWorker.Companion.getDownloadFinishMessage());
         localBroadcastManager.registerReceiver(mDownloadFinishReceiver, downloadIntentFilter);
 
         mUploadFinishReceiver = new UploadFinishReceiver();
@@ -408,19 +419,8 @@ public class PreviewImageActivity extends FileActivity implements
     }
 
     public void requestForDownload(OCFile file, String downloadBehaviour) {
-        if (mDownloaderBinder == null) {
-            Log_OC.d(TAG, "requestForDownload called without binder to download service");
-
-        } else if (!mDownloaderBinder.isDownloading(getUserAccountManager().getUser(), file)) {
-            final User user = getUser().orElseThrow(RuntimeException::new);
-            Intent i = new Intent(this, FileDownloader.class);
-            i.putExtra(FileDownloader.EXTRA_USER, user);
-            i.putExtra(FileDownloader.EXTRA_FILE, file);
-            if (downloadBehaviour != null) {
-                i.putExtra(OCFileListFragment.DOWNLOAD_BEHAVIOUR, downloadBehaviour);
-            }
-            startService(i);
-        }
+        final User user = getUser().orElseThrow(RuntimeException::new);
+        FileDownloadHelper.Companion.instance().downloadFileIfNotStartedBefore(user, file);
     }
 
     /**
@@ -433,7 +433,7 @@ public class PreviewImageActivity extends FileActivity implements
     public void onPageSelected(int position) {
         mSavedPosition = position;
         mHasSavedPosition = true;
-        if (mDownloaderBinder == null) {
+        if (!isDownloadWorkStarted) {
             mRequestWaitingForBinder = true;
         } else {
             OCFile currentFile = mPreviewImagePagerAdapter.getFileAt(position);
@@ -484,7 +484,7 @@ public class PreviewImageActivity extends FileActivity implements
     }
 
     /**
-     * Class waiting for broadcast events from the {@link FileDownloader} service.
+     * Class waiting for broadcast events from the {@link FileDownloadWorker} service.
      *
      * Updates the UI when a download is started or finished, provided that it is relevant for the
      * folder displayed in the gallery.
@@ -504,12 +504,12 @@ public class PreviewImageActivity extends FileActivity implements
     }
 
     private void previewNewImage(Intent intent) {
-        String accountName = intent.getStringExtra(FileDownloader.ACCOUNT_NAME);
-        String downloadedRemotePath = intent.getStringExtra(FileDownloader.EXTRA_REMOTE_PATH);
+        String accountName = intent.getStringExtra(FileDownloadWorker.EXTRA_ACCOUNT_NAME);
+        String downloadedRemotePath = intent.getStringExtra(FileDownloadWorker.EXTRA_REMOTE_PATH);
         String downloadBehaviour = intent.getStringExtra(OCFileListFragment.DOWNLOAD_BEHAVIOUR);
         if (getAccount().name.equals(accountName) && downloadedRemotePath != null) {
             OCFile file = getStorageManager().getFileByPath(downloadedRemotePath);
-            boolean downloadWasFine = intent.getBooleanExtra(FileDownloader.EXTRA_DOWNLOAD_RESULT, false);
+            boolean downloadWasFine = intent.getBooleanExtra(FileDownloadWorker.EXTRA_DOWNLOAD_RESULT, false);
 
             if (EditImageActivity.OPEN_IMAGE_EDITOR.equals(downloadBehaviour)) {
                 startImageEditor(file);

+ 0 - 1
app/src/main/java/com/owncloud/android/ui/preview/PreviewImagePagerAdapter.java

@@ -19,7 +19,6 @@
  */
 package com.owncloud.android.ui.preview;
 
-import android.content.Intent;
 import android.util.SparseArray;
 import android.view.ViewGroup;
 

+ 13 - 36
app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaActivity.kt

@@ -27,9 +27,7 @@
 package com.owncloud.android.ui.preview
 
 import android.app.Activity
-import android.content.ComponentName
 import android.content.Intent
-import android.content.ServiceConnection
 import android.content.res.Configuration
 import android.graphics.Bitmap
 import android.graphics.BitmapFactory
@@ -39,7 +37,6 @@ import android.net.Uri
 import android.os.AsyncTask
 import android.os.Bundle
 import android.os.Handler
-import android.os.IBinder
 import android.os.Looper
 import android.view.Menu
 import android.view.MenuItem
@@ -65,6 +62,7 @@ import androidx.media3.ui.PlayerView
 import com.nextcloud.client.account.User
 import com.nextcloud.client.account.UserAccountManager
 import com.nextcloud.client.di.Injectable
+import com.nextcloud.client.files.downloader.FileDownloadHelper
 import com.nextcloud.client.jobs.BackgroundJobManager
 import com.nextcloud.client.media.ExoplayerListener
 import com.nextcloud.client.media.NextcloudExoPlayer.createNextcloudExoplayer
@@ -80,13 +78,12 @@ import com.owncloud.android.databinding.ActivityPreviewMediaBinding
 import com.owncloud.android.datamodel.OCFile
 import com.owncloud.android.datamodel.ThumbnailsCacheManager
 import com.owncloud.android.files.StreamMediaFileOperation
-import com.owncloud.android.files.services.FileDownloader
-import com.owncloud.android.files.services.FileDownloader.FileDownloaderBinder
 import com.owncloud.android.lib.common.OwnCloudClient
 import com.owncloud.android.lib.common.operations.OnRemoteOperationListener
 import com.owncloud.android.lib.common.operations.RemoteOperation
 import com.owncloud.android.lib.common.operations.RemoteOperationResult
 import com.owncloud.android.lib.common.utils.Log_OC
+import com.owncloud.android.operations.DownloadType
 import com.owncloud.android.operations.RemoveFileOperation
 import com.owncloud.android.operations.SynchronizeFileOperation
 import com.owncloud.android.ui.activity.FileActivity
@@ -563,33 +560,12 @@ class PreviewMediaActivity :
         }
     }
 
-    override fun newTransferenceServiceConnection(): ServiceConnection {
-        return PreviewMediaServiceConnection()
-    }
-
     private fun onSynchronizeFileOperationFinish(result: RemoteOperationResult<*>?) {
         result?.let {
             invalidateOptionsMenu()
         }
     }
 
-    private inner class PreviewMediaServiceConnection : ServiceConnection {
-        override fun onServiceConnected(componentName: ComponentName?, service: IBinder?) {
-            componentName?.let {
-                if (it == ComponentName(this@PreviewMediaActivity, FileDownloader::class.java)) {
-                    mDownloaderBinder = service as FileDownloaderBinder
-                }
-            }
-        }
-
-        override fun onServiceDisconnected(componentName: ComponentName?) {
-            if (componentName == ComponentName(this@PreviewMediaActivity, FileDownloader::class.java)) {
-                Log_OC.d(PreviewImageActivity.TAG, "Download service suddenly disconnected")
-                mDownloaderBinder = null
-            }
-        }
-    }
-
     override fun downloadFile(file: OCFile?, packageName: String?, activityName: String?) {
         requestForDownload(file, OCFileListFragment.DOWNLOAD_SEND, packageName, activityName)
     }
@@ -600,21 +576,22 @@ class PreviewMediaActivity :
         packageName: String? = null,
         activityName: String? = null
     ) {
-        if (fileDownloaderBinder.isDownloading(user, file)) {
+        if (FileDownloadHelper.instance().isDownloading(user, file)) {
             return
         }
 
-        val intent = Intent(this, FileDownloader::class.java).apply {
-            putExtra(FileDownloader.EXTRA_USER, user)
-            putExtra(FileDownloader.EXTRA_FILE, file)
-            downloadBehavior?.let { behavior ->
-                putExtra(OCFileListFragment.DOWNLOAD_BEHAVIOUR, behavior)
+        user?.let { user ->
+            file?.let { file ->
+                FileDownloadHelper.instance().downloadFile(
+                    user,
+                    file,
+                    downloadBehavior ?: "",
+                    DownloadType.DOWNLOAD,
+                    packageName ?: "",
+                    activityName ?: ""
+                )
             }
-            putExtra(SendShareDialog.PACKAGE_NAME, packageName)
-            putExtra(SendShareDialog.ACTIVITY_NAME, activityName)
         }
-
-        startService(intent)
     }
 
     private fun seeDetails() {

+ 2 - 7
app/src/main/java/com/owncloud/android/ui/preview/PreviewMediaFragment.java

@@ -53,6 +53,7 @@ import android.view.ViewGroup;
 import com.nextcloud.client.account.User;
 import com.nextcloud.client.account.UserAccountManager;
 import com.nextcloud.client.di.Injectable;
+import com.nextcloud.client.files.downloader.FileDownloadHelper;
 import com.nextcloud.client.jobs.BackgroundJobManager;
 import com.nextcloud.client.media.ExoplayerListener;
 import com.nextcloud.client.media.NextcloudExoPlayer;
@@ -66,7 +67,6 @@ import com.owncloud.android.databinding.FragmentPreviewMediaBinding;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.datamodel.ThumbnailsCacheManager;
 import com.owncloud.android.files.StreamMediaFileOperation;
-import com.owncloud.android.files.services.FileDownloader;
 import com.owncloud.android.lib.common.OwnCloudClient;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.lib.common.utils.Log_OC;
@@ -478,12 +478,7 @@ public class PreviewMediaFragment extends FileFragment implements OnTouchListene
                                                                     getView(),
                                                                     backgroundJobManager);
         } else if (itemId == R.id.action_download_file) {
-            if (!containerActivity.getFileDownloaderBinder().isDownloading(user, getFile())) {
-                Intent i = new Intent(requireActivity(), FileDownloader.class);
-                i.putExtra(FileDownloader.EXTRA_USER, user);
-                i.putExtra(FileDownloader.EXTRA_FILE, getFile());
-                requireActivity().startService(i);
-            }
+            FileDownloadHelper.Companion.instance().downloadFileIfNotStartedBefore(user, getFile());
         }
     }
 

+ 5 - 5
app/src/main/java/third_parties/sufficientlysecure/SaveCalendar.java

@@ -42,11 +42,11 @@ import android.view.WindowManager;
 import android.widget.EditText;
 
 import com.nextcloud.client.account.User;
-import com.nextcloud.client.files.downloader.PostUploadAction;
-import com.nextcloud.client.files.downloader.Request;
-import com.nextcloud.client.files.downloader.TransferManagerConnection;
-import com.nextcloud.client.files.downloader.UploadRequest;
-import com.nextcloud.client.files.downloader.UploadTrigger;
+import com.nextcloud.client.files.Request;
+import com.nextcloud.client.files.UploadRequest;
+import com.nextcloud.client.files.transfer.TransferManagerConnection;
+import com.nextcloud.client.files.upload.PostUploadAction;
+import com.nextcloud.client.files.upload.UploadTrigger;
 import com.nextcloud.client.preferences.AppPreferences;
 import com.owncloud.android.R;
 import com.owncloud.android.datamodel.OCFile;

+ 5 - 1
app/src/main/res/values/strings.xml

@@ -168,12 +168,16 @@
     <string name="uploads_view_later_waiting_to_upload">Waiting to upload</string>
     <string name="uploads_view_group_header" translatable="false">%1$s (%2$d)</string>
     <string name="downloader_download_in_progress_ticker">Downloading…</string>
+    <string name="downloader_download_in_progress">%1$d%% %2$s</string>
     <string name="downloader_download_in_progress_content">%1$d%% Downloading %2$s</string>
     <string name="downloader_download_succeeded_ticker">Downloaded</string>
     <string name="downloader_download_succeeded_content">%1$s downloaded</string>
     <string name="downloader_download_failed_ticker">Download failed</string>
     <string name="downloader_download_failed_content">Could not download %1$s</string>
     <string name="downloader_not_downloaded_yet">Not downloaded yet</string>
+    <string name="downloader_file_download_cancelled">Certain files were canceled during the download by user</string>
+    <string name="downloader_file_download_failed">Error occurred while downloading files</string>
+    <string name="downloader_unexpected_error">Unexpected error occurred while downloading files</string>
     <string name="downloader_download_failed_credentials_error">Download failed, log in again</string>
     <string name="common_choose_account">Choose account</string>
     <string name="common_switch_account">Switch account</string>
@@ -636,7 +640,7 @@
     <string name="resharing_is_not_allowed">Resharing is not allowed</string>
 
     <string name="foreground_service_upload">Uploading files…</string>
-    <string name="foreground_service_download">Downloading files…</string>
+    <string name="worker_download">Downloading files…</string>
 
     <string name="prefs_sourcecode">Get source code</string>
     <string name="prefs_license">License</string>

+ 1 - 1
build.gradle

@@ -11,7 +11,7 @@ buildscript {
         androidLibraryVersion = "master-SNAPSHOT"
         mockitoVersion = "4.11.0"
         mockitoKotlinVersion = "4.1.0"
-        mockkVersion = "1.13.3"
+        mockkVersion = "1.13.8"
         espressoVersion = "3.5.1"
         workRuntime = "2.8.1"
         fidoVersion = "4.1.0-patch2"