New upload manager

Signed-off-by: Chris Narkiewicz <hello@ezaquarii.com>
This commit is contained in:
Chris Narkiewicz 2021-01-20 04:14:29 +00:00 committed by Andy Scherzinger
parent be29f9aa9a
commit ebf63f064b
No known key found for this signature in database
GPG key ID: 6CADC7E3523C308B
51 changed files with 913 additions and 372 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -29,6 +29,7 @@ import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.datamodel.UploadsStorageManager;
import com.owncloud.android.db.OCUpload;
import com.owncloud.android.files.services.FileUploader;
import com.owncloud.android.files.services.NameCollisionPolicy;
import com.owncloud.android.lib.common.OwnCloudClient;
import com.owncloud.android.lib.common.OwnCloudClientFactory;
import com.owncloud.android.lib.common.accounts.AccountUtils;
@ -360,7 +361,7 @@ public abstract class AbstractIT {
user,
null,
ocUpload,
FileUploader.NameCollisionPolicy.DEFAULT,
NameCollisionPolicy.DEFAULT,
FileUploader.LOCAL_BEHAVIOUR_COPY,
targetContext,
false,

View file

@ -20,6 +20,7 @@ import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.datamodel.UploadsStorageManager;
import com.owncloud.android.db.OCUpload;
import com.owncloud.android.files.services.FileUploader;
import com.owncloud.android.files.services.NameCollisionPolicy;
import com.owncloud.android.lib.common.OwnCloudClient;
import com.owncloud.android.lib.common.OwnCloudClientFactory;
import com.owncloud.android.lib.common.accounts.AccountUtils;
@ -208,7 +209,7 @@ public abstract class AbstractOnServerIT extends AbstractIT {
user,
null,
ocUpload,
FileUploader.NameCollisionPolicy.DEFAULT,
NameCollisionPolicy.DEFAULT,
localBehaviour,
targetContext,
false,

View file

@ -30,6 +30,7 @@ import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.datamodel.UploadsStorageManager;
import com.owncloud.android.db.OCUpload;
import com.owncloud.android.files.services.FileUploader;
import com.owncloud.android.files.services.NameCollisionPolicy;
import com.owncloud.android.lib.common.operations.RemoteOperationResult;
import com.owncloud.android.operations.RefreshFolderOperation;
import com.owncloud.android.operations.RemoveFileOperation;
@ -229,7 +230,7 @@ public class UploadIT extends AbstractOnServerIT {
user,
null,
ocUpload,
FileUploader.NameCollisionPolicy.DEFAULT,
NameCollisionPolicy.DEFAULT,
FileUploader.LOCAL_BEHAVIOUR_COPY,
targetContext,
false,
@ -276,7 +277,7 @@ public class UploadIT extends AbstractOnServerIT {
user,
null,
ocUpload,
FileUploader.NameCollisionPolicy.DEFAULT,
NameCollisionPolicy.DEFAULT,
FileUploader.LOCAL_BEHAVIOUR_COPY,
targetContext,
false,
@ -315,7 +316,7 @@ public class UploadIT extends AbstractOnServerIT {
user,
null,
ocUpload,
FileUploader.NameCollisionPolicy.DEFAULT,
NameCollisionPolicy.DEFAULT,
FileUploader.LOCAL_BEHAVIOUR_COPY,
targetContext,
true,
@ -344,7 +345,7 @@ public class UploadIT extends AbstractOnServerIT {
user,
null,
ocUpload,
FileUploader.NameCollisionPolicy.DEFAULT,
NameCollisionPolicy.DEFAULT,
FileUploader.LOCAL_BEHAVIOUR_COPY,
targetContext,
true,
@ -392,7 +393,7 @@ public class UploadIT extends AbstractOnServerIT {
user,
null,
ocUpload,
FileUploader.NameCollisionPolicy.DEFAULT,
NameCollisionPolicy.DEFAULT,
FileUploader.LOCAL_BEHAVIOUR_COPY,
targetContext,
true,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,29 @@
/*
* Nextcloud Android client application
*
* @author Chris Narkiewicz
* Copyright (C) 2021 Chris Narkiewicz <hello@ezaquarii.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.client.files.downloader
import com.owncloud.android.files.services.FileUploader
enum class PostUploadAction(val value: Int) {
NONE(FileUploader.LOCAL_BEHAVIOUR_FORGET),
COPY_TO_APP(FileUploader.LOCAL_BEHAVIOUR_COPY),
MOVE_TO_APP(FileUploader.LOCAL_BEHAVIOUR_MOVE),
DELETE_SOURCE(FileUploader.LOCAL_BEHAVIOUR_DELETE);
}

View file

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

View file

@ -2,7 +2,7 @@
* Nextcloud Android client application
*
* @author Chris Narkiewicz
* Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
* Copyright (C) 2021 Chris Narkiewicz <hello@ezaquarii.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@ -34,6 +34,7 @@ import java.util.UUID
* @property progress transfer progress, 0-100 percent
* @property file transferred file
* @property request initial transfer request
* @property direction transfer direction, download or upload
*/
data class Transfer(
val uuid: UUID,
@ -46,4 +47,9 @@ data class Transfer(
* True if download is no longer running, false if it is still being processed.
*/
val isFinished: Boolean get() = state == TransferState.COMPLETED || state == TransferState.FAILED
val direction: Direction get() = when (request) {
is DownloadRequest -> Direction.DOWNLOAD
is UploadRequest -> Direction.UPLOAD
}
}

View file

@ -2,7 +2,7 @@
* Nextcloud Android client application
*
* @author Chris Narkiewicz
* Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
* Copyright (C) 2021 Chris Narkiewicz <hello@ezaquarii.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@ -22,6 +22,9 @@ package com.nextcloud.client.files.downloader
import com.owncloud.android.datamodel.OCFile
import java.util.UUID
/**
* Transfer manager provides API to upload and download files.
*/
interface TransferManager {
/**
@ -48,7 +51,7 @@ interface TransferManager {
val status: Status
/**
* Register transfer progress listener. Registration is idempotent - listener can be registered only once.
* Register transfer progress listener. Registration is idempotent - a listener will be registered only once.
*/
fun registerTransferListener(listener: (Transfer) -> Unit)
@ -58,7 +61,7 @@ interface TransferManager {
fun removeTransferListener(listener: (Transfer) -> Unit)
/**
* Register transfer manager status listener. Registration is idempotent - listener can be registered only once.
* Register transfer manager status listener. Registration is idempotent - a listener will be registered only once.
*/
fun registerStatusListener(listener: (Status) -> Unit)

View file

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

View file

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

View file

@ -0,0 +1,93 @@
/*
* Nextcloud Android client application
*
* @author Chris Narkiewicz
* Copyright (C) 2021 Chris Narkiewicz <hello@ezaquarii.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.client.files.downloader
import android.content.Context
import com.nextcloud.client.account.User
import com.nextcloud.client.device.PowerManagementService
import com.nextcloud.client.network.ConnectivityService
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.datamodel.UploadsStorageManager
import com.owncloud.android.db.OCUpload
import com.owncloud.android.files.services.NameCollisionPolicy
import com.owncloud.android.lib.common.OwnCloudClient
import com.owncloud.android.operations.UploadFileOperation
@Suppress("LongParameterList")
class UploadTask(
private val applicationContext: Context,
private val uploadsStorageManager: UploadsStorageManager,
private val connectivityService: ConnectivityService,
private val powerManagementService: PowerManagementService,
private val clientProvider: () -> OwnCloudClient
) {
data class Result(val file: OCFile, val success: Boolean)
/**
* This class is a helper factory to to keep static dependencies
* injection out of the upload task instance.
*/
@Suppress("LongParameterList")
class Factory(
private val applicationContext: Context,
private val uploadsStorageManager: UploadsStorageManager,
private val connectivityService: ConnectivityService,
private val powerManagementService: PowerManagementService,
private val clientProvider: () -> OwnCloudClient
) {
fun create(): UploadTask {
return UploadTask(
applicationContext,
uploadsStorageManager,
connectivityService,
powerManagementService,
clientProvider
)
}
}
fun upload(user: User, upload: OCUpload): Result {
val file = UploadFileOperation.obtainNewOCFileToUpload(
upload.remotePath,
upload.localPath,
upload.mimeType
)
val op = UploadFileOperation(
uploadsStorageManager,
connectivityService,
powerManagementService,
user,
file,
upload,
NameCollisionPolicy.ASK_USER,
upload.localAction,
applicationContext,
upload.isUseWifiOnly,
upload.isWhileChargingOnly,
false
)
val client = clientProvider()
uploadsStorageManager.updateDatabaseUploadStart(op)
val result = op.execute(client)
uploadsStorageManager.updateDatabaseUploadResult(result, op)
return Result(file, result.isSuccess)
}
}

View file

@ -0,0 +1,53 @@
/*
* Nextcloud Android client application
*
* @author Chris Narkiewicz
* Copyright (C) 2021 Chris Narkiewicz <hello@ezaquarii.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.client.files.downloader
import com.owncloud.android.operations.UploadFileOperation
/**
* Upload transfer trigger.
*/
enum class UploadTrigger(val value: Int) {
/**
* Transfer triggered manually by the user.
*/
USER(UploadFileOperation.CREATED_BY_USER),
/**
* Transfer triggered automatically by taking a photo.
*/
PHOTO(UploadFileOperation.CREATED_AS_INSTANT_PICTURE),
/**
* Transfer triggered automatically by making a video.
*/
VIDEO(UploadFileOperation.CREATED_AS_INSTANT_VIDEO);
companion object {
@JvmStatic
fun fromValue(value: Int) = when (value) {
UploadFileOperation.CREATED_BY_USER -> USER
UploadFileOperation.CREATED_AS_INSTANT_PICTURE -> PHOTO
UploadFileOperation.CREATED_AS_INSTANT_VIDEO -> VIDEO
else -> USER
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -29,6 +29,7 @@ import com.nextcloud.client.account.User;
import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.files.services.FileDownloader;
import com.owncloud.android.files.services.FileUploader;
import com.owncloud.android.files.services.NameCollisionPolicy;
import com.owncloud.android.lib.common.OwnCloudClient;
import com.owncloud.android.lib.common.operations.RemoteOperationResult;
import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode;
@ -296,7 +297,7 @@ public class SynchronizeFileOperation extends SyncOperation {
mUser.toPlatformAccount(),
file,
FileUploader.LOCAL_BEHAVIOUR_MOVE,
FileUploader.NameCollisionPolicy.OVERWRITE
NameCollisionPolicy.OVERWRITE
);
mTransferWasRequested = true;

View file

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

View file

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

View file

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

View file

@ -31,6 +31,7 @@ import com.owncloud.android.datamodel.UploadsStorageManager;
import com.owncloud.android.db.OCUpload;
import com.owncloud.android.files.services.FileDownloader;
import com.owncloud.android.files.services.FileUploader;
import com.owncloud.android.files.services.NameCollisionPolicy;
import com.owncloud.android.lib.common.operations.RemoteOperationResult;
import com.owncloud.android.lib.common.utils.Log_OC;
import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation;
@ -124,7 +125,7 @@ public class ConflictsResolveActivity extends FileActivity implements OnConflict
getAccount(),
file,
localBehaviour,
FileUploader.NameCollisionPolicy.OVERWRITE
NameCollisionPolicy.OVERWRITE
);
uploadsStorageManager.removeUpload(upload);
@ -135,7 +136,7 @@ public class ConflictsResolveActivity extends FileActivity implements OnConflict
getAccount(),
file,
localBehaviour,
FileUploader.NameCollisionPolicy.RENAME
NameCollisionPolicy.RENAME
);
uploadsStorageManager.removeUpload(upload);

View file

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

View file

@ -69,6 +69,7 @@ import com.owncloud.android.MainApp;
import com.owncloud.android.R;
import com.owncloud.android.datamodel.OCFile;
import com.owncloud.android.files.services.FileUploader;
import com.owncloud.android.files.services.NameCollisionPolicy;
import com.owncloud.android.lib.common.operations.RemoteOperation;
import com.owncloud.android.lib.common.operations.RemoteOperationResult;
import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode;
@ -906,7 +907,7 @@ public class ReceiveExternalFilesActivity extends FileActivity
UploadFileOperation.CREATED_BY_USER,
false,
false,
FileUploader.NameCollisionPolicy.ASK_USER
NameCollisionPolicy.ASK_USER
);
finish();
}

View file

@ -61,6 +61,7 @@ import com.owncloud.android.datamodel.SyncedFolder;
import com.owncloud.android.datamodel.SyncedFolderDisplayItem;
import com.owncloud.android.datamodel.SyncedFolderProvider;
import com.owncloud.android.files.services.FileUploader;
import com.owncloud.android.files.services.NameCollisionPolicy;
import com.owncloud.android.ui.adapter.SyncedFolderAdapter;
import com.owncloud.android.ui.decoration.MediaGridItemDecoration;
import com.owncloud.android.ui.dialog.SyncedFolderPreferencesDialogFragment;
@ -445,7 +446,7 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
false,
getAccount().name,
FileUploader.LOCAL_BEHAVIOUR_FORGET,
FileUploader.NameCollisionPolicy.ASK_USER.serialize(),
NameCollisionPolicy.ASK_USER.serialize(),
false,
clock.getCurrentTime(),
mediaFolder.filePaths,
@ -533,7 +534,7 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
false,
getAccount().name,
FileUploader.LOCAL_BEHAVIOUR_FORGET,
FileUploader.NameCollisionPolicy.ASK_USER.serialize(),
NameCollisionPolicy.ASK_USER.serialize(),
false,
clock.getCurrentTime(),
null,

View file

@ -33,6 +33,7 @@ import android.widget.Toast;
import com.nextcloud.client.account.User;
import com.owncloud.android.R;
import com.owncloud.android.files.services.FileUploader;
import com.owncloud.android.files.services.NameCollisionPolicy;
import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode;
import com.owncloud.android.lib.common.utils.Log_OC;
import com.owncloud.android.operations.UploadFileOperation;
@ -260,7 +261,7 @@ public class CopyAndUploadContentUrisTask extends AsyncTask<Object, Void, Result
UploadFileOperation.CREATED_BY_USER,
false,
false,
FileUploader.NameCollisionPolicy.ASK_USER
NameCollisionPolicy.ASK_USER
);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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