diff --git a/app/src/androidTest/java/com/nextcloud/client/jobs/BackgroundJobManagerTest.kt b/app/src/androidTest/java/com/nextcloud/client/jobs/BackgroundJobManagerTest.kt index d59256f786c0..01d1de3837b1 100644 --- a/app/src/androidTest/java/com/nextcloud/client/jobs/BackgroundJobManagerTest.kt +++ b/app/src/androidTest/java/com/nextcloud/client/jobs/BackgroundJobManagerTest.kt @@ -16,10 +16,13 @@ import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequest import androidx.work.PeriodicWorkRequest +import androidx.work.WorkContinuation import androidx.work.WorkInfo import androidx.work.WorkManager import com.nextcloud.client.account.User import com.nextcloud.client.core.Clock +import com.nextcloud.client.jobs.upload.FileUploadWorker +import com.nextcloud.client.preferences.AppPreferences import com.nextcloud.utils.extensions.toByteArray import com.owncloud.android.lib.common.utils.Log_OC import org.apache.commons.io.FileUtils @@ -42,6 +45,7 @@ import org.mockito.kotlin.argThat import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.eq import org.mockito.kotlin.mock +import org.mockito.kotlin.timeout import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import java.io.File @@ -64,6 +68,7 @@ import java.util.concurrent.TimeoutException BackgroundJobManagerTest.PeriodicContactsBackup::class, BackgroundJobManagerTest.ImmediateContactsBackup::class, BackgroundJobManagerTest.ImmediateContactsImport::class, + BackgroundJobManagerTest.FilesUpload::class, BackgroundJobManagerTest.Tags::class ) class BackgroundJobManagerTest { @@ -90,6 +95,7 @@ class BackgroundJobManagerTest { internal lateinit var user: User internal lateinit var workManager: WorkManager internal lateinit var clock: Clock + internal lateinit var preferences: AppPreferences internal lateinit var backgroundJobManager: BackgroundJobManagerImpl internal lateinit var context: Context @@ -100,9 +106,10 @@ class BackgroundJobManagerTest { whenever(user.accountName).thenReturn(USER_ACCOUNT_NAME) workManager = mock() clock = mock() + preferences = mock() whenever(clock.currentTime).thenReturn(TIMESTAMP) whenever(clock.currentDate).thenReturn(Date(TIMESTAMP)) - backgroundJobManager = BackgroundJobManagerImpl(workManager, clock, mock()) + backgroundJobManager = BackgroundJobManagerImpl(workManager, clock, preferences) } fun assertHasRequiredTags(tags: Set, jobName: String, user: User? = null) { @@ -385,6 +392,67 @@ class BackgroundJobManagerTest { } } + class FilesUpload : Fixture() { + + @Test + fun start_files_upload_job_enqueues_batches() { + val uploadIds = longArrayOf(1, 2, 3, 4, 5) + whenever(preferences.maxConcurrentUploads).thenReturn(2) + + val continuation: WorkContinuation = mock() + whenever( + workManager.beginUniqueWork(any(), any(), any>()) + ).thenReturn(continuation) + + backgroundJobManager.startFilesUploadJob(user, uploadIds, true) + + val tagCaptor = argumentCaptor() + val requestsCaptor = argumentCaptor>() + + verify(workManager, timeout(1000)).beginUniqueWork( + tagCaptor.capture(), + eq(ExistingWorkPolicy.KEEP), + requestsCaptor.capture() + ) + + val tag = tagCaptor.firstValue + assertTrue(tag.startsWith(BackgroundJobManagerImpl.JOB_FILES_UPLOAD + USER_ACCOUNT_NAME + "_")) + + val requests = requestsCaptor.firstValue + assertEquals(3, requests.size) + + // Check first batch [1, 2] + val data1 = requests[0].workSpec.input + assertEquals(true, data1.getBoolean(FileUploadWorker.SHOW_SAME_FILE_ALREADY_EXISTS_NOTIFICATION, false)) + assertEquals(USER_ACCOUNT_NAME, data1.getString(FileUploadWorker.ACCOUNT)) + assertEquals(5, data1.getInt(FileUploadWorker.TOTAL_UPLOAD_SIZE, 0)) + assertTrue(longArrayOf(1, 2).contentEquals(data1.getLongArray(FileUploadWorker.UPLOAD_IDS)!!)) + assertEquals(0, data1.getInt(FileUploadWorker.CURRENT_BATCH_INDEX, -1)) + + // Check second batch [3, 4] + val data2 = requests[1].workSpec.input + assertTrue(longArrayOf(3, 4).contentEquals(data2.getLongArray(FileUploadWorker.UPLOAD_IDS)!!)) + assertEquals(1, data2.getInt(FileUploadWorker.CURRENT_BATCH_INDEX, -1)) + + // Check third batch [5] + val data3 = requests[2].workSpec.input + assertTrue(longArrayOf(5).contentEquals(data3.getLongArray(FileUploadWorker.UPLOAD_IDS)!!)) + assertEquals(2, data3.getInt(FileUploadWorker.CURRENT_BATCH_INDEX, -1)) + + verify(continuation).enqueue() + } + + @Test + fun start_files_upload_job_does_nothing_when_empty() { + val uploadIds = longArrayOf() + whenever(preferences.maxConcurrentUploads).thenReturn(2) + + backgroundJobManager.startFilesUploadJob(user, uploadIds, true) + + verify(workManager, timeout(1000).times(0)).beginUniqueWork(any(), any(), any>()) + } + } + class Tags { @Test fun split_tag_key_and_value() { 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 3e67df51fa92..6434300fabf1 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt @@ -29,6 +29,7 @@ import com.nextcloud.client.jobs.metadata.MetadataWorker import com.nextcloud.client.jobs.offlineOperations.OfflineOperationsWorker import com.nextcloud.client.jobs.folderDownload.FolderDownloadWorker import com.nextcloud.client.jobs.upload.FileUploadWorker +import com.nextcloud.client.jobs.upload.UploadNotificationManager import com.nextcloud.client.logger.Logger import com.nextcloud.client.network.ConnectivityService import com.nextcloud.client.preferences.AppPreferences @@ -39,6 +40,7 @@ import com.owncloud.android.utils.theme.ViewThemeUtils import org.greenrobot.eventbus.EventBus import javax.inject.Inject import javax.inject.Provider +import kotlin.random.Random /** * This factory is responsible for creating all background jobs and for injecting worker dependencies. @@ -238,6 +240,7 @@ class BackgroundJobFactory @Inject constructor( backgroundJobManager.get(), preferences, context, + UploadNotificationManager(context, viewThemeUtils.get(), Random.nextInt()), params ) 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 0ad01e66c7ad..0374d0bd7ed6 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt @@ -657,48 +657,41 @@ internal class BackgroundJobManagerImpl( */ override fun startFilesUploadJob(user: User, uploadIds: LongArray, showSameFileAlreadyExistsNotification: Boolean) { defaultDispatcherScope.launch { - val batchSize = FileUploadHelper.MAX_FILE_COUNT - val batches = uploadIds.toList().chunked(batchSize) - val tag = startFileUploadJobTag(user.accountName) + val chunkSize = (uploadIds.size / preferences.maxConcurrentUploads).coerceAtLeast(1) + val batches = uploadIds.toList().chunked(chunkSize) + val executionId = System.currentTimeMillis() + val tag = "${startFileUploadJobTag(user.accountName)}_$executionId" val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() - val dataBuilder = Data.Builder() - .putBoolean( - FileUploadWorker.SHOW_SAME_FILE_ALREADY_EXISTS_NOTIFICATION, - showSameFileAlreadyExistsNotification - ) - .putString(FileUploadWorker.ACCOUNT, user.accountName) - .putInt(FileUploadWorker.TOTAL_UPLOAD_SIZE, uploadIds.size) - val workRequests = batches.mapIndexed { index, batch -> - dataBuilder + val data = Data.Builder() + .putBoolean( + FileUploadWorker.SHOW_SAME_FILE_ALREADY_EXISTS_NOTIFICATION, + showSameFileAlreadyExistsNotification + ) + .putString(FileUploadWorker.ACCOUNT, user.accountName) + .putInt(FileUploadWorker.TOTAL_UPLOAD_SIZE, uploadIds.size) .putLongArray(FileUploadWorker.UPLOAD_IDS, batch.toLongArray()) .putInt(FileUploadWorker.CURRENT_BATCH_INDEX, index) + .build() oneTimeRequestBuilder(FileUploadWorker::class, JOB_FILES_UPLOAD, user) .addTag(tag) - .setInputData(dataBuilder.build()) + .setInputData(data) .setConstraints(constraints) .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) .build() } - // Chain the work requests sequentially if (workRequests.isNotEmpty()) { - var workChain = workManager.beginUniqueWork( + workManager.enqueueUniqueWork( tag, - ExistingWorkPolicy.APPEND_OR_REPLACE, - workRequests.first() + ExistingWorkPolicy.KEEP, + workRequests ) - - workRequests.drop(1).forEach { request -> - workChain = workChain.then(request) - } - - workChain.enqueue() } } } diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt index 0604e5db75f2..5ec404cf3568 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt @@ -18,10 +18,10 @@ import com.nextcloud.client.database.entity.toUploadEntity import com.nextcloud.client.device.BatteryStatus import com.nextcloud.client.device.PowerManagementService import com.nextcloud.client.jobs.BackgroundJobManager -import com.nextcloud.client.jobs.upload.FileUploadWorker.Companion.currentUploadFileOperation -import com.nextcloud.client.notifications.AppWideNotificationManager +import com.nextcloud.client.jobs.upload.FileUploadWorker.Companion.activeUploadFileOperations import com.nextcloud.client.network.Connectivity import com.nextcloud.client.network.ConnectivityService +import com.nextcloud.client.notifications.AppWideNotificationManager import com.nextcloud.utils.extensions.getUploadIds import com.owncloud.android.MainApp import com.owncloud.android.R @@ -372,17 +372,14 @@ class FileUploadHelper { @Suppress("ReturnCount") fun isUploadingNow(upload: OCUpload?): Boolean { - val currentUploadFileOperation = currentUploadFileOperation - if (currentUploadFileOperation == null || currentUploadFileOperation.user == null) return false - if (upload == null || upload.accountName != currentUploadFileOperation.user.accountName) return false - - return if (currentUploadFileOperation.oldFile != null) { - // For file conflicts check old file remote path - upload.remotePath == currentUploadFileOperation.remotePath || - upload.remotePath == currentUploadFileOperation.oldFile!! - .remotePath - } else { - upload.remotePath == currentUploadFileOperation.remotePath + upload ?: return false + + return activeUploadFileOperations.values.any { operation -> + operation.user?.accountName == upload.accountName && + ( + upload.remotePath == operation.remotePath || + upload.remotePath == operation.oldFile?.remotePath + ) } } diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt index 275dce4b470c..5f8fd207a971 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt @@ -47,7 +47,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ensureActive import kotlinx.coroutines.withContext import java.io.File -import kotlin.random.Random +import java.util.concurrent.ConcurrentHashMap @Suppress("LongParameterList", "TooGenericExceptionCaught") class FileUploadWorker( @@ -60,6 +60,7 @@ class FileUploadWorker( private val backgroundJobManager: BackgroundJobManager, val preferences: AppPreferences, val context: Context, + val notificationManager: UploadNotificationManager, params: WorkerParameters ) : CoroutineWorker(context, params), OnDatatransferProgressListener { @@ -75,8 +76,7 @@ class FileUploadWorker( const val TOTAL_UPLOAD_SIZE = "total_upload_size" const val SHOW_SAME_FILE_ALREADY_EXISTS_NOTIFICATION = "show_same_file_already_exists_notification" - var currentUploadFileOperation: UploadFileOperation? = null - + val activeUploadFileOperations = ConcurrentHashMap() private const val UPLOADS_ADDED_MESSAGE = "UPLOADS_ADDED" private const val UPLOAD_START_MESSAGE = "UPLOAD_START" private const val UPLOAD_FINISH_MESSAGE = "UPLOAD_FINISH" @@ -103,20 +103,16 @@ class FileUploadWorker( fun getUploadFinishMessage(): String = FileUploadWorker::class.java.name + UPLOAD_FINISH_MESSAGE fun cancelCurrentUpload(remotePath: String, accountName: String, onCompleted: () -> Unit) { - currentUploadFileOperation?.let { + activeUploadFileOperations.values.forEach { if (it.remotePath == remotePath && it.user.accountName == accountName) { it.cancel(ResultCode.USER_CANCELLED) - onCompleted() } } + onCompleted() } - fun isUploading(remotePath: String?, accountName: String?): Boolean { - currentUploadFileOperation?.let { - return it.remotePath == remotePath && it.user.accountName == accountName - } - - return false + fun isUploading(remotePath: String?, accountName: String?): Boolean = activeUploadFileOperations.values.any { + it.remotePath == remotePath && it.user.accountName == accountName } fun getUploadAction(action: String): Int = when (action) { @@ -127,9 +123,9 @@ class FileUploadWorker( } } - private var lastPercent = 0 - private val notificationId = Random.nextInt() - private val notificationManager = UploadNotificationManager(context, viewThemeUtils, notificationId) + private val lastPercents = ConcurrentHashMap() + private val lastUpdateTimes = ConcurrentHashMap() + private val intents = FileUploaderIntents(context) private val fileUploaderDelegate = FileUploaderDelegate() @@ -171,7 +167,7 @@ class FileUploadWorker( val notification = createNotification(notificationTitle) return ForegroundServiceHelper.createWorkerForegroundInfo( - notificationId, + notificationManager.getId(), notification, ForegroundServiceType.DataSync ) @@ -179,7 +175,7 @@ class FileUploadWorker( private suspend fun updateForegroundInfo(notification: Notification) { val foregroundInfo = ForegroundServiceHelper.createWorkerForegroundInfo( - notificationId, + notificationManager.getId(), notification, ForegroundServiceType.DataSync ) @@ -202,7 +198,7 @@ class FileUploadWorker( Log_OC.e(TAG, "FileUploadWorker stopped") setIdleWorkerState() - currentUploadFileOperation?.cancel(null) + activeUploadFileOperations.values.forEach { it.cancel(null) } notificationManager.dismissNotification() } @@ -211,7 +207,8 @@ class FileUploadWorker( } private fun setIdleWorkerState() { - WorkerStateObserver.send(WorkerState.FileUploadCompleted(currentUploadFileOperation?.file)) + val lastOp = activeUploadFileOperations.values.lastOrNull() + WorkerStateObserver.send(WorkerState.FileUploadCompleted(lastOp?.file)) } @Suppress("ReturnCount", "LongMethod", "DEPRECATION") @@ -271,7 +268,7 @@ class FileUploadWorker( setWorkerState(user) val operation = createUploadFileOperation(upload, user) - currentUploadFileOperation = operation + activeUploadFileOperations[operation.originalStoragePath] = operation val currentIndex = (index + 1) val currentUploadIndex = (currentIndex + previouslyUploadedFileSize) @@ -287,7 +284,7 @@ class FileUploadWorker( } val entity = uploadsStorageManager.uploadDao.getUploadById(upload.uploadId, accountName) uploadsStorageManager.updateStatus(entity, result.isSuccess) - currentUploadFileOperation = null + activeUploadFileOperations.remove(operation.originalStoragePath) if (result.code == ResultCode.QUOTA_EXCEEDED) { Log_OC.w(TAG, "Quota exceeded, stopping uploads") @@ -410,20 +407,24 @@ class FileUploadWorker( totalToTransfer: Long, fileAbsoluteName: String ) { + val operation = activeUploadFileOperations[fileAbsoluteName] ?: return val percent = getPercent(totalTransferredSoFar, totalToTransfer) val currentTime = System.currentTimeMillis() + val lastPercent = lastPercents[fileAbsoluteName] ?: 0 + val lastUpdateTime = lastUpdateTimes[fileAbsoluteName] ?: 0L + if (percent != lastPercent && (currentTime - lastUpdateTime) >= minProgressUpdateInterval) { notificationManager.run { - val accountName = currentUploadFileOperation?.user?.accountName - val remotePath = currentUploadFileOperation?.remotePath + val accountName = operation.user.accountName + val remotePath = operation.remotePath - updateUploadProgress(percent, currentUploadFileOperation) + updateUploadProgress(percent, operation) if (accountName != null && remotePath != null) { val key: String = FileUploadHelper.buildRemoteName(accountName, remotePath) val boundListener = FileUploadHelper.mBoundListeners[key] - val filename = currentUploadFileOperation?.fileName ?: "" + val filename = operation.fileName ?: "" boundListener?.onTransferProgress( progressRate, @@ -433,11 +434,10 @@ class FileUploadWorker( ) } - dismissOldErrorNotification(currentUploadFileOperation) + dismissOldErrorNotification(operation) } - lastUpdateTime = currentTime + lastUpdateTimes[fileAbsoluteName] = currentTime + lastPercents[fileAbsoluteName] = percent } - - lastPercent = percent } } diff --git a/app/src/main/java/com/nextcloud/client/preferences/AppPreferences.java b/app/src/main/java/com/nextcloud/client/preferences/AppPreferences.java index 699b2d927e87..486857c5f9b8 100644 --- a/app/src/main/java/com/nextcloud/client/preferences/AppPreferences.java +++ b/app/src/main/java/com/nextcloud/client/preferences/AppPreferences.java @@ -396,4 +396,7 @@ default void onDarkThemeModeChanged(DarkMode mode) { String getLastDisplayedAccountName(); void setLastDisplayedAccountName(String lastDisplayedAccountName); + + int getMaxConcurrentUploads(); + void setMaxConcurrentUploads(int value); } diff --git a/app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java b/app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java index 3d6330923ec6..c511a5d2d287 100644 --- a/app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java +++ b/app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java @@ -111,6 +111,8 @@ public final class AppPreferencesImpl implements AppPreferences { private static final String PREF_LAST_DISPLAYED_ACCOUNT_NAME = "last_displayed_user"; + private static final String PREF_MAX_CONCURRENT_UPLOADS = "max_concurrent_uploads"; + private static final String LOG_ENTRY = "log_entry"; private final Context context; @@ -843,4 +845,14 @@ public String getLastDisplayedAccountName() { public void setLastDisplayedAccountName(String lastDisplayedAccountName) { preferences.edit().putString(PREF_LAST_DISPLAYED_ACCOUNT_NAME, lastDisplayedAccountName).apply(); } + + @Override + public int getMaxConcurrentUploads() { + return Integer.parseInt(preferences.getString(PREF_MAX_CONCURRENT_UPLOADS, "10")); + } + + @Override + public void setMaxConcurrentUploads(int value) { + preferences.edit().putInt(PREF_MAX_CONCURRENT_UPLOADS, value).apply(); + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ec083c6fe0d8..72db52af26f8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -130,6 +130,7 @@ Manage folders for auto upload All files access + Max concurrent uploads Allow the app to access and manage all files on your device Help diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index adada5180474..521c057a4101 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -84,6 +84,13 @@ android:title="@string/prefs_all_files_access_title" android:key="allFilesAccess" android:summary="@string/prefs_all_files_access_summary" /> + + (relaxed = true) + val viewThemeUtils = ViewThemeUtils(materialSchemes, mockk(relaxed = true)) + + val connectivity = mockk() + every { connectivity.isConnected } returns true + every { connectivityService.getConnectivity() } returns connectivity + every { connectivityService.isConnected } returns true + every { connectivityService.isInternetWalled } returns false + + worker = FileUploadWorker( + uploadsStorageManager, + connectivityService, + powerManagementService, + userAccountManager, + viewThemeUtils, + localBroadcastManager, + backgroundJobManager, + preferences, + context, + uploadNotificationManager, + params + ) + } + + @After + fun tearDown() { + unmockkAll() + FileUploadWorker.activeUploadFileOperations.clear() + } + + @Test + fun `doWork returns failure when account name is missing`() = runBlocking { + // GIVEN + every { params.inputData.getString(FileUploadWorker.ACCOUNT) } returns null + + // WHEN + val result = worker.doWork() + + // THEN + assertEquals(ListenableWorker.Result.failure(), result) + } + + @Test + fun `doWork returns failure when upload ids are missing`() = runBlocking { + // GIVEN + every { params.inputData.getString(FileUploadWorker.ACCOUNT) } returns "account" + every { params.inputData.getLongArray(FileUploadWorker.UPLOAD_IDS) } returns null + + // WHEN + val result = worker.doWork() + + // THEN + assertEquals(ListenableWorker.Result.failure(), result) + } + + @Test + fun `doWork returns failure when batch index is missing`() = runBlocking { + // GIVEN + every { params.inputData.getString(FileUploadWorker.ACCOUNT) } returns "account" + every { params.inputData.getLongArray(FileUploadWorker.UPLOAD_IDS) } returns longArrayOf(1L) + every { params.inputData.getInt(FileUploadWorker.CURRENT_BATCH_INDEX, -1) } returns -1 + + // WHEN + val result = worker.doWork() + + // THEN + assertEquals(ListenableWorker.Result.failure(), result) + } + + @Test + fun `doWork returns failure when user is not found`() = runBlocking { + // GIVEN + val accountName = "account" + every { params.inputData.getString(FileUploadWorker.ACCOUNT) } returns accountName + every { params.inputData.getLongArray(FileUploadWorker.UPLOAD_IDS) } returns longArrayOf(1L) + every { params.inputData.getInt(FileUploadWorker.CURRENT_BATCH_INDEX, any()) } returns 0 + every { params.inputData.getInt(FileUploadWorker.TOTAL_UPLOAD_SIZE, any()) } returns 1 + every { userAccountManager.getUser(accountName) } returns Optional.empty() + + // WHEN + val result = worker.doWork() + + // THEN + assertEquals(ListenableWorker.Result.failure(), result) + } + + @Test + fun `onTransferProgress updates notification manager`() { + // GIVEN + val fileName = "testFile" + val operation = mockk(relaxed = true) + FileUploadWorker.activeUploadFileOperations[fileName] = operation + + // WHEN + worker.onTransferProgress(100, 50, 100, fileName) + + // THEN + verify { uploadNotificationManager.updateUploadProgress(50, operation) } + } + + @Test + fun `cancelCurrentUpload cancels matching operations`() { + // GIVEN + val remotePath = "path" + val accountName = "account" + val operation = mockk(relaxed = true) + every { operation.remotePath } returns remotePath + every { operation.user.accountName } returns accountName + FileUploadWorker.activeUploadFileOperations["key"] = operation + + // WHEN + var completed = false + FileUploadWorker.cancelCurrentUpload(remotePath, accountName) { + completed = true + } + + // THEN + verify { operation.cancel(ResultCode.USER_CANCELLED) } + assertTrue(completed) + } + + @Test + fun `isUploading returns true when operation exists`() { + // GIVEN + val remotePath = "path" + val accountName = "account" + val operation = mockk(relaxed = true) + every { operation.remotePath } returns remotePath + every { operation.user.accountName } returns accountName + FileUploadWorker.activeUploadFileOperations["key"] = operation + + // WHEN & THEN + assertTrue(FileUploadWorker.isUploading(remotePath, accountName)) + assertFalse(FileUploadWorker.isUploading("other", accountName)) + } + + @Test + fun `getUploadAction returns correct values`() { + assertEquals( + FileUploadWorker.LOCAL_BEHAVIOUR_FORGET, + FileUploadWorker.getUploadAction("LOCAL_BEHAVIOUR_FORGET") + ) + assertEquals( + FileUploadWorker.LOCAL_BEHAVIOUR_MOVE, + FileUploadWorker.getUploadAction("LOCAL_BEHAVIOUR_MOVE") + ) + assertEquals( + FileUploadWorker.LOCAL_BEHAVIOUR_DELETE, + FileUploadWorker.getUploadAction("LOCAL_BEHAVIOUR_DELETE") + ) + assertEquals( + FileUploadWorker.LOCAL_BEHAVIOUR_FORGET, + FileUploadWorker.getUploadAction("UNKNOWN") + ) + } +} diff --git a/app/src/test/java/com/nextcloud/client/preferences/TestAppPreferences.java b/app/src/test/java/com/nextcloud/client/preferences/TestAppPreferences.java index a47b0abe815b..d128e8aa7047 100644 --- a/app/src/test/java/com/nextcloud/client/preferences/TestAppPreferences.java +++ b/app/src/test/java/com/nextcloud/client/preferences/TestAppPreferences.java @@ -20,6 +20,7 @@ import org.mockito.MockitoAnnotations; import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.inOrder; @@ -142,6 +143,7 @@ public static class Preferences { public void setUp() { MockitoAnnotations.initMocks(this); when(editor.remove(anyString())).thenReturn(editor); + when(editor.putInt(anyString(), anyInt())).thenReturn(editor); when(sharedPreferences.edit()).thenReturn(editor); appPreferences = new AppPreferencesImpl(testContext, sharedPreferences, userAccountManager); } @@ -177,5 +179,15 @@ public void testBruteForceDelay() { assertEquals(10, appPreferences.computeBruteForceDelay(50)); assertEquals(10, appPreferences.computeBruteForceDelay(100)); } + + @Test + public void maxConcurrentUploads() { + when(sharedPreferences.getInt("max_concurrent_uploads", 10)).thenReturn(10); + assertEquals(10, appPreferences.getMaxConcurrentUploads()); + + appPreferences.setMaxConcurrentUploads(5); + verify(editor).putInt("max_concurrent_uploads", 5); + verify(editor).apply(); + } } }