diff --git a/owncloud-android-library b/owncloud-android-library index 81d0e6d7d18..f5cf272acdf 160000 --- a/owncloud-android-library +++ b/owncloud-android-library @@ -1 +1 @@ -Subproject commit 81d0e6d7d1883079445c2b4e585829c3a1cce695 +Subproject commit f5cf272acdf09f05a5db792f836fcd22959af9b1 diff --git a/owncloudApp/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.kt b/owncloudApp/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.kt index d55e285aedc..6757c98de4f 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.kt @@ -625,131 +625,6 @@ class FileDataStorageManager : KoinComponent { // return updatedCount > 0 } - /** - * Updates database and file system for a file or folder that was moved to a different location. - * - * TODO explore better (faster) implementations - * TODO throw exceptions up ! - */ - fun moveLocalFile(file: OCFile?, targetPath: String, targetParentPath: String) { - // FIXME: 13/10/2020 : New_arch: Move file - -// if (file != null && file.fileExists() && ROOT_PATH != file.fileName) { -// -// val targetParent = getFileByPath(targetParentPath) -// ?: throw IllegalStateException( -// "Parent folder of the target path does not exist!!" -// ) -// -// /// 1. get all the descendants of the moved element in a single QUERY -// val c: Cursor? = -// try { -// performQuery( -// uri = CONTENT_URI, -// projection = null, -// selection = "$FILE_ACCOUNT_OWNER=? AND $FILE_PATH LIKE ? ", -// selectionArgs = arrayOf(account.name, "${file.remotePath}%"), -// sortOrder = "$FILE_PATH ASC " -// ) -// } catch (e: RemoteException) { -// Timber.e(e) -// null -// } -// -// val originalPathsToTriggerMediaScan = ArrayList() -// val newPathsToTriggerMediaScan = ArrayList() -// val defaultSavePath = FileStorageUtils.getSavePath(account.name) -// -// /// 2. prepare a batch of update operations to change all the descendants -// if (c != null) { -// val operations = ArrayList(c.count) -// if (c.moveToFirst()) { -// val lengthOfOldPath = file.remotePath.length -// val lengthOfOldStoragePath = defaultSavePath.length + lengthOfOldPath -// do { -// val cv = ContentValues() // keep construction in the loop -// val child = createFileInstance(c) -// cv.put(FILE_PATH, targetPath + child!!.remotePath.substring(lengthOfOldPath)) -// if (child.storagePath != null && child.storagePath.startsWith(defaultSavePath)) { -// // update link to downloaded content - but local move is not done here! -// val targetLocalPath = defaultSavePath + targetPath + -// child.storagePath.substring(lengthOfOldStoragePath) -// -// cv.put(FILE_STORAGE_PATH, targetLocalPath) -// -// originalPathsToTriggerMediaScan.add(child.storagePath) -// newPathsToTriggerMediaScan.add(targetLocalPath) -// -// } -// if (targetParent.availableOfflineStatus != NOT_AVAILABLE_OFFLINE) { -// // moving to an available offline subfolder -// cv.put(FILE_KEEP_IN_SYNC, AVAILABLE_OFFLINE_PARENT.value) -// } else { -// // moving to a not available offline subfolder - with care -// if (file.availableOfflineStatus == AVAILABLE_OFFLINE_PARENT) { -// cv.put(FILE_KEEP_IN_SYNC, NOT_AVAILABLE_OFFLINE.value) -// } -// } -// -// if (child.remotePath == file.remotePath) { -// cv.put(FILE_PARENT, targetParent.fileId) -// } -// operations.add( -// ContentProviderOperation.newUpdate(CONTENT_URI).withValues(cv).withSelection( -// "$_ID=?", -// arrayOf(child.fileId.toString()) -// ) -// .build() -// ) -// -// } while (c.moveToNext()) -// } -// c.close() -// -// /// 3. apply updates in batch -// try { -// if (contentResolver != null) { -// contentResolver!!.applyBatch(MainApp.authority, operations) -// -// } else { -// contentProviderClient!!.applyBatch(operations) -// } -// -// } catch (e: Exception) { -// Timber.e(e, "Fail to update ${file.fileId} and descendants in database") -// } -// -// } -// -// /// 4. move in local file system -// val originalLocalPath = FileStorageUtils.getDefaultSavePathFor(account.name, file) -// val targetLocalPath = defaultSavePath + targetPath -// val localFile = File(originalLocalPath) -// var renamed = false -// if (localFile.exists()) { -// val targetFile = File(targetLocalPath) -// val targetFolder = targetFile.parentFile -// if (targetFolder != null && !targetFolder.exists()) { -// targetFolder.mkdirs() -// } -// renamed = localFile.renameTo(targetFile) -// } -// -// if (renamed) { -// var it = originalPathsToTriggerMediaScan.iterator() -// while (it.hasNext()) { -// // Notify MediaScanner about removed file -// deleteFileInMediaScan(it.next()) -// } -// it = newPathsToTriggerMediaScan.iterator() -// while (it.hasNext()) { -// // Notify MediaScanner about new file/folder -// triggerMediaScan(it.next()) -// } -// } -// } - } - fun copyLocalFile(originalFile: OCFile?, targetPath: String, targetFileRemoteId: String) { // FIXME: 13/10/2020 : New_arch: Copy file diff --git a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt index 3a750dde9ef..bd56fa00008 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/UseCaseModule.kt @@ -37,6 +37,7 @@ import com.owncloud.android.domain.files.usecases.GetFolderImagesUseCase import com.owncloud.android.domain.files.usecases.MoveFileUseCase import com.owncloud.android.domain.files.usecases.RefreshFolderFromServerAsyncUseCase import com.owncloud.android.domain.files.usecases.RemoveFileUseCase +import com.owncloud.android.domain.files.usecases.RenameFileUseCase import com.owncloud.android.domain.files.usecases.SaveFileOrFolderUseCase import com.owncloud.android.domain.server.usecases.GetServerInfoAsyncUseCase import com.owncloud.android.domain.sharing.sharees.GetShareesAsyncUseCase @@ -85,6 +86,7 @@ val useCaseModule = module { factory { MoveFileUseCase(get()) } factory { RefreshFolderFromServerAsyncUseCase(get()) } factory { RemoveFileUseCase(get()) } + factory { RenameFileUseCase(get()) } factory { SaveFileOrFolderUseCase(get()) } // Sharing diff --git a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt index bdff248b991..8e7890d2461 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt @@ -64,5 +64,5 @@ val viewModelModule = module { viewModel { PreviewImageViewModel(get(), get(), get()) } viewModel { FileDetailsViewModel(get(), get(), get(), get(), get()) } - viewModel { FileOperationViewModel(get(), get(), get(), get()) } + viewModel { FileOperationViewModel(get(), get(), get(), get(), get()) } } diff --git a/owncloudApp/src/main/java/com/owncloud/android/operations/RenameFileOperation.java b/owncloudApp/src/main/java/com/owncloud/android/operations/RenameFileOperation.java deleted file mode 100644 index 2a2a7c349d6..00000000000 --- a/owncloudApp/src/main/java/com/owncloud/android/operations/RenameFileOperation.java +++ /dev/null @@ -1,194 +0,0 @@ -/** - * ownCloud Android client application - * - * @author David A. Velasco - * @author masensio - * @authro Christian Schabesberger - * Copyright (C) 2020 ownCloud GmbH. - *

- * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2, - * as published by the Free Software Foundation. - *

- * 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 General Public License for more details. - *

- * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.owncloud.android.operations; - -import com.owncloud.android.domain.files.model.OCFile; -import com.owncloud.android.lib.common.OwnCloudClient; -import com.owncloud.android.lib.common.operations.RemoteOperationResult; -import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode; -import com.owncloud.android.lib.resources.files.RenameRemoteFileOperation; -import com.owncloud.android.operations.common.SyncOperation; -import com.owncloud.android.utils.FileStorageUtils; -import timber.log.Timber; - -import java.io.File; -import java.io.IOException; - -/** - * Remote operation performing the rename of a remote file (or folder?) in the ownCloud server. - */ -public class RenameFileOperation extends SyncOperation { - - private OCFile mFile; - private String mRemotePath; - private String mNewName; - private String mNewRemotePath; - - /** - * Constructor - * - * @param remotePath RemotePath of the OCFile instance describing the remote file or - * folder to rename - * @param newName New name to set as the name of file. - */ - public RenameFileOperation(String remotePath, String newName) { - mRemotePath = remotePath; - mNewName = newName; - mNewRemotePath = null; - } - - public OCFile getFile() { - return mFile; - } - - /** - * Performs the rename operation. - * - * @param client Client object to communicate with the remote ownCloud server. - */ - @Override - protected RemoteOperationResult run(OwnCloudClient client) { - RemoteOperationResult result = null; - - mFile = getStorageManager().getFileByPath(mRemotePath); - - // check if the new name is valid in the local file system - try { - if (!isValidNewName()) { - return new RemoteOperationResult(ResultCode.INVALID_LOCAL_FILE_NAME); - } - String parent = (new File(mFile.getRemotePath())).getParent(); - parent = (parent.endsWith(File.separator)) ? parent : parent + - File.separator; - mNewRemotePath = parent + mNewName; - if (mFile.isFolder()) { - mNewRemotePath += File.separator; - } - - // check local overwrite - if (getStorageManager().getFileByPath(mNewRemotePath) != null) { - return new RemoteOperationResult(ResultCode.INVALID_OVERWRITE); - } - - RenameRemoteFileOperation operation = new RenameRemoteFileOperation(mFile.getFileName(), - mFile.getRemotePath(), - mNewName, mFile.isFolder()); - result = operation.execute(client); - - if (result.isSuccess()) { - if (mFile.isFolder()) { - saveLocalDirectory(parent); - - } else { - saveLocalFile(); - } - } - - } catch (IOException e) { - Timber.e(e, "Rename " + mFile.getRemotePath() + " to " + ((mNewRemotePath == null) ? mNewName : - mNewRemotePath) + " failed"); - } - - return result; - } - - private void saveLocalDirectory(String parent) { - getStorageManager().moveLocalFile(mFile, mNewRemotePath, parent); - // FIXME: 29/10/2020 : New_arch: Rename -// mFile.setFileName(mNewName); - } - - private void saveLocalFile() { - // FIXME: 29/10/2020 : New_arch: Rename -// mFile.setFileName(mNewName); - - if (mFile.isAvailableLocally()) { - // rename the local copy of the file - String oldPath = mFile.getStoragePath(); - File f = new File(oldPath); - String parentStoragePath = f.getParent(); - if (!parentStoragePath.endsWith(File.separator)) { - parentStoragePath += File.separator; - } - if (f.renameTo(new File(parentStoragePath + mNewName))) { - String newPath = parentStoragePath + mNewName; - mFile.setStoragePath(newPath); - - // notify MediaScanner about removed file - getStorageManager().deleteFileInMediaScan(oldPath); - // notify to scan about new file - getStorageManager().triggerMediaScan(newPath); - } - // else - NOTHING: the link to the local file is kept although the local name - // can't be updated - // TODO - study conditions when this could be a problem - } - - getStorageManager().saveFile(mFile); - } - - /** - * Checks if the new name to set is valid in the file system - *

- * The only way to be sure is trying to create a file with that name. It's made in the - * temporal directory for downloads, out of any account, and then removed. - *

- * IMPORTANT: The test must be made in the same file system where files are download. - * The internal storage could be formatted with a different file system. - *

- * TODO move this method, and maybe FileDownload.get***Path(), to a class with utilities - * specific for the interactions with the file system - * - * @return 'True' if a temporal file named with the name to set could be - * created in the file system where local files are stored. - * @throws IOException When the temporal folder can not be created. - */ - private boolean isValidNewName() throws IOException { - // check tricky names - if (mNewName == null || mNewName.length() <= 0 || mNewName.contains(File.separator)) { - return false; - } - // create a test file - String tmpFolderName = FileStorageUtils.getTemporalPath(""); - File testFile = new File(tmpFolderName + mNewName); - File tmpFolder = testFile.getParentFile(); - tmpFolder.mkdirs(); - if (!tmpFolder.isDirectory()) { - throw new IOException("Unexpected error: temporal directory could not be created"); - } - try { - testFile.createNewFile(); // return value is ignored; it could be 'false' because - // the file already existed, that doesn't invalidate the name - } catch (IOException e) { - Timber.i("Test for validity of name " + mNewName + " in the file system failed"); - return false; - } - boolean result = (testFile.exists() && testFile.isFile()); - - // cleaning ; result is ignored, since there is not much we could do in case of failure, - // but repeat and repeat... - testFile.delete(); - - return result; - } - -} diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/files/operations/FileOperation.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/files/operations/FileOperation.kt index c950da7e0b1..ed0a0fa6d7a 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/files/operations/FileOperation.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/files/operations/FileOperation.kt @@ -24,4 +24,5 @@ import com.owncloud.android.domain.files.model.OCFile sealed class FileOperation { data class MoveOperation(val listOfFilesToMove: List, val targetFolder: OCFile) : FileOperation() data class RemoveOperation(val listOfFilesToRemove: List, val removeOnlyLocalCopy: Boolean) : FileOperation() + data class RenameOperation(val ocFileToRename: OCFile, val newName: String) : FileOperation() } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/files/operations/FileOperationViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/files/operations/FileOperationViewModel.kt index 7c4ecd79b0b..18e527a03f7 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/files/operations/FileOperationViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/ui/files/operations/FileOperationViewModel.kt @@ -23,11 +23,13 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.owncloud.android.domain.BaseUseCaseWithResult import com.owncloud.android.domain.UseCaseResult import com.owncloud.android.domain.exceptions.NoNetworkConnectionException import com.owncloud.android.domain.files.model.OCFile import com.owncloud.android.domain.files.usecases.MoveFileUseCase import com.owncloud.android.domain.files.usecases.RemoveFileUseCase +import com.owncloud.android.domain.files.usecases.RenameFileUseCase import com.owncloud.android.domain.utils.Event import com.owncloud.android.presentation.UIResult import com.owncloud.android.providers.ContextProvider @@ -38,72 +40,82 @@ import timber.log.Timber class FileOperationViewModel( private val moveFileUseCase: MoveFileUseCase, private val removeFileUseCase: RemoveFileUseCase, + private val renameFileUseCase: RenameFileUseCase, private val contextProvider: ContextProvider, private val coroutinesDispatcherProvider: CoroutinesDispatcherProvider ) : ViewModel() { + private val _moveFileLiveData = MediatorLiveData>>() + val moveFileLiveData: LiveData>> = _moveFileLiveData + private val _removeFileLiveData = MediatorLiveData>>>() val removeFileLiveData: LiveData>>> = _removeFileLiveData - private val _moveFileLiveData = MediatorLiveData>>() - val moveFileLiveData: LiveData>> = _moveFileLiveData + private val _renameFileLiveData = MediatorLiveData>>() + val renameFileLiveData: LiveData>> = _renameFileLiveData fun performOperation(fileOperation: FileOperation) { when (fileOperation) { is FileOperation.MoveOperation -> moveOperation(fileOperation) is FileOperation.RemoveOperation -> removeOperation(fileOperation) + is FileOperation.RenameOperation -> renameOperation(fileOperation) } } private fun moveOperation(fileOperation: FileOperation.MoveOperation) { - viewModelScope.launch(coroutinesDispatcherProvider.io) { - _moveFileLiveData.postValue(Event(UIResult.Loading())) - - if (!contextProvider.isConnected()) { - _moveFileLiveData.postValue(Event(UIResult.Error(error = NoNetworkConnectionException()))) - Timber.w("${moveFileUseCase.javaClass.simpleName} will not be executed due to lack of network connection") - return@launch - } - val useCaseResult = - moveFileUseCase.execute(MoveFileUseCase.Params(fileOperation.listOfFilesToMove, fileOperation.targetFolder)) - - Timber.d("Use case executed: ${moveFileUseCase.javaClass.simpleName} with result: $useCaseResult") + runOperation( + liveData = _moveFileLiveData, + useCase = moveFileUseCase, + useCaseParams = MoveFileUseCase.Params(fileOperation.listOfFilesToMove, fileOperation.targetFolder), + postValue = fileOperation.targetFolder + ) + } - when (useCaseResult) { - is UseCaseResult.Success -> { - _moveFileLiveData.postValue(Event(UIResult.Success(fileOperation.targetFolder))) - } - is UseCaseResult.Error -> { - _moveFileLiveData.postValue(Event(UIResult.Error(error = useCaseResult.throwable))) - } - } + private fun removeOperation(fileOperation: FileOperation.RemoveOperation) { + runOperation( + liveData = _removeFileLiveData, + useCase = removeFileUseCase, + useCaseParams = RemoveFileUseCase.Params(fileOperation.listOfFilesToRemove, fileOperation.removeOnlyLocalCopy), + postValue = fileOperation.listOfFilesToRemove + ) + } - } + private fun renameOperation(fileOperation: FileOperation.RenameOperation) { + runOperation( + liveData = _renameFileLiveData, + useCase = renameFileUseCase, + useCaseParams = RenameFileUseCase.Params(fileOperation.ocFileToRename, fileOperation.newName), + postValue = fileOperation.ocFileToRename + ) } - private fun removeOperation(fileOperation: FileOperation.RemoveOperation) { + private fun runOperation( + liveData: MediatorLiveData>>, + useCase: BaseUseCaseWithResult, + useCaseParams: Params, + postValue: PostResult? = null + ) { viewModelScope.launch(coroutinesDispatcherProvider.io) { - _removeFileLiveData.postValue(Event(UIResult.Loading())) + liveData.postValue(Event(UIResult.Loading())) if (!contextProvider.isConnected()) { - _removeFileLiveData.postValue(Event(UIResult.Error(error = NoNetworkConnectionException()))) - Timber.w("${removeFileUseCase.javaClass.simpleName} will not be executed due to lack of network connection") + liveData.postValue(Event(UIResult.Error(error = NoNetworkConnectionException()))) + Timber.w("${useCase.javaClass.simpleName} will not be executed due to lack of network connection") return@launch } - val useCaseResult = - removeFileUseCase.execute(RemoveFileUseCase.Params(fileOperation.listOfFilesToRemove, fileOperation.removeOnlyLocalCopy)) - Timber.d("Use case executed: ${removeFileUseCase.javaClass.simpleName} with result: $useCaseResult") + val useCaseResult = useCase.execute(useCaseParams).also { + Timber.d("Use case executed: ${useCase.javaClass.simpleName} with result: $it") + } when (useCaseResult) { is UseCaseResult.Success -> { - _removeFileLiveData.postValue(Event(UIResult.Success(fileOperation.listOfFilesToRemove))) + liveData.postValue(Event(UIResult.Success(postValue))) } is UseCaseResult.Error -> { - _removeFileLiveData.postValue(Event(UIResult.Error(error = useCaseResult.throwable))) + liveData.postValue(Event(UIResult.Error(error = useCaseResult.throwable))) } } - } } } diff --git a/owncloudApp/src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.kt b/owncloudApp/src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.kt index 51284e1ad37..fbba5639b22 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/providers/DocumentsStorageProvider.kt @@ -46,12 +46,12 @@ import com.owncloud.android.domain.files.model.OCFile import com.owncloud.android.domain.files.usecases.CreateFolderAsyncUseCase import com.owncloud.android.domain.files.usecases.MoveFileUseCase import com.owncloud.android.domain.files.usecases.RemoveFileUseCase +import com.owncloud.android.domain.files.usecases.RenameFileUseCase import com.owncloud.android.files.services.FileUploader import com.owncloud.android.files.services.TransferRequester import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.operations.CopyFileOperation import com.owncloud.android.operations.RefreshFolderOperation -import com.owncloud.android.operations.RenameFileOperation import com.owncloud.android.operations.SynchronizeFileOperation import com.owncloud.android.operations.UploadFileOperation import com.owncloud.android.providers.cursors.FileCursor @@ -309,13 +309,12 @@ class DocumentsStorageProvider : DocumentsProvider() { val file = getFileByIdOrException(docId) - RenameFileOperation(file.remotePath, displayName).apply { - execute(currentStorageManager, context).also { - checkOperationResult( - it, - file.parentId.toString() - ) - } + val renameFileUseCase: RenameFileUseCase by inject() + renameFileUseCase.execute(RenameFileUseCase.Params(file, displayName)).also { + checkUseCaseResult( + it, + file.parentId.toString() + ) } return null diff --git a/owncloudApp/src/main/java/com/owncloud/android/services/OperationsService.java b/owncloudApp/src/main/java/com/owncloud/android/services/OperationsService.java index 3fa578cf9e2..b84888b152a 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/services/OperationsService.java +++ b/owncloudApp/src/main/java/com/owncloud/android/services/OperationsService.java @@ -49,7 +49,6 @@ import com.owncloud.android.lib.resources.status.OwnCloudVersion; import com.owncloud.android.operations.CheckCurrentCredentialsOperation; import com.owncloud.android.operations.CopyFileOperation; -import com.owncloud.android.operations.RenameFileOperation; import com.owncloud.android.operations.SynchronizeFileOperation; import com.owncloud.android.operations.SynchronizeFolderOperation; import com.owncloud.android.operations.common.SyncOperation; @@ -65,7 +64,6 @@ public class OperationsService extends Service { public static final String EXTRA_ACCOUNT = "ACCOUNT"; public static final String EXTRA_SERVER_URL = "SERVER_URL"; public static final String EXTRA_REMOTE_PATH = "REMOTE_PATH"; - public static final String EXTRA_NEWNAME = "NEWNAME"; public static final String EXTRA_NEW_PARENT_PATH = "NEW_PARENT_PATH"; public static final String EXTRA_FILE = "FILE"; public static final String EXTRA_PUSH_ONLY = "PUSH_ONLY"; @@ -73,7 +71,6 @@ public class OperationsService extends Service { public static final String EXTRA_COOKIE = "COOKIE"; - public static final String ACTION_RENAME = "RENAME"; public static final String ACTION_SYNC_FILE = "SYNC_FILE"; public static final String ACTION_SYNC_FOLDER = "SYNC_FOLDER"; public static final String ACTION_COPY_FILE = "COPY_FILE"; @@ -446,14 +443,6 @@ private Pair newOperation(Intent operationIntent) { String action = operationIntent.getAction(); if (action != null) { switch (action) { - case ACTION_RENAME: { - // Rename file or folder - String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH); - String newName = operationIntent.getStringExtra(EXTRA_NEWNAME); - operation = new RenameFileOperation(remotePath, newName); - - break; - } case ACTION_SYNC_FILE: { // Sync file String remotePath = operationIntent.getStringExtra(EXTRA_REMOTE_PATH); diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FileActivity.java b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FileActivity.java index e31a5c015c6..06f4994e45a 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FileActivity.java +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FileActivity.java @@ -48,7 +48,6 @@ 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; -import com.owncloud.android.operations.RenameFileOperation; import com.owncloud.android.operations.SynchronizeFileOperation; import com.owncloud.android.operations.SynchronizeFolderOperation; import com.owncloud.android.presentation.ui.authentication.AuthenticatorConstants; @@ -297,8 +296,6 @@ public void onRemoteOperationFinish(RemoteOperation operation, RemoteOperationRe } else if (operation instanceof SynchronizeFileOperation) { onSynchronizeFileOperationFinish((SynchronizeFileOperation) operation, result); - } else if (operation instanceof RenameFileOperation && result.isSuccess()) { - result.getData(); } } diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt index 94891da5fb7..130a69f5f33 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt @@ -77,7 +77,6 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCo import com.owncloud.android.lib.resources.status.OwnCloudVersion import com.owncloud.android.operations.CopyFileOperation import com.owncloud.android.operations.RefreshFolderOperation -import com.owncloud.android.operations.RenameFileOperation import com.owncloud.android.operations.SynchronizeFileOperation import com.owncloud.android.operations.UploadFileOperation import com.owncloud.android.presentation.UIResult @@ -1182,7 +1181,6 @@ class FileDisplayActivity : FileActivity(), FileFragment.ContainerActivity, OnEn super.onRemoteOperationFinish(operation, result) when (operation) { - is RenameFileOperation -> onRenameFileOperationFinish(operation, result) is SynchronizeFileOperation -> onSynchronizeFileOperationFinish(operation, result) is CopyFileOperation -> onCopyFileOperationFinish(operation, result) } @@ -1293,36 +1291,40 @@ class FileDisplayActivity : FileActivity(), FileFragment.ContainerActivity, OnEn * Updates the view associated to the activity after the finish of an operation trying to rename * a file. * - * @param operation Renaming operation performed. - * @param result Result of the renaming. + * @param uiResult - UIResult wrapping the file that was renamed */ private fun onRenameFileOperationFinish( - operation: RenameFileOperation, - result: RemoteOperationResult<*> + uiResult: UIResult ) { - var renamedFile: OCFile? = operation.file - if (result.isSuccess) { - val details = secondFragment - if (details != null && renamedFile == details.file) { - renamedFile = storageManager.getFileById(renamedFile!!.id!!) - file = renamedFile - details.onFileMetadataChanged(renamedFile) - updateToolbar(renamedFile) + when (uiResult) { + is UIResult.Loading -> { + showLoadingDialog(R.string.wait_a_moment) } + is UIResult.Success -> { + dismissLoadingDialog() - if (storageManager.getFileById(renamedFile!!.parentId!!) == currentDir) { - refreshListOfFilesFragment(true) + val details = secondFragment + uiResult.data?.id?.let { fileId -> + if (details != null && uiResult.data == details.file) { + val updatedRenamedFile = storageManager.getFileById(fileId) + file = updatedRenamedFile + details.onFileMetadataChanged(updatedRenamedFile) + updateToolbar(updatedRenamedFile) + } + } + + if (uiResult.data?.parentId == currentDir.id) { + refreshListOfFilesFragment(true) + } } + is UIResult.Error -> { + dismissLoadingDialog() - } else { - showMessageInSnackbar( - R.id.list_layout, - ErrorMessageAdapter.getResultMessage(result, operation, resources) - ) + showErrorInSnackbar(R.string.rename_server_fail_msg, uiResult.error) - if (result.isSslRecoverableException) { - lastSslUntrustedServerResult = result - showUntrustedCertDialog(lastSslUntrustedServerResult) + if (uiResult.getThrowableOrNull() is SSLRecoverablePeerUnverifiedException) { + showUntrustedCertDialogForThrowable(uiResult.getThrowableOrNull()) + } } } } @@ -1628,11 +1630,14 @@ class FileDisplayActivity : FileActivity(), FileFragment.ContainerActivity, OnEn } private fun startListeningToOperations() { + fileOperationViewModel.moveFileLiveData.observe(this, Event.EventObserver { + onMoveFileOperationFinish(it) + }) fileOperationViewModel.removeFileLiveData.observe(this, Event.EventObserver { onRemoveFileOperationResult(it) }) - fileOperationViewModel.moveFileLiveData.observe(this, Event.EventObserver { - onMoveFileOperationFinish(it) + fileOperationViewModel.renameFileLiveData.observe(this, Event.EventObserver { + onRenameFileOperationFinish(it) }) } diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.java b/owncloudApp/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.java deleted file mode 100644 index f41d711b1d3..00000000000 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.java +++ /dev/null @@ -1,148 +0,0 @@ -/** - * ownCloud Android client application - * - * @author David A. Velasco - * @author Christian Schabesberger - * @author David González Verdugo - * Copyright (C) 2020 ownCloud GmbH. - *

- * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License version 2, - * as published by the Free Software Foundation. - *

- * 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 General Public License for more details. - *

- * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.owncloud.android.ui.dialog; - -/** - * Dialog to input a new name for an {@link OCFile} being renamed. - *

- * Triggers the rename operation. - */ - -import android.app.Dialog; -import android.content.DialogInterface; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.WindowManager.LayoutParams; -import android.widget.EditText; -import android.widget.TextView; - -import androidx.appcompat.app.AlertDialog; -import androidx.fragment.app.DialogFragment; -import com.google.android.material.snackbar.Snackbar; -import com.owncloud.android.R; -import com.owncloud.android.domain.files.model.OCFile; -import com.owncloud.android.lib.resources.files.FileUtils; -import com.owncloud.android.ui.activity.ComponentsGetter; -import com.owncloud.android.utils.PreferenceUtils; - -/** - * Dialog to input a new name for a file or folder to rename. - * - * Triggers the rename operation when name is confirmed. - */ -public class RenameFileDialogFragment - extends DialogFragment implements DialogInterface.OnClickListener { - - private static final String ARG_TARGET_FILE = "TARGET_FILE"; - - /** - * Public factory method to create new RenameFileDialogFragment instances. - * - * @param file File to rename. - * @return Dialog ready to show. - */ - public static RenameFileDialogFragment newInstance(OCFile file) { - RenameFileDialogFragment frag = new RenameFileDialogFragment(); - Bundle args = new Bundle(); - args.putParcelable(ARG_TARGET_FILE, file); - frag.setArguments(args); - return frag; - - } - - private OCFile mTargetFile; - - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - mTargetFile = getArguments().getParcelable(ARG_TARGET_FILE); - - // Inflate the layout for the dialog - LayoutInflater inflater = getActivity().getLayoutInflater(); - View v = inflater.inflate(R.layout.edit_box_dialog, null); - - // Allow or disallow touches with other visible windows - v.setFilterTouchesWhenObscured( - PreferenceUtils.shouldDisallowTouchesWithOtherVisibleWindows(getContext()) - ); - - // Setup layout - String currentName = mTargetFile.getFileName(); - EditText inputText = v.findViewById(R.id.user_input); - inputText.setText(currentName); - int selectionStart = 0; - int extensionStart = mTargetFile.isFolder() ? -1 : currentName.lastIndexOf("."); - int selectionEnd = (extensionStart >= 0) ? extensionStart : currentName.length(); - if (selectionStart >= 0 && selectionEnd >= 0) { - inputText.setSelection( - Math.min(selectionStart, selectionEnd), - Math.max(selectionStart, selectionEnd)); - } - inputText.requestFocus(); - - // Build the dialog - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setView(v) - .setPositiveButton(android.R.string.ok, this) - .setNegativeButton(android.R.string.cancel, this) - .setTitle(R.string.rename_dialog_title); - Dialog d = builder.create(); - d.getWindow().setSoftInputMode(LayoutParams.SOFT_INPUT_STATE_VISIBLE); - return d; - } - - @Override - public void onClick(DialogInterface dialog, int which) { - if (which == AlertDialog.BUTTON_POSITIVE) { - String newFileName = - ((TextView) (getDialog().findViewById(R.id.user_input))) - .getText().toString().trim(); - - if (newFileName.length() <= 0) { - showSnackMessage(R.string.filename_empty); - return; - } - - if (!FileUtils.isValidName(newFileName)) { - showSnackMessage(R.string.filename_forbidden_charaters_from_server); - return; - } - - ((ComponentsGetter) getActivity()).getFileOperationsHelper(). - renameFile(mTargetFile, newFileName); - } - } - - /** - * Show a temporary message in a Snackbar bound to the content view of the parent Activity - * - * @param messageResource Message to show. - */ - private void showSnackMessage(int messageResource) { - Snackbar snackbar = Snackbar.make( - getActivity().findViewById(android.R.id.content), - messageResource, - Snackbar.LENGTH_LONG - ); - snackbar.show(); - } -} diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.kt b/owncloudApp/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.kt new file mode 100644 index 00000000000..aa0392e0578 --- /dev/null +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/dialog/RenameFileDialogFragment.kt @@ -0,0 +1,132 @@ +/** + * ownCloud Android client application + * + * @author David A. Velasco + * @author Christian Schabesberger + * @author David González Verdugo + * Copyright (C) 2021 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see //www.gnu.org/licenses/>. + */ +package com.owncloud.android.ui.dialog + +import android.app.Dialog +import android.content.DialogInterface +import android.os.Bundle +import android.view.View +import android.view.WindowManager +import android.widget.EditText +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import com.owncloud.android.R +import com.owncloud.android.domain.files.model.OCFile +import com.owncloud.android.extensions.showMessageInSnackbar +import com.owncloud.android.lib.resources.files.FileUtils +import com.owncloud.android.presentation.ui.files.operations.FileOperation +import com.owncloud.android.presentation.ui.files.operations.FileOperationViewModel +import com.owncloud.android.utils.PreferenceUtils +import org.koin.androidx.viewmodel.ext.android.sharedViewModel + +/** + * Dialog to input a new name for a file or folder to rename. + * + * Triggers the rename operation when name is confirmed. + */ +class RenameFileDialogFragment : DialogFragment(), DialogInterface.OnClickListener { + + private var targetFile: OCFile? = null + private val filesViewModel: FileOperationViewModel by sharedViewModel() + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + targetFile = requireArguments().getParcelable(ARG_TARGET_FILE) + + // Inflate the layout for the dialog + val inflater = requireActivity().layoutInflater + val view = inflater.inflate(R.layout.edit_box_dialog, null) + + // Allow or disallow touches with other visible windows + view.filterTouchesWhenObscured = + PreferenceUtils.shouldDisallowTouchesWithOtherVisibleWindows(context) + + // Setup layout + val currentName = targetFile!!.fileName + val inputText = view.findViewById(R.id.user_input) + inputText.setText(currentName) + val selectionStart = 0 + val extensionStart = if (targetFile!!.isFolder) -1 else currentName.lastIndexOf(".") + val selectionEnd = if (extensionStart >= 0) extensionStart else currentName.length + if (selectionStart >= 0 && selectionEnd >= 0) { + inputText.setSelection( + selectionStart.coerceAtMost(selectionEnd), + selectionStart.coerceAtLeast(selectionEnd) + ) + } + inputText.requestFocus() + + // Build the dialog + return AlertDialog.Builder(requireActivity()).apply { + setView(view) + setPositiveButton(android.R.string.ok, this@RenameFileDialogFragment) + setNegativeButton(android.R.string.cancel, this@RenameFileDialogFragment) + setTitle(R.string.rename_dialog_title) + }.create().apply { + window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE) + } + } + + override fun onClick(dialog: DialogInterface, which: Int) { + if (which == AlertDialog.BUTTON_POSITIVE) { + // These checks are done in the RenameFileUseCase too, we could remove them too. + val newFileName = (getDialog()!!.findViewById(R.id.user_input) as TextView).text.toString() + if (newFileName.isBlank()) { + showSnackMessage(R.string.filename_empty) + return + } + if (!FileUtils.isValidName(newFileName)) { + showSnackMessage(R.string.filename_forbidden_charaters_from_server) + return + } + filesViewModel.performOperation(FileOperation.RenameOperation(targetFile!!, newFileName)) + } + } + + /** + * Show a temporary message in a Snackbar bound to the content view of the parent Activity + * + * @param messageResource Message to show. + */ + private fun showSnackMessage(messageResource: Int) { + showMessageInSnackbar( + message = getString(messageResource) + ) + } + + companion object { + private const val ARG_TARGET_FILE = "TARGET_FILE" + + /** + * Public factory method to create new RenameFileDialogFragment instances. + * + * @param file File to rename. + * @return Dialog ready to show. + */ + @JvmStatic + fun newInstance(file: OCFile): RenameFileDialogFragment { + val args = Bundle().apply { + putParcelable(ARG_TARGET_FILE, file) + } + return RenameFileDialogFragment().apply { arguments = args } + } + } +} diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/errorhandling/ErrorMessageAdapter.kt b/owncloudApp/src/main/java/com/owncloud/android/ui/errorhandling/ErrorMessageAdapter.kt index bdb141c93a4..3fcc417e1cf 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/errorhandling/ErrorMessageAdapter.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/errorhandling/ErrorMessageAdapter.kt @@ -31,7 +31,6 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode import com.owncloud.android.operations.CopyFileOperation import com.owncloud.android.operations.CreateFolderOperation -import com.owncloud.android.operations.RenameFileOperation import com.owncloud.android.operations.SynchronizeFileOperation import com.owncloud.android.operations.SynchronizeFolderOperation import com.owncloud.android.operations.UploadFileOperation @@ -144,7 +143,6 @@ class ErrorMessageAdapter { R.string.forbidden_permissions, R.string.uploader_upload_forbidden_permissions ) - if (operation is RenameFileOperation) formatter.forbidden(R.string.forbidden_permissions_rename) if (operation is CreateFolderOperation) formatter.forbidden(R.string.forbidden_permissions_create) if (operation is CopyFileOperation) formatter.forbidden(R.string.forbidden_permissions_copy) else formatter.format( R.string.filename_forbidden_charaters_from_server @@ -157,7 +155,6 @@ class ErrorMessageAdapter { ResultCode.FILE_NOT_FOUND -> { if (operation is UploadFileOperation) formatter.format(R.string.uploads_view_upload_status_failed_folder_error) - if (operation is RenameFileOperation) formatter.format(R.string.rename_server_fail_msg) if (operation is SynchronizeFolderOperation) formatter.format( R.string.sync_current_folder_was_removed, @@ -239,7 +236,6 @@ class ErrorMessageAdapter { return when (operation) { is UploadFileOperation -> formatter.format(R.string.uploader_upload_failed_content_single, operation.fileName) - is RenameFileOperation -> formatter.format(R.string.rename_server_fail_msg) is CreateFolderOperation -> formatter.format(R.string.create_dir_fail_msg) is SynchronizeFolderOperation -> formatter.format( R.string.sync_folder_failed_content, diff --git a/owncloudApp/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java b/owncloudApp/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java index 55494331eae..9378b3a4040 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java +++ b/owncloudApp/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java @@ -291,18 +291,6 @@ public void toggleAvailableOffline(OCFile file, boolean isAvailableOffline) { // } } - public void renameFile(OCFile file, String newFilename) { - // RenameFile - Intent service = new Intent(mFileActivity, OperationsService.class); - service.setAction(OperationsService.ACTION_RENAME); - service.putExtra(OperationsService.EXTRA_ACCOUNT, mFileActivity.getAccount()); - service.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath()); - service.putExtra(OperationsService.EXTRA_NEWNAME, newFilename); - mWaitingForOpId = mFileActivity.getOperationsServiceBinder().queueNewOperation(service); - - mFileActivity.showLoadingDialog(R.string.wait_a_moment); - } - /** * Cancel the transference in downloads (files/folders) and file uploads * diff --git a/owncloudData/src/main/java/com/owncloud/android/data/LocalStorageProvider.kt b/owncloudData/src/main/java/com/owncloud/android/data/LocalStorageProvider.kt index e8c95016719..880c0f15b15 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/LocalStorageProvider.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/LocalStorageProvider.kt @@ -44,6 +44,19 @@ class LocalStorageProvider( */ fun getDefaultSavePathFor(accountName: String?, remotePath: String): String = getSavePath(accountName) + remotePath + /** + * Get expected remote path for a file creation, rename, move etc + */ + fun getExpectedRemotePath(remotePath: String, newName: String, isFolder: Boolean): String { + var parent = (File(remotePath)).parent ?: throw IllegalArgumentException() + parent = if (parent.endsWith(File.separator)) parent else parent + File.separator + val newRemotePath = parent + newName + if (isFolder) { + newRemotePath.plus(File.separator) + } + return newRemotePath + } + /** * Get absolute path to tmp folder inside datafolder in sd-card for given accountName. */ diff --git a/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/LocalFileDataSource.kt b/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/LocalFileDataSource.kt index c1cee5b300b..93b8d1e1e5b 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/LocalFileDataSource.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/LocalFileDataSource.kt @@ -31,4 +31,5 @@ interface LocalFileDataSource { fun saveFilesInFolder(listOfFiles: List, folder: OCFile) fun saveFile(file: OCFile) fun removeFile(fileId: Long) + fun renameFile(fileToRename: OCFile, finalRemotePath: String, finalStoragePath: String) } diff --git a/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/RemoteFileDataSource.kt b/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/RemoteFileDataSource.kt index 25ce655b0d3..7fe4e283de0 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/RemoteFileDataSource.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/RemoteFileDataSource.kt @@ -2,7 +2,7 @@ * ownCloud Android client application * * @author Abel García de Prada - * Copyright (C) 2020 ownCloud GmbH. + * Copyright (C) 2021 ownCloud GmbH. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2, @@ -22,15 +22,33 @@ package com.owncloud.android.data.files.datasources import com.owncloud.android.domain.files.model.OCFile interface RemoteFileDataSource { - fun checkPathExistence(path: String, checkUserCredentials: Boolean): Boolean + fun checkPathExistence( + path: String, + checkUserCredentials: Boolean + ): Boolean - fun createFolder(remotePath: String, createFullPath: Boolean, isChunksFolder: Boolean) + fun createFolder( + remotePath: String, + createFullPath: Boolean, + isChunksFolder: Boolean + ) fun getAvailableRemotePath(remotePath: String): String fun moveFile(sourceRemotePath: String, targetRemotePath: String) - fun refreshFolder(remotePath: String): List + fun refreshFolder( + remotePath: String + ): List - fun removeFile(remotePath: String) + fun removeFile( + remotePath: String + ) + + fun renameFile( + oldName: String, + oldRemotePath: String, + newName: String, + isFolder: Boolean, + ) } diff --git a/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/implementation/OCLocalFileDataSource.kt b/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/implementation/OCLocalFileDataSource.kt index 0ec9fff7585..4c0dfde6b5e 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/implementation/OCLocalFileDataSource.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/implementation/OCLocalFileDataSource.kt @@ -91,4 +91,13 @@ class OCLocalFileDataSource( override fun removeFile(fileId: Long) { fileDao.deleteFileWithId(fileId) } + + override fun renameFile(fileToRename: OCFile, finalRemotePath: String, finalStoragePath: String) { + fileDao.moveFile( + sourceFile = ocFileMapper.toEntity(fileToRename)!!, + targetFile = fileDao.getFileById(fileToRename.parentId!!)!!, + finalRemotePath = finalRemotePath, + finalStoragePath = fileToRename.storagePath?.let { finalStoragePath } + ) + } } diff --git a/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/implementation/OCRemoteFileDataSource.kt b/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/implementation/OCRemoteFileDataSource.kt index 9c101a6a318..cb7b87d9a9c 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/implementation/OCRemoteFileDataSource.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/files/datasources/implementation/OCRemoteFileDataSource.kt @@ -42,7 +42,7 @@ class OCRemoteFileDataSource( remotePath: String, createFullPath: Boolean, isChunksFolder: Boolean - ): Unit = executeRemoteOperation { + ) = executeRemoteOperation { clientManager.getFileService().createFolder( remotePath = remotePath, createFullPath = createFullPath, @@ -99,7 +99,9 @@ class OCRemoteFileDataSource( ) } - override fun refreshFolder(remotePath: String): List = + override fun refreshFolder( + remotePath: String + ): List = // Assert not null, service should return an empty list if no files there. executeRemoteOperation { clientManager.getFileService().refreshFolder( @@ -109,10 +111,25 @@ class OCRemoteFileDataSource( listOfRemote.map { remoteFile -> remoteFileMapper.toModel(remoteFile)!! } } - override fun removeFile(remotePath: String) = - executeRemoteOperation { - clientManager.getFileService().removeFile( - remotePath = remotePath - ) - } + override fun removeFile( + remotePath: String + ) = executeRemoteOperation { + clientManager.getFileService().removeFile( + remotePath = remotePath + ) + } + + override fun renameFile( + oldName: String, + oldRemotePath: String, + newName: String, + isFolder: Boolean + ) = executeRemoteOperation { + clientManager.getFileService().renameFile( + oldName = oldName, + oldRemotePath = oldRemotePath, + newName = newName, + isFolder = isFolder + ) + } } diff --git a/owncloudData/src/main/java/com/owncloud/android/data/files/repository/OCFileRepository.kt b/owncloudData/src/main/java/com/owncloud/android/data/files/repository/OCFileRepository.kt index 01c681af389..f2b19613fa4 100644 --- a/owncloudData/src/main/java/com/owncloud/android/data/files/repository/OCFileRepository.kt +++ b/owncloudData/src/main/java/com/owncloud/android/data/files/repository/OCFileRepository.kt @@ -24,6 +24,7 @@ import com.owncloud.android.data.LocalStorageProvider import com.owncloud.android.data.files.datasources.LocalFileDataSource import com.owncloud.android.data.files.datasources.RemoteFileDataSource import com.owncloud.android.domain.exceptions.ConflictException +import com.owncloud.android.domain.exceptions.FileAlreadyExistsException import com.owncloud.android.domain.exceptions.FileNotFoundException import com.owncloud.android.domain.files.FileRepository import com.owncloud.android.domain.files.model.MIME_DIR @@ -147,6 +148,41 @@ class OCFileRepository( } } + override fun renameFile(ocFile: OCFile, newName: String) { + // 1. Compose new remote path + val newRemotePath = localStorageProvider.getExpectedRemotePath( + remotePath = ocFile.remotePath, + newName = newName, + isFolder = ocFile.isFolder + ) + + // 2. Check if file already exists in database + if (localFileDataSource.getFileByRemotePath(newRemotePath, ocFile.owner) != null) { + throw FileAlreadyExistsException() + } + + // 3. Perform remote operation + remoteFileDataSource.renameFile( + oldName = ocFile.fileName, + oldRemotePath = ocFile.remotePath, + newName = newName, + isFolder = ocFile.isFolder + ) + + // 4. Save new remote path in the local database + localFileDataSource.renameFile( + fileToRename = ocFile, + finalRemotePath = newRemotePath, + finalStoragePath = localStorageProvider.getDefaultSavePathFor(ocFile.owner, newRemotePath) + ) + + // 5. Update local storage + localStorageProvider.moveLocalFile( + ocFile = ocFile, + finalStoragePath = localStorageProvider.getDefaultSavePathFor(ocFile.owner, newRemotePath) + ) + } + override fun saveFile(file: OCFile) { localFileDataSource.saveFile(file) } diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/exceptions/FileAlreadyExistsException.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/exceptions/FileAlreadyExistsException.kt new file mode 100644 index 00000000000..3b71d9b137b --- /dev/null +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/exceptions/FileAlreadyExistsException.kt @@ -0,0 +1,24 @@ +/** + * ownCloud Android client application + * + * @author David González Verdugo + * Copyright (C) 2020 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.domain.exceptions + +import java.lang.Exception + +class FileAlreadyExistsException : Exception() diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/files/FileRepository.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/files/FileRepository.kt index d49297c93ea..c015a8c76d1 100644 --- a/owncloudDomain/src/main/java/com/owncloud/android/domain/files/FileRepository.kt +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/files/FileRepository.kt @@ -31,5 +31,6 @@ interface FileRepository { fun moveFile(listOfFilesToMove: List, targetFile: OCFile) fun refreshFolder(remotePath: String) fun removeFile(listOfFilesToRemove: List, removeOnlyLocalCopy: Boolean) + fun renameFile(ocFile: OCFile, newName: String) fun saveFile(file: OCFile) } diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/files/usecases/CreateFolderAsyncUseCase.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/files/usecases/CreateFolderAsyncUseCase.kt index 24080e47172..112ee787b6c 100644 --- a/owncloudDomain/src/main/java/com/owncloud/android/domain/files/usecases/CreateFolderAsyncUseCase.kt +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/files/usecases/CreateFolderAsyncUseCase.kt @@ -19,9 +19,6 @@ package com.owncloud.android.domain.files.usecases import com.owncloud.android.domain.BaseUseCaseWithResult -import com.owncloud.android.domain.exceptions.validation.FileNameException -import com.owncloud.android.domain.exceptions.validation.FileNameException.FileNameExceptionType.FILE_NAME_EMPTY -import com.owncloud.android.domain.exceptions.validation.FileNameException.FileNameExceptionType.FILE_NAME_FORBIDDEN_CHARACTERS import com.owncloud.android.domain.files.FileRepository import com.owncloud.android.domain.files.model.OCFile import com.owncloud.android.domain.validator.FileNameValidator @@ -33,17 +30,11 @@ class CreateFolderAsyncUseCase( private val fileNameValidator = FileNameValidator() override fun run(params: Params) { + fileNameValidator.validateOrThrowException(params.folderName) - val folderNameTrimmed = params.folderName.trim() - - if (folderNameTrimmed.isBlank()) throw FileNameException(type = FILE_NAME_EMPTY) - - if (!fileNameValidator.validate(folderNameTrimmed)) throw FileNameException(type = FILE_NAME_FORBIDDEN_CHARACTERS) - - val remotePath = params.parentFile.remotePath.plus(folderNameTrimmed).plus(OCFile.PATH_SEPARATOR) + val remotePath = params.parentFile.remotePath.plus(params.folderName).plus(OCFile.PATH_SEPARATOR) return fileRepository.createFolder(remotePath = remotePath, parentFolder = params.parentFile) } data class Params(val folderName: String, val parentFile: OCFile) - } diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/files/usecases/RenameFileUseCase.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/files/usecases/RenameFileUseCase.kt new file mode 100644 index 00000000000..226d99eaa10 --- /dev/null +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/files/usecases/RenameFileUseCase.kt @@ -0,0 +1,45 @@ +/** + * ownCloud Android client application + * + * @author Abel García de Prada + * Copyright (C) 2021 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.owncloud.android.domain.files.usecases + +import com.owncloud.android.domain.BaseUseCaseWithResult +import com.owncloud.android.domain.files.FileRepository +import com.owncloud.android.domain.files.model.OCFile +import com.owncloud.android.domain.validator.FileNameValidator + +class RenameFileUseCase( + private val fileRepository: FileRepository +) : BaseUseCaseWithResult() { + + private val fileNameValidator = FileNameValidator() + + override fun run(params: Params) { + fileNameValidator.validateOrThrowException(params.newName) + + return fileRepository.renameFile( + ocFile = params.ocFileToRename, + newName = params.newName, + ) + } + + data class Params( + val ocFileToRename: OCFile, + val newName: String + ) +} diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/validator/FileNameValidator.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/validator/FileNameValidator.kt index 2e3f88bae94..40ac9166f4e 100644 --- a/owncloudDomain/src/main/java/com/owncloud/android/domain/validator/FileNameValidator.kt +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/validator/FileNameValidator.kt @@ -18,11 +18,19 @@ */ package com.owncloud.android.domain.validator +import com.owncloud.android.domain.exceptions.validation.FileNameException import java.util.regex.Pattern class FileNameValidator { - fun validate(string: String): Boolean = !FILE_NAME_REGEX.containsMatchIn(string) + @Throws(FileNameException::class) + fun validateOrThrowException(string: String) { + if (string.trim().isBlank()) { + throw FileNameException(type = FileNameException.FileNameExceptionType.FILE_NAME_EMPTY) + } else if (FILE_NAME_REGEX.containsMatchIn(string)) { + throw FileNameException(type = FileNameException.FileNameExceptionType.FILE_NAME_FORBIDDEN_CHARACTERS) + } + } companion object { // Regex to check both slashes '/' and '\' diff --git a/owncloudDomain/src/test/java/com/owncloud/android/domain/files/usecases/RenameFileUseCaseTest.kt b/owncloudDomain/src/test/java/com/owncloud/android/domain/files/usecases/RenameFileUseCaseTest.kt new file mode 100644 index 00000000000..9de11908111 --- /dev/null +++ b/owncloudDomain/src/test/java/com/owncloud/android/domain/files/usecases/RenameFileUseCaseTest.kt @@ -0,0 +1,60 @@ +/** + * ownCloud Android client application + * + * @author Abel García de Prada + * Copyright (C) 2021 ownCloud GmbH. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.owncloud.android.domain.files.usecases + +import com.owncloud.android.domain.exceptions.UnauthorizedException +import com.owncloud.android.domain.files.FileRepository +import com.owncloud.android.testutil.OC_FILE +import com.owncloud.android.testutil.OC_FOLDER +import io.mockk.every +import io.mockk.spyk +import io.mockk.verify +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class RenameFileUseCaseTest { + private val repository: FileRepository = spyk() + private val useCase = RenameFileUseCase(repository) + private val useCaseParams = RenameFileUseCase.Params(OC_FILE, "Video.mp4") + + @Test + fun `rename file - ok`() { + every { repository.renameFile(any(), any()) } returns Unit + + val useCaseResult = useCase.execute(useCaseParams) + + assertTrue(useCaseResult.isSuccess) + assertEquals(Unit, useCaseResult.getDataOrNull()) + + verify(exactly = 1) { repository.renameFile(any(), useCaseParams.newName) } + } + + @Test + fun `rename file - ko - other exception`() { + every { repository.renameFile(any(), any()) } throws UnauthorizedException() + + val useCaseResult = useCase.execute(useCaseParams) + + assertTrue(useCaseResult.isError) + assertTrue(useCaseResult.getThrowableOrNull() is UnauthorizedException) + + verify(exactly = 1) { repository.renameFile(any(), any()) } + } +} diff --git a/owncloudDomain/src/test/java/com/owncloud/android/domain/validator/FileNameValidatorTest.kt b/owncloudDomain/src/test/java/com/owncloud/android/domain/validator/FileNameValidatorTest.kt index 329e5b36f93..0ecb1f1f3da 100644 --- a/owncloudDomain/src/test/java/com/owncloud/android/domain/validator/FileNameValidatorTest.kt +++ b/owncloudDomain/src/test/java/com/owncloud/android/domain/validator/FileNameValidatorTest.kt @@ -19,7 +19,10 @@ package com.owncloud.android.domain.validator -import org.junit.Assert +import com.owncloud.android.domain.exceptions.validation.FileNameException +import com.owncloud.android.domain.exceptions.validation.FileNameException.FileNameExceptionType +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue import org.junit.Test class FileNameValidatorTest { @@ -28,22 +31,45 @@ class FileNameValidatorTest { @Test fun `validate name - ok`() { - Assert.assertTrue(validator.validate("Photos")) + val result = runCatching { validator.validateOrThrowException("Photos") } + assertEquals(Unit, result.getOrNull()) + } + + @Test + fun `validate name - ko - empty`() { + val result = runCatching { validator.validateOrThrowException(" ") } + + validateExceptionAndType(result, FileNameExceptionType.FILE_NAME_EMPTY) } @Test fun `validate name - ko - back slash`() { - Assert.assertFalse(validator.validate("/Photos")) + val result = runCatching { validator.validateOrThrowException("/Photos") } + + validateExceptionAndType(result, FileNameExceptionType.FILE_NAME_FORBIDDEN_CHARACTERS) } @Test fun `validate name - ko - forward slash`() { - Assert.assertFalse(validator.validate("\\Photos")) + val result = runCatching { validator.validateOrThrowException("\\Photos") } + + validateExceptionAndType(result, FileNameExceptionType.FILE_NAME_FORBIDDEN_CHARACTERS) } @Test fun `validate name - ko - both slashes()`() { - Assert.assertFalse(validator.validate("\\Photos/")) + val result = runCatching { validator.validateOrThrowException("\\Photos/") } + + validateExceptionAndType(result, FileNameExceptionType.FILE_NAME_FORBIDDEN_CHARACTERS) } +} +private fun validateExceptionAndType( + result: Result, + type: FileNameExceptionType +) { + with(result.exceptionOrNull()) { + assertTrue(this is FileNameException) + assertEquals(type, (this as FileNameException).type) + } }