diff --git a/app/src/main/java/com/amaze/filemanager/adapters/RecyclerAdapter.java b/app/src/main/java/com/amaze/filemanager/adapters/RecyclerAdapter.java index bffe1cdd6c..72c262372d 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/RecyclerAdapter.java +++ b/app/src/main/java/com/amaze/filemanager/adapters/RecyclerAdapter.java @@ -47,8 +47,10 @@ import com.amaze.filemanager.adapters.holders.SpecialViewHolder; import com.amaze.filemanager.application.AppConfig; import com.amaze.filemanager.fileoperations.filesystem.OpenMode; +import com.amaze.filemanager.filesystem.PasteHelper; import com.amaze.filemanager.filesystem.files.CryptUtil; import com.amaze.filemanager.ui.ItemPopupMenu; +import com.amaze.filemanager.ui.activities.MainActivity; import com.amaze.filemanager.ui.activities.superclasses.PreferenceActivity; import com.amaze.filemanager.ui.colors.ColorUtils; import com.amaze.filemanager.ui.drag.RecyclerAdapterDragListener; @@ -62,6 +64,7 @@ import com.amaze.filemanager.ui.views.CircleGradientDrawable; import com.amaze.filemanager.utils.AnimUtils; import com.amaze.filemanager.utils.GlideConstants; +import com.amaze.filemanager.utils.MainActivityActionMode; import com.amaze.filemanager.utils.Utils; import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader; import com.bumptech.glide.load.DataSource; @@ -85,6 +88,7 @@ import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.widget.PopupMenu; +import android.widget.Toast; import androidx.annotation.IntDef; import androidx.annotation.NonNull; @@ -763,6 +767,7 @@ private void bindViewHolderList(@NonNull final ItemViewHolder holder, int positi holder.baseItemView.setOnLongClickListener( p1 -> { + if (hasPendingPasteOperation()) return false; if (!isBackButton) { if (dragAndDropPreference == PreferencesConstants.PREFERENCE_DRAG_DEFAULT || (dragAndDropPreference == PreferencesConstants.PREFERENCE_DRAG_TO_MOVE_COPY @@ -976,6 +981,7 @@ private void bindViewHolderGrid(@NonNull final ItemViewHolder holder, int positi holder.baseItemView.setOnLongClickListener( p1 -> { + if (hasPendingPasteOperation()) return false; if (!isBackButton) { if (dragAndDropPreference == PreferencesConstants.PREFERENCE_DRAG_DEFAULT || (dragAndDropPreference == PreferencesConstants.PREFERENCE_DRAG_TO_MOVE_COPY @@ -1366,6 +1372,7 @@ public boolean onResourceReady( } private void showPopup(@NonNull View view, @NonNull final LayoutElementParcelable rowItem) { + if (hasPendingPasteOperation()) return; Context currentContext = this.context; if (mainFragment.getMainActivity().getAppTheme().getSimpleTheme(mainFragment.requireContext()) == AppTheme.BLACK) { @@ -1428,6 +1435,31 @@ private void showPopup(@NonNull View view, @NonNull final LayoutElementParcelabl popupMenu.show(); } + /** + * Helps in deciding whether to allow file modification or not, depending on the state of the + * copy/paste operation. + * + * @return true if there is an unfinished copy/paste operation, false otherwise. + */ + private boolean hasPendingPasteOperation() { + MainActivity mainActivity = mainFragment.getMainActivity(); + if (mainActivity == null) return false; + MainActivityActionMode mainActivityActionMode = mainActivity.mainActivityActionMode; + PasteHelper pasteHelper = mainActivityActionMode.getPasteHelper(); + + if (pasteHelper != null + && pasteHelper.getSnackbar() != null + && pasteHelper.getSnackbar().isShown()) { + Toast.makeText( + mainFragment.requireContext(), + mainFragment.getString(R.string.complete_paste_warning), + Toast.LENGTH_LONG) + .show(); + return true; + } + return false; + } + private boolean getBoolean(String key) { return preferenceActivity.getBoolean(key); } diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/movecopy/MoveFiles.java b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/movecopy/MoveFiles.java index a76c2b3a28..cfcabd66b1 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/movecopy/MoveFiles.java +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/movecopy/MoveFiles.java @@ -47,7 +47,7 @@ /** * AsyncTask that moves files from source to destination by trying to rename files first, if they're * in the same filesystem, else starting the copy service. Be advised - do not start this AsyncTask - * directly but use {@link PrepareCopyTask} instead + * directly but use {@link PreparePasteTask} instead */ public class MoveFiles implements Callable { diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/movecopy/PrepareCopyTask.java b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/movecopy/PrepareCopyTask.java deleted file mode 100644 index 9d06ddb19d..0000000000 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/movecopy/PrepareCopyTask.java +++ /dev/null @@ -1,446 +0,0 @@ -/* - * Copyright (C) 2014-2020 Arpit Khurana , Vishal Nehra , - * Emmanuel Messulam, Raymond Lai and Contributors. - * - * This file is part of Amaze File Manager. - * - * Amaze File Manager is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * 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.amaze.filemanager.asynchronous.asynctasks.movecopy; - -import static com.amaze.filemanager.fileoperations.filesystem.FolderStateKt.CAN_CREATE_FILES; -import static com.amaze.filemanager.fileoperations.filesystem.OperationTypeKt.COPY; -import static com.amaze.filemanager.fileoperations.filesystem.OperationTypeKt.MOVE; - -import java.io.File; -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.Set; - -import com.afollestad.materialdialogs.DialogAction; -import com.afollestad.materialdialogs.MaterialDialog; -import com.amaze.filemanager.R; -import com.amaze.filemanager.asynchronous.asynctasks.TaskKt; -import com.amaze.filemanager.asynchronous.management.ServiceWatcherUtil; -import com.amaze.filemanager.asynchronous.services.CopyService; -import com.amaze.filemanager.databinding.CopyDialogBinding; -import com.amaze.filemanager.fileoperations.filesystem.FolderState; -import com.amaze.filemanager.fileoperations.filesystem.OpenMode; -import com.amaze.filemanager.filesystem.HybridFile; -import com.amaze.filemanager.filesystem.HybridFileParcelable; -import com.amaze.filemanager.filesystem.files.FileUtils; -import com.amaze.filemanager.ui.activities.MainActivity; -import com.amaze.filemanager.utils.Utils; - -import android.app.ProgressDialog; -import android.content.Context; -import android.content.Intent; -import android.os.AsyncTask; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.Toast; - -import androidx.annotation.IntDef; -import androidx.appcompat.widget.AppCompatCheckBox; - -/** - * This AsyncTask works by creating a tree where each folder that can be fusioned together with - * another in the destination is a node (CopyNode). While the tree is being created an indeterminate - * ProgressDialog is shown. Each node is copied when the conflicts are dealt with (the dialog is - * shown, and the tree is walked via a BFS). If the process is cancelled (via the button in the - * dialog) the dialog closes without any more code to be executed, finishCopying() is never executed - * so no changes are made. - */ -public class PrepareCopyTask extends AsyncTask { - - private final String path; - private final Boolean move; - private final WeakReference mainActivity; - private final WeakReference context; - private int counter = 0; - private ProgressDialog dialog; - private boolean rootMode = false; - private OpenMode openMode = OpenMode.FILE; - private @DialogState int dialogState = UNKNOWN; - private boolean isRenameMoveSupport = false; - - // causes folder containing filesToCopy to be deleted - private ArrayList deleteCopiedFolder = null; - private CopyNode copyFolder; - private final ArrayList paths = new ArrayList<>(); - private final ArrayList> filesToCopyPerFolder = new ArrayList<>(); - private final ArrayList filesToCopy; // a copy of params sent to this - - private static final int UNKNOWN = -1; - private static final int DO_NOT_REPLACE = 0; - private static final int REPLACE = 1; - - @IntDef({UNKNOWN, DO_NOT_REPLACE, REPLACE}) - @interface DialogState {} - - public PrepareCopyTask( - String path, - Boolean move, - MainActivity con, - boolean rootMode, - OpenMode openMode, - ArrayList filesToCopy) { - this.move = move; - mainActivity = new WeakReference<>(con); - context = new WeakReference<>(con); - this.openMode = openMode; - this.rootMode = rootMode; - this.path = path; - this.filesToCopy = filesToCopy; - } - - @Override - protected void onPreExecute() { - super.onPreExecute(); - dialog = - ProgressDialog.show(context.get(), "", context.get().getString(R.string.processing), true); - } - - @Override - public void onProgressUpdate(String... message) { - Toast.makeText(context.get(), message[0], Toast.LENGTH_LONG).show(); - } - - @Override - protected CopyNode doInBackground(Void... params) { - long totalBytes = 0; - - if (openMode == OpenMode.OTG - || openMode == OpenMode.DROPBOX - || openMode == OpenMode.BOX - || openMode == OpenMode.GDRIVE - || openMode == OpenMode.ONEDRIVE - || openMode == OpenMode.ROOT) { - // no helper method for OTG to determine storage space - return null; - } - - HybridFile destination = new HybridFile(openMode, path); - destination.generateMode(context.get()); - - if (move - && destination.getMode() == openMode - && MoveFiles.getOperationSupportedFileSystem().contains(openMode)) { - // move/rename supported filesystems, skip checking for space - isRenameMoveSupport = true; - } - - totalBytes = FileUtils.getTotalBytes(filesToCopy, context.get()); - - if (destination.getUsableSpace() < totalBytes && !isRenameMoveSupport) { - publishProgress(context.get().getResources().getString(R.string.in_safe)); - return null; - } - - copyFolder = new CopyNode(path, filesToCopy); - - return copyFolder; - } - - private ArrayList checkConflicts( - final ArrayList filesToCopy, HybridFile destination) { - final ArrayList conflictingFiles = new ArrayList<>(); - destination.forEachChildrenFile( - context.get(), - rootMode, - file -> { - for (HybridFileParcelable j : filesToCopy) { - if (file.getName(context.get()).equals((j).getName(context.get()))) { - conflictingFiles.add(j); - } - } - }); - return conflictingFiles; - } - - @Override - protected void onPostExecute(CopyNode copyFolder) { - super.onPostExecute(copyFolder); - if (openMode == OpenMode.OTG - || openMode == OpenMode.GDRIVE - || openMode == OpenMode.DROPBOX - || openMode == OpenMode.BOX - || openMode == OpenMode.ONEDRIVE - || openMode == OpenMode.ROOT) { - - startService(filesToCopy, path, openMode); - } else { - - if (copyFolder == null) { - // not starting service as there's no sufficient space - dialog.dismiss(); - return; - } - - onEndDialog(null, null, null); - } - - dialog.dismiss(); - } - - private void startService( - ArrayList sourceFiles, String target, OpenMode openmode) { - Intent intent = new Intent(context.get(), CopyService.class); - intent.putParcelableArrayListExtra(CopyService.TAG_COPY_SOURCES, sourceFiles); - intent.putExtra(CopyService.TAG_COPY_TARGET, target); - intent.putExtra(CopyService.TAG_COPY_OPEN_MODE, openmode.ordinal()); - intent.putExtra(CopyService.TAG_COPY_MOVE, move); - intent.putExtra(CopyService.TAG_IS_ROOT_EXPLORER, rootMode); - ServiceWatcherUtil.runService(context.get(), intent); - } - - private void showDialog( - final String path, - final ArrayList filesToCopy, - final ArrayList conflictingFiles) { - int accentColor = mainActivity.get().getAccent(); - final MaterialDialog.Builder dialogBuilder = new MaterialDialog.Builder(context.get()); - CopyDialogBinding copyDialogBinding = - CopyDialogBinding.inflate(LayoutInflater.from(mainActivity.get())); - dialogBuilder.customView(copyDialogBinding.getRoot(), true); - - // textView - copyDialogBinding.fileNameText.setText(conflictingFiles.get(counter).getName(context.get())); - - // checkBox - final AppCompatCheckBox checkBox = copyDialogBinding.checkBox; - Utils.setTint(context.get(), checkBox, accentColor); - dialogBuilder.theme(mainActivity.get().getAppTheme().getMaterialDialogTheme(context.get())); - dialogBuilder.title(context.get().getResources().getString(R.string.paste)); - dialogBuilder.positiveText(R.string.skip); - dialogBuilder.negativeText(R.string.overwrite); - dialogBuilder.neutralText(R.string.cancel); - dialogBuilder.positiveColor(accentColor); - dialogBuilder.negativeColor(accentColor); - dialogBuilder.neutralColor(accentColor); - dialogBuilder.onPositive( - (dialog, which) -> { - if (checkBox.isChecked()) dialogState = DO_NOT_REPLACE; - doNotReplaceFiles(path, filesToCopy, conflictingFiles); - }); - dialogBuilder.onNegative( - (dialog, which) -> { - if (checkBox.isChecked()) dialogState = REPLACE; - replaceFiles(path, filesToCopy, conflictingFiles); - }); - - final MaterialDialog dialog = dialogBuilder.build(); - dialog.show(); - if (filesToCopy.get(0).getParent(context.get()).equals(path)) { - View negative = dialog.getActionButton(DialogAction.NEGATIVE); - negative.setEnabled(false); - } - } - - private void onEndDialog( - String path, - ArrayList filesToCopy, - ArrayList conflictingFiles) { - if (conflictingFiles != null - && counter != conflictingFiles.size() - && conflictingFiles.size() > 0) { - if (dialogState == UNKNOWN) { - showDialog(path, filesToCopy, conflictingFiles); - } else if (dialogState == DO_NOT_REPLACE) { - doNotReplaceFiles(path, filesToCopy, conflictingFiles); - } else if (dialogState == REPLACE) { - replaceFiles(path, filesToCopy, conflictingFiles); - } - } else { - CopyNode c = !copyFolder.hasStarted() ? copyFolder.startCopy() : copyFolder.goToNextNode(); - - if (c != null) { - counter = 0; - - paths.add(c.getPath()); - filesToCopyPerFolder.add(c.filesToCopy); - - if (dialogState == UNKNOWN) { - onEndDialog(c.path, c.filesToCopy, c.conflictingFiles); - } else if (dialogState == DO_NOT_REPLACE) { - doNotReplaceFiles(c.path, c.filesToCopy, c.conflictingFiles); - } else if (dialogState == REPLACE) { - replaceFiles(c.path, c.filesToCopy, c.conflictingFiles); - } - } else { - finishCopying(paths, filesToCopyPerFolder); - } - } - } - - private void doNotReplaceFiles( - String path, - ArrayList filesToCopy, - ArrayList conflictingFiles) { - if (counter < conflictingFiles.size()) { - if (dialogState != UNKNOWN) { - filesToCopy.remove(conflictingFiles.get(counter)); - counter++; - } else { - for (int j = counter; j < conflictingFiles.size(); j++) { - filesToCopy.remove(conflictingFiles.get(j)); - } - counter = conflictingFiles.size(); - } - } - - onEndDialog(path, filesToCopy, conflictingFiles); - } - - private void replaceFiles( - String path, - ArrayList filesToCopy, - ArrayList conflictingFiles) { - if (counter < conflictingFiles.size()) { - if (dialogState != UNKNOWN) { - counter++; - } else { - counter = conflictingFiles.size(); - } - } - - onEndDialog(path, filesToCopy, conflictingFiles); - } - - private void finishCopying( - ArrayList paths, ArrayList> filesToCopyPerFolder) { - for (int i = 0; i < filesToCopyPerFolder.size(); i++) { - if (filesToCopyPerFolder.get(i) == null || filesToCopyPerFolder.get(i).size() == 0) { - filesToCopyPerFolder.remove(i); - paths.remove(i); - i--; - } - } - - if (filesToCopyPerFolder.size() != 0) { - @FolderState - int mode = mainActivity.get().mainActivityHelper.checkFolder(path, openMode, context.get()); - if (mode == CAN_CREATE_FILES && !path.contains("otg:/")) { - // This is used because in newer devices the user has to accept a permission, - // see MainActivity.onActivityResult() - mainActivity.get().oparrayListList = filesToCopyPerFolder; - mainActivity.get().oparrayList = null; - mainActivity.get().operation = move ? MOVE : COPY; - mainActivity.get().oppatheList = paths; - } else { - if (!move) { - for (int i = 0; i < filesToCopyPerFolder.size(); i++) { - startService(filesToCopyPerFolder.get(i), paths.get(i), openMode); - } - } else { - TaskKt.fromTask( - new MoveFilesTask( - filesToCopyPerFolder, rootMode, path, context.get(), openMode, paths)); - } - } - } else { - Toast.makeText( - context.get(), - context.get().getResources().getString(R.string.no_file_overwrite), - Toast.LENGTH_SHORT) - .show(); - } - } - - class CopyNode { - private final String path; - private final ArrayList filesToCopy; - private final ArrayList conflictingFiles; - private final ArrayList nextNodes = new ArrayList<>(); - - CopyNode(String p, ArrayList filesToCopy) { - path = p; - this.filesToCopy = filesToCopy; - - HybridFile destination = new HybridFile(openMode, path); - conflictingFiles = checkConflicts(filesToCopy, destination); - - for (int i = 0; i < conflictingFiles.size(); i++) { - if (conflictingFiles.get(i).isDirectory()) { - if (deleteCopiedFolder == null) deleteCopiedFolder = new ArrayList<>(); - - deleteCopiedFolder.add(new File(conflictingFiles.get(i).getPath())); - - nextNodes.add( - new CopyNode( - path + "/" + conflictingFiles.get(i).getName(context.get()), - conflictingFiles.get(i).listFiles(context.get(), rootMode))); - - filesToCopy.remove(filesToCopy.indexOf(conflictingFiles.get(i))); - conflictingFiles.remove(i); - i--; - } - } - } - - /** The next 2 methods are a BFS that runs through one node at a time. */ - private LinkedList queue = null; - - private Set visited = null; - - CopyNode startCopy() { - queue = new LinkedList<>(); - visited = new HashSet<>(); - - queue.add(this); - visited.add(this); - return this; - } - - /** - * @return true if there are no more nodes - */ - CopyNode goToNextNode() { - if (queue.isEmpty()) return null; - else { - CopyNode node = queue.element(); - CopyNode child; - if ((child = getUnvisitedChildNode(visited, node)) != null) { - visited.add(child); - queue.add(child); - return child; - } else { - queue.remove(); - return goToNextNode(); - } - } - } - - boolean hasStarted() { - return queue != null; - } - - String getPath() { - return path; - } - - private CopyNode getUnvisitedChildNode(Set visited, CopyNode node) { - for (CopyNode n : node.nextNodes) { - if (!visited.contains(n)) { - return n; - } - } - - return null; - } - } -} diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/movecopy/PreparePasteTask.kt b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/movecopy/PreparePasteTask.kt new file mode 100644 index 0000000000..3d28c24f83 --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/movecopy/PreparePasteTask.kt @@ -0,0 +1,485 @@ +/* + * Copyright (C) 2014-2020 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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.amaze.filemanager.asynchronous.asynctasks.movecopy + +import android.app.ProgressDialog +import android.content.Intent +import android.view.LayoutInflater +import android.widget.Toast +import androidx.appcompat.widget.AppCompatCheckBox +import com.afollestad.materialdialogs.DialogAction +import com.afollestad.materialdialogs.MaterialDialog +import com.amaze.filemanager.R +import com.amaze.filemanager.asynchronous.asynctasks.fromTask +import com.amaze.filemanager.asynchronous.management.ServiceWatcherUtil +import com.amaze.filemanager.asynchronous.services.CopyService +import com.amaze.filemanager.databinding.CopyDialogBinding +import com.amaze.filemanager.fileoperations.filesystem.CAN_CREATE_FILES +import com.amaze.filemanager.fileoperations.filesystem.COPY +import com.amaze.filemanager.fileoperations.filesystem.FolderState +import com.amaze.filemanager.fileoperations.filesystem.MOVE +import com.amaze.filemanager.fileoperations.filesystem.OpenMode +import com.amaze.filemanager.filesystem.FilenameHelper +import com.amaze.filemanager.filesystem.HybridFile +import com.amaze.filemanager.filesystem.HybridFileParcelable +import com.amaze.filemanager.filesystem.MakeDirectoryOperation +import com.amaze.filemanager.filesystem.files.FileUtils +import com.amaze.filemanager.ui.activities.MainActivity +import com.amaze.filemanager.utils.OnFileFound +import com.amaze.filemanager.utils.Utils +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.lang.ref.WeakReference +import java.util.LinkedList + +/** + * This helper class works by checking the conflicts during paste operation. After checking + * conflicts [MaterialDialog] is shown to user for each conflicting file. If the conflicting file + * is a directory, the conflicts are resolved by inserting a node in [CopyNode] tree and then doing + * BFS on this tree. + */ +class PreparePasteTask(strongRefMain: MainActivity) { + + private lateinit var targetPath: String + private var isMove = false + private var isRootMode = false + private lateinit var openMode: OpenMode + private lateinit var filesToCopy: MutableList + + private val pathsList = ArrayList() + private val filesToCopyPerFolder = ArrayList>() + + private val context = WeakReference(strongRefMain) + + @Suppress("DEPRECATION") + private var progressDialog: ProgressDialog? = null + private val coroutineScope = CoroutineScope(Job() + Dispatchers.Default) + + private lateinit var destination: HybridFile + private val conflictingFiles: MutableList = mutableListOf() + private val conflictingDirActionMap = HashMap() + + private var skipAll = false + private var renameAll = false + private var overwriteAll = false + + private fun startService( + sourceFiles: ArrayList, + target: String, + openMode: OpenMode, + isMove: Boolean, + isRootMode: Boolean + ) { + val intent = Intent(context.get(), CopyService::class.java) + intent.putParcelableArrayListExtra(CopyService.TAG_COPY_SOURCES, sourceFiles) + intent.putExtra(CopyService.TAG_COPY_TARGET, target) + intent.putExtra(CopyService.TAG_COPY_OPEN_MODE, openMode.ordinal) + intent.putExtra(CopyService.TAG_COPY_MOVE, isMove) + intent.putExtra(CopyService.TAG_IS_ROOT_EXPLORER, isRootMode) + ServiceWatcherUtil.runService(context.get(), intent) + } + + /** + * Starts execution of [PreparePasteTask] class. + */ + fun execute( + targetPath: String, + isMove: Boolean, + isRootMode: Boolean, + openMode: OpenMode, + filesToCopy: ArrayList + ) { + this.targetPath = targetPath + this.isMove = isMove + this.isRootMode = isRootMode + this.openMode = openMode + this.filesToCopy = filesToCopy + + val isCloudOrRootMode = openMode == OpenMode.OTG || + openMode == OpenMode.GDRIVE || + openMode == OpenMode.DROPBOX || + openMode == OpenMode.BOX || + openMode == OpenMode.ONEDRIVE || + openMode == OpenMode.ROOT + + if (isCloudOrRootMode) { + startService(filesToCopy, targetPath, openMode, isMove, isRootMode) + } + + val totalBytes = FileUtils.getTotalBytes(filesToCopy, context.get()) + destination = HybridFile(openMode, targetPath) + destination.generateMode(context.get()) + + if (filesToCopy.isNotEmpty() && + isMove && + filesToCopy[0].getParent(context.get()) == targetPath + ) { + Toast.makeText(context.get(), R.string.same_dir_move_error, Toast.LENGTH_SHORT).show() + return + } + + val isMoveSupported = isMove && + destination.mode == openMode && + MoveFiles.getOperationSupportedFileSystem().contains(openMode) + + if (destination.usableSpace < totalBytes && + !isMoveSupported + ) { + Toast.makeText(context.get(), R.string.in_safe, Toast.LENGTH_SHORT).show() + return + } + @Suppress("DEPRECATION") + progressDialog = ProgressDialog.show( + context.get(), + "", + context.get()?.getString(R.string.checking_conflicts) + ) + checkConflicts( + isRootMode, + filesToCopy, + destination, + conflictingFiles, + conflictingDirActionMap + ) + } + + private fun checkConflicts( + isRootMode: Boolean, + filesToCopy: ArrayList, + destination: HybridFile, + conflictingFiles: MutableList, + conflictingDirActionMap: HashMap + ) { + coroutineScope.launch { + destination.forEachChildrenFile( + context.get(), + isRootMode, + object : OnFileFound { + override fun onFileFound(file: HybridFileParcelable) { + for (fileToCopy in filesToCopy) { + if (file.getName(context.get()) == fileToCopy.getName(context.get())) { + conflictingFiles.add(fileToCopy) + } + } + } + } + ) + withContext(Dispatchers.Main) { + prepareDialog(conflictingFiles, filesToCopy) + @Suppress("DEPRECATION") + progressDialog?.setMessage(context.get()?.getString(R.string.copying)) + } + resolveConflict(conflictingFiles, conflictingDirActionMap, filesToCopy) + } + } + + private suspend fun prepareDialog( + conflictingFiles: MutableList, + filesToCopy: ArrayList + ) { + if (conflictingFiles.isEmpty()) return + + val contextRef = context.get() ?: return + val accentColor = contextRef.accent + val dialogBuilder = MaterialDialog.Builder(contextRef) + val copyDialogBinding: CopyDialogBinding = + CopyDialogBinding.inflate(LayoutInflater.from(contextRef)) + dialogBuilder.customView(copyDialogBinding.root, true) + val checkBox: AppCompatCheckBox = copyDialogBinding.checkBox + + Utils.setTint(contextRef, checkBox, accentColor) + dialogBuilder.theme(contextRef.appTheme.getMaterialDialogTheme(contextRef)) + dialogBuilder.title(contextRef.resources.getString(R.string.paste)) + dialogBuilder.positiveText(R.string.rename) + dialogBuilder.neutralText(R.string.skip) + dialogBuilder.positiveColor(accentColor) + dialogBuilder.negativeColor(accentColor) + dialogBuilder.neutralColor(accentColor) + dialogBuilder.negativeText(R.string.overwrite) + showDialog( + conflictingFiles, + filesToCopy, + copyDialogBinding, + dialogBuilder, + checkBox + ) + } + + private suspend fun showDialog( + conflictingFiles: MutableList, + filesToCopy: ArrayList, + copyDialogBinding: CopyDialogBinding, + dialogBuilder: MaterialDialog.Builder, + checkBox: AppCompatCheckBox + ) { + val iterator = conflictingFiles.iterator() + while (iterator.hasNext()) { + val hybridFileParcelable = iterator.next() + copyDialogBinding.fileNameText.text = hybridFileParcelable.name + val dialog = dialogBuilder.build() + val resultDeferred = CompletableDeferred() + dialogBuilder.onPositive { _, _ -> + resultDeferred.complete(DialogAction.POSITIVE) + } + dialogBuilder.onNegative { _, _ -> + resultDeferred.complete(DialogAction.NEGATIVE) + } + dialogBuilder.onNeutral { _, _ -> + resultDeferred.complete(DialogAction.NEUTRAL) + } + dialog.show() + when (resultDeferred.await()) { + DialogAction.POSITIVE -> { + if (checkBox.isChecked) { + renameAll = true + return + } else conflictingDirActionMap[hybridFileParcelable] = Action.RENAME + iterator.remove() + } + DialogAction.NEGATIVE -> { + if (hybridFileParcelable.getParent(context.get()) == targetPath) { + Toast.makeText( + context.get(), + R.string.same_dir_overwrite_error, + Toast.LENGTH_SHORT + ).show() + if (checkBox.isChecked) { + filesToCopy.removeAll(conflictingFiles.toSet()) + conflictingFiles.clear() + return + } + filesToCopy.remove(hybridFileParcelable) + } else if (checkBox.isChecked) { + overwriteAll = true + return + } else conflictingDirActionMap[hybridFileParcelable] = Action.OVERWRITE + iterator.remove() + } + DialogAction.NEUTRAL -> { + if (checkBox.isChecked) { + skipAll = true + return + } else conflictingDirActionMap[hybridFileParcelable] = Action.SKIP + iterator.remove() + } + } + } + } + + private fun resolveConflict( + conflictingFiles: MutableList, + conflictingDirActionMap: HashMap, + filesToCopy: ArrayList + ) = coroutineScope.launch { + var index = conflictingFiles.size - 1 + if (renameAll) { + while (conflictingFiles.isNotEmpty()) { + conflictingDirActionMap[conflictingFiles[index]] = Action.RENAME + conflictingFiles.removeAt(index) + index-- + } + } else if (overwriteAll) { + while (conflictingFiles.isNotEmpty()) { + conflictingDirActionMap[conflictingFiles[index]] = Action.OVERWRITE + conflictingFiles.removeAt(index) + index-- + } + } else if (skipAll) { + while (conflictingFiles.isNotEmpty()) { + filesToCopy.remove(conflictingFiles.removeAt(index)) + index-- + } + } + + val rootNode = CopyNode(targetPath, ArrayList(filesToCopy)) + var currentNode: CopyNode? = rootNode.startCopy() + + while (currentNode != null) { + pathsList.add(currentNode.path) + filesToCopyPerFolder.add(currentNode.filesToCopy) + currentNode = rootNode.goToNextNode() + } + finishCopying() + } + + private suspend fun finishCopying() { + var index = 0 + while (index < filesToCopyPerFolder.size) { + if (filesToCopyPerFolder[index].size == 0) { + filesToCopyPerFolder.removeAt(index) + pathsList.removeAt(index) + index-- + } + index++ + } + if (filesToCopyPerFolder.isNotEmpty()) { + @FolderState + val mode: Int = context.get()?.mainActivityHelper!! + .checkFolder(targetPath, openMode, context.get()) + if (mode == CAN_CREATE_FILES && !targetPath.contains("otg:/")) { + // This is used because in newer devices the user has to accept a permission, + // see MainActivity.onActivityResult() + context.get()?.oparrayListList = filesToCopyPerFolder + context.get()?.oparrayList = null + context.get()?.operation = if (isMove) MOVE else COPY + context.get()?.oppatheList = pathsList + } else { + if (!isMove) { + for (foldersIndex in filesToCopyPerFolder.indices) + startService( + filesToCopyPerFolder[foldersIndex], + pathsList[foldersIndex], + openMode, + isMove, + isRootMode + ) + } else { + fromTask( + MoveFilesTask( + filesToCopyPerFolder, + isRootMode, + targetPath, + context.get()!!, + openMode, + pathsList + ) + ) + } + } + } else { + withContext(Dispatchers.Main) { + Toast.makeText( + context.get(), + context.get()!!.resources.getString(R.string.no_file_overwrite), + Toast.LENGTH_SHORT + ).show() + } + } + withContext(Dispatchers.Main) { + progressDialog?.dismiss() + } + coroutineScope.cancel() + } + + private inner class CopyNode( + val path: String, + val filesToCopy: ArrayList + ) { + private val nextNodes: MutableList = mutableListOf() + private var queue: LinkedList? = null + private var visited: HashSet? = null + + init { + val iterator = filesToCopy.iterator() + while (iterator.hasNext()) { + val hybridFileParcelable = iterator.next() + if (conflictingDirActionMap.contains(hybridFileParcelable)) { + when (conflictingDirActionMap[hybridFileParcelable]) { + Action.RENAME -> { + if (hybridFileParcelable.isDirectory) { + val newName = + FilenameHelper.increment( + hybridFileParcelable + ).getName(context.get()) + val newPath = "$path/$newName" + val hybridFile = HybridFile(hybridFileParcelable.mode, newPath) + MakeDirectoryOperation.mkdirs(context.get()!!, hybridFile) + @Suppress("DEPRECATION") + nextNodes.add( + CopyNode( + newPath, + hybridFileParcelable.listFiles(context.get(), isRootMode) + ) + ) + iterator.remove() + } else { + filesToCopy[filesToCopy.indexOf(hybridFileParcelable)].name = + FilenameHelper.increment( + hybridFileParcelable + ).getName(context.get()) + } + } + + Action.SKIP -> iterator.remove() + } + } + } + } + + /** + * Starts BFS traversal of tree. + * + * @return Root node + */ + fun startCopy(): CopyNode { + queue = LinkedList() + visited = HashSet() + queue!!.add(this) + visited!!.add(this) + return this + } + + /** + * Moves to the next unvisited node in tree. + * + * @return The next unvisited node if available, otherwise returns null. + */ + fun goToNextNode(): CopyNode? = + if (queue.isNullOrEmpty()) null + else { + val node = queue!!.element() + val child = getUnvisitedChildNode(visited!!, node) + if (child != null) { + visited!!.add(child) + queue!!.add(child) + child + } else { + queue!!.remove() + goToNextNode() + } + } + + private fun getUnvisitedChildNode( + visited: Set, + node: CopyNode + ): CopyNode? { + for (currentNode in node.nextNodes) { + if (!visited.contains(currentNode)) { + return currentNode + } + } + return null + } + } + + private class Action { + companion object { + const val SKIP = "skip" + const val RENAME = "rename" + const val OVERWRITE = "overwrite" + } + } +} diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/MakeDirectoryOperation.kt b/app/src/main/java/com/amaze/filemanager/filesystem/MakeDirectoryOperation.kt index 5d44a4a697..1765130374 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/MakeDirectoryOperation.kt +++ b/app/src/main/java/com/amaze/filemanager/filesystem/MakeDirectoryOperation.kt @@ -76,6 +76,12 @@ object MakeDirectoryOperation { } else false } + /** + * Creates the directories on given [file] path, including nonexistent parent directories. + * So use proper [HybridFile] constructor as per your need. + * + * @return true if successfully created directory, otherwise returns false. + */ @JvmStatic fun mkdirs(context: Context, file: HybridFile): Boolean { var isSuccessful = true diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/PasteHelper.java b/app/src/main/java/com/amaze/filemanager/filesystem/PasteHelper.java index ac83ec6726..2113c1c220 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/PasteHelper.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/PasteHelper.java @@ -29,14 +29,13 @@ import org.slf4j.LoggerFactory; import com.amaze.filemanager.R; -import com.amaze.filemanager.asynchronous.asynctasks.movecopy.PrepareCopyTask; +import com.amaze.filemanager.asynchronous.asynctasks.movecopy.PreparePasteTask; import com.amaze.filemanager.ui.activities.MainActivity; import com.amaze.filemanager.ui.fragments.MainFragment; import com.amaze.filemanager.utils.Utils; import com.google.android.material.snackbar.BaseTransientBottomBar; import com.google.android.material.snackbar.Snackbar; -import android.os.AsyncTask; import android.os.Parcel; import android.os.Parcelable; import android.text.Spanned; @@ -168,14 +167,13 @@ public void onSuccess(Spanned spanned) { ArrayList arrayList = new ArrayList<>(Arrays.asList(paths)); boolean move = operation == PasteHelper.OPERATION_CUT; - new PrepareCopyTask( + new PreparePasteTask(mainActivity) + .execute( path, move, - mainActivity, mainActivity.isRootExplorer(), mainFragment.getMainFragmentViewModel().getOpenMode(), - arrayList) - .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + arrayList); dismissSnackbar(true); }, () -> dismissSnackbar(true)); diff --git a/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivity.java b/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivity.java index 1bf3d3a248..941e235571 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivity.java +++ b/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivity.java @@ -324,7 +324,7 @@ public class MainActivity extends PermissionsActivity public static final int REQUEST_CODE_CLOUD_LIST_KEY = 5472; private PasteHelper pasteHelper; - private MainActivityActionMode mainActivityActionMode; + public MainActivityActionMode mainActivityActionMode; private static final String DEFAULT_FALLBACK_STORAGE_PATH = "/storage/sdcard0"; private static final String INTERNAL_SHARED_STORAGE = "Internal shared storage"; diff --git a/app/src/main/java/com/amaze/filemanager/ui/dialogs/DragAndDropDialog.kt b/app/src/main/java/com/amaze/filemanager/ui/dialogs/DragAndDropDialog.kt index 88693d6111..a9992f0b2a 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/dialogs/DragAndDropDialog.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/dialogs/DragAndDropDialog.kt @@ -22,7 +22,6 @@ package com.amaze.filemanager.ui.dialogs import android.app.Dialog import android.content.Context -import android.os.AsyncTask import android.os.Bundle import androidx.appcompat.widget.AppCompatButton import androidx.appcompat.widget.AppCompatCheckBox @@ -31,7 +30,7 @@ import com.afollestad.materialdialogs.DialogAction import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.Theme import com.amaze.filemanager.R -import com.amaze.filemanager.asynchronous.asynctasks.movecopy.PrepareCopyTask +import com.amaze.filemanager.asynchronous.asynctasks.movecopy.PreparePasteTask import com.amaze.filemanager.filesystem.HybridFileParcelable import com.amaze.filemanager.ui.activities.MainActivity import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants @@ -106,15 +105,16 @@ class DragAndDropDialog : DialogFragment() { move: Boolean, mainActivity: MainActivity ) { - PrepareCopyTask( - pasteLocation, - move, - mainActivity, - mainActivity.isRootExplorer, - mainActivity.currentMainFragment?.mainFragmentViewModel?.openMode, - files - ) - .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR) + val openMode = + mainActivity.currentMainFragment?.mainFragmentViewModel?.openMode ?: return + PreparePasteTask(mainActivity) + .execute( + pasteLocation, + move, + mainActivity.isRootExplorer, + openMode, + files + ) } } diff --git a/app/src/main/java/com/amaze/filemanager/utils/MainActivityActionMode.kt b/app/src/main/java/com/amaze/filemanager/utils/MainActivityActionMode.kt index e84b8f3034..03f626a3f1 100644 --- a/app/src/main/java/com/amaze/filemanager/utils/MainActivityActionMode.kt +++ b/app/src/main/java/com/amaze/filemanager/utils/MainActivityActionMode.kt @@ -49,6 +49,7 @@ class MainActivityActionMode(private val mainActivityReference: WeakReferenceTry Indexed Search! Recent Results + Please complete paste operation first + Destination and source folder shouldn\'t match to move. + Checking for conflicts + Destination and source folder shouldn\'t match to overwrite.