diff --git a/app/src/androidTest/java/com/nextcloud/client/files/downloader/DownloaderServiceTest.kt b/app/src/androidTest/java/com/nextcloud/client/files/downloader/DownloaderServiceTest.kt deleted file mode 100644 index 86029131b3dc..000000000000 --- a/app/src/androidTest/java/com/nextcloud/client/files/downloader/DownloaderServiceTest.kt +++ /dev/null @@ -1,58 +0,0 @@ -/** - * Nextcloud Android client application - * - * @author Chris Narkiewicz - * Copyright (C) 2020 Chris Narkiewicz - * - * 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 . - */ -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 io.mockk.MockKAnnotations -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import java.util.concurrent.TimeUnit -import java.util.concurrent.TimeoutException - -class DownloaderServiceTest { - - @get:Rule - val service = ServiceTestRule.withTimeout(3, TimeUnit.SECONDS) - - val user = MockUser() - - @Before - fun setUp() { - MockKAnnotations.init(this, relaxed = true) - } - - @Test(expected = TimeoutException::class) - fun cannot_bind_to_service_without_user() { - val intent = FileTransferService.createBindIntent(getApplicationContext(), user) - intent.removeExtra(FileTransferService.EXTRA_USER) - service.bindService(intent) - } - - @Test - fun bind_with_user() { - val intent = FileTransferService.createBindIntent(getApplicationContext(), user) - val binder = service.bindService(intent) - assertTrue(binder is FileTransferService.Binder) - } -} diff --git a/app/src/androidTest/java/com/nextcloud/client/files/downloader/TransferManagerConnectionTest.kt b/app/src/androidTest/java/com/nextcloud/client/files/downloader/TransferManagerConnectionTest.kt index d5bbe707058c..cb082d3f8290 100644 --- a/app/src/androidTest/java/com/nextcloud/client/files/downloader/TransferManagerConnectionTest.kt +++ b/app/src/androidTest/java/com/nextcloud/client/files/downloader/TransferManagerConnectionTest.kt @@ -19,9 +19,9 @@ */ package com.nextcloud.client.files.downloader -import android.content.ComponentName import android.content.Context import com.nextcloud.client.account.MockUser +import com.nextcloud.client.jobs.BackgroundJobManager import com.owncloud.android.datamodel.OCFile import io.mockk.MockKAnnotations import io.mockk.every @@ -54,16 +54,18 @@ class TransferManagerConnectionTest { lateinit var secondStatusListener: (TransferManager.Status) -> Unit @MockK - lateinit var binder: FileTransferService.Binder + lateinit var manager: FileTransferWorker.Manager + + @MockK + lateinit var backgroundJobManager: BackgroundJobManager val file get() = OCFile("/path") - val componentName = ComponentName("", FileTransferService::class.java.simpleName) val user = MockUser() @Before fun setUp() { MockKAnnotations.init(this, relaxed = true) - connection = TransferManagerConnection(context, user) + connection = TransferManagerConnection(backgroundJobManager, user) } @Test @@ -76,12 +78,12 @@ class TransferManagerConnectionTest { // WHEN // service is bound - connection.onServiceConnected(componentName, binder) + connection.onBound() // THEN // all listeners are passed to the service val listeners = mutableListOf<(Transfer) -> Unit>() - verify { binder.registerTransferListener(capture(listeners)) } + verify { manager.registerTransferListener(capture(listeners)) } assertEquals(listOf(firstDownloadListener, secondDownloadListener), listeners) } @@ -89,7 +91,7 @@ class TransferManagerConnectionTest { fun listeners_are_set_immediately_when_connected() { // GIVEN // service is bound - connection.onServiceConnected(componentName, binder) + connection.onBound() // WHEN // listeners are added @@ -97,7 +99,7 @@ class TransferManagerConnectionTest { // THEN // listener is forwarded to service - verify { binder.registerTransferListener(firstDownloadListener) } + verify { manager.registerTransferListener(firstDownloadListener) } } @Test @@ -105,18 +107,18 @@ class TransferManagerConnectionTest { // GIVEN // service is bound // service has some listeners - connection.onServiceConnected(componentName, binder) + connection.onBound() connection.registerTransferListener(firstDownloadListener) connection.registerTransferListener(secondDownloadListener) // WHEN // service unbound - connection.unbind() + connection.onUnbind() // THEN // listeners removed from service - verify { binder.removeTransferListener(firstDownloadListener) } - verify { binder.removeTransferListener(secondDownloadListener) } + verify { manager.removeTransferListener(firstDownloadListener) } + verify { manager.removeTransferListener(secondDownloadListener) } } @Test @@ -136,12 +138,12 @@ class TransferManagerConnectionTest { connection.enqueue(request2) val download2 = Transfer(request2.uuid, TransferState.RUNNING, 50, request2.file, request1) - every { binder.getTransfer(request1.uuid) } returns download1 - every { binder.getTransfer(request2.uuid) } returns download2 + every { manager.getTransfer(request1.uuid) } returns download1 + every { manager.getTransfer(request2.uuid) } returns download2 // WHEN // service is bound - connection.onServiceConnected(componentName, binder) + connection.onBound() // THEN // listeners receive current download state for pending downloads @@ -160,13 +162,13 @@ class TransferManagerConnectionTest { // not bound // has status listeners val mockStatus: TransferManager.Status = mockk() - every { binder.status } returns mockStatus + every { manager.status } returns mockStatus connection.registerStatusListener(firstStatusListener) connection.registerStatusListener(secondStatusListener) // WHEN // service is bound - connection.onServiceConnected(componentName, binder) + connection.onBound() // THEN // downloader status is delivered @@ -182,11 +184,11 @@ class TransferManagerConnectionTest { // WHEN // service is bound - connection.onServiceConnected(componentName, binder) + connection.onBound() // THEN // downloader status is not requested - verify(exactly = 0) { binder.status } + verify(exactly = 0) { manager.status } } @Test @@ -194,7 +196,7 @@ class TransferManagerConnectionTest { // GIVEN // downloader is running // connection not bound - every { binder.isRunning } returns true + every { manager.isRunning } returns true // THEN // not running @@ -205,8 +207,8 @@ class TransferManagerConnectionTest { fun is_running_from_binder_if_connected() { // GIVEN // service bound - every { binder.isRunning } returns true - connection.onServiceConnected(componentName, binder) + every { manager.isRunning } returns true + connection.onBound() // WHEN // is runnign flag accessed @@ -215,7 +217,7 @@ class TransferManagerConnectionTest { // THEN // call delegated to binder assertTrue(isRunning) - verify(exactly = 1) { binder.isRunning } + verify(exactly = 1) { manager.isRunning } } @Test @@ -227,11 +229,11 @@ class TransferManagerConnectionTest { connection.enqueue(request) val download = Transfer(request.uuid, TransferState.RUNNING, 50, request.file, request) connection.registerTransferListener(firstDownloadListener) - every { binder.getTransfer(request.uuid) } returns download + every { manager.getTransfer(request.uuid) } returns download // WHEN // service is bound - connection.onServiceConnected(componentName, binder) + connection.onBound() // THEN // missed updates not redelivered diff --git a/app/src/androidTest/java/com/nextcloud/sso/InputStreamBinderTest.kt b/app/src/androidTest/java/com/nextcloud/sso/InputStreamManagerTest.kt similarity index 98% rename from app/src/androidTest/java/com/nextcloud/sso/InputStreamBinderTest.kt rename to app/src/androidTest/java/com/nextcloud/sso/InputStreamManagerTest.kt index a800c8c7c965..46215182b70e 100644 --- a/app/src/androidTest/java/com/nextcloud/sso/InputStreamBinderTest.kt +++ b/app/src/androidTest/java/com/nextcloud/sso/InputStreamManagerTest.kt @@ -26,7 +26,7 @@ import com.nextcloud.android.sso.QueryParam import org.junit.Assert.assertEquals import org.junit.Test -class InputStreamBinderTest { +class InputStreamManagerTest { @Test fun convertMapToNVP() { val source = mutableMapOf() diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f9cc7c311c33..3a829288bce7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -386,10 +386,6 @@ android:name=".files.services.FileDownloader" android:foregroundServiceType="dataSync" android:exported="false" /> - get() { return defaultPreferences.all diff --git a/app/src/main/java/com/nextcloud/client/etm/pages/EtmFileTransferFragment.kt b/app/src/main/java/com/nextcloud/client/etm/pages/EtmFileTransferFragment.kt index 989035917d81..0b911fec6a14 100644 --- a/app/src/main/java/com/nextcloud/client/etm/pages/EtmFileTransferFragment.kt +++ b/app/src/main/java/com/nextcloud/client/etm/pages/EtmFileTransferFragment.kt @@ -117,13 +117,13 @@ class EtmFileTransferFragment : EtmBaseFragment() { override fun onResume() { super.onResume() - vm.transferManagerConnection.bind() + vm.transferManagerConnection.onBound() vm.transferManagerConnection.registerStatusListener(this::onDownloaderStatusChanged) } override fun onPause() { super.onPause() - vm.transferManagerConnection.unbind() + vm.transferManagerConnection.onUnbind() } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { diff --git a/app/src/main/java/com/nextcloud/client/files/downloader/FileTransferService.kt b/app/src/main/java/com/nextcloud/client/files/downloader/FileTransferService.kt deleted file mode 100644 index c9b2f7c300b6..000000000000 --- a/app/src/main/java/com/nextcloud/client/files/downloader/FileTransferService.kt +++ /dev/null @@ -1,186 +0,0 @@ -/* - * Nextcloud Android client application - * - * @author Chris Narkiewicz - * Copyright (C) 2021 Chris Narkiewicz - * - * 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 . - */ -package com.nextcloud.client.files.downloader - -import android.app.Service -import android.content.Context -import android.content.Intent -import android.os.IBinder -import com.nextcloud.client.account.User -import com.nextcloud.client.core.AsyncRunner -import com.nextcloud.client.core.LocalBinder -import com.nextcloud.client.device.PowerManagementService -import com.nextcloud.client.logger.Logger -import com.nextcloud.client.network.ClientFactory -import com.nextcloud.client.network.ConnectivityService -import com.nextcloud.client.notifications.AppNotificationManager -import com.nextcloud.utils.ForegroundServiceHelper -import com.nextcloud.utils.extensions.getParcelableArgument -import com.owncloud.android.datamodel.FileDataStorageManager -import com.owncloud.android.datamodel.ForegroundServiceType -import com.owncloud.android.datamodel.UploadsStorageManager -import dagger.android.AndroidInjection -import javax.inject.Inject -import javax.inject.Named - -class FileTransferService : Service() { - - companion object { - const val TAG = "DownloaderService" - const val ACTION_TRANSFER = "transfer" - const val EXTRA_REQUEST = "request" - const val EXTRA_USER = "user" - - fun createBindIntent(context: Context, user: User): Intent { - return Intent(context, FileTransferService::class.java).apply { - putExtra(EXTRA_USER, user) - } - } - - fun createTransferRequestIntent(context: Context, request: Request): Intent { - return Intent(context, FileTransferService::class.java).apply { - action = ACTION_TRANSFER - putExtra(EXTRA_REQUEST, request) - } - } - } - - /** - * Binder forwards [TransferManager] API calls to selected instance of downloader. - */ - class Binder( - downloader: TransferManagerImpl, - service: FileTransferService - ) : LocalBinder(service), - TransferManager by downloader - - @Inject - lateinit var notificationsManager: AppNotificationManager - - @Inject - lateinit var clientFactory: ClientFactory - - @Inject - @Named("io") - lateinit var runner: AsyncRunner - - @Inject - lateinit var logger: Logger - - @Inject - lateinit var uploadsStorageManager: UploadsStorageManager - - @Inject - lateinit var connectivityService: ConnectivityService - - @Inject - lateinit var powerManagementService: PowerManagementService - - @Inject - lateinit var fileDataStorageManager: FileDataStorageManager - - val isRunning: Boolean get() = downloaders.any { it.value.isRunning } - - private val downloaders: MutableMap = mutableMapOf() - - override fun onCreate() { - AndroidInjection.inject(this) - } - - override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { - if (intent.action != ACTION_TRANSFER) { - return START_NOT_STICKY - } - - if (!isRunning) { - ForegroundServiceHelper.startService( - this, - AppNotificationManager.TRANSFER_NOTIFICATION_ID, - notificationsManager.buildDownloadServiceForegroundNotification(), - ForegroundServiceType.DataSync - ) - } - - val request: Request = intent.getParcelableArgument(EXTRA_REQUEST, Request::class.java)!! - val transferManager = getTransferManager(request.user) - transferManager.enqueue(request) - - logger.d(TAG, "Enqueued new transfer: ${request.uuid} ${request.file.remotePath}") - - return START_NOT_STICKY - } - - override fun onBind(intent: Intent?): IBinder? { - val user = intent?.getParcelableArgument(EXTRA_USER, User::class.java) ?: return null - return Binder(getTransferManager(user), this) - } - - private fun onTransferUpdate(transfer: Transfer) { - if (!isRunning) { - logger.d(TAG, "All downloads completed") - notificationsManager.cancelTransferNotification() - stopForeground(true) - stopSelf() - } else if (transfer.direction == Direction.DOWNLOAD) { - notificationsManager.postDownloadTransferProgress( - fileOwner = transfer.request.user, - file = transfer.request.file, - progress = transfer.progress, - allowPreview = !transfer.request.test - ) - } else if (transfer.direction == Direction.UPLOAD) { - notificationsManager.postUploadTransferProgress( - fileOwner = transfer.request.user, - file = transfer.request.file, - progress = transfer.progress - ) - } - } - - override fun onDestroy() { - super.onDestroy() - logger.d(TAG, "Stopping downloader service") - } - - private fun getTransferManager(user: User): TransferManagerImpl { - val existingDownloader = downloaders[user.accountName] - return if (existingDownloader != null) { - existingDownloader - } else { - val downloadTaskFactory = DownloadTask.Factory( - applicationContext, - { clientFactory.create(user) }, - contentResolver - ) - val uploadTaskFactory = UploadTask.Factory( - applicationContext, - uploadsStorageManager, - connectivityService, - powerManagementService, - { clientFactory.create(user) }, - fileDataStorageManager - ) - val newDownloader = TransferManagerImpl(runner, downloadTaskFactory, uploadTaskFactory) - newDownloader.registerTransferListener(this::onTransferUpdate) - downloaders[user.accountName] = newDownloader - newDownloader - } - } -} diff --git a/app/src/main/java/com/nextcloud/client/files/downloader/FileTransferWorker.kt b/app/src/main/java/com/nextcloud/client/files/downloader/FileTransferWorker.kt new file mode 100644 index 000000000000..2976354c7f38 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/files/downloader/FileTransferWorker.kt @@ -0,0 +1,134 @@ +/* + * Nextcloud Android client application + * + * @author Chris Narkiewicz + * Copyright (C) 2021 Chris Narkiewicz + * + * 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 . + */ +package com.nextcloud.client.files.downloader + +import android.annotation.SuppressLint +import android.content.Context +import androidx.work.Worker +import androidx.work.WorkerParameters +import com.google.gson.Gson +import com.nextcloud.client.account.User +import com.nextcloud.client.core.AsyncRunner +import com.nextcloud.client.device.PowerManagementService +import com.nextcloud.client.logger.Logger +import com.nextcloud.client.network.ClientFactory +import com.nextcloud.client.network.ConnectivityService +import com.nextcloud.client.notifications.AppNotificationManager +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.UploadsStorageManager + +@Suppress("LongParameterList") +class FileTransferWorker( + private val notificationsManager: AppNotificationManager, + private val clientFactory: ClientFactory, + private val runner: AsyncRunner, + private val logger: Logger, + private val uploadsStorageManager: UploadsStorageManager, + private val connectivityService: ConnectivityService, + private val powerManagementService: PowerManagementService, + private val fileDataStorageManager: FileDataStorageManager, + private val context: Context, + params: WorkerParameters +) : Worker(context, params) { + + private val gson = Gson() + + companion object { + const val TAG = "DownloaderService" + const val EXTRA_REQUEST = "request" + + var manager: Manager? = null + } + + @Suppress("TooGenericExceptionCaught") + override fun doWork(): Result { + return try { + val request = gson.fromJson(inputData.keyValueMap[EXTRA_REQUEST] as String, Request::class.java) + + if (!isRunning) { + notificationsManager.buildDownloadServiceForegroundNotification() + } + + val transferManager = getTransferManager(request.user) + transferManager.enqueue(request) + manager = Manager(transferManager) + + logger.d(TAG, "Enqueued new transfer: ${request.uuid} ${request.file.remotePath}") + + Result.success() + } catch (t: Throwable) { + logger.d(TAG, "Error caught at FileTransferWorker: ${t.localizedMessage}") + Result.failure() + } + } + + class Manager(downloader: TransferManagerImpl) : TransferManager by downloader + + val isRunning: Boolean get() = downloader.any { it.value.isRunning } + + private val downloader: MutableMap = mutableMapOf() + + @SuppressLint("RestrictedApi") + private fun onTransferUpdate(transfer: Transfer) { + if (!isRunning) { + logger.d(TAG, "All downloads completed") + notificationsManager.cancelTransferNotification() + stop() + } else if (transfer.direction == Direction.DOWNLOAD) { + notificationsManager.postDownloadTransferProgress( + fileOwner = transfer.request.user, + file = transfer.request.file, + progress = transfer.progress, + allowPreview = !transfer.request.test + ) + } else if (transfer.direction == Direction.UPLOAD) { + notificationsManager.postUploadTransferProgress( + fileOwner = transfer.request.user, + file = transfer.request.file, + progress = transfer.progress + ) + } + } + + private fun getTransferManager(user: User): TransferManagerImpl { + val existingDownloader = downloader[user.accountName] + return if (existingDownloader != null) { + existingDownloader + } else { + val downloadTaskFactory = DownloadTask.Factory( + applicationContext, + { clientFactory.create(user) }, + context.contentResolver + ) + val uploadTaskFactory = UploadTask.Factory( + applicationContext, + uploadsStorageManager, + connectivityService, + powerManagementService, + { clientFactory.create(user) }, + fileDataStorageManager + ) + val newDownloader = TransferManagerImpl(runner, downloadTaskFactory, uploadTaskFactory) + newDownloader.registerTransferListener(this::onTransferUpdate) + downloader[user.accountName] = newDownloader + newDownloader + } + } +} diff --git a/app/src/main/java/com/nextcloud/client/files/downloader/TransferManagerConnection.kt b/app/src/main/java/com/nextcloud/client/files/downloader/TransferManagerConnection.kt index be2ea7fe45ef..fffaf7d6edaa 100644 --- a/app/src/main/java/com/nextcloud/client/files/downloader/TransferManagerConnection.kt +++ b/app/src/main/java/com/nextcloud/client/files/downloader/TransferManagerConnection.kt @@ -19,74 +19,65 @@ */ package com.nextcloud.client.files.downloader -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.jobs.BackgroundJobManager import com.owncloud.android.datamodel.OCFile import java.util.UUID class TransferManagerConnection( - context: Context, + private val backgroundJobManager: BackgroundJobManager, val user: User -) : LocalConnection(context), TransferManager { +) : TransferManager { private var transferListeners: MutableSet<(Transfer) -> Unit> = mutableSetOf() private var statusListeners: MutableSet<(TransferManager.Status) -> Unit> = mutableSetOf() - private var binder: FileTransferService.Binder? = null + private var manager: FileTransferWorker.Manager? = null private val transfersRequiringStatusRedelivery: MutableSet = mutableSetOf() override val isRunning: Boolean - get() = binder?.isRunning ?: false + get() = manager?.isRunning ?: false override val status: TransferManager.Status - get() = binder?.status ?: TransferManager.Status.EMPTY + get() = manager?.status ?: TransferManager.Status.EMPTY - override fun getTransfer(uuid: UUID): Transfer? = binder?.getTransfer(uuid) + override fun getTransfer(uuid: UUID): Transfer? = manager?.getTransfer(uuid) - override fun getTransfer(file: OCFile): Transfer? = binder?.getTransfer(file) + override fun getTransfer(file: OCFile): Transfer? = manager?.getTransfer(file) override fun enqueue(request: Request) { - val intent = FileTransferService.createTransferRequestIntent(context, request) - context.startService(intent) - if (!isConnected && transferListeners.size > 0) { - transfersRequiringStatusRedelivery.add(request.uuid) + if (transferListeners.size > 0) { + backgroundJobManager.startFileTransfer(request) } } override fun registerTransferListener(listener: (Transfer) -> Unit) { transferListeners.add(listener) - binder?.registerTransferListener(listener) + manager?.registerTransferListener(listener) } override fun removeTransferListener(listener: (Transfer) -> Unit) { transferListeners.remove(listener) - binder?.removeTransferListener(listener) + manager?.removeTransferListener(listener) } override fun registerStatusListener(listener: (TransferManager.Status) -> Unit) { statusListeners.add(listener) - binder?.registerStatusListener(listener) + manager?.registerStatusListener(listener) } override fun removeStatusListener(listener: (TransferManager.Status) -> Unit) { statusListeners.remove(listener) - binder?.removeStatusListener(listener) + manager?.removeStatusListener(listener) } - override fun createBindIntent(): Intent { - return FileTransferService.createBindIntent(context, user) - } + fun onBound() { + this.manager = FileTransferWorker.manager - override fun onBound(binder: IBinder) { - super.onBound(binder) - this.binder = binder as FileTransferService.Binder transferListeners.forEach { listener -> - binder.registerTransferListener(listener) + manager?.registerTransferListener(listener) } statusListeners.forEach { listener -> - binder.registerStatusListener(listener) + manager?.registerStatusListener(listener) } deliverMissedUpdates() } @@ -101,7 +92,7 @@ class TransferManagerConnection( */ private fun deliverMissedUpdates() { val transferUpdates = transfersRequiringStatusRedelivery.mapNotNull { uuid -> - binder?.getTransfer(uuid) + manager?.getTransfer(uuid) } transferListeners.forEach { listener -> transferUpdates.forEach { update -> @@ -111,15 +102,14 @@ class TransferManagerConnection( transfersRequiringStatusRedelivery.clear() if (statusListeners.isNotEmpty()) { - binder?.status?.let { status -> + manager?.status?.let { status -> statusListeners.forEach { it.invoke(status) } } } } - override fun onUnbind() { - super.onUnbind() - transferListeners.forEach { binder?.removeTransferListener(it) } - statusListeners.forEach { binder?.removeStatusListener(it) } + fun onUnbind() { + transferListeners.forEach { manager?.removeTransferListener(it) } + statusListeners.forEach { manager?.removeStatusListener(it) } } } diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt index 1fb333f5ce37..5f3e05475f80 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt @@ -29,16 +29,21 @@ import androidx.work.ListenableWorker import androidx.work.WorkerFactory import androidx.work.WorkerParameters import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.client.core.AsyncRunner import com.nextcloud.client.core.Clock 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.FileTransferWorker import com.nextcloud.client.integrations.deck.DeckApi import com.nextcloud.client.logger.Logger +import com.nextcloud.client.network.ClientFactory import com.nextcloud.client.network.ConnectivityService +import com.nextcloud.client.notifications.AppNotificationManager import com.nextcloud.client.preferences.AppPreferences import com.owncloud.android.datamodel.ArbitraryDataProvider +import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.SyncedFolderProvider import com.owncloud.android.datamodel.UploadsStorageManager import com.owncloud.android.utils.theme.ViewThemeUtils @@ -51,8 +56,12 @@ import javax.inject.Provider * * This class is doing too many things and should be split up into smaller factories. */ -@Suppress("LongParameterList") // satisfied by DI +@Suppress("LongParameterList", "TooManyFunctions") // satisfied by DI class BackgroundJobFactory @Inject constructor( + private val appNotificationManager: AppNotificationManager, + private val clientFactory: ClientFactory, + private val fileDataStorageManager: FileDataStorageManager, + private val runner: AsyncRunner, private val logger: Logger, private val preferences: AppPreferences, private val contentResolver: ContentResolver, @@ -100,6 +109,7 @@ class BackgroundJobFactory @Inject constructor( AccountRemovalWork::class -> createAccountRemovalWork(context, workerParameters) CalendarBackupWork::class -> createCalendarBackupWork(context, workerParameters) CalendarImportWork::class -> createCalendarImportWork(context, workerParameters) + FileTransferWorker::class -> createFileTransferWorker(context, workerParameters) FilesExportWork::class -> createFilesExportWork(context, workerParameters) FilesUploadWorker::class -> createFilesUploadWorker(context, workerParameters) GeneratePdfFromImagesWork::class -> createPDFGenerateWork(context, workerParameters) @@ -142,6 +152,7 @@ class BackgroundJobFactory @Inject constructor( resources, arbitraryDataProvider, contentResolver, + backgroundJobManager.get(), accountManager ) } @@ -161,7 +172,8 @@ class BackgroundJobFactory @Inject constructor( params, contentResolver, accountManager, - preferences + preferences, + backgroundJobManager.get() ) } @@ -237,6 +249,21 @@ class BackgroundJobFactory @Inject constructor( ) } + private fun createFileTransferWorker(context: Context, params: WorkerParameters): FileTransferWorker { + return FileTransferWorker( + appNotificationManager, + clientFactory, + runner, + logger, + uploadsStorageManager, + connectivityService, + powerManagementService, + fileDataStorageManager, + context, + params + ) + } + private fun createFilesUploadWorker(context: Context, params: WorkerParameters): FilesUploadWorker { return FilesUploadWorker( uploadsStorageManager, diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt index cc8bc6a52f69..faa0443d7335 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManager.kt @@ -21,6 +21,7 @@ package com.nextcloud.client.jobs import androidx.lifecycle.LiveData import com.nextcloud.client.account.User +import com.nextcloud.client.files.downloader.Request import com.owncloud.android.datamodel.OCFile /** @@ -149,4 +150,6 @@ interface BackgroundJobManager { fun cancelAllJobs() fun schedulePeriodicHealthStatus() fun startHealthStatus() + + fun startFileTransfer(request: Request) } diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt index 7e954a0b29db..3f8178167779 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt @@ -34,9 +34,12 @@ import androidx.work.PeriodicWorkRequest import androidx.work.WorkInfo import androidx.work.WorkManager import androidx.work.workDataOf +import com.google.gson.Gson import com.nextcloud.client.account.User import com.nextcloud.client.core.Clock import com.nextcloud.client.documentscan.GeneratePdfFromImagesWork +import com.nextcloud.client.files.downloader.FileTransferWorker +import com.nextcloud.client.files.downloader.Request import com.owncloud.android.datamodel.OCFile import java.util.Date import java.util.UUID @@ -76,6 +79,7 @@ internal class BackgroundJobManagerImpl( const val JOB_PERIODIC_OFFLINE_SYNC = "periodic_offline_sync" const val JOB_PERIODIC_MEDIA_FOLDER_DETECTION = "periodic_media_folder_detection" const val JOB_IMMEDIATE_MEDIA_FOLDER_DETECTION = "immediate_media_folder_detection" + const val JOB_FILES_TRANSFER = "files_transfer" const val JOB_NOTIFICATION = "notification" const val JOB_ACCOUNT_REMOVAL = "account_removal" const val JOB_FILES_UPLOAD = "files_upload" @@ -467,6 +471,20 @@ internal class BackgroundJobManagerImpl( workManager.cancelJob(JOB_FILES_UPLOAD, user) } + override fun startFileTransfer(request: Request) { + val gson = Gson() + + val data = workDataOf( + FileTransferWorker.EXTRA_REQUEST to gson.toJson(request) + ) + + val requestData = oneTimeRequestBuilder(FileTransferWorker::class, JOB_FILES_TRANSFER) + .setInputData(data) + .build() + + workManager.enqueueUniqueWork(JOB_FILES_TRANSFER + request.uuid, ExistingWorkPolicy.REPLACE, requestData) + } + override fun startPdfGenerateAndUploadWork( user: User, uploadFolder: String, diff --git a/app/src/main/java/com/nextcloud/client/jobs/CalendarBackupWork.kt b/app/src/main/java/com/nextcloud/client/jobs/CalendarBackupWork.kt index 46f019a7eae3..e7463925c2e5 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/CalendarBackupWork.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/CalendarBackupWork.kt @@ -34,12 +34,14 @@ import third_parties.sufficientlysecure.AndroidCalendar import third_parties.sufficientlysecure.SaveCalendar import java.util.Calendar +@Suppress("LongParameterList") class CalendarBackupWork( appContext: Context, params: WorkerParameters, private val contentResolver: ContentResolver, private val accountManager: UserAccountManager, - private val preferences: AppPreferences + private val preferences: AppPreferences, + private val backgroundJobManager: BackgroundJobManager ) : Worker(appContext, params) { companion object { @@ -68,7 +70,8 @@ class CalendarBackupWork( applicationContext, calendar, preferences, - optionalUser.get() + optionalUser.get(), + backgroundJobManager ).start() } diff --git a/app/src/main/java/com/nextcloud/client/jobs/ContactsBackupWork.kt b/app/src/main/java/com/nextcloud/client/jobs/ContactsBackupWork.kt index e3f7b708b303..ddde6aaa9418 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/ContactsBackupWork.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/ContactsBackupWork.kt @@ -64,6 +64,7 @@ class ContactsBackupWork( private val resources: Resources, private val arbitraryDataProvider: ArbitraryDataProvider, private val contentResolver: ContentResolver, + private val backgroundJobManager: BackgroundJobManager, private val accountManager: UserAccountManager ) : Worker(appContext, params) { @@ -172,7 +173,7 @@ class ContactsBackupWork( .setRequireCharging(false) .build() - val connection = TransferManagerConnection(applicationContext, user) + val connection = TransferManagerConnection(backgroundJobManager, user) connection.enqueue(request) } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupListFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupListFragment.java index 5e1a38e38951..a20441d1c117 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupListFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/contactsbackup/BackupListFragment.java @@ -206,9 +206,9 @@ public View onCreateView(@NonNull final LayoutInflater inflater, ViewGroup conta } User user = BundleExtensionsKt.getParcelableArgument(getArguments(), USER, User.class); - fileDownloader = new TransferManagerConnection(getActivity(), user); + fileDownloader = new TransferManagerConnection(backgroundJobManager, user); fileDownloader.registerTransferListener(this::onDownloadUpdate); - fileDownloader.bind(); + fileDownloader.onBound(); for (OCFile file : ocFiles) { if (!file.isDown()) { @@ -258,7 +258,7 @@ public View onCreateView(@NonNull final LayoutInflater inflater, ViewGroup conta public void onDetach() { super.onDetach(); if (fileDownloader != null) { - fileDownloader.unbind(); + fileDownloader.onUnbind(); } } diff --git a/app/src/main/java/third_parties/sufficientlysecure/SaveCalendar.java b/app/src/main/java/third_parties/sufficientlysecure/SaveCalendar.java index fcdf8ffc960a..1f646bad4fe6 100644 --- a/app/src/main/java/third_parties/sufficientlysecure/SaveCalendar.java +++ b/app/src/main/java/third_parties/sufficientlysecure/SaveCalendar.java @@ -47,6 +47,7 @@ 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.jobs.BackgroundJobManager; import com.nextcloud.client.preferences.AppPreferences; import com.owncloud.android.R; import com.owncloud.android.datamodel.OCFile; @@ -112,6 +113,7 @@ public class SaveCalendar { private final Context activity; private final AndroidCalendar selectedCal; private final AppPreferences preferences; + private final BackgroundJobManager backgroundJobManager; private final User user; // UID generation @@ -134,11 +136,12 @@ public class SaveCalendar { Reminders.MINUTES, Reminders.METHOD }; - public SaveCalendar(Context activity, AndroidCalendar calendar, AppPreferences preferences, User user) { + public SaveCalendar(Context activity, AndroidCalendar calendar, AppPreferences preferences, User user, BackgroundJobManager backgroundJobManager) { this.activity = activity; // TODO rename this.selectedCal = calendar; this.preferences = preferences; this.user = user; + this.backgroundJobManager = backgroundJobManager; } public void start() throws Exception { @@ -618,7 +621,7 @@ private void upload(File file) { .setRequireCharging(false) .build(); - TransferManagerConnection connection = new TransferManagerConnection(activity, user); + TransferManagerConnection connection = new TransferManagerConnection(backgroundJobManager, user); connection.enqueue(request); } } diff --git a/app/src/test/java/com/nextcloud/client/jobs/BackgroundJobFactoryTest.kt b/app/src/test/java/com/nextcloud/client/jobs/BackgroundJobFactoryTest.kt index 3bc9c77d665d..883ebed0161f 100644 --- a/app/src/test/java/com/nextcloud/client/jobs/BackgroundJobFactoryTest.kt +++ b/app/src/test/java/com/nextcloud/client/jobs/BackgroundJobFactoryTest.kt @@ -27,15 +27,19 @@ import android.os.Build import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.work.WorkerParameters import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.client.core.AsyncRunner import com.nextcloud.client.core.Clock import com.nextcloud.client.device.DeviceInfo import com.nextcloud.client.device.PowerManagementService import com.nextcloud.client.documentscan.GeneratePDFUseCase import com.nextcloud.client.integrations.deck.DeckApi import com.nextcloud.client.logger.Logger +import com.nextcloud.client.network.ClientFactory import com.nextcloud.client.network.ConnectivityService +import com.nextcloud.client.notifications.AppNotificationManager import com.nextcloud.client.preferences.AppPreferences import com.owncloud.android.datamodel.ArbitraryDataProvider +import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.SyncedFolderProvider import com.owncloud.android.datamodel.UploadsStorageManager import com.owncloud.android.utils.theme.ViewThemeUtils @@ -112,12 +116,28 @@ class BackgroundJobFactoryTest { @Mock private lateinit var syncedFolderProvider: SyncedFolderProvider + @Mock + private lateinit var appNotificationManager: AppNotificationManager + + @Mock + private lateinit var clientFactory: ClientFactory + + @Mock + private lateinit var fileDataStorageManager: FileDataStorageManager + + @Mock + private lateinit var asyncRunner: AsyncRunner + private lateinit var factory: BackgroundJobFactory @Before fun setUp() { MockitoAnnotations.initMocks(this) factory = BackgroundJobFactory( + appNotificationManager, + clientFactory, + fileDataStorageManager, + asyncRunner, logger, preferences, contentResolver,