From 47c770f7744df2d7fdb4a05edd76f5e57a4f50f6 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 15 Sep 2026 11:36:37 +0200 Subject: [PATCH 1/8] do not cancel previous job so that list action such as create can react Signed-off-by: alperozturk96 --- .../com/nextcloud/client/network/ConnectivityServiceImpl.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.kt b/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.kt index bd9ea8c79fae..fb7f334dc509 100644 --- a/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.kt +++ b/app/src/main/java/com/nextcloud/client/network/ConnectivityServiceImpl.kt @@ -46,7 +46,6 @@ class ConnectivityServiceImpl( // region private values private val scope = CoroutineScope(Dispatchers.IO) - private var availabilityCheckJob: Job? = null private var notifyJob: Job? = null private val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager private val listeners = mutableSetOf() @@ -82,8 +81,7 @@ class ConnectivityServiceImpl( // region overridden methods override fun isNetworkAndServerAvailable(onCompleted: (Boolean) -> Unit) { - availabilityCheckJob?.cancel() - availabilityCheckJob = scope.launch { + scope.launch { val available = !isInternetWalled() Log_OC.d(TAG, "isNetworkAndServerAvailable: $available") withContext(Dispatchers.Main) { From 88f69a2b875340be944ea6b53677bfb9a7eb0dd0 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 15 Sep 2026 11:37:34 +0200 Subject: [PATCH 2/8] return value for fail scenario as well Signed-off-by: alperozturk96 --- .../operations/CreateFolderOperation.java | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java b/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java index 6c57e9965b23..47b4fce4cd65 100644 --- a/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/CreateFolderOperation.java @@ -516,20 +516,23 @@ private String createRandomFileName(DecryptedFolderMetadataFileV1 metadata) { private RemoteOperationResult normalCreate(OwnCloudClient client) { final var result = new CreateFolderRemoteOperation(remotePath, true).execute(client); - if (result.isSuccess()) { - final var remoteFolderOperationResult = new ReadFolderRemoteOperation(remotePath) - .execute(client); + if (!result.isSuccess()) { + Log_OC.e(TAG, remotePath + " hasn't been created"); + return result; + } - if (remoteFolderOperationResult.isSuccess() && - remoteFolderOperationResult.getData().get(0) instanceof RemoteFile remoteFile) { - createdRemoteFolder = remoteFile; - } + final var readResult = new ReadFolderRemoteOperation(remotePath).execute(client); + final var readData = readResult.getData(); - saveFolderInDB(); - } else { - Log_OC.e(TAG, remotePath + " hasn't been created"); + if (!readResult.isSuccess() || readData == null || readData.isEmpty() || + !(readData.get(0) instanceof RemoteFile remoteFolder)) { + Log_OC.e(TAG, remotePath + " has been created but could not be read back"); + return readResult; } + createdRemoteFolder = remoteFolder; + saveFolderInDB(); + return result; } From 149207c3034b0a9ba08aa331a0d5f5c32c39b516 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 15 Sep 2026 11:37:50 +0200 Subject: [PATCH 3/8] early exit for remove files if its empty Signed-off-by: alperozturk96 --- .../com/nextcloud/utils/extensions/FileActivityExtensions.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/com/nextcloud/utils/extensions/FileActivityExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/FileActivityExtensions.kt index d558b38014fd..54b717ab2a03 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/FileActivityExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/FileActivityExtensions.kt @@ -19,6 +19,11 @@ fun FileActivity.removeFiles( onlyLocalCopy: Boolean, filesRemovedListener: OnFilesRemovedListener? ) { + if (files.isEmpty()) { + filesRemovedListener?.onFilesRemoved() + return + } + connectivityService.isNetworkAndServerAvailable { isAvailable -> if (isAvailable) { showLoadingDialog(getString(R.string.wait_a_moment)) From db7c97f6100147498547a72b73062b0331c0e117 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 15 Sep 2026 11:38:59 +0200 Subject: [PATCH 4/8] early exit for remove files if its empty Signed-off-by: alperozturk96 --- .../android/datamodel/FileDataStorageManager.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java index 42d5db1feb35..c8a1ea0eba17 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java @@ -235,6 +235,7 @@ public void createPendingFile(String path, String mimeType, long createdAt, long file.setMimeType(mimeType); file.setCreationTimestamp(createdAt); file.setModificationTimestamp(modificationTimestamp); + file.setPermissions(getParentPermissions(path)); saveFileWithParent(file, MainApp.getAppContext()); } @@ -243,9 +244,21 @@ public void createPendingDirectory(String path, long createdAt, long modificatio directory.setMimeType(MimeType.DIRECTORY); directory.setCreationTimestamp(createdAt); directory.setModificationTimestamp(modificationTimestamp); + directory.setPermissions(getParentPermissions(path)); saveFileWithParent(directory, MainApp.getAppContext()); } + @Nullable + private String getParentPermissions(String path) { + String parentPath = FileStorageUtils.getParentPath(path); + if (parentPath == null) { + return null; + } + + OCFile parent = getFileByDecryptedRemotePath(parentPath); + return parent == null ? null : parent.getPermissions(); + } + public void deleteOfflineOperation(OCFile file) { offlineOperationsRepository.deleteOperation(file); } From 6da2b7ff62f6e0bb83bf74704d2b5c6f70c782b0 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 15 Sep 2026 11:40:31 +0200 Subject: [PATCH 5/8] use result success since policy is keep and check remote file to prevent unnecessary job Signed-off-by: alperozturk96 --- .../OfflineOperationsWorker.kt | 48 ++++++++++++++----- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/OfflineOperationsWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/OfflineOperationsWorker.kt index 8a1b966031ad..a4646cb44a5b 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/OfflineOperationsWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/offlineOperations/OfflineOperationsWorker.kt @@ -32,6 +32,7 @@ import com.owncloud.android.lib.resources.files.model.RemoteFile import com.owncloud.android.operations.CreateFolderOperation import com.owncloud.android.operations.RemoveFileOperation import com.owncloud.android.operations.RenameFileOperation +import com.owncloud.android.utils.FileStorageUtils import com.owncloud.android.utils.MimeTypeUtil import com.owncloud.android.utils.theme.ViewThemeUtils import kotlinx.coroutines.Dispatchers @@ -67,8 +68,8 @@ class OfflineOperationsWorker( // check network connection if (!connectivityService.isNetworkAndServerAvailableSuspended()) { - Log_OC.w(TAG, "⚠️ No internet/server connection. Retrying later...") - return@withContext Result.retry() + Log_OC.w(TAG, "⚠️ No internet/server connection. Waiting for the next trigger...") + return@withContext Result.success() } // check offline operations @@ -160,30 +161,53 @@ class OfflineOperationsWorker( } // endregion + private fun getExecutionPath(operation: OfflineOperationEntity): String? { + val path = operation.path ?: return null + + return if (operation.type is OfflineOperationType.CreateFile) { + path.removeSuffix(OCFile.PATH_SEPARATOR) + } else { + path + } + } + + private fun adoptRemoteFolder(operation: OfflineOperationEntity, remoteFile: RemoteFile, ocFile: OCFile?) { + ocFile?.let { + val adoptedFile = FileStorageUtils.fillOCFile(remoteFile).apply { + fileId = it.fileId + parentId = it.parentId + decryptedRemotePath = it.decryptedRemotePath + } + + fileDataStorageManager.saveFile(adoptedFile) + } + + repository.updateNextOperations(operation) + fileDataStorageManager.offlineOperationDao.delete(operation) + notificationManager.dismissNotification(operation.id) + } + // region Operation Execution @Suppress("ComplexCondition", "LongMethod") private suspend fun executeOperation( operation: OfflineOperationEntity, client: OwnCloudClient ): OfflineOperationResult? = withContext(Dispatchers.IO) { - var path = (operation.path) + val path = getExecutionPath(operation) if (path == null) { Log_OC.w(TAG, "⚠️ Skipped: path is null for operation id=${operation.id}") return@withContext null } - if (operation.type is OfflineOperationType.CreateFile && path.endsWith(OCFile.PATH_SEPARATOR)) { - Log_OC.w( - TAG, - "Create file operation should not ends with path separator removing suffix, " + - "operation id=${operation.id}" - ) - path = path.removeSuffix(OCFile.PATH_SEPARATOR) - } - val remoteFile = getRemoteFile(path) val ocFile = fileDataStorageManager.getFileByDecryptedRemotePath(path) + if (operation.type is OfflineOperationType.CreateFolder && remoteFile != null) { + Log_OC.d(TAG, "📂 Folder already exists on server, adopting it: $path") + adoptRemoteFolder(operation, remoteFile, ocFile) + return@withContext null + } + if (remoteFile != null && ocFile != null && isFileChanged(remoteFile, ocFile)) { Log_OC.w(TAG, "⚠️ Conflict detected: File already exists on server. Skipping operation id=${operation.id}") From d11ba19636664c73ae89c3154de90f201620e1cd Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 15 Sep 2026 11:40:44 +0200 Subject: [PATCH 6/8] do in io scope Signed-off-by: alperozturk96 --- .../owncloud/android/ui/dialog/RemoveFilesDialogFragment.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/RemoveFilesDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/RemoveFilesDialogFragment.kt index 7f08c9b88b20..0c7b3446c225 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/RemoveFilesDialogFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/RemoveFilesDialogFragment.kt @@ -84,12 +84,12 @@ class RemoveFilesDialogFragment : ?.partition { it.isOfflineOperation } ?: (emptyList() to emptyList()) - offlineFiles.forEach(fileDataStorageManager::deleteOfflineOperation) - val listener = getTypedActivity(OnFilesRemovedListener::class.java) val fileActivity = getTypedActivity(FileActivity::class.java) fileActivity?.lifecycleScope?.launch(Dispatchers.IO) { + offlineFiles.forEach(fileDataStorageManager::deleteOfflineOperation) + val (autoUploadEntities, filesToRemove) = FileUploadHelper.instance().splitFilesByAutoUpload(files, userAccountManager.user.accountName) withContext(Dispatchers.Main) { From 2d8b8ab7c029997b23cd7a1183123b6bc4a38bc9 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 15 Sep 2026 11:41:25 +0200 Subject: [PATCH 7/8] walled connection should not cached for 5 minutes, separate walled and reachability cache timing Signed-off-by: alperozturk96 --- .../com/nextcloud/client/network/WalledCheckCache.kt | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/network/WalledCheckCache.kt b/app/src/main/java/com/nextcloud/client/network/WalledCheckCache.kt index f4b0a9de8902..7be0ca54a081 100644 --- a/app/src/main/java/com/nextcloud/client/network/WalledCheckCache.kt +++ b/app/src/main/java/com/nextcloud/client/network/WalledCheckCache.kt @@ -12,6 +12,8 @@ import com.nextcloud.client.core.ClockImpl import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject import javax.inject.Singleton +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds @Singleton class WalledCheckCache @Inject constructor() { @@ -29,9 +31,10 @@ class WalledCheckCache @Inject constructor() { } fun getValue(key: ConnectivityKey): Boolean? { - val entry = walledStatusCache[key] ?: return null - val isExpired = (clock.currentTime - entry.first) >= CACHE_TIME_MS - return if (isExpired) null else entry.second + val (checkedAt, isWalled) = walledStatusCache[key] ?: return null + val cacheTime = if (isWalled) WALLED_CACHE_TIME_MS else REACHABLE_CACHE_TIME_MS + val isExpired = (clock.currentTime - checkedAt) >= cacheTime + return if (isExpired) null else isWalled } fun putConnectivityValue(key: ConnectivityKey, connectivity: Connectivity) { @@ -39,6 +42,7 @@ class WalledCheckCache @Inject constructor() { } companion object { - private const val CACHE_TIME_MS = 10 * 60 * 1000 + private val REACHABLE_CACHE_TIME_MS = 10.minutes.inWholeMilliseconds + private val WALLED_CACHE_TIME_MS = 30.seconds.inWholeMilliseconds } } From 51e6f641baa9adbb9dc92e84475e6748293e6286 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 15 Sep 2026 12:01:24 +0200 Subject: [PATCH 8/8] fix file list refresh after adding offline operation Signed-off-by: alperozturk96 --- .../ui/activity/FileDisplayActivity.kt | 4 +++ .../ui/dialog/CreateFolderDialogFragment.kt | 25 ++++++++++--------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt index e48cdc9f8912..9ef563369a6e 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt @@ -1156,6 +1156,10 @@ class FileDisplayActivity : } else { lifecycleScope.launch(Dispatchers.IO) { fileDataStorageManager.addCreateFileOfflineOperation(filePaths, decryptedRemotePaths) + + withContext(Dispatchers.Main) { + refreshCurrentDirectory() + } } } } diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt b/app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt index 7188ef4ac677..b40eb3f219d0 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/CreateFolderDialogFragment.kt @@ -196,28 +196,29 @@ class CreateFolderDialogFragment : val path = parentFolder?.decryptedRemotePath + newFolderName + OCFile.PATH_SEPARATOR val componentGetter = typedActivity() - val fda = typedActivity() connectivityService.isNetworkAndServerAvailable { if (it) { componentGetter?.fileOperationsHelper?.createFolder(path, encrypted) } else { Log_OC.d(TAG, "Network not available, creating offline operation") - lifecycleScope.launch(Dispatchers.IO) { - fileDataStorageManager.addCreateFolderOfflineOperation( - path, - newFolderName, - parentFolder?.fileId - ) - - withContext(Dispatchers.Main) { - fda?.refreshCurrentDirectory() - } - } + createFolderOfflineOperation(path, newFolderName) } } } } + private fun createFolderOfflineOperation(path: String, folderName: String) { + val activity = typedActivity() ?: return + + activity.lifecycleScope.launch(Dispatchers.IO) { + fileDataStorageManager.addCreateFolderOfflineOperation(path, folderName, parentFolder?.fileId) + + withContext(Dispatchers.Main) { + activity.refreshCurrentDirectory() + } + } + } + companion object { private const val TAG = "CreateFolderDialogFragment" private const val ARG_PARENT_FOLDER = "PARENT_FOLDER"