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}") 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) { 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 } } 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)) 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); } 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; } 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" 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) {