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/DeleteTask.java b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/DeleteTask.java index 15ec67cace..c63bbe3ef6 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/DeleteTask.java +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/DeleteTask.java @@ -38,7 +38,7 @@ import com.amaze.filemanager.filesystem.SafRootHolder; import com.amaze.filemanager.filesystem.cloud.CloudUtil; import com.amaze.filemanager.filesystem.files.CryptUtil; -import com.amaze.filemanager.filesystem.files.FileUtils; +import com.amaze.filemanager.filesystem.files.MediaConnectionUtils; import com.amaze.filemanager.ui.activities.MainActivity; import com.amaze.filemanager.ui.fragments.CompressedExplorerFragment; import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants; @@ -48,12 +48,9 @@ import com.cloudrail.si.interfaces.CloudStorage; import android.app.NotificationManager; -import android.content.ContentResolver; import android.content.Context; import android.content.Intent; -import android.net.Uri; import android.os.AsyncTask; -import android.provider.MediaStore; import android.widget.Toast; import androidx.annotation.NonNull; @@ -112,13 +109,9 @@ protected final AsyncTaskResult doInBackground( } // delete file from media database - if (!file.isSmb()) { - try { - deleteFromMediaDatabase(applicationContext, file.getPath()); - } catch (Exception e) { - FileUtils.scanFile(applicationContext, files.toArray(new HybridFile[files.size()])); - } - } + if (!file.isSmb()) + MediaConnectionUtils.scanFile( + applicationContext, files.toArray(new HybridFile[files.size()])); // delete file entry from encrypted database if (file.getName(applicationContext).endsWith(CryptUtil.CRYPT_EXTENSION)) { @@ -194,13 +187,4 @@ private boolean doDeleteFile(@NonNull HybridFileParcelable file) throws Exceptio } } } - - private void deleteFromMediaDatabase(final Context context, final String file) { - final String where = MediaStore.MediaColumns.DATA + "=?"; - final String[] selectionArgs = new String[] {file}; - final ContentResolver contentResolver = context.getContentResolver(); - final Uri filesUri = MediaStore.Files.getContentUri("external"); - // Delete the entry from the media database. This will actually delete media files. - contentResolver.delete(filesUri, where, selectionArgs); - } } 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/MoveFilesTask.kt b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/movecopy/MoveFilesTask.kt index 196f05d668..988cfb8bde 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/movecopy/MoveFilesTask.kt +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/movecopy/MoveFilesTask.kt @@ -34,7 +34,7 @@ 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.CryptUtil -import com.amaze.filemanager.filesystem.files.FileUtils +import com.amaze.filemanager.filesystem.files.MediaConnectionUtils import com.amaze.filemanager.ui.activities.MainActivity import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -106,8 +106,8 @@ class MoveFilesTask( for (hybridFileParcelables in files) { sourcesFiles.addAll(hybridFileParcelables) } - FileUtils.scanFile(applicationContext, sourcesFiles.toTypedArray()) - FileUtils.scanFile(applicationContext, targetFiles.toTypedArray()) + MediaConnectionUtils.scanFile(applicationContext, sourcesFiles.toTypedArray()) + MediaConnectionUtils.scanFile(applicationContext, targetFiles.toTypedArray()) } // updating encrypted db entry if any encrypted file was moved 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/asynchronous/services/CopyService.java b/app/src/main/java/com/amaze/filemanager/asynchronous/services/CopyService.java index fbb0ae012a..2ddfca97e3 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/services/CopyService.java +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/services/CopyService.java @@ -43,6 +43,7 @@ import com.amaze.filemanager.filesystem.files.CryptUtil; import com.amaze.filemanager.filesystem.files.FileUtils; import com.amaze.filemanager.filesystem.files.GenericCopyUtil; +import com.amaze.filemanager.filesystem.files.MediaConnectionUtils; import com.amaze.filemanager.filesystem.root.CopyFilesCommand; import com.amaze.filemanager.filesystem.root.MoveFileCommand; import com.amaze.filemanager.ui.activities.MainActivity; @@ -461,7 +462,7 @@ void copyRoot(HybridFileParcelable sourceFile, HybridFile targetFile, boolean mo e); failedFOps.add(sourceFile); } - FileUtils.scanFile(c, new HybridFile[] {targetFile}); + MediaConnectionUtils.scanFile(c, new HybridFile[] {targetFile}); } private void copyFiles( diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/HybridFile.java b/app/src/main/java/com/amaze/filemanager/filesystem/HybridFile.java index 649143bc6f..934b153380 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/HybridFile.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/HybridFile.java @@ -411,13 +411,17 @@ public long length(Context context) { } /** - * Path accessor. Avoid direct access to path since path may have been URL encoded. + * Path accessor. Avoid direct access to path (for non-local files) since path may have been URL + * encoded. * - * @return URL decoded path + * @return URL decoded path (for non-local files); the actual path for local files */ public String getPath() { + + if (isLocal() || isRoot() || isDocumentFile() || isAndroidDataDir()) return path; + try { - return URLDecoder.decode(path.replace("+", "%2b"), "UTF-8"); + return URLDecoder.decode(path, "UTF-8"); } catch (UnsupportedEncodingException | IllegalArgumentException e) { LOG.warn("failed to decode path {}", path, e); return path; 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/Operations.java b/app/src/main/java/com/amaze/filemanager/filesystem/Operations.java index 5838f3d593..d247ee43e2 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/Operations.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/Operations.java @@ -41,6 +41,7 @@ import com.amaze.filemanager.fileoperations.filesystem.OpenMode; import com.amaze.filemanager.filesystem.cloud.CloudUtil; import com.amaze.filemanager.filesystem.files.FileUtils; +import com.amaze.filemanager.filesystem.files.MediaConnectionUtils; import com.amaze.filemanager.filesystem.ftp.FtpClientTemplate; import com.amaze.filemanager.filesystem.ftp.NetCopyClientUtils; import com.amaze.filemanager.filesystem.root.MakeDirectoryCommand; @@ -716,7 +717,7 @@ protected void onPostExecute(Void aVoid) { super.onPostExecute(aVoid); if (newFile != null && oldFile != null) { HybridFile[] hybridFiles = {newFile, oldFile}; - FileUtils.scanFile(context, hybridFiles); + MediaConnectionUtils.scanFile(context, hybridFiles); } } }.executeOnExecutor(executor); 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/filesystem/files/FileUtils.java b/app/src/main/java/com/amaze/filemanager/filesystem/files/FileUtils.java index cf8a2cd612..35d3d7b5d0 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/files/FileUtils.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/files/FileUtils.java @@ -30,7 +30,6 @@ import java.util.Date; import java.util.LinkedList; import java.util.List; -import java.util.concurrent.Callable; import java.util.concurrent.atomic.AtomicLong; import org.slf4j.Logger; @@ -42,7 +41,6 @@ import com.amaze.filemanager.application.AppConfig; import com.amaze.filemanager.fileoperations.filesystem.OpenMode; import com.amaze.filemanager.fileoperations.filesystem.smbstreamer.Streamer; -import com.amaze.filemanager.filesystem.ExternalSdCardOperation; import com.amaze.filemanager.filesystem.HybridFile; import com.amaze.filemanager.filesystem.HybridFileParcelable; import com.amaze.filemanager.filesystem.Operations; @@ -71,7 +69,6 @@ import android.Manifest; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; -import android.annotation.SuppressLint; import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.Context; @@ -79,7 +76,6 @@ import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; -import android.media.MediaScannerConnection; import android.net.Uri; import android.os.AsyncTask; import android.os.Build; @@ -92,8 +88,6 @@ import androidx.core.util.Pair; import androidx.documentfile.provider.DocumentFile; -import io.reactivex.Flowable; -import io.reactivex.schedulers.Schedulers; import jcifs.smb.SmbFile; import kotlin.collections.ArraysKt; import net.schmizz.sshj.sftp.RemoteResourceInfo; @@ -217,76 +211,6 @@ public static long getBaseFileSize(HybridFileParcelable baseFile, Context contex } } - /** - * Triggers media scanner for multiple paths. The paths must all belong to same filesystem. It's - * upto the caller to call the mediastore scan on multiple files or only one source/target - * directory. Don't use filesystem API directly as files might not be present anymore (eg. - * move/rename) which may lead to {@link java.io.FileNotFoundException} - * - * @param hybridFiles - * @param context - */ - @SuppressLint("CheckResult") - public static void scanFile(@NonNull Context context, @NonNull HybridFile[] hybridFiles) { - Flowable.fromCallable( - (Callable) - () -> { - if (hybridFiles[0].exists(context) && hybridFiles[0].isLocal()) { - String[] paths = new String[hybridFiles.length]; - for (int i = 0; i < hybridFiles.length; i++) { - HybridFile hybridFile = hybridFiles[i]; - paths[i] = hybridFile.getPath(); - } - MediaScannerConnection.scanFile(context, paths, null, null); - } - for (HybridFile hybridFile : hybridFiles) { - scanFile(hybridFile, context); - } - return null; - }) - .subscribeOn(Schedulers.io()); - } - - /** - * Triggers media store for the file path - * - * @param hybridFile the file which was changed (directory not supported) - * @param context given context - */ - private static void scanFile(@NonNull HybridFile hybridFile, Context context) { - - if ((hybridFile.isLocal() || hybridFile.isOtgFile()) && hybridFile.exists(context)) { - - Uri uri = null; - if (Build.VERSION.SDK_INT >= 19) { - DocumentFile documentFile = - ExternalSdCardOperation.getDocumentFile( - hybridFile.getFile(), hybridFile.isDirectory(context), context); - // If FileUtil.getDocumentFile() returns null, fall back to DocumentFile.fromFile() - if (documentFile == null) documentFile = DocumentFile.fromFile(hybridFile.getFile()); - uri = documentFile.getUri(); - } else { - if (hybridFile.isLocal()) { - uri = Uri.fromFile(hybridFile.getFile()); - } - } - if (uri != null) { - FileUtils.scanFile(uri, context); - } - } - } - - /** - * Triggers {@link Intent#ACTION_MEDIA_SCANNER_SCAN_FILE} intent to refresh the media store. - * - * @param uri File's {@link Uri} - * @param c {@link Context} - */ - private static void scanFile(@NonNull Uri uri, @NonNull Context c) { - Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri); - c.sendBroadcast(mediaScanIntent); - } - public static void crossfade(View buttons, final View pathbar) { // Set the content view to 0% opacity but visible, so that it is visible // (but fully transparent) during the animation. diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/files/GenericCopyUtil.java b/app/src/main/java/com/amaze/filemanager/filesystem/files/GenericCopyUtil.java index 82080fd658..4bed1f8397 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/files/GenericCopyUtil.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/files/GenericCopyUtil.java @@ -271,7 +271,7 @@ private void startCopy( // If target file is copied onto the device and copy was successful, trigger media store // rescan if (mTargetFile != null) { - FileUtils.scanFile(mContext, new HybridFile[] {mTargetFile}); + MediaConnectionUtils.scanFile(mContext, new HybridFile[] {mTargetFile}); } } } diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/files/MediaConnectionUtils.kt b/app/src/main/java/com/amaze/filemanager/filesystem/files/MediaConnectionUtils.kt new file mode 100644 index 0000000000..577c33199d --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/filesystem/files/MediaConnectionUtils.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2014-2022 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.filesystem.files + +import android.content.Context +import android.media.MediaScannerConnection +import android.net.Uri +import com.amaze.filemanager.filesystem.HybridFile +import org.slf4j.LoggerFactory + +object MediaConnectionUtils { + + private val LOG = LoggerFactory.getLogger(MediaConnectionUtils::class.java) + + /** + * Invokes MediaScannerConnection#scanFile for the given files + * + * @param context the context + * @param hybridFiles files to be scanned + */ + @JvmStatic + fun scanFile(context: Context, hybridFiles: Array) { + val paths = arrayOfNulls(hybridFiles.size) + + for (i in hybridFiles.indices) paths[i] = hybridFiles[i].path + + MediaScannerConnection.scanFile( + context, + paths, + null + ) { path: String, _: Uri? -> + LOG.info("MediaConnectionUtils#scanFile finished scanning path$path") + } + } +} 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 77d128233d..43b71b1908 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/activities/texteditor/TextEditorActivity.java b/app/src/main/java/com/amaze/filemanager/ui/activities/texteditor/TextEditorActivity.java index f3bebaf489..295a309a79 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/activities/texteditor/TextEditorActivity.java +++ b/app/src/main/java/com/amaze/filemanager/ui/activities/texteditor/TextEditorActivity.java @@ -49,34 +49,30 @@ import com.amaze.filemanager.utils.Utils; import com.google.android.material.snackbar.Snackbar; -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ObjectAnimator; import android.content.Context; -import android.graphics.Color; import android.graphics.Typeface; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.text.Editable; import android.text.Spanned; import android.text.TextWatcher; import android.text.style.BackgroundColorSpan; -import android.util.DisplayMetrics; import android.view.Menu; import android.view.MenuItem; import android.view.View; -import android.view.ViewAnimationUtils; -import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; -import android.widget.RelativeLayout; import android.widget.ScrollView; import android.widget.Toast; import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.appcompat.app.ActionBar; import androidx.appcompat.widget.AppCompatEditText; import androidx.appcompat.widget.AppCompatImageButton; +import androidx.constraintlayout.widget.ConstraintLayout; import androidx.lifecycle.ViewModelProvider; public class TextEditorActivity extends ThemedActivity @@ -95,13 +91,14 @@ public class TextEditorActivity extends ThemedActivity private static final String KEY_ORIGINAL_TEXT = "original"; private static final String KEY_MONOFONT = "monofont"; - private RelativeLayout searchViewLayout; + private ConstraintLayout searchViewLayout; public AppCompatImageButton upButton; public AppCompatImageButton downButton; - public AppCompatImageButton closeButton; private Snackbar loadingSnackbar; + private TextEditorActivityViewModel viewModel; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -109,16 +106,15 @@ public void onCreate(Bundle savedInstanceState) { toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); - final TextEditorActivityViewModel viewModel = - new ViewModelProvider(this).get(TextEditorActivityViewModel.class); + viewModel = new ViewModelProvider(this).get(TextEditorActivityViewModel.class); + + searchViewLayout = findViewById(R.id.textEditorSearchBar); - searchViewLayout = findViewById(R.id.searchview); searchViewLayout.setBackgroundColor(getPrimary()); - searchEditText = searchViewLayout.findViewById(R.id.search_box); - upButton = searchViewLayout.findViewById(R.id.prev); - downButton = searchViewLayout.findViewById(R.id.next); - closeButton = searchViewLayout.findViewById(R.id.close); + searchEditText = searchViewLayout.findViewById(R.id.textEditorSearchBox); + upButton = searchViewLayout.findViewById(R.id.textEditorSearchPrevButton); + downButton = searchViewLayout.findViewById(R.id.textEditorSearchNextButton); searchEditText.addTextChangedListener(this); @@ -126,14 +122,9 @@ public void onCreate(Bundle savedInstanceState) { // upButton.setEnabled(false); downButton.setOnClickListener(this); // downButton.setEnabled(false); - closeButton.setOnClickListener(this); - - boolean useNewStack = getBoolean(PREFERENCE_TEXTEDITOR_NEWSTACK); - getSupportActionBar().setDisplayHomeAsUpEnabled(!useNewStack); - - mainTextView = findViewById(R.id.fname); - scrollView = findViewById(R.id.editscroll); + mainTextView = findViewById(R.id.textEditorMainEditText); + scrollView = findViewById(R.id.textEditorScrollView); final Uri uri = getIntent().getData(); if (uri != null) { @@ -144,14 +135,23 @@ public void onCreate(Bundle savedInstanceState) { return; } - getSupportActionBar().setTitle(viewModel.getFile().name); + ActionBar actionBar = getSupportActionBar(); + + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(!getBoolean(PREFERENCE_TEXTEDITOR_NEWSTACK)); + actionBar.setTitle(viewModel.getFile().name); + } mainTextView.addTextChangedListener(this); if (getAppTheme().equals(AppTheme.DARK)) { - mainTextView.setBackgroundColor(Utils.getColor(this, R.color.holo_dark_background)); + mainTextView.setBackgroundColor(Utils.getColor(this, R.color.holo_dark_action_mode)); + mainTextView.setTextColor(Utils.getColor(this, R.color.primary_white)); } else if (getAppTheme().equals(AppTheme.BLACK)) { mainTextView.setBackgroundColor(Utils.getColor(this, android.R.color.black)); + mainTextView.setTextColor(Utils.getColor(this, R.color.primary_white)); + } else { + mainTextView.setTextColor(Utils.getColor(this, R.color.primary_grey_900)); } if (mainTextView.getTypeface() == null) { @@ -172,16 +172,17 @@ public void onCreate(Bundle savedInstanceState) { } else { load(this); } - initStatusBarResources(findViewById(R.id.texteditor)); + initStatusBarResources(findViewById(R.id.textEditorRootView)); } @Override - protected void onSaveInstanceState(Bundle outState) { + protected void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); final TextEditorActivityViewModel viewModel = new ViewModelProvider(this).get(TextEditorActivityViewModel.class); - outState.putString(KEY_MODIFIED_TEXT, mainTextView.getText().toString()); + outState.putString( + KEY_MODIFIED_TEXT, mainTextView.getText() != null ? mainTextView.getText().toString() : ""); outState.putInt(KEY_INDEX, mainTextView.getScrollY()); outState.putString(KEY_ORIGINAL_TEXT, viewModel.getOriginal()); outState.putBoolean(KEY_MONOFONT, inputTypefaceMono.equals(mainTextView.getTypeface())); @@ -193,6 +194,7 @@ private void checkUnsavedChanges() { if (viewModel.getOriginal() != null && mainTextView.isShown() + && mainTextView.getText() != null && !viewModel.getOriginal().equals(mainTextView.getText().toString())) { new MaterialDialog.Builder(this) .title(R.string.unsaved_changes) @@ -293,10 +295,13 @@ public boolean onOptionsItemSelected(MenuItem item) { break; case R.id.save: // Make sure EditText is visible before saving! - saveFile(this, mainTextView.getText().toString()); + if (mainTextView.getText() != null) { + saveFile(this, mainTextView.getText().toString()); + } break; case R.id.details: if (editableFileAbstraction.scheme.equals(FILE) + && editableFileAbstraction.hybridFileParcelable.getFile() != null && editableFileAbstraction.hybridFileParcelable.getFile().exists()) { GeneralDialogCreation.showPropertiesDialogWithoutPermissions( editableFileAbstraction.hybridFileParcelable, this, getAppTheme()); @@ -316,7 +321,7 @@ public boolean onOptionsItemSelected(MenuItem item) { case R.id.openwith: if (editableFileAbstraction.scheme.equals(FILE)) { File currentFile = editableFileAbstraction.hybridFileParcelable.getFile(); - if (currentFile.exists()) { + if (currentFile != null && currentFile.exists()) { boolean useNewStack = getBoolean(PREFERENCE_TEXTEDITOR_NEWSTACK); FileUtils.openWith(currentFile, this, useNewStack); } else { @@ -355,7 +360,8 @@ public void onDestroy() { @Override public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) { // condition to check if callback is called in search editText - if (searchEditText != null && charSequence.hashCode() == searchEditText.getText().hashCode()) { + if (searchEditText.getText() != null + && charSequence.hashCode() == searchEditText.getText().hashCode()) { final TextEditorActivityViewModel viewModel = new ViewModelProvider(this).get(TextEditorActivityViewModel.class); @@ -371,7 +377,8 @@ public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) @Override public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) { - if (charSequence.hashCode() == mainTextView.getText().hashCode()) { + if (mainTextView.getText() != null + && charSequence.hashCode() == mainTextView.getText().hashCode()) { final TextEditorActivityViewModel viewModel = new ViewModelProvider(this).get(TextEditorActivityViewModel.class); final Timer oldTimer = viewModel.getTimer(); @@ -400,11 +407,12 @@ public void run() { new ViewModelProvider(textEditorActivity).get(TextEditorActivityViewModel.class); modified = - !textEditorActivity - .mainTextView - .getText() - .toString() - .equals(viewModel.getOriginal()); + textEditorActivity.mainTextView.getText() != null + && !textEditorActivity + .mainTextView + .getText() + .toString() + .equals(viewModel.getOriginal()); if (viewModel.getModified() != modified) { viewModel.setModified(modified); invalidateOptionsMenu(); @@ -420,7 +428,8 @@ public void run() { @Override public void afterTextChanged(Editable editable) { // searchBox callback block - if (searchEditText != null && editable.hashCode() == searchEditText.getText().hashCode()) { + if (searchEditText.getText() != null + && editable.hashCode() == searchEditText.getText().hashCode()) { final WeakReference textEditorActivityWR = new WeakReference<>(this); final OnProgressUpdate onProgressUpdate = @@ -429,7 +438,7 @@ public void afterTextChanged(Editable editable) { if (textEditorActivity == null) { return; } - textEditorActivity.unhighlightSearchResult(index); + textEditorActivity.colorSearchResult(index, getPrimary()); }; final OnAsyncTaskFinished> onAsyncTaskFinished = @@ -445,7 +454,7 @@ public void afterTextChanged(Editable editable) { viewModel.setSearchResultIndices(data); for (SearchResultIndex searchResultIndex : data) { - textEditorActivity.unhighlightSearchResult(searchResultIndex); + textEditorActivity.colorSearchResult(searchResultIndex, getPrimary()); } if (data.size() != 0) { @@ -460,6 +469,8 @@ public void afterTextChanged(Editable editable) { } }; + if (mainTextView.getText() == null) return; + searchTextTask = new SearchTextTask( mainTextView.getText().toString(), @@ -470,77 +481,60 @@ public void afterTextChanged(Editable editable) { } } - /** show search view with a circular reveal animation */ private void revealSearchView() { - int startRadius = 4; - int endRadius = Math.max(searchViewLayout.getWidth(), searchViewLayout.getHeight()); - DisplayMetrics metrics = new DisplayMetrics(); - getWindowManager().getDefaultDisplay().getMetrics(metrics); + searchViewLayout.setVisibility(View.VISIBLE); - // hardcoded and completely random - int cx = metrics.widthPixels - 160; - int cy = toolbar.getBottom(); - Animator animator; + Animation animation = AnimationUtils.loadAnimation(this, R.anim.fade_in_top); - // FIXME: 2016/11/18 ViewAnimationUtils Compatibility - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) - animator = - ViewAnimationUtils.createCircularReveal(searchViewLayout, cx, cy, startRadius, endRadius); - else animator = ObjectAnimator.ofFloat(searchViewLayout, "alpha", 0f, 1f); + animation.setAnimationListener( + new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) {} - animator.setInterpolator(new AccelerateDecelerateInterpolator()); - animator.setDuration(600); - searchViewLayout.setVisibility(View.VISIBLE); - searchEditText.setText(""); - animator.start(); - animator.addListener( - new AnimatorListenerAdapter() { @Override - public void onAnimationEnd(Animator animation) { + public void onAnimationEnd(Animation animation) { + searchEditText.requestFocus(); - InputMethodManager imm = - (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(searchEditText, InputMethodManager.SHOW_IMPLICIT); + + ((InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE)) + .showSoftInput(searchEditText, InputMethodManager.SHOW_IMPLICIT); } + + @Override + public void onAnimationRepeat(Animation animation) {} }); + + searchViewLayout.startAnimation(animation); } - /** hide search view with a circular reveal animation */ private void hideSearchView() { - int endRadius = 4; - int startRadius = Math.max(searchViewLayout.getWidth(), searchViewLayout.getHeight()); - DisplayMetrics metrics = new DisplayMetrics(); - getWindowManager().getDefaultDisplay().getMetrics(metrics); + Animation animation = AnimationUtils.loadAnimation(this, R.anim.fade_out_top); - // hardcoded and completely random - int cx = metrics.widthPixels - 160; - int cy = toolbar.getBottom(); - - Animator animator; - // FIXME: 2016/11/18 ViewAnimationUtils Compatibility - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - animator = - ViewAnimationUtils.createCircularReveal(searchViewLayout, cx, cy, startRadius, endRadius); - } else { - animator = ObjectAnimator.ofFloat(searchViewLayout, "alpha", 0f, 1f); - } + animation.setAnimationListener( + new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) {} - animator.setInterpolator(new AccelerateDecelerateInterpolator()); - animator.setDuration(600); - animator.start(); - animator.addListener( - new AnimatorListenerAdapter() { @Override - public void onAnimationEnd(Animator animation) { + public void onAnimationEnd(Animation animation) { + searchViewLayout.setVisibility(View.GONE); - InputMethodManager inputMethodManager = - (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); - inputMethodManager.hideSoftInputFromWindow( - searchEditText.getWindowToken(), InputMethodManager.HIDE_IMPLICIT_ONLY); + + cleanSpans(viewModel); + searchEditText.setText(""); + + ((InputMethodManager) getSystemService(INPUT_METHOD_SERVICE)) + .hideSoftInputFromWindow( + searchEditText.getWindowToken(), InputMethodManager.HIDE_IMPLICIT_ONLY); } + + @Override + public void onAnimationRepeat(Animation animation) {} }); + + searchViewLayout.startAnimation(animation); } @Override @@ -549,7 +543,7 @@ public void onClick(View v) { new ViewModelProvider(this).get(TextEditorActivityViewModel.class); switch (v.getId()) { - case R.id.prev: + case R.id.textEditorSearchPrevButton: // upButton if (viewModel.getCurrent() > 0) { unhighlightCurrentSearchResult(viewModel); @@ -560,7 +554,7 @@ public void onClick(View v) { highlightCurrentSearchResult(viewModel); } break; - case R.id.next: + case R.id.textEditorSearchNextButton: // downButton if (viewModel.getCurrent() < viewModel.getSearchResultIndices().size() - 1) { unhighlightCurrentSearchResult(viewModel); @@ -570,11 +564,6 @@ public void onClick(View v) { highlightCurrentSearchResult(viewModel); } break; - case R.id.close: - // closeButton - findViewById(R.id.searchview).setVisibility(View.GONE); - cleanSpans(viewModel); - break; default: throw new IllegalStateException(); } @@ -586,14 +575,16 @@ private void unhighlightCurrentSearchResult(final TextEditorActivityViewModel vi } SearchResultIndex resultIndex = viewModel.getSearchResultIndices().get(viewModel.getCurrent()); - unhighlightSearchResult(resultIndex); + colorSearchResult(resultIndex, getPrimary()); } private void highlightCurrentSearchResult(final TextEditorActivityViewModel viewModel) { SearchResultIndex keyValueNew = viewModel.getSearchResultIndices().get(viewModel.getCurrent()); - colorSearchResult(keyValueNew, Utils.getColor(this, R.color.search_text_highlight)); + colorSearchResult(keyValueNew, getAccent()); // scrolling to the highlighted element + if (getSupportActionBar() != null) return; + scrollView.scrollTo( 0, (Integer) keyValueNew.getLineNumber() @@ -602,18 +593,9 @@ private void highlightCurrentSearchResult(final TextEditorActivityViewModel view - getSupportActionBar().getHeight()); } - private void unhighlightSearchResult(SearchResultIndex resultIndex) { - @ColorInt int color; - if (getAppTheme().equals(AppTheme.LIGHT)) { - color = Color.YELLOW; - } else { - color = Color.LTGRAY; - } - - colorSearchResult(resultIndex, color); - } - private void colorSearchResult(SearchResultIndex resultIndex, @ColorInt int color) { + if (mainTextView.getText() == null) return; + mainTextView .getText() .setSpan( @@ -630,6 +612,8 @@ private void cleanSpans(TextEditorActivityViewModel viewModel) { viewModel.setLine(0); // clearing textView spans + if (mainTextView.getText() == null) return; + BackgroundColorSpan[] colorSpans = mainTextView.getText().getSpans(0, mainTextView.length(), BackgroundColorSpan.class); for (BackgroundColorSpan colorSpan : colorSpans) { 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/ui/fragments/MainFragment.java b/app/src/main/java/com/amaze/filemanager/ui/fragments/MainFragment.java index c8a11f114b..13f2cb06a5 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/MainFragment.java +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/MainFragment.java @@ -63,6 +63,7 @@ import com.amaze.filemanager.filesystem.files.EncryptDecryptUtils; import com.amaze.filemanager.filesystem.files.FileListSorter; import com.amaze.filemanager.filesystem.files.FileUtils; +import com.amaze.filemanager.filesystem.files.MediaConnectionUtils; import com.amaze.filemanager.ui.ExtensionsKt; import com.amaze.filemanager.ui.activities.MainActivity; import com.amaze.filemanager.ui.activities.MainActivityViewModel; @@ -1403,7 +1404,7 @@ public void hide(String path) { LOG.warn("failure when hiding file", e); } } - FileUtils.scanFile( + MediaConnectionUtils.scanFile( requireMainActivity(), new HybridFile[] {new HybridFile(OpenMode.FILE, path)}); } } 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: WeakReference + android:interpolator="@android:interpolator/decelerate_cubic" + android:shareInterpolator="true"> + android:duration="200" + android:fromXDelta="0%" + android:fromYDelta="-50%" + android:toXDelta="0%" + android:toYDelta="0%" /> + android:toAlpha="1" /> \ No newline at end of file diff --git a/app/src/main/res/anim/fade_out_top.xml b/app/src/main/res/anim/fade_out_top.xml new file mode 100644 index 0000000000..814163e4d9 --- /dev/null +++ b/app/src/main/res/anim/fade_out_top.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/actionmode_textviewer.xml b/app/src/main/res/layout/actionmode_textviewer.xml deleted file mode 100644 index 7a7c84ae0d..0000000000 --- a/app/src/main/res/layout/actionmode_textviewer.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/search.xml b/app/src/main/res/layout/search.xml index 2d3ec8df10..22adfcb697 100644 --- a/app/src/main/res/layout/search.xml +++ b/app/src/main/res/layout/search.xml @@ -16,49 +16,50 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . --> - - + android:orientation="vertical"> - + + - + android:layout_height="?actionBarSize" /> + + + - + + + android:lineSpacingExtra="1dp" + android:padding="16dp" + android:textSize="14sp" /> - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/text_editor_search_bar.xml b/app/src/main/res/layout/text_editor_search_bar.xml new file mode 100644 index 0000000000..fc8989bdc1 --- /dev/null +++ b/app/src/main/res/layout/text_editor_search_bar.xml @@ -0,0 +1,52 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index a76ae45d01..0d246815be 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -25,7 +25,6 @@ #575757 #484848 #50575757 - #FF9632 #f2f2f2 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c06df0c6df..d753159e4b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -814,5 +814,9 @@ You only need to do this once, until the next time you select a new location for Recent Results Invalid, unsupported, or no URI was provided + 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. diff --git a/app/src/test/java/com/amaze/filemanager/ui/activities/TextEditorActivityTest.java b/app/src/test/java/com/amaze/filemanager/ui/activities/TextEditorActivityTest.java index 4137809960..3209d48ad1 100644 --- a/app/src/test/java/com/amaze/filemanager/ui/activities/TextEditorActivityTest.java +++ b/app/src/test/java/com/amaze/filemanager/ui/activities/TextEditorActivityTest.java @@ -104,7 +104,7 @@ private void generateActivity(Intent intent) { Robolectric.buildActivity(TextEditorActivity.class, intent).create().start().visible(); TextEditorActivity activity = controller.get(); - text = activity.findViewById(R.id.fname); + text = activity.findViewById(R.id.textEditorMainEditText); activity.onDestroy(); }