diff --git a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt index 593fb4c..d52447d 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/explorers/ExplorerActivity.kt @@ -344,22 +344,29 @@ class ExplorerActivity : BaseExplorerActivity() { invalidateOptionsMenu() } } else if (currentItemAction == ItemsActions.MOVE){ - mapFileForMove(itemsToProcess, itemsToProcess[0].explorerElement.parentPath) - checkPathOverwrite(itemsToProcess, currentDirectoryPath){ items -> - items?.let { - lifecycleScope.launch { - val failedItem = fileOperationService.moveElements(it.toMutableList() as ArrayList) - if (failedItem == null) { - Toast.makeText(this@ExplorerActivity, R.string.move_success, Toast.LENGTH_SHORT).show() - } else { - CustomAlertDialogBuilder(this@ExplorerActivity, themeValue) - .setTitle(R.string.error) - .setMessage(getString(R.string.move_failed, failedItem)) - .setPositiveButton(R.string.ok, null) - .show() - } - setCurrentPath(currentDirectoryPath) + itemsToProcess.forEach { + it.dstPath = PathUtils.pathJoin(currentDirectoryPath, it.explorerElement.name) + it.overwriteConfirmed = false // reset the field in case of a previous cancelled move + } + val toMove = ArrayList(itemsToProcess.size) + val toClean = ArrayList() + prepareFilesForMove( + itemsToProcess, + toMove, + toClean, + ) { + lifecycleScope.launch { + val failedItem = fileOperationService.moveElements(toMove, toClean) + if (failedItem == null) { + Toast.makeText(this@ExplorerActivity, R.string.move_success, Toast.LENGTH_SHORT).show() + } else { + CustomAlertDialogBuilder(this@ExplorerActivity, themeValue) + .setTitle(R.string.error) + .setMessage(getString(R.string.move_failed, failedItem)) + .setPositiveButton(R.string.ok, null) + .show() } + setCurrentPath(currentDirectoryPath) } cancelItemAction() invalidateOptionsMenu() @@ -403,22 +410,85 @@ class ExplorerActivity : BaseExplorerActivity() { } } - private fun mapFileForMove(items: ArrayList, srcDirectoryPath: String): ArrayList { - val newItems = ArrayList() - items.forEach { - if (it.explorerElement.isDirectory){ - if (gocryptfsVolume.pathExists(PathUtils.pathJoin(currentDirectoryPath, PathUtils.getRelativePath(srcDirectoryPath, it.explorerElement.fullPath)))){ - newItems.addAll( - mapFileForMove( - gocryptfsVolume.listDir(it.explorerElement.fullPath).map { e -> OperationFile.fromExplorerElement(e) } as ArrayList, - srcDirectoryPath + /** + * Ask the user what to do if an item would overwrite another item in case of a move. + * + * All [OperationFile] must have a non-null [dstPath][OperationFile.dstPath]. + */ + private fun checkMoveOverwrite(items: List, callback: (List?) -> Unit) { + for (item in items) { + if (gocryptfsVolume.pathExists(item.dstPath!!) && !item.overwriteConfirmed) { + CustomAlertDialogBuilder(this, themeValue) + .setTitle(R.string.warning) + .setMessage( + getString( + if (item.explorerElement.isDirectory) { + R.string.dir_overwrite_question + } else { + R.string.file_overwrite_question + }, + item.dstPath!! ) ) + .setPositiveButton(R.string.yes) {_, _ -> + item.overwriteConfirmed = true + checkMoveOverwrite(items, callback) + } + .setNegativeButton(R.string.no) { _, _ -> + with(EditTextDialog(this, R.string.enter_new_name) { + item.dstPath = PathUtils.pathJoin(PathUtils.getParentPath(item.dstPath!!), it) + checkMoveOverwrite(items, callback) + }) { + setSelectedText(item.explorerElement.name) + show() + } + } + .show() + return + } + } + callback(items) + } + + /** + * Check for destination overwriting in case of a move operation. + * + * If the user decides to merge the content of a folder, the function recursively tests all + * children of the source folder to see if they will overwrite. + * + * The items to be moved are stored in [toMove]. We also need to keep track of the merged + * folders to delete them after the move. These folders are stored in [toClean]. + */ + private fun prepareFilesForMove( + items: List, + toMove: ArrayList, + toClean: ArrayList, + onReady: () -> Unit + ) { + checkMoveOverwrite(items) { checkedItems -> + checkedItems?.let { + for (item in checkedItems) { + if (!item.overwriteConfirmed || !item.explorerElement.isDirectory) { + toMove.add(item) + } + } + val toCheck = mutableListOf() + for (item in checkedItems) { + if (item.overwriteConfirmed && item.explorerElement.isDirectory) { + val children = gocryptfsVolume.listDir(item.explorerElement.fullPath) + toCheck.addAll(children.map { + OperationFile(it, PathUtils.pathJoin(item.dstPath!!, it.name)) + }) + toClean.add(item.explorerElement) + } + } + if (toCheck.isEmpty()) { + onReady() + } else { + prepareFilesForMove(toCheck, toMove, toClean, onReady) } } } - items.addAll(newItems) - return items } private fun cancelItemAction() { diff --git a/app/src/main/java/sushi/hardcore/droidfs/file_operations/FileOperationService.kt b/app/src/main/java/sushi/hardcore/droidfs/file_operations/FileOperationService.kt index 1dddfa8..e8a72a8 100644 --- a/app/src/main/java/sushi/hardcore/droidfs/file_operations/FileOperationService.kt +++ b/app/src/main/java/sushi/hardcore/droidfs/file_operations/FileOperationService.kt @@ -183,32 +183,26 @@ class FileOperationService : Service() { waitForTask(notification, task).failedItem } - suspend fun moveElements(items: ArrayList): String? = coroutineScope { - val notification = showNotification(R.string.file_op_move_msg, items.size) - val task = async { + suspend fun moveElements(toMove: List, toClean: List): String? = coroutineScope { + val notification = showNotification(R.string.file_op_move_msg, toMove.size) + val task = async(Dispatchers.IO) { + val total = toMove.size+toClean.size var failedItem: String? = null - withContext(Dispatchers.IO) { - val mergedFolders = ArrayList() - for (i in 0 until items.size) { - if (items[i].explorerElement.isDirectory && gocryptfsVolume.pathExists(items[i].dstPath!!)) { //folder will be merged - mergedFolders.add(items[i].explorerElement.fullPath) - } else { - if (!gocryptfsVolume.rename(items[i].explorerElement.fullPath, items[i].dstPath!!)) { - failedItem = items[i].explorerElement.fullPath - break - } else { - updateNotificationProgress(notification, i+1, items.size) - } - } + for ((i, item) in toMove.withIndex()) { + if (!gocryptfsVolume.rename(item.explorerElement.fullPath, item.dstPath!!)) { + failedItem = item.explorerElement.fullPath + break + } else { + updateNotificationProgress(notification, i+1, total) } - if (failedItem == null) { - for (i in 0 until mergedFolders.size) { - if (!gocryptfsVolume.rmdir(mergedFolders[i])) { - failedItem = mergedFolders[i] - break - } else { - updateNotificationProgress(notification, items.size-(mergedFolders.size-i), items.size) - } + } + if (failedItem == null) { + for ((i, folder) in toClean.asReversed().withIndex()) { + if (!gocryptfsVolume.rmdir(folder.fullPath)) { + failedItem = folder.fullPath + break + } else { + updateNotificationProgress(notification, toMove.size+i+1, total) } } }