From e851e54320053de9717cc8b8d7f02d6dfb69c95c Mon Sep 17 00:00:00 2001 From: Vishal Nehra Date: Thu, 21 Sep 2023 11:43:31 +0530 Subject: [PATCH 1/7] 971: Implement recycle bin --- app/build.gradle | 6 +- app/proguard.cfg | 4 +- .../adapters/AppsRecyclerAdapter.kt | 2 +- .../filemanager/adapters/HiddenAdapter.kt | 2 +- .../filemanager/adapters/RecyclerAdapter.java | 4 + .../filemanager/application/AppConfig.java | 58 +++++ .../asynchronous/asynctasks/DeleteTask.java | 14 +- .../asynctasks/LoadFilesListTask.java | 33 ++- .../filemanager/filesystem/HybridFile.java | 61 ++++- .../filesystem/files/FileUtils.java | 2 +- .../amaze/filemanager/ui/ItemPopupMenu.java | 2 + .../ui/activities/MainActivityViewModel.kt | 143 ++++++++++++ .../ui/dialogs/GeneralDialogCreation.java | 209 +++++++++++++++++- .../PreferencesConstants.kt | 4 + .../filemanager/ui/views/drawer/Drawer.java | 10 + .../filemanager/utils/MainActivityHelper.java | 6 +- .../res/drawable/round_delete_outline_24.xml | 5 + app/src/main/res/layout/dialog_delete.xml | 10 + app/src/main/res/menu/contextual.xml | 6 + app/src/main/res/values/strings.xml | 5 + .../asynctasks/AbstractDeleteTaskTestBase.kt | 4 +- app/tests-proguard.cfg | 2 + build.gradle | 2 +- .../fileoperations/filesystem/OpenMode.java | 3 +- 24 files changed, 574 insertions(+), 23 deletions(-) create mode 100644 app/src/main/res/drawable/round_delete_outline_24.xml diff --git a/app/build.gradle b/app/build.gradle index 6d4e53e4a4..763238b9c7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,7 +6,7 @@ apply plugin: 'com.hiya.jacoco-android' apply plugin: "com.starter.easylauncher" android { - compileSdkVersion 31 + compileSdkVersion 32 packagingOptions { resources { excludes += ['proguard-project.txt', 'project.properties', 'META-INF/LICENSE.txt', 'META-INF/LICENSE', 'META-INF/NOTICE.txt', 'META-INF/NOTICE', 'META-INF/DEPENDENCIES.txt', 'META-INF/DEPENDENCIES'] @@ -17,7 +17,7 @@ android { defaultConfig { applicationId "com.amaze.filemanager" minSdkVersion 19 - targetSdkVersion 31 + targetSdkVersion 32 versionCode 118 versionName "3.8.5" multiDexEnabled true @@ -249,7 +249,7 @@ dependencies { runtimeOnly "com.github.tony19:logback-android:$logbackAndroidVersion" implementation 'com.google.code.gson:gson:2.9.1' - + implementation 'com.github.TeamAmaze:AmazeTrashBin:1.0.7' } configurations.all { diff --git a/app/proguard.cfg b/app/proguard.cfg index 23298be7fc..a478cb0d5a 100644 --- a/app/proguard.cfg +++ b/app/proguard.cfg @@ -117,4 +117,6 @@ #Keep constructors that involves an InputStream -keepclassmembers class * extends org.apache.commons.compress.compressors.CompressorInputStream { (java.io.InputStream); -} \ No newline at end of file +} + +-keep class com.amaze.trashbin.** { *; } \ No newline at end of file diff --git a/app/src/main/java/com/amaze/filemanager/adapters/AppsRecyclerAdapter.kt b/app/src/main/java/com/amaze/filemanager/adapters/AppsRecyclerAdapter.kt index 66e7445d45..2fafd8c04c 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/AppsRecyclerAdapter.kt +++ b/app/src/main/java/com/amaze/filemanager/adapters/AppsRecyclerAdapter.kt @@ -521,7 +521,7 @@ class AppsRecyclerAdapter( } else { files.add(f1) } - DeleteTask(fragment.requireContext()).execute(files) + DeleteTask(fragment.requireContext(), false).execute(files) } .build() .show() diff --git a/app/src/main/java/com/amaze/filemanager/adapters/HiddenAdapter.kt b/app/src/main/java/com/amaze/filemanager/adapters/HiddenAdapter.kt index 0e7ba31efe..468059a5fd 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/HiddenAdapter.kt +++ b/app/src/main/java/com/amaze/filemanager/adapters/HiddenAdapter.kt @@ -86,7 +86,7 @@ class HiddenAdapter( nomediaFile.mode = OpenMode.FILE val filesToDelete = ArrayList() filesToDelete.add(nomediaFile) - val task = DeleteTask(context) + val task = DeleteTask(context, false) task.execute(filesToDelete) } DataUtils.getInstance().removeHiddenFile(hiddenFiles[position].path) 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..82cca785f4 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/RecyclerAdapter.java +++ b/app/src/main/java/com/amaze/filemanager/adapters/RecyclerAdapter.java @@ -1381,6 +1381,10 @@ private void showPopup(@NonNull View view, @NonNull final LayoutElementParcelabl view, sharedPrefs); popupMenu.inflate(R.menu.item_extras); + boolean isLocalFile = rowItem.generateBaseFile().isCustomPath() && rowItem.generateBaseFile().getPath().equals("7"); + if (rowItem.generateBaseFile().isCustomPath() && isLocalFile) { + popupMenu.getMenu().findItem(R.id.restore).setVisible(true); + } String description = rowItem.desc.toLowerCase(); if (rowItem.isDirectory) { diff --git a/app/src/main/java/com/amaze/filemanager/application/AppConfig.java b/app/src/main/java/com/amaze/filemanager/application/AppConfig.java index 0c0c18379f..05cc5f2c71 100644 --- a/app/src/main/java/com/amaze/filemanager/application/AppConfig.java +++ b/app/src/main/java/com/amaze/filemanager/application/AppConfig.java @@ -20,6 +20,7 @@ package com.amaze.filemanager.application; +import java.io.File; import java.lang.ref.WeakReference; import java.util.concurrent.Callable; @@ -39,13 +40,21 @@ import com.amaze.filemanager.database.ExplorerDatabase; import com.amaze.filemanager.database.UtilitiesDatabase; import com.amaze.filemanager.database.UtilsHandler; +import com.amaze.filemanager.fileoperations.exceptions.ShellNotRunningException; +import com.amaze.filemanager.fileoperations.filesystem.OpenMode; +import com.amaze.filemanager.filesystem.HybridFile; import com.amaze.filemanager.filesystem.ssh.CustomSshJConfig; +import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants; import com.amaze.filemanager.ui.provider.UtilitiesProvider; import com.amaze.filemanager.utils.ScreenUtils; +import com.amaze.trashbin.TrashBin; +import com.amaze.trashbin.TrashBinConfig; import android.app.Activity; import android.app.Application; import android.content.Context; +import android.content.SharedPreferences; +import android.os.Environment; import android.os.StrictMode; import android.widget.Toast; @@ -53,11 +62,14 @@ import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.appcompat.app.AppCompatDelegate; +import androidx.preference.PreferenceManager; import io.reactivex.Completable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; import jcifs.Config; +import jcifs.smb.SmbException; +import kotlin.jvm.functions.Function1; @AcraCore( buildConfigClass = BuildConfig.class, @@ -78,6 +90,11 @@ public class AppConfig extends GlideApplication { private ExplorerDatabase explorerDatabase; + private TrashBinConfig trashBinConfig; + private TrashBin trashBin; + private static final String TRASH_BIN_BASE_PATH = Environment.getExternalStorageDirectory().getPath() + + File.separator + ".AmazeData"; + public UtilitiesProvider getUtilsProvider() { return utilsProvider; } @@ -253,4 +270,45 @@ protected void initACRA() { R.string.app_ui_crash)); } } + + public TrashBin getTrashBinInstance() { + if (trashBin == null) { + trashBin = new TrashBin(getTrashBinConfig(), s -> { + runInBackground(() -> { + HybridFile file = new HybridFile(OpenMode.FILE, s); + try { + file.delete(getMainActivityContext(), false); + } catch (ShellNotRunningException | SmbException e) { + log.warn("failed to delete file in trash bin cleanup", e); + } + }); + return true; + }, null); + } + return trashBin; + } + + private TrashBinConfig getTrashBinConfig() { + if (trashBinConfig == null) { + SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); + + int days = sharedPrefs.getInt( + PreferencesConstants.KEY_TRASH_BIN_RETENTION_DAYS, + TrashBinConfig.RETENTION_DAYS_INFINITE + ); + long bytes = sharedPrefs.getLong( + PreferencesConstants.KEY_TRASH_BIN_RETENTION_BYTES, + TrashBinConfig.RETENTION_BYTES_INFINITE + ); + int numOfFiles = sharedPrefs.getInt( + PreferencesConstants.KEY_TRASH_BIN_RETENTION_NUM_OF_FILES, + TrashBinConfig.RETENTION_NUM_OF_FILES + ); + trashBinConfig = new TrashBinConfig( + TRASH_BIN_BASE_PATH, days, bytes, + numOfFiles, false, true + ); + } + return trashBinConfig; + } } 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..d2e230738b 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 @@ -71,18 +71,23 @@ public class DeleteTask private final Context applicationContext; private final boolean rootMode; private CompressedExplorerFragment compressedExplorerFragment; + + private boolean doDeletePermanently; private final DataUtils dataUtils = DataUtils.getInstance(); - public DeleteTask(@NonNull Context applicationContext) { + public DeleteTask(@NonNull Context applicationContext, @NonNull boolean doDeletePermanently) { this.applicationContext = applicationContext.getApplicationContext(); + this.doDeletePermanently = doDeletePermanently; rootMode = PreferenceManager.getDefaultSharedPreferences(applicationContext) .getBoolean(PreferencesConstants.PREFERENCE_ROOTMODE, false); } - public DeleteTask( - @NonNull Context applicationContext, CompressedExplorerFragment compressedExplorerFragment) { + public DeleteT`ask( + @NonNull Context applicationContext, + CompressedExplorerFragment compressedExplorerFragment) { this.applicationContext = applicationContext.getApplicationContext(); + this.doDeletePermanently = false; rootMode = PreferenceManager.getDefaultSharedPreferences(applicationContext) .getBoolean(PreferencesConstants.PREFERENCE_ROOTMODE, false); @@ -187,6 +192,9 @@ private boolean doDeleteFile(@NonNull HybridFileParcelable file) throws Exceptio } default: try { + if (!doDeletePermanently) { + return file.moveToBin(applicationContext); + } return (file.delete(applicationContext, rootMode)); } catch (ShellNotRunningException | SmbException e) { LOG.warn("failed to delete files", e); diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/LoadFilesListTask.java b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/LoadFilesListTask.java index 4ae437aaa6..f5c60835a6 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/LoadFilesListTask.java +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/LoadFilesListTask.java @@ -31,6 +31,8 @@ import java.util.Date; import java.util.LinkedList; import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -59,6 +61,8 @@ import com.amaze.filemanager.utils.OTGUtil; import com.amaze.filemanager.utils.OnAsyncTaskFinished; import com.amaze.filemanager.utils.OnFileFound; +import com.amaze.trashbin.TrashBin; +import com.amaze.trashbin.TrashBinFile; import com.cloudrail.si.interfaces.CloudStorage; import android.content.ContentResolver; @@ -189,7 +193,8 @@ public LoadFilesListTask( } if (list != null - && !(openmode == OpenMode.CUSTOM && ((path).equals("5") || (path).equals("6")))) { + && !(openmode == OpenMode.CUSTOM && (("5").equals(path) || ("6").equals(path) + || ("7").equals(path)))) { postListCustomPathProcess(list, mainFragment); } @@ -212,6 +217,7 @@ private List getCachedMediaList( int mediaType = Integer.parseInt(path); if (5 == mediaType || 6 == mediaType + || 7 == mediaType || mainActivityViewModel.getMediaCacheHash().get(mediaType) == null || forceReload) { switch (Integer.parseInt(path)) { @@ -236,10 +242,13 @@ private List getCachedMediaList( case 6: list = listRecentFiles(); break; + case 7: + list = listTrashBinFiles(); + break; default: throw new IllegalStateException(); } - if (5 != mediaType && 6 != mediaType) { + if (5 != mediaType && 6 != mediaType && 7 != mediaType) { // not saving recent files in cache mainActivityViewModel.getMediaCacheHash().set(mediaType, list); } @@ -554,6 +563,26 @@ else if (cursor.getCount() > 0 && cursor.moveToFirst()) { return recentFiles; } + private @Nullable List listTrashBinFiles() { + final Context context = this.context.get(); + + if (context == null) { + cancel(true); + return null; + } + + TrashBin trashBin = AppConfig.getInstance().getTrashBinInstance(); + List deletedFiles = new ArrayList<>(); + if (trashBin != null) { + for (TrashBinFile trashBinFile : trashBin.listFilesInBin()) { + deletedFiles.add(new HybridFile(OpenMode.TRASH_BIN, trashBinFile.getPath(), + trashBinFile.getFileName(), trashBinFile.isDirectory()) + .generateLayoutElement(context, false)); + } + } + return deletedFiles; + } + private @NonNull List listAppDataDirectories(@NonNull String basePath) { if (!GenericExtKt.containsPath(FileProperties.ANDROID_DEVICE_DATA_DIRS, basePath)) { throw new IllegalArgumentException("Invalid base path: [" + basePath + "]"); 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 2f91894d0d..9c52e97835 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/HybridFile.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/HybridFile.java @@ -43,6 +43,7 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; +import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.Locale; @@ -87,6 +88,8 @@ import com.amaze.filemanager.utils.OnFileFound; import com.amaze.filemanager.utils.Utils; import com.amaze.filemanager.utils.smb.SmbUtil; +import com.amaze.trashbin.TrashBin; +import com.amaze.trashbin.TrashBinFile; import com.cloudrail.si.interfaces.CloudStorage; import com.cloudrail.si.types.SpaceAllocation; @@ -99,6 +102,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.content.FileProvider; import androidx.documentfile.provider.DocumentFile; import androidx.preference.PreferenceManager; @@ -112,6 +116,7 @@ import jcifs.smb.SmbFile; import kotlin.collections.ArraysKt; import kotlin.io.ByteStreamsKt; +import kotlin.jvm.functions.Function2; import kotlin.text.Charsets; import net.schmizz.sshj.SSHClient; import net.schmizz.sshj.common.Buffer; @@ -531,7 +536,8 @@ public boolean isCustomPath() { || path.equals("3") || path.equals("4") || path.equals("5") - || path.equals("6"); + || path.equals("6") + || path.equals("7"); } /** Helper method to get parent path */ @@ -1512,6 +1518,55 @@ public Boolean executeWithFtpClient(@NonNull FTPClient ftpClient) return !exists(); } + public void restoreFromBin(Context context) { + List trashBinFiles = Collections.singletonList(this.toTrashBinFile(context)); + TrashBin trashBin = AppConfig.getInstance().getTrashBinInstance(); + if (trashBin != null) { + trashBin.moveToBin(trashBinFiles, true, (originalFilePath, + trashBinDestination) -> { + File source = new File(originalFilePath); + File dest = new File(trashBinDestination); + if (!source.renameTo(dest)) { + return false; + } + Uri uri = FileProvider.getUriForFile( + context, + context.getPackageName(), new File(originalFilePath) + ); + FileUtils.scanFile( + uri, + context + ); + return true; + }); + } + } + + public boolean moveToBin(Context context) { + List trashBinFiles = Collections.singletonList(this.toTrashBinFile(context)); + TrashBin trashBin = AppConfig.getInstance().getTrashBinInstance(); + if (trashBin != null) { + trashBin.moveToBin(trashBinFiles, true, (originalFilePath, + trashBinDestination) -> { + File source = new File(originalFilePath); + File dest = new File(trashBinDestination); + if (!source.renameTo(dest)) { + return false; + } + Uri uri = FileProvider.getUriForFile( + context, + context.getPackageName(), new File(originalFilePath) + ); + FileUtils.scanFile( + uri, + context + ); + return true; + }); + } + return true; + } + /** * Returns the name of file excluding it's extension If no extension is found then whole file name * is returned @@ -1732,6 +1787,10 @@ public void onError(Throwable e) { } }); } + public TrashBinFile toTrashBinFile(Context context) { + return new TrashBinFile(name, isDirectory(context), path, length(context), null); + } + private SshClientSessionTemplate getSftpHash(String command) { return new SshClientSessionTemplate(path) { 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 d55e7f09b3..3ac4b88c00 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 @@ -282,7 +282,7 @@ private static void scanFile(@NonNull HybridFile hybridFile, Context context) { * @param uri File's {@link Uri} * @param c {@link Context} */ - private static void scanFile(@NonNull Uri uri, @NonNull Context c) { + public static void scanFile(@NonNull Uri uri, @NonNull Context c) { Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri); c.sendBroadcast(mediaScanIntent); } diff --git a/app/src/main/java/com/amaze/filemanager/ui/ItemPopupMenu.java b/app/src/main/java/com/amaze/filemanager/ui/ItemPopupMenu.java index a3ede65503..3e6408fb65 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/ItemPopupMenu.java +++ b/app/src/main/java/com/amaze/filemanager/ui/ItemPopupMenu.java @@ -160,6 +160,8 @@ public boolean onMenuItemClick(MenuItem item) { GeneralDialogCreation.deleteFilesDialog( context, mainActivity, positions, utilitiesProvider.getAppTheme()); return true; + case R.id.restore: + case R.id.open_with: boolean useNewStack = sharedPrefs.getBoolean(PreferencesConstants.PREFERENCE_TEXTEDITOR_NEWSTACK, false); diff --git a/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivityViewModel.kt b/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivityViewModel.kt index 81192a0d18..9e57e8fb87 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivityViewModel.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivityViewModel.kt @@ -21,20 +21,32 @@ package com.amaze.filemanager.ui.activities import android.app.Application +import android.os.Environment import android.provider.MediaStore import androidx.collection.LruCache +import androidx.core.content.FileProvider import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import androidx.preference.PreferenceManager import com.amaze.filemanager.adapters.data.LayoutElementParcelable +import com.amaze.filemanager.asynchronous.asynctasks.DeleteTask import com.amaze.filemanager.fileoperations.filesystem.OpenMode +import com.amaze.filemanager.filesystem.HybridFile import com.amaze.filemanager.filesystem.HybridFileParcelable import com.amaze.filemanager.filesystem.RootHelper +import com.amaze.filemanager.filesystem.files.FileUtils import com.amaze.filemanager.filesystem.root.ListFilesCommand.listFiles +import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_SHOW_HIDDENFILES +import com.amaze.trashbin.DeletePermanentlyCallback +import com.amaze.trashbin.MoveFilesCallback +import com.amaze.trashbin.TrashBin +import com.amaze.trashbin.TrashBinConfig import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import org.slf4j.LoggerFactory import java.io.File import java.util.Locale @@ -43,12 +55,20 @@ class MainActivityViewModel(val applicationContext: Application) : var mediaCacheHash: List?> = List(5) { null } var listCache: LruCache> = LruCache(50) + var trashBinFilesLiveData: MutableLiveData?>? = null + + private var trashBinConfig: TrashBinConfig? = null + private var trashBin: TrashBin? = null + + private val TRASH_BIN_BASE_PATH = Environment.getExternalStorageDirectory() + .path + File.separator + ".AmazeData" companion object { /** * size of list to be cached for local files */ val CACHE_LOCAL_LIST_THRESHOLD: Int = 100 + private val LOG = LoggerFactory.getLogger(MainActivityViewModel::class.java) } /** @@ -174,4 +194,127 @@ class MainActivityViewModel(val applicationContext: Application) : return mutableLiveData } + + fun getTrashBinInstance(): TrashBin { + if (trashBin == null) { + trashBin = TrashBin( + getTrashbinConfig(), + object : DeletePermanentlyCallback { + override fun invoke(deletePath: String): Boolean { + viewModelScope.launch(Dispatchers.IO) { + val hybridFile = HybridFile(OpenMode.FILE, deletePath) + hybridFile.delete(applicationContext, false) + } + return true + } + }, + null + ) + } + return trashBin!! + } + + fun getTrashbinConfig(): TrashBinConfig { + if (trashBinConfig == null) { + val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(applicationContext) + + val days = sharedPrefs.getInt( + PreferencesConstants.KEY_TRASH_BIN_RETENTION_DAYS, + TrashBinConfig.RETENTION_DAYS_INFINITE + ) + val bytes = sharedPrefs.getLong( + PreferencesConstants.KEY_TRASH_BIN_RETENTION_BYTES, + TrashBinConfig.RETENTION_BYTES_INFINITE + ) + val numOfFiles = sharedPrefs.getInt( + PreferencesConstants.KEY_TRASH_BIN_RETENTION_NUM_OF_FILES, + TrashBinConfig.RETENTION_NUM_OF_FILES + ) + trashBinConfig = TrashBinConfig( + TRASH_BIN_BASE_PATH, days, bytes, + numOfFiles, false, true + ) + } + return trashBinConfig!! + } + + fun moveToBinLightWeight(mediaFileInfoList: List) { + viewModelScope.launch(Dispatchers.IO) { + val trashBinFilesList = mediaFileInfoList.map { it.generateBaseFile() + .toTrashBinFile(applicationContext) } + getTrashBinInstance().moveToBin( + trashBinFilesList, true, + object : MoveFilesCallback { + override fun invoke( + originalFilePath: String, + trashBinDestination: String + ): Boolean { + val source = File(originalFilePath) + val dest = File(trashBinDestination) + if (!source.renameTo(dest)) { + return false + } + val uri = FileProvider.getUriForFile( + applicationContext, + applicationContext.packageName, File(originalFilePath) + ) + FileUtils.scanFile( + uri, + applicationContext + ) + return true + } + } + ) + } + } + + fun restoreFromBin(mediaFileInfoList: List) { + viewModelScope.launch(Dispatchers.IO) { + LOG.info("Moving media files to bin $mediaFileInfoList") + val trashBinFilesList = mediaFileInfoList.map { it.generateBaseFile() + .toTrashBinFile(applicationContext) } + getTrashBinInstance().restore( + trashBinFilesList, true, + object : MoveFilesCallback { + override fun invoke(source: String, dest: String): Boolean { + val sourceFile = File(source) + val destFile = File(dest) + if (!sourceFile.renameTo(destFile)) { + return false + } + val uri = FileProvider.getUriForFile( + applicationContext, + applicationContext.packageName, File(dest) + ) + FileUtils.scanFile( + uri, + applicationContext + ) + return true + } + } + ) + } + } + + fun progressTrashBinFilesLiveData(): MutableLiveData?> { + if (trashBinFilesLiveData == null) { + trashBinFilesLiveData = MutableLiveData() + trashBinFilesLiveData?.value = null + viewModelScope.launch(Dispatchers.IO) { + trashBinFilesLiveData?.postValue( + ArrayList( + getTrashBinInstance().listFilesInBin() + .map { + HybridFile(OpenMode.FILE, it.path, it.fileName, it.isDirectory) + .generateLayoutElement(applicationContext, false + ) + } + ) + ) + } + } + return trashBinFilesLiveData!! + } } diff --git a/app/src/main/java/com/amaze/filemanager/ui/dialogs/GeneralDialogCreation.java b/app/src/main/java/com/amaze/filemanager/ui/dialogs/GeneralDialogCreation.java index c6ad3aa548..3f86501b6a 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/dialogs/GeneralDialogCreation.java +++ b/app/src/main/java/com/amaze/filemanager/ui/dialogs/GeneralDialogCreation.java @@ -208,11 +208,18 @@ public static void deleteFilesDialog( sharedPreferences.getBoolean( PreferencesConstants.PREFERENCE_DELETE_CONFIRMATION, PreferencesConstants.DEFAULT_PREFERENCE_DELETE_CONFIRMATION); + View dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_delete, null); + final AppCompatCheckBox deletePermanentlyCheckbox = dialogView.findViewById(R.id.delete_permanently_checkbox); + if (positions.get(0).generateBaseFile().isLocal()) { + // FIXME: make sure dialog is not shown for zero items + // allow trash bin delete only for local files for now + deletePermanentlyCheckbox.setVisibility(View.VISIBLE); + } // Build dialog with custom view layout and accent color. MaterialDialog dialog = new MaterialDialog.Builder(context) .title(context.getString(R.string.dialog_delete_title)) - .customView(R.layout.dialog_delete, true) + .customView(dialogView, true) .theme(appTheme.getMaterialDialogTheme(context)) .negativeText(context.getString(R.string.cancel).toUpperCase()) .positiveText(context.getString(R.string.delete).toUpperCase()) @@ -222,7 +229,8 @@ public static void deleteFilesDialog( (dialog1, which) -> { Toast.makeText(context, context.getString(R.string.deleting), Toast.LENGTH_SHORT) .show(); - mainActivity.mainActivityHelper.deleteFiles(itemsToDelete); + mainActivity.mainActivityHelper.deleteFiles(itemsToDelete, + deletePermanentlyCheckbox.isChecked()); }) .build(); @@ -327,7 +335,8 @@ protected void onPostExecute(Void aVoid) { updateViews(sizeTotal, files, directories, counterFiles, counterDirectories); } else { Toast.makeText(context, context.getString(R.string.deleting), Toast.LENGTH_SHORT).show(); - mainActivity.mainActivityHelper.deleteFiles(itemsToDelete); + mainActivity.mainActivityHelper.deleteFiles(itemsToDelete, + deletePermanentlyCheckbox.isChecked()); } } @@ -398,6 +407,200 @@ private void updateViews( } } + @SuppressWarnings("ConstantConditions") + public static void restoreFilesDialog( + @NonNull final Context context, + @NonNull final MainActivity mainActivity, + @NonNull final List positions, + @NonNull AppTheme appTheme) { + + final ArrayList itemsToDelete = new ArrayList<>(); + int accentColor = mainActivity.getAccent(); + View dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_delete, null); + final AppCompatCheckBox deletePermanentlyCheckbox = dialogView.findViewById(R.id.delete_permanently_checkbox); + // Build dialog with custom view layout and accent color. + MaterialDialog dialog = + new MaterialDialog.Builder(context) + .title(context.getString(R.string.restore_files)) + .customView(dialogView, true) + .theme(appTheme.getMaterialDialogTheme(context)) + .negativeText(context.getString(R.string.cancel).toUpperCase()) + .positiveText(context.getString(R.string.done).toUpperCase()) + .positiveColor(accentColor) + .negativeColor(accentColor) + .onPositive( + (dialog1, which) -> { + Toast.makeText(context, context.getString(R.string.processing), Toast.LENGTH_SHORT) + .show(); + mainActivity.mainActivityHelper.deleteFiles(itemsToDelete, + deletePermanentlyCheckbox.isChecked()); + }) + .build(); + + // Get views from custom layout to set text values. + final AppCompatTextView categoryDirectories = + dialog.getCustomView().findViewById(R.id.category_directories); + final AppCompatTextView categoryFiles = + dialog.getCustomView().findViewById(R.id.category_files); + final AppCompatTextView listDirectories = + dialog.getCustomView().findViewById(R.id.list_directories); + final AppCompatTextView listFiles = dialog.getCustomView().findViewById(R.id.list_files); + final AppCompatTextView total = dialog.getCustomView().findViewById(R.id.total); + + new AsyncTask() { + + long sizeTotal = 0; + StringBuilder files = new StringBuilder(); + StringBuilder directories = new StringBuilder(); + int counterDirectories = 0; + int counterFiles = 0; + + @Override + protected void onPreExecute() { + super.onPreExecute(); + listFiles.setText(context.getString(R.string.loading)); + listDirectories.setText(context.getString(R.string.loading)); + total.setText(context.getString(R.string.loading)); + } + + @Override + protected Void doInBackground(Void... params) { + + for (int i = 0; i < positions.size(); i++) { + final LayoutElementParcelable layoutElement = positions.get(i); + itemsToDelete.add(layoutElement.generateBaseFile()); + // Build list of directories to delete. + if (layoutElement.isDirectory) { + // Don't add newline between category and list. + if (counterDirectories != 0) { + directories.append("\n"); + } + + long sizeDirectory = layoutElement.generateBaseFile().folderSize(context); + + directories + .append(++counterDirectories) + .append(". ") + .append(layoutElement.title) + .append(" (") + .append(Formatter.formatFileSize(context, sizeDirectory)) + .append(")"); + sizeTotal += sizeDirectory; + // Build list of files to delete. + } else { + // Don't add newline between category and list. + if (counterFiles != 0) { + files.append("\n"); + } + + files + .append(++counterFiles) + .append(". ") + .append(layoutElement.title) + .append(" (") + .append(layoutElement.size) + .append(")"); + sizeTotal += layoutElement.longSize; + } + + publishProgress(sizeTotal, counterFiles, counterDirectories, files, directories); + } + return null; + } + + @Override + protected void onProgressUpdate(Object... result) { + super.onProgressUpdate(result); + int tempCounterFiles = (int) result[1]; + int tempCounterDirectories = (int) result[2]; + long tempSizeTotal = (long) result[0]; + StringBuilder tempFilesStringBuilder = (StringBuilder) result[3]; + StringBuilder tempDirectoriesStringBuilder = (StringBuilder) result[4]; + + updateViews( + tempSizeTotal, + tempFilesStringBuilder, + tempDirectoriesStringBuilder, + tempCounterFiles, + tempCounterDirectories); + } + + @Override + protected void onPostExecute(Void aVoid) { + super.onPostExecute(aVoid); + Toast.makeText(context, context.getString(R.string.restoring), Toast.LENGTH_SHORT).show(); + + mainActivity.mainActivityHelper.deleteFiles(itemsToDelete, + deletePermanentlyCheckbox.isChecked()); + } + + private void updateViews( + long tempSizeTotal, + StringBuilder filesStringBuilder, + StringBuilder directoriesStringBuilder, + int... values) { + + int tempCounterFiles = values[0]; + int tempCounterDirectories = values[1]; + + // Hide category and list for directories when zero. + if (tempCounterDirectories == 0) { + + if (tempCounterDirectories == 0) { + + categoryDirectories.setVisibility(View.GONE); + listDirectories.setVisibility(View.GONE); + } + // Hide category and list for files when zero. + } + + if (tempCounterFiles == 0) { + + categoryFiles.setVisibility(View.GONE); + listFiles.setVisibility(View.GONE); + } + + if (tempCounterDirectories != 0 || tempCounterFiles != 0) { + listDirectories.setText(directoriesStringBuilder); + if (listDirectories.getVisibility() != View.VISIBLE && tempCounterDirectories != 0) + listDirectories.setVisibility(View.VISIBLE); + listFiles.setText(filesStringBuilder); + if (listFiles.getVisibility() != View.VISIBLE && tempCounterFiles != 0) + listFiles.setVisibility(View.VISIBLE); + + if (categoryDirectories.getVisibility() != View.VISIBLE && tempCounterDirectories != 0) + categoryDirectories.setVisibility(View.VISIBLE); + if (categoryFiles.getVisibility() != View.VISIBLE && tempCounterFiles != 0) + categoryFiles.setVisibility(View.VISIBLE); + } + + // Show total size with at least one directory or file and size is not zero. + if (tempCounterFiles + tempCounterDirectories > 1 && tempSizeTotal > 0) { + StringBuilder builderTotal = + new StringBuilder() + .append(context.getString(R.string.total)) + .append(" ") + .append(Formatter.formatFileSize(context, tempSizeTotal)); + total.setText(builderTotal); + if (total.getVisibility() != View.VISIBLE) total.setVisibility(View.VISIBLE); + } else { + total.setVisibility(View.GONE); + } + } + }.execute(); + + // Set category text color for Jelly Bean (API 16) and later. + if (SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + categoryDirectories.setTextColor(accentColor); + categoryFiles.setTextColor(accentColor); + } + + if (needConfirmation) { + // Show dialog on screen. + dialog.show(); + } + } + public static void showPropertiesDialogWithPermissions( @NonNull HybridFileParcelable baseFile, @Nullable final String permissions, diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/PreferencesConstants.kt b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/PreferencesConstants.kt index 91978ffd33..43797a30c9 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/PreferencesConstants.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/PreferencesConstants.kt @@ -108,5 +108,9 @@ object PreferencesConstants { const val PREFERENCE_APPLIST_SORTBY = "AppsListFragment.sortBy" const val PREFERENCE_APPLIST_ISASCENDING = "AppsListFragment.isAscending" + const val KEY_TRASH_BIN_RETENTION_DAYS = "trash_bin_retention_days" + const val KEY_TRASH_BIN_RETENTION_BYTES = "trash_bin_retention_bytes" + const val KEY_TRASH_BIN_RETENTION_NUM_OF_FILES = "trash_bin_retention_num_of_files" + const val DEFAULT_PREFERENCE_DELETE_CONFIRMATION = true } diff --git a/app/src/main/java/com/amaze/filemanager/ui/views/drawer/Drawer.java b/app/src/main/java/com/amaze/filemanager/ui/views/drawer/Drawer.java index 06b390a4e8..d770d31d59 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/views/drawer/Drawer.java +++ b/app/src/main/java/com/amaze/filemanager/ui/views/drawer/Drawer.java @@ -589,6 +589,16 @@ public void refreshDrawer() { R.drawable.ic_round_analytics_24, null); + // initially load trash bin items with "7" but ones listed they're referred as @link{OpenMode.TRASH_BIN} + addNewItem( + menu, + LASTGROUP, + order++, + R.string.trasbin_bin, + new MenuMetadata("7"), + R.drawable.round_delete_outline_24, + null); + addNewItem( menu, LASTGROUP, diff --git a/app/src/main/java/com/amaze/filemanager/utils/MainActivityHelper.java b/app/src/main/java/com/amaze/filemanager/utils/MainActivityHelper.java index 7a927ec5c1..4261f6f174 100644 --- a/app/src/main/java/com/amaze/filemanager/utils/MainActivityHelper.java +++ b/app/src/main/java/com/amaze/filemanager/utils/MainActivityHelper.java @@ -679,10 +679,10 @@ public void invalidName(final HybridFile file) { }); } - public void deleteFiles(ArrayList files) { + public void deleteFiles(ArrayList files, boolean doDeletePermanently) { if (files == null || files.size() == 0) return; if (files.get(0).isSmb() || files.get(0).isFtp()) { - new DeleteTask(mainActivity).execute(files); + new DeleteTask(mainActivity, doDeletePermanently).execute(files); return; } @FolderState @@ -692,7 +692,7 @@ public void deleteFiles(ArrayList files) { mainActivity.oparrayList = (files); mainActivity.operation = DELETE; } else if (mode == WRITABLE_OR_ON_SDCARD || mode == DOESNT_EXIST) - new DeleteTask(mainActivity).execute((files)); + new DeleteTask(mainActivity, doDeletePermanently).execute((files)); else Toast.makeText(mainActivity, R.string.not_allowed, Toast.LENGTH_SHORT).show(); } diff --git a/app/src/main/res/drawable/round_delete_outline_24.xml b/app/src/main/res/drawable/round_delete_outline_24.xml new file mode 100644 index 0000000000..465e04b82a --- /dev/null +++ b/app/src/main/res/drawable/round_delete_outline_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/dialog_delete.xml b/app/src/main/res/layout/dialog_delete.xml index 681cf9389e..469fc16546 100644 --- a/app/src/main/res/layout/dialog_delete.xml +++ b/app/src/main/res/layout/dialog_delete.xml @@ -69,4 +69,14 @@ tools:text="Total: 6.00 MB" > + diff --git a/app/src/main/res/menu/contextual.xml b/app/src/main/res/menu/contextual.xml index 192568855a..8a497495a5 100644 --- a/app/src/main/res/menu/contextual.xml +++ b/app/src/main/res/menu/contextual.xml @@ -34,6 +34,12 @@ android:title="@string/delete" android:icon="@drawable/ic_delete_white_36dp" app:showAsAction="always" /> + Try Indexed Search! Recent Results + Trash Bin + Delete Permanently + Restore + Restore Files + Restoring… diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/AbstractDeleteTaskTestBase.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/AbstractDeleteTaskTestBase.kt index bf85edb25a..db67b7037b 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/AbstractDeleteTaskTestBase.kt +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/AbstractDeleteTaskTestBase.kt @@ -99,7 +99,7 @@ abstract class AbstractDeleteTaskTestBase { } protected fun doTestDeleteFileOk(file: HybridFileParcelable) { - val task = DeleteTask(ctx!!) + val task = DeleteTask(ctx!!, false) val result = task.doInBackground(ArrayList(listOf(file))) assertTrue(result.result) assertNull(result.exception) @@ -117,7 +117,7 @@ abstract class AbstractDeleteTaskTestBase { shadowOf(Looper.getMainLooper()).idle() }.moveToState(Lifecycle.State.STARTED).onActivity { activity -> - val task = DeleteTask(ctx!!) + val task = DeleteTask(ctx!!, false) val result = task.doInBackground(ArrayList(listOf(file))) if (result.result != null) { assertFalse(result.result) diff --git a/app/tests-proguard.cfg b/app/tests-proguard.cfg index 3144e4ec9f..4ae9b3f682 100644 --- a/app/tests-proguard.cfg +++ b/app/tests-proguard.cfg @@ -9,5 +9,7 @@ -dontwarn org.junit.** -dontwarn org.hamcrest.** -dontwarn com.squareup.javawriter.JavaWriter + +-keep class com.amaze.trashbin.** { *; } # Uncomment this if you use Mockito #-dontwarn org.mockito.** \ No newline at end of file diff --git a/build.gradle b/build.gradle index 3d018b2808..eca696ceda 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,7 @@ buildscript { awaitilityVersion = "3.1.6" androidXCoreVersion = "1.7.0" androidMaterialVersion = "1.4.0" // Upgrade to 1.5 requires targetSdkVersion 31 - androidXFragmentVersion = "1.4.1" + androidXFragmentVersion = "1.5.1" androidXAppCompatVersion = "1.4.1" androidXAnnotationVersion = "1.3.0" androidXPrefVersion = "1.2.0" diff --git a/file_operations/src/main/java/com/amaze/filemanager/fileoperations/filesystem/OpenMode.java b/file_operations/src/main/java/com/amaze/filemanager/fileoperations/filesystem/OpenMode.java index 227e24fb80..e154a69846 100644 --- a/file_operations/src/main/java/com/amaze/filemanager/fileoperations/filesystem/OpenMode.java +++ b/file_operations/src/main/java/com/amaze/filemanager/fileoperations/filesystem/OpenMode.java @@ -47,7 +47,8 @@ public enum OpenMode { BOX, ONEDRIVE, - ANDROID_DATA; + ANDROID_DATA, + TRASH_BIN; /** * Get open mode based on the id assigned. Generally used to retrieve this type after config From a3361d1baac2275b5946f896ff72dae5a559e7b0 Mon Sep 17 00:00:00 2001 From: Vishal Nehra Date: Mon, 30 Oct 2023 04:29:00 +0530 Subject: [PATCH 2/7] 971: minor fix --- .../amaze/filemanager/asynchronous/asynctasks/DeleteTask.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d2e230738b..e36b36dd55 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 @@ -83,7 +83,7 @@ public DeleteTask(@NonNull Context applicationContext, @NonNull boolean doDelete .getBoolean(PreferencesConstants.PREFERENCE_ROOTMODE, false); } - public DeleteT`ask( + public DeleteTask( @NonNull Context applicationContext, CompressedExplorerFragment compressedExplorerFragment) { this.applicationContext = applicationContext.getApplicationContext(); From 4a017383494ea98250ff0ebd1287f7955aff2057 Mon Sep 17 00:00:00 2001 From: Vishal Nehra Date: Wed, 1 Nov 2023 05:29:44 +0530 Subject: [PATCH 3/7] 971: Implement trash bin --- app/build.gradle | 2 +- .../filemanager/adapters/RecyclerAdapter.java | 19 +- .../data/LayoutElementParcelable.java | 6 +- .../filemanager/application/AppConfig.java | 63 +++--- .../asynchronous/asynctasks/DeleteTask.java | 5 +- .../asynctasks/LoadFilesListTask.java | 33 ++- .../asynchronous/services/CopyService.java | 2 +- .../filemanager/filesystem/HybridFile.java | 151 ++++++++++---- .../amaze/filemanager/ui/ItemPopupMenu.java | 6 +- .../ui/activities/MainActivity.java | 2 +- .../ui/activities/MainActivityViewModel.kt | 130 +++++------- .../ui/dialogs/GeneralDialogCreation.java | 132 ++++++------ .../ui/fragments/MainFragment.java | 21 +- .../BehaviorPrefsFragment.kt | 192 ++++++++++++++++++ .../PreferencesConstants.kt | 5 + .../ui/views/appbar/BottomBar.java | 5 +- .../filemanager/ui/views/drawer/Drawer.java | 17 +- .../utils/MainActivityActionMode.kt | 25 ++- .../filemanager/utils/MainActivityHelper.java | 3 + .../drawable/round_restore_from_trash_24.xml | 5 + app/src/main/res/layout-v16/dialog_delete.xml | 13 +- app/src/main/res/layout/dialog_delete.xml | 3 +- app/src/main/res/menu/contextual.xml | 4 +- app/src/main/res/menu/item_extras.xml | 5 + app/src/main/res/values/strings.xml | 12 +- app/src/main/res/xml/behavior_prefs.xml | 26 +++ 26 files changed, 625 insertions(+), 262 deletions(-) create mode 100644 app/src/main/res/drawable/round_restore_from_trash_24.xml diff --git a/app/build.gradle b/app/build.gradle index 32a6c9f8db..ce99855e11 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -248,7 +248,7 @@ dependencies { runtimeOnly "com.github.tony19:logback-android:$logbackAndroidVersion" implementation 'com.google.code.gson:gson:2.9.1' - implementation 'com.github.TeamAmaze:AmazeTrashBin:1.0.7' + implementation 'com.github.TeamAmaze:AmazeTrashBin:1.0.10' } kotlin { 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 5294e07d76..f2c0027bb0 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/RecyclerAdapter.java +++ b/app/src/main/java/com/amaze/filemanager/adapters/RecyclerAdapter.java @@ -1413,10 +1413,6 @@ private void showPopup(@NonNull View view, @NonNull final LayoutElementParcelabl view, sharedPrefs); popupMenu.inflate(R.menu.item_extras); - boolean isLocalFile = rowItem.generateBaseFile().isCustomPath() && rowItem.generateBaseFile().getPath().equals("7"); - if (rowItem.generateBaseFile().isCustomPath() && isLocalFile) { - popupMenu.getMenu().findItem(R.id.restore).setVisible(true); - } String description = rowItem.desc.toLowerCase(); if (rowItem.isDirectory) { @@ -1460,6 +1456,21 @@ private void showPopup(@NonNull View view, @NonNull final LayoutElementParcelabl popupMenu.getMenu().findItem(R.id.encrypt).setVisible(true); } } + if (rowItem.getMode() == OpenMode.TRASH_BIN) { + popupMenu.getMenu().findItem(R.id.return_select).setVisible(false); + popupMenu.getMenu().findItem(R.id.cut).setVisible(false); + popupMenu.getMenu().findItem(R.id.cpy).setVisible(false); + popupMenu.getMenu().findItem(R.id.rename).setVisible(false); + popupMenu.getMenu().findItem(R.id.encrypt).setVisible(false); + popupMenu.getMenu().findItem(R.id.decrypt).setVisible(false); + popupMenu.getMenu().findItem(R.id.about).setVisible(false); + popupMenu.getMenu().findItem(R.id.compress).setVisible(false); + popupMenu.getMenu().findItem(R.id.share).setVisible(false); + popupMenu.getMenu().findItem(R.id.ex).setVisible(false); + popupMenu.getMenu().findItem(R.id.book).setVisible(false); + popupMenu.getMenu().findItem(R.id.restore).setVisible(true); + popupMenu.getMenu().findItem(R.id.delete).setVisible(true); + } popupMenu.show(); } diff --git a/app/src/main/java/com/amaze/filemanager/adapters/data/LayoutElementParcelable.java b/app/src/main/java/com/amaze/filemanager/adapters/data/LayoutElementParcelable.java index 0a4d34b670..c47e6c2254 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/data/LayoutElementParcelable.java +++ b/app/src/main/java/com/amaze/filemanager/adapters/data/LayoutElementParcelable.java @@ -47,10 +47,10 @@ public class LayoutElementParcelable implements Parcelable { public final String desc; public final String permissions; public final String symlink; - public final String size; + public String size; public final boolean isDirectory; - public final long date, longSize; - public final String dateModification; + public long date, longSize; + public String dateModification; public final boolean header; // same as hfile.modes but different than openmode in Main.java diff --git a/app/src/main/java/com/amaze/filemanager/application/AppConfig.java b/app/src/main/java/com/amaze/filemanager/application/AppConfig.java index 05cc5f2c71..a09fe2cfeb 100644 --- a/app/src/main/java/com/amaze/filemanager/application/AppConfig.java +++ b/app/src/main/java/com/amaze/filemanager/application/AppConfig.java @@ -69,7 +69,6 @@ import io.reactivex.schedulers.Schedulers; import jcifs.Config; import jcifs.smb.SmbException; -import kotlin.jvm.functions.Function1; @AcraCore( buildConfigClass = BuildConfig.class, @@ -92,8 +91,8 @@ public class AppConfig extends GlideApplication { private TrashBinConfig trashBinConfig; private TrashBin trashBin; - private static final String TRASH_BIN_BASE_PATH = Environment.getExternalStorageDirectory().getPath() - + File.separator + ".AmazeData"; + private static final String TRASH_BIN_BASE_PATH = + Environment.getExternalStorageDirectory().getPath() + File.separator + ".AmazeData"; public UtilitiesProvider getUtilsProvider() { return utilsProvider; @@ -273,17 +272,24 @@ protected void initACRA() { public TrashBin getTrashBinInstance() { if (trashBin == null) { - trashBin = new TrashBin(getTrashBinConfig(), s -> { - runInBackground(() -> { - HybridFile file = new HybridFile(OpenMode.FILE, s); - try { - file.delete(getMainActivityContext(), false); - } catch (ShellNotRunningException | SmbException e) { - log.warn("failed to delete file in trash bin cleanup", e); - } - }); - return true; - }, null); + trashBin = + new TrashBin( + getApplicationContext(), + true, + getTrashBinConfig(), + s -> { + runInBackground( + () -> { + HybridFile file = new HybridFile(OpenMode.TRASH_BIN, s); + try { + file.delete(getMainActivityContext(), false); + } catch (ShellNotRunningException | SmbException e) { + log.warn("failed to delete file in trash bin cleanup", e); + } + }); + return true; + }, + null); } return trashBin; } @@ -292,22 +298,25 @@ private TrashBinConfig getTrashBinConfig() { if (trashBinConfig == null) { SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); - int days = sharedPrefs.getInt( + int days = + sharedPrefs.getInt( PreferencesConstants.KEY_TRASH_BIN_RETENTION_DAYS, - TrashBinConfig.RETENTION_DAYS_INFINITE - ); - long bytes = sharedPrefs.getLong( + TrashBinConfig.RETENTION_DAYS_INFINITE); + long bytes = + sharedPrefs.getLong( PreferencesConstants.KEY_TRASH_BIN_RETENTION_BYTES, - TrashBinConfig.RETENTION_BYTES_INFINITE - ); - int numOfFiles = sharedPrefs.getInt( + TrashBinConfig.RETENTION_BYTES_INFINITE); + int numOfFiles = + sharedPrefs.getInt( PreferencesConstants.KEY_TRASH_BIN_RETENTION_NUM_OF_FILES, - TrashBinConfig.RETENTION_NUM_OF_FILES - ); - trashBinConfig = new TrashBinConfig( - TRASH_BIN_BASE_PATH, days, bytes, - numOfFiles, false, true - ); + TrashBinConfig.RETENTION_NUM_OF_FILES); + int intervalHours = + sharedPrefs.getInt( + PreferencesConstants.KEY_TRASH_BIN_CLEANUP_INTERVAL_HOURS, + TrashBinConfig.INTERVAL_CLEANUP_HOURS); + trashBinConfig = + new TrashBinConfig( + TRASH_BIN_BASE_PATH, days, bytes, numOfFiles, intervalHours, false, true); } return trashBinConfig; } 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 5fd2e06810..51d2d811f7 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 @@ -81,8 +81,7 @@ public DeleteTask(@NonNull Context applicationContext, @NonNull boolean doDelete } public DeleteTask( - @NonNull Context applicationContext, - CompressedExplorerFragment compressedExplorerFragment) { + @NonNull Context applicationContext, CompressedExplorerFragment compressedExplorerFragment) { this.applicationContext = applicationContext.getApplicationContext(); this.doDeletePermanently = false; rootMode = @@ -188,7 +187,7 @@ private boolean doDeleteFile(@NonNull HybridFileParcelable file) throws Exceptio if (!doDeletePermanently) { return file.moveToBin(applicationContext); } - return (file.delete(applicationContext, rootMode)); + return file.delete(applicationContext, rootMode); } catch (ShellNotRunningException | SmbException e) { LOG.warn("failed to delete files", e); throw e; diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/LoadFilesListTask.java b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/LoadFilesListTask.java index aec499b0a9..67997f3fc2 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/LoadFilesListTask.java +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/LoadFilesListTask.java @@ -33,8 +33,6 @@ import java.util.Date; import java.util.LinkedList; import java.util.List; -import java.util.function.Consumer; -import java.util.function.Function; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -63,6 +61,7 @@ import com.amaze.filemanager.utils.OTGUtil; import com.amaze.filemanager.utils.OnAsyncTaskFinished; import com.amaze.filemanager.utils.OnFileFound; +import com.amaze.filemanager.utils.Utils; import com.amaze.trashbin.TrashBin; import com.amaze.trashbin.TrashBinFile; import com.cloudrail.si.interfaces.CloudStorage; @@ -139,7 +138,9 @@ public LoadFilesListTask( MainFragmentViewModel mainFragmentViewModel = mainFragment.getMainFragmentViewModel(); MainActivityViewModel mainActivityViewModel = mainFragment.getMainActivityViewModel(); - if (OpenMode.UNKNOWN.equals(openmode) || OpenMode.CUSTOM.equals(openmode)) { + if (OpenMode.UNKNOWN.equals(openmode) + || OpenMode.CUSTOM.equals(openmode) + || OpenMode.TRASH_BIN.equals(openmode)) { hFile = new HybridFile(openmode, path); hFile.generateMode(mainFragment.getActivity()); openmode = hFile.getMode(); @@ -163,6 +164,7 @@ public LoadFilesListTask( list = listSftp(mainActivityViewModel); break; case CUSTOM: + case TRASH_BIN: list = getCachedMediaList(mainActivityViewModel); break; case OTG: @@ -195,8 +197,8 @@ public LoadFilesListTask( } if (list != null - && !(openmode == OpenMode.CUSTOM && (("5").equals(path) || ("6").equals(path) - || ("7").equals(path)))) { + && !(openmode == OpenMode.CUSTOM + && (("5").equals(path) || ("6").equals(path) || ("7").equals(path)))) { postListCustomPathProcess(list, mainFragment); } @@ -219,7 +221,7 @@ private List getCachedMediaList( int mediaType = Integer.parseInt(path); if (5 == mediaType || 6 == mediaType - || 7 == mediaType + || 7 == mediaType || mainActivityViewModel.getMediaCacheHash().get(mediaType) == null || forceReload) { switch (Integer.parseInt(path)) { @@ -577,9 +579,22 @@ else if (cursor.getCount() > 0 && cursor.moveToFirst()) { List deletedFiles = new ArrayList<>(); if (trashBin != null) { for (TrashBinFile trashBinFile : trashBin.listFilesInBin()) { - deletedFiles.add(new HybridFile(OpenMode.TRASH_BIN, trashBinFile.getPath(), - trashBinFile.getFileName(), trashBinFile.isDirectory()) - .generateLayoutElement(context, false)); + HybridFile hybridFile = + new HybridFile( + OpenMode.TRASH_BIN, + trashBinFile.getDeletedPath( + AppConfig.getInstance().getTrashBinInstance().getConfig()), + trashBinFile.getFileName(), + trashBinFile.isDirectory()); + if (trashBinFile.getDeleteTime() != null) { + hybridFile.setLastModified(trashBinFile.getDeleteTime() * 1000); + } + LayoutElementParcelable element = hybridFile.generateLayoutElement(context, true); + element.date = trashBinFile.getDeleteTime(); + element.longSize = trashBinFile.getSizeBytes(); + element.size = Formatter.formatFileSize(context, trashBinFile.getSizeBytes()); + element.dateModification = Utils.getDate(context, trashBinFile.getDeleteTime() * 1000); + deletedFiles.add(element); } } return deletedFiles; 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 2ddfca97e3..cf07cdb8c2 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 @@ -441,7 +441,7 @@ public void execute( for (HybridFileParcelable a : sourceFiles) { if (!failedFOps.contains(a)) toDelete.add(a); } - new DeleteTask(c).execute((toDelete)); + new DeleteTask(c, true).execute((toDelete)); } } 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 9aa4894238..6471230700 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/HybridFile.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/HybridFile.java @@ -42,11 +42,11 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; -import java.util.Calendar; import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.Locale; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; @@ -68,6 +68,7 @@ import com.amaze.filemanager.filesystem.cloud.CloudUtil; 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.ftp.ExtensionsKt; import com.amaze.filemanager.filesystem.ftp.FTPClientImpl; import com.amaze.filemanager.filesystem.ftp.FtpClientTemplate; @@ -104,7 +105,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.arch.core.util.Function; -import androidx.core.content.FileProvider; import androidx.documentfile.provider.DocumentFile; import androidx.preference.PreferenceManager; @@ -117,7 +117,6 @@ import jcifs.smb.SmbFile; import kotlin.collections.ArraysKt; import kotlin.io.ByteStreamsKt; -import kotlin.jvm.functions.Function2; import kotlin.text.Charsets; import net.schmizz.sshj.SSHClient; import net.schmizz.sshj.common.Buffer; @@ -163,6 +162,8 @@ public HybridFile(OpenMode mode, String path, String name, boolean isDirectory) } else if (isRoot() && path.equals("/")) { // root of filesystem, don't concat another '/' this.path += name; + } else if (isTrashBin()) { + this.path = path; } else { this.path += "/" + name; } @@ -190,6 +191,8 @@ public void generateMode(Context context) { mode = OpenMode.GDRIVE; } else if (path.startsWith(CloudHandler.CLOUD_PREFIX_DROPBOX)) { mode = OpenMode.DROPBOX; + } else if (path.equals("7") || isTrashBin()) { + mode = OpenMode.TRASH_BIN; } else if (context == null) { mode = OpenMode.FILE; } else { @@ -237,6 +240,10 @@ public boolean isRoot() { return mode == OpenMode.ROOT; } + public boolean isTrashBin() { + return mode == OpenMode.TRASH_BIN; + } + public boolean isSmb() { return mode == OpenMode.SMB; } @@ -339,6 +346,7 @@ public Long execute(@NonNull SFTPClient client) throws IOException { case NFS: break; case FILE: + case TRASH_BIN: return getFile().lastModified(); case DOCUMENT_FILE: return getDocumentFile(false).lastModified(); @@ -384,6 +392,7 @@ public long length(Context context) { return s; case NFS: case FILE: + case TRASH_BIN: s = getFile().length(); return s; case ROOT: @@ -424,7 +433,8 @@ public long length(Context context) { */ public String getPath() { - if (isLocal() || isRoot() || isDocumentFile() || isAndroidDataDir()) return path; + if (isLocal() || isTrashBin() || isRoot() || isDocumentFile() || isAndroidDataDir()) + return path; try { return URLDecoder.decode(path, "UTF-8"); @@ -471,6 +481,8 @@ public String getName(Context context) { return OTGUtil.getDocumentFile( path, SafRootHolder.getUriRoot(), context, OpenMode.DOCUMENT_FILE, false) .getName(); + case TRASH_BIN: + return name; default: if (path.isEmpty()) { return ""; @@ -535,8 +547,7 @@ public boolean isCustomPath() { || path.equals("3") || path.equals("4") || path.equals("5") - || path.equals("6") - || path.equals("7"); + || path.equals("6"); } /** Helper method to get parent path */ @@ -552,6 +563,8 @@ public String getParent(Context context) { case FILE: case ROOT: return getFile().getParent(); + case TRASH_BIN: + return "7"; case SFTP: case DOCUMENT_FILE: String thisPath = path; @@ -619,6 +632,7 @@ public boolean isDirectory() { isDirectory = false; break; case FILE: + case TRASH_BIN: default: isDirectory = getFile().isDirectory(); break; @@ -684,6 +698,7 @@ public Boolean execute(@NonNull SFTPClient client) { .getFolder()) .subscribeOn(Schedulers.io()) .blockingGet(); + case TRASH_BIN: default: // also handles the case `FILE` File file = getFile(); return file != null && file.isDirectory(); @@ -705,6 +720,7 @@ public long folderSize() { size = smbFile != null ? FileUtils.folderSize(getSmbFile()) : 0; break; case FILE: + case TRASH_BIN: size = FileUtils.folderSize(getFile(), null); break; case ROOT: @@ -747,6 +763,7 @@ public long folderSize(Context context) { size = (smbFile != null) ? FileUtils.folderSize(smbFile) : 0L; break; case FILE: + case TRASH_BIN: size = FileUtils.folderSize(getFile(), null); break; case ROOT: @@ -774,7 +791,6 @@ public long folderSize(Context context) { mode, dataUtils.getAccount(mode).getMetadata(CloudUtil.stripPath(mode, path))); break; case FTP: - default: return 0l; } @@ -796,6 +812,7 @@ public long getUsableSpace() { break; case FILE: case ROOT: + case TRASH_BIN: size = getFile().getUsableSpace(); break; case DROPBOX: @@ -881,6 +898,7 @@ public long getTotal(Context context) { break; case FILE: case ROOT: + case TRASH_BIN: size = getFile().getTotalSpace(); break; case DROPBOX: @@ -1023,6 +1041,7 @@ public FTPFile[] executeWithFtpClient(@NonNull FTPClient ftpClient) LOG.warn("failed to get children file for cloud file", e); } break; + case TRASH_BIN: default: ListFilesCommand.INSTANCE.listFiles( path, @@ -1174,6 +1193,7 @@ public InputStream executeWithFtpClient(@NonNull FTPClient ftpClient) LOG.debug(CloudUtil.stripPath(mode, path)); inputStream = cloudStorageOneDrive.download(CloudUtil.stripPath(mode, path)); break; + case TRASH_BIN: default: try { inputStream = new FileInputStream(path); @@ -1264,6 +1284,7 @@ public OutputStream executeWithFtpClient(@NonNull FTPClient ftpClient) outputStream = null; } break; + case TRASH_BIN: default: try { outputStream = FileUtil.getOutputStream(getFile(), context); @@ -1324,6 +1345,9 @@ public Boolean execute(SFTPClient client) throws IOException { exists = getFile().exists(); } else if (isRoot()) { return RootHelper.fileExists(path); + } else if (isTrashBin()) { + if (getFile() != null) return getFile().exists(); + else return false; } return exists; @@ -1364,7 +1388,8 @@ public boolean isSimpleFile() { && !isDropBoxFile() && !isBoxFile() && !isSftp() - && !isFtp(); + && !isFtp() + && !isTrashBin(); } public boolean setLastModified(final long date) { @@ -1411,6 +1436,9 @@ public Boolean execute(@NonNull Session session) throws IOException { return 0 == cmd.getExitStatus(); } })); + } else if (isTrashBin()) { + // do nothing + return true; } else { File f = getFile(); return f.setLastModified(date); @@ -1473,6 +1501,7 @@ public Boolean executeWithFtpClient(@NonNull FTPClient ftpClient) throws IOExcep } catch (Exception e) { LOG.warn("failed to create folder for cloud file", e); } + } else if (isTrashBin()) { // do nothing } else MakeDirectoryOperation.mkdirs(context, this); } @@ -1509,6 +1538,13 @@ public Boolean executeWithFtpClient(@NonNull FTPClient ftpClient) LOG.error("Error delete SMB file", e); throw e; } + } else if (isTrashBin()) { + try { + deletePermanentlyFromBin(context); + } catch (Exception e) { + LOG.error("failed to delete trash bin file", e); + throw e; + } } else { if (isRoot() && rootmode) { setMode(OpenMode.ROOT); @@ -1524,23 +1560,18 @@ public void restoreFromBin(Context context) { List trashBinFiles = Collections.singletonList(this.toTrashBinFile(context)); TrashBin trashBin = AppConfig.getInstance().getTrashBinInstance(); if (trashBin != null) { - trashBin.moveToBin(trashBinFiles, true, (originalFilePath, - trashBinDestination) -> { - File source = new File(originalFilePath); - File dest = new File(trashBinDestination); - if (!source.renameTo(dest)) { - return false; - } - Uri uri = FileProvider.getUriForFile( - context, - context.getPackageName(), new File(originalFilePath) - ); - FileUtils.scanFile( - uri, - context - ); - return true; - }); + trashBin.moveToBin( + trashBinFiles, + true, + (originalFilePath, trashBinDestination) -> { + File source = new File(originalFilePath); + File dest = new File(trashBinDestination); + if (!source.renameTo(dest)) { + return false; + } + MediaConnectionUtils.scanFile(context, new HybridFile[] {this}); + return true; + }); } } @@ -1548,27 +1579,36 @@ public boolean moveToBin(Context context) { List trashBinFiles = Collections.singletonList(this.toTrashBinFile(context)); TrashBin trashBin = AppConfig.getInstance().getTrashBinInstance(); if (trashBin != null) { - trashBin.moveToBin(trashBinFiles, true, (originalFilePath, - trashBinDestination) -> { - File source = new File(originalFilePath); - File dest = new File(trashBinDestination); - if (!source.renameTo(dest)) { - return false; - } - Uri uri = FileProvider.getUriForFile( - context, - context.getPackageName(), new File(originalFilePath) - ); - FileUtils.scanFile( - uri, - context - ); - return true; - }); + trashBin.moveToBin( + trashBinFiles, + true, + (originalFilePath, trashBinDestination) -> { + File source = new File(originalFilePath); + File dest = new File(trashBinDestination); + return source.renameTo(dest); + }); } return true; } + public boolean deletePermanentlyFromBin(Context context) { + List trashBinFiles = + Collections.singletonList(this.toTrashBinRestoreFile(context)); + TrashBin trashBin = AppConfig.getInstance().getTrashBinInstance(); + AtomicBoolean isDelete = new AtomicBoolean(false); + if (trashBin != null) { + trashBin.deletePermanently( + trashBinFiles, + s -> { + LOG.info("deleting from bin at path " + s); + isDelete.set(DeleteOperation.deleteFile(getFile(), context)); + return isDelete.get(); + }, + true); + } + return isDelete.get(); + } + /** * Returns the name of file excluding it's extension If no extension is found then whole file name * is returned @@ -1589,6 +1629,7 @@ public LayoutElementParcelable generateLayoutElement(@NonNull Context c, boolean switch (mode) { case FILE: case ROOT: + case TRASH_BIN: File file = getFile(); LayoutElementParcelable layoutElement; if (isDirectory(c)) { @@ -1789,10 +1830,34 @@ public void onError(Throwable e) { } }); } + + /** + * Returns trash bin file with path that points to deleted path + * + * @param context + * @return + */ public TrashBinFile toTrashBinFile(Context context) { - return new TrashBinFile(name, isDirectory(context), path, length(context), null); + return new TrashBinFile(getName(context), isDirectory(context), path, length(context), null); } + /** + * Returns trash bin file with path that points to where the file should be restored + * + * @param context + * @return + */ + public TrashBinFile toTrashBinRestoreFile(Context context) { + TrashBin trashBin = AppConfig.getInstance().getTrashBinInstance(); + for (TrashBinFile trashBinFile : trashBin.listFilesInBin()) { + if (trashBinFile.getDeletedPath(trashBin.getConfig()).equals(path)) { + // finding path to restore to + return new TrashBinFile( + getName(context), isDirectory(context), trashBinFile.getPath(), length(context), null); + } + } + return null; + } private SshClientSessionTemplate getRemoteShellCommandLineResult(String command) { return new SshClientSessionTemplate(path) { diff --git a/app/src/main/java/com/amaze/filemanager/ui/ItemPopupMenu.java b/app/src/main/java/com/amaze/filemanager/ui/ItemPopupMenu.java index 3e6408fb65..49a32af9eb 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/ItemPopupMenu.java +++ b/app/src/main/java/com/amaze/filemanager/ui/ItemPopupMenu.java @@ -161,7 +161,11 @@ public boolean onMenuItemClick(MenuItem item) { context, mainActivity, positions, utilitiesProvider.getAppTheme()); return true; case R.id.restore: - + ArrayList p2 = new ArrayList<>(); + p2.add(rowItem); + GeneralDialogCreation.restoreFilesDialog( + context, mainActivity, p2, utilitiesProvider.getAppTheme()); + return true; case R.id.open_with: boolean useNewStack = sharedPrefs.getBoolean(PreferencesConstants.PREFERENCE_TEXTEDITOR_NEWSTACK, false); 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 391950311d..be67a348b5 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 @@ -1581,7 +1581,7 @@ protected void onActivityResult(int requestCode, int responseCode, Intent intent mainFragment -> { switch (operation) { case DELETE: // deletion - new DeleteTask(mainActivity).execute((oparrayList)); + new DeleteTask(mainActivity, true).execute((oparrayList)); break; case COPY: // copying // legacy compatibility diff --git a/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivityViewModel.kt b/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivityViewModel.kt index 9e57e8fb87..66235ae0ea 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivityViewModel.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivityViewModel.kt @@ -21,29 +21,24 @@ package com.amaze.filemanager.ui.activities import android.app.Application -import android.os.Environment +import android.content.Intent import android.provider.MediaStore import androidx.collection.LruCache -import androidx.core.content.FileProvider import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import androidx.preference.PreferenceManager import com.amaze.filemanager.adapters.data.LayoutElementParcelable -import com.amaze.filemanager.asynchronous.asynctasks.DeleteTask +import com.amaze.filemanager.application.AppConfig import com.amaze.filemanager.fileoperations.filesystem.OpenMode import com.amaze.filemanager.filesystem.HybridFile import com.amaze.filemanager.filesystem.HybridFileParcelable import com.amaze.filemanager.filesystem.RootHelper -import com.amaze.filemanager.filesystem.files.FileUtils +import com.amaze.filemanager.filesystem.files.MediaConnectionUtils.scanFile import com.amaze.filemanager.filesystem.root.ListFilesCommand.listFiles -import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_SHOW_HIDDENFILES -import com.amaze.trashbin.DeletePermanentlyCallback import com.amaze.trashbin.MoveFilesCallback -import com.amaze.trashbin.TrashBin -import com.amaze.trashbin.TrashBinConfig +import com.amaze.trashbin.TrashBinFile import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.slf4j.LoggerFactory @@ -57,12 +52,6 @@ class MainActivityViewModel(val applicationContext: Application) : var listCache: LruCache> = LruCache(50) var trashBinFilesLiveData: MutableLiveData?>? = null - private var trashBinConfig: TrashBinConfig? = null - private var trashBin: TrashBin? = null - - private val TRASH_BIN_BASE_PATH = Environment.getExternalStorageDirectory() - .path + File.separator + ".AmazeData" - companion object { /** * size of list to be cached for local files @@ -195,55 +184,15 @@ class MainActivityViewModel(val applicationContext: Application) : return mutableLiveData } - fun getTrashBinInstance(): TrashBin { - if (trashBin == null) { - trashBin = TrashBin( - getTrashbinConfig(), - object : DeletePermanentlyCallback { - override fun invoke(deletePath: String): Boolean { - viewModelScope.launch(Dispatchers.IO) { - val hybridFile = HybridFile(OpenMode.FILE, deletePath) - hybridFile.delete(applicationContext, false) - } - return true - } - }, - null - ) - } - return trashBin!! - } - - fun getTrashbinConfig(): TrashBinConfig { - if (trashBinConfig == null) { - val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(applicationContext) - - val days = sharedPrefs.getInt( - PreferencesConstants.KEY_TRASH_BIN_RETENTION_DAYS, - TrashBinConfig.RETENTION_DAYS_INFINITE - ) - val bytes = sharedPrefs.getLong( - PreferencesConstants.KEY_TRASH_BIN_RETENTION_BYTES, - TrashBinConfig.RETENTION_BYTES_INFINITE - ) - val numOfFiles = sharedPrefs.getInt( - PreferencesConstants.KEY_TRASH_BIN_RETENTION_NUM_OF_FILES, - TrashBinConfig.RETENTION_NUM_OF_FILES - ) - trashBinConfig = TrashBinConfig( - TRASH_BIN_BASE_PATH, days, bytes, - numOfFiles, false, true - ) - } - return trashBinConfig!! - } - fun moveToBinLightWeight(mediaFileInfoList: List) { viewModelScope.launch(Dispatchers.IO) { - val trashBinFilesList = mediaFileInfoList.map { it.generateBaseFile() - .toTrashBinFile(applicationContext) } - getTrashBinInstance().moveToBin( - trashBinFilesList, true, + val trashBinFilesList = mediaFileInfoList.map { + it.generateBaseFile() + .toTrashBinFile(applicationContext) + } + AppConfig.getInstance().trashBinInstance.moveToBin( + trashBinFilesList, + true, object : MoveFilesCallback { override fun invoke( originalFilePath: String, @@ -254,14 +203,16 @@ class MainActivityViewModel(val applicationContext: Application) : if (!source.renameTo(dest)) { return false } - val uri = FileProvider.getUriForFile( - applicationContext, - applicationContext.packageName, File(originalFilePath) - ) - FileUtils.scanFile( - uri, - applicationContext + val hybridFile = HybridFile( + OpenMode.TRASH_BIN, + originalFilePath ) + scanFile(applicationContext, arrayOf(hybridFile)) + val intent = Intent(MainActivity.KEY_INTENT_LOAD_LIST) + hybridFile.getParent(applicationContext)?.let { + intent.putExtra(MainActivity.KEY_INTENT_LOAD_LIST_FILE, it) + applicationContext.sendBroadcast(intent) + } return true } } @@ -271,11 +222,18 @@ class MainActivityViewModel(val applicationContext: Application) : fun restoreFromBin(mediaFileInfoList: List) { viewModelScope.launch(Dispatchers.IO) { - LOG.info("Moving media files to bin $mediaFileInfoList") - val trashBinFilesList = mediaFileInfoList.map { it.generateBaseFile() - .toTrashBinFile(applicationContext) } - getTrashBinInstance().restore( - trashBinFilesList, true, + LOG.info("Restoring media files from bin $mediaFileInfoList") + val filesToRestore = mutableListOf() + for (element in mediaFileInfoList) { + val restoreFile = element.generateBaseFile() + .toTrashBinRestoreFile(applicationContext) + if (restoreFile != null) { + filesToRestore.add(restoreFile) + } + } + AppConfig.getInstance().trashBinInstance.restore( + filesToRestore, + true, object : MoveFilesCallback { override fun invoke(source: String, dest: String): Boolean { val sourceFile = File(source) @@ -283,14 +241,16 @@ class MainActivityViewModel(val applicationContext: Application) : if (!sourceFile.renameTo(destFile)) { return false } - val uri = FileProvider.getUriForFile( - applicationContext, - applicationContext.packageName, File(dest) - ) - FileUtils.scanFile( - uri, - applicationContext + val hybridFile = HybridFile( + OpenMode.TRASH_BIN, + source ) + scanFile(applicationContext, arrayOf(hybridFile)) + val intent = Intent(MainActivity.KEY_INTENT_LOAD_LIST) + hybridFile.getParent(applicationContext)?.let { + intent.putExtra(MainActivity.KEY_INTENT_LOAD_LIST_FILE, it) + applicationContext.sendBroadcast(intent) + } return true } } @@ -305,11 +265,13 @@ class MainActivityViewModel(val applicationContext: Application) : viewModelScope.launch(Dispatchers.IO) { trashBinFilesLiveData?.postValue( ArrayList( - getTrashBinInstance().listFilesInBin() + AppConfig.getInstance().trashBinInstance.listFilesInBin() .map { HybridFile(OpenMode.FILE, it.path, it.fileName, it.isDirectory) - .generateLayoutElement(applicationContext, false - ) + .generateLayoutElement( + applicationContext, + false + ) } ) ) diff --git a/app/src/main/java/com/amaze/filemanager/ui/dialogs/GeneralDialogCreation.java b/app/src/main/java/com/amaze/filemanager/ui/dialogs/GeneralDialogCreation.java index 3f86501b6a..bb60ab0917 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/dialogs/GeneralDialogCreation.java +++ b/app/src/main/java/com/amaze/filemanager/ui/dialogs/GeneralDialogCreation.java @@ -94,6 +94,7 @@ import android.view.LayoutInflater; import android.view.View; import android.widget.LinearLayout; +import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; @@ -209,11 +210,15 @@ public static void deleteFilesDialog( PreferencesConstants.PREFERENCE_DELETE_CONFIRMATION, PreferencesConstants.DEFAULT_PREFERENCE_DELETE_CONFIRMATION); View dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_delete, null); - final AppCompatCheckBox deletePermanentlyCheckbox = dialogView.findViewById(R.id.delete_permanently_checkbox); + TextView deleteDisclaimerTextView = dialogView.findViewById(R.id.dialog_delete_disclaimer); + final AppCompatCheckBox deletePermanentlyCheckbox = + dialogView.findViewById(R.id.delete_permanently_checkbox); if (positions.get(0).generateBaseFile().isLocal()) { // FIXME: make sure dialog is not shown for zero items // allow trash bin delete only for local files for now deletePermanentlyCheckbox.setVisibility(View.VISIBLE); + } else { + deleteDisclaimerTextView.setText(context.getString(R.string.dialog_delete_disclaimer)); } // Build dialog with custom view layout and accent color. MaterialDialog dialog = @@ -229,8 +234,10 @@ public static void deleteFilesDialog( (dialog1, which) -> { Toast.makeText(context, context.getString(R.string.deleting), Toast.LENGTH_SHORT) .show(); - mainActivity.mainActivityHelper.deleteFiles(itemsToDelete, - deletePermanentlyCheckbox.isChecked()); + mainActivity.mainActivityHelper.deleteFiles( + itemsToDelete, + deletePermanentlyCheckbox.isChecked() + || deletePermanentlyCheckbox.getVisibility() == View.GONE); }) .build(); @@ -335,8 +342,10 @@ protected void onPostExecute(Void aVoid) { updateViews(sizeTotal, files, directories, counterFiles, counterDirectories); } else { Toast.makeText(context, context.getString(R.string.deleting), Toast.LENGTH_SHORT).show(); - mainActivity.mainActivityHelper.deleteFiles(itemsToDelete, - deletePermanentlyCheckbox.isChecked()); + mainActivity.mainActivityHelper.deleteFiles( + itemsToDelete, + deletePermanentlyCheckbox.isChecked() + || deletePermanentlyCheckbox.getVisibility() == View.GONE); } } @@ -409,41 +418,45 @@ private void updateViews( @SuppressWarnings("ConstantConditions") public static void restoreFilesDialog( - @NonNull final Context context, - @NonNull final MainActivity mainActivity, - @NonNull final List positions, - @NonNull AppTheme appTheme) { + @NonNull final Context context, + @NonNull final MainActivity mainActivity, + @NonNull final List positions, + @NonNull AppTheme appTheme) { final ArrayList itemsToDelete = new ArrayList<>(); int accentColor = mainActivity.getAccent(); View dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_delete, null); - final AppCompatCheckBox deletePermanentlyCheckbox = dialogView.findViewById(R.id.delete_permanently_checkbox); + TextView deleteDisclaimerTextView = dialogView.findViewById(R.id.dialog_delete_disclaimer); + deleteDisclaimerTextView.setText(context.getString(R.string.dialog_restore_disclaimer)); // Build dialog with custom view layout and accent color. MaterialDialog dialog = - new MaterialDialog.Builder(context) - .title(context.getString(R.string.restore_files)) - .customView(dialogView, true) - .theme(appTheme.getMaterialDialogTheme(context)) - .negativeText(context.getString(R.string.cancel).toUpperCase()) - .positiveText(context.getString(R.string.done).toUpperCase()) - .positiveColor(accentColor) - .negativeColor(accentColor) - .onPositive( - (dialog1, which) -> { - Toast.makeText(context, context.getString(R.string.processing), Toast.LENGTH_SHORT) - .show(); - mainActivity.mainActivityHelper.deleteFiles(itemsToDelete, - deletePermanentlyCheckbox.isChecked()); - }) - .build(); + new MaterialDialog.Builder(context) + .title(context.getString(R.string.restore_files)) + .customView(dialogView, true) + .theme(appTheme.getMaterialDialogTheme(context)) + .negativeText(context.getString(R.string.cancel).toUpperCase()) + .positiveText(context.getString(R.string.done).toUpperCase()) + .positiveColor(accentColor) + .negativeColor(accentColor) + .onPositive( + (dialog1, which) -> { + Toast.makeText( + context, context.getString(R.string.processing), Toast.LENGTH_SHORT) + .show(); + mainActivity + .getCurrentMainFragment() + .getMainActivityViewModel() + .restoreFromBin(positions); + }) + .build(); // Get views from custom layout to set text values. final AppCompatTextView categoryDirectories = - dialog.getCustomView().findViewById(R.id.category_directories); + dialog.getCustomView().findViewById(R.id.category_directories); final AppCompatTextView categoryFiles = - dialog.getCustomView().findViewById(R.id.category_files); + dialog.getCustomView().findViewById(R.id.category_files); final AppCompatTextView listDirectories = - dialog.getCustomView().findViewById(R.id.list_directories); + dialog.getCustomView().findViewById(R.id.list_directories); final AppCompatTextView listFiles = dialog.getCustomView().findViewById(R.id.list_files); final AppCompatTextView total = dialog.getCustomView().findViewById(R.id.total); @@ -479,12 +492,12 @@ protected Void doInBackground(Void... params) { long sizeDirectory = layoutElement.generateBaseFile().folderSize(context); directories - .append(++counterDirectories) - .append(". ") - .append(layoutElement.title) - .append(" (") - .append(Formatter.formatFileSize(context, sizeDirectory)) - .append(")"); + .append(++counterDirectories) + .append(". ") + .append(layoutElement.title) + .append(" (") + .append(Formatter.formatFileSize(context, sizeDirectory)) + .append(")"); sizeTotal += sizeDirectory; // Build list of files to delete. } else { @@ -494,12 +507,12 @@ protected Void doInBackground(Void... params) { } files - .append(++counterFiles) - .append(". ") - .append(layoutElement.title) - .append(" (") - .append(layoutElement.size) - .append(")"); + .append(++counterFiles) + .append(". ") + .append(layoutElement.title) + .append(" (") + .append(layoutElement.size) + .append(")"); sizeTotal += layoutElement.longSize; } @@ -518,27 +531,24 @@ protected void onProgressUpdate(Object... result) { StringBuilder tempDirectoriesStringBuilder = (StringBuilder) result[4]; updateViews( - tempSizeTotal, - tempFilesStringBuilder, - tempDirectoriesStringBuilder, - tempCounterFiles, - tempCounterDirectories); + tempSizeTotal, + tempFilesStringBuilder, + tempDirectoriesStringBuilder, + tempCounterFiles, + tempCounterDirectories); } @Override protected void onPostExecute(Void aVoid) { super.onPostExecute(aVoid); - Toast.makeText(context, context.getString(R.string.restoring), Toast.LENGTH_SHORT).show(); - - mainActivity.mainActivityHelper.deleteFiles(itemsToDelete, - deletePermanentlyCheckbox.isChecked()); + // do nothing } private void updateViews( - long tempSizeTotal, - StringBuilder filesStringBuilder, - StringBuilder directoriesStringBuilder, - int... values) { + long tempSizeTotal, + StringBuilder filesStringBuilder, + StringBuilder directoriesStringBuilder, + int... values) { int tempCounterFiles = values[0]; int tempCounterDirectories = values[1]; @@ -577,10 +587,10 @@ private void updateViews( // Show total size with at least one directory or file and size is not zero. if (tempCounterFiles + tempCounterDirectories > 1 && tempSizeTotal > 0) { StringBuilder builderTotal = - new StringBuilder() - .append(context.getString(R.string.total)) - .append(" ") - .append(Formatter.formatFileSize(context, tempSizeTotal)); + new StringBuilder() + .append(context.getString(R.string.total)) + .append(" ") + .append(Formatter.formatFileSize(context, tempSizeTotal)); total.setText(builderTotal); if (total.getVisibility() != View.VISIBLE) total.setVisibility(View.VISIBLE); } else { @@ -594,11 +604,7 @@ private void updateViews( categoryDirectories.setTextColor(accentColor); categoryFiles.setTextColor(accentColor); } - - if (needConfirmation) { - // Show dialog on screen. - dialog.show(); - } + dialog.show(); } public static void showPropertiesDialogWithPermissions( 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 3f95d74ca0..386eb1e204 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 @@ -486,8 +486,13 @@ public void onListItemClicked( : layoutElementParcelable.symlink; if (layoutElementParcelable.isDirectory) { - computeScroll(); - loadlist(path, false, mainFragmentViewModel.getOpenMode(), false); + if (layoutElementParcelable.getMode() == OpenMode.TRASH_BIN) { + // don't open file hierarchy for trash bin + adapter.toggleChecked(position, imageView); + } else { + computeScroll(); + loadlist(path, false, mainFragmentViewModel.getOpenMode(), false); + } } else if (layoutElementParcelable.desc.endsWith(CryptUtil.CRYPT_EXTENSION) || layoutElementParcelable.desc.endsWith(CryptUtil.AESCRYPT_EXTENSION)) { // decrypt the file @@ -814,7 +819,8 @@ public void reloadListElements(boolean back, boolean results, boolean grid) { mainFragmentViewModel.setStopAnims(true); - if (mainFragmentViewModel.getOpenMode() != OpenMode.CUSTOM) { + if (mainFragmentViewModel.getOpenMode() != OpenMode.CUSTOM + && mainFragmentViewModel.getOpenMode() != OpenMode.TRASH_BIN) { DataUtils.getInstance().addHistoryFile(mainFragmentViewModel.getCurrentPath()); } @@ -916,7 +922,8 @@ private void resumeDecryptOperations() { if (!mainFragmentViewModel.isEncryptOpen() && !Utils.isNullOrEmpty(mainFragmentViewModel.getEncryptBaseFiles())) { // we've opened the file and are ready to delete it - new DeleteTask(requireMainActivity()).execute(mainFragmentViewModel.getEncryptBaseFiles()); + new DeleteTask(requireMainActivity(), true) + .execute(mainFragmentViewModel.getEncryptBaseFiles()); mainFragmentViewModel.setEncryptBaseFiles(new ArrayList<>()); } } @@ -1025,7 +1032,8 @@ public void computeScroll() { } public void goBack() { - if (mainFragmentViewModel.getOpenMode() == OpenMode.CUSTOM) { + if (mainFragmentViewModel.getOpenMode() == OpenMode.CUSTOM + || mainFragmentViewModel.getOpenMode() == OpenMode.TRASH_BIN) { loadlist(mainFragmentViewModel.getHome(), false, OpenMode.FILE, false); return; } @@ -1189,7 +1197,8 @@ public void reauthenticateSmb() { } public void goBackItemClick() { - if (mainFragmentViewModel.getOpenMode() == OpenMode.CUSTOM) { + if (mainFragmentViewModel.getOpenMode() == OpenMode.CUSTOM + || mainFragmentViewModel.getOpenMode() == OpenMode.TRASH_BIN) { loadlist(mainFragmentViewModel.getHome(), false, OpenMode.FILE, false); return; } diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BehaviorPrefsFragment.kt b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BehaviorPrefsFragment.kt index dcd9273c05..418f3ba763 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BehaviorPrefsFragment.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/BehaviorPrefsFragment.kt @@ -22,11 +22,15 @@ package com.amaze.filemanager.ui.fragments.preferencefragments import android.os.Bundle import android.os.Environment +import android.text.InputType import androidx.preference.Preference +import androidx.preference.PreferenceManager +import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.folderselector.FolderChooserDialog import com.amaze.filemanager.R import com.amaze.filemanager.application.AppConfig import com.amaze.filemanager.ui.dialogs.OpenFileDialogFragment.Companion.clearPreferences +import com.amaze.trashbin.TrashBinConfig import java.io.File class BehaviorPrefsFragment : BasePrefsFragment(), FolderChooserDialog.FolderCallback { @@ -59,6 +63,26 @@ class BehaviorPrefsFragment : BasePrefsFragment(), FolderChooserDialog.FolderCal .show(activity) true } + findPreference(PreferencesConstants.PREFERENCE_TRASH_BIN_RETENTION_NUM_OF_FILES) + ?.onPreferenceClickListener = Preference.OnPreferenceClickListener { + trashBinRetentionNumOfFiles() + true + } + findPreference(PreferencesConstants.PREFERENCE_TRASH_BIN_RETENTION_DAYS) + ?.onPreferenceClickListener = Preference.OnPreferenceClickListener { + trashBinRetentionDays() + true + } + findPreference(PreferencesConstants.PREFERENCE_TRASH_BIN_RETENTION_BYTES) + ?.onPreferenceClickListener = Preference.OnPreferenceClickListener { + trashBinRetentionBytes() + true + } + findPreference(PreferencesConstants.PREFERENCE_TRASH_BIN_CLEANUP_INTERVAL) + ?.onPreferenceClickListener = Preference.OnPreferenceClickListener { + trashBinCleanupInterval() + true + } } override fun onFolderSelection(dialog: FolderChooserDialog, folder: File) { @@ -71,4 +95,172 @@ class BehaviorPrefsFragment : BasePrefsFragment(), FolderChooserDialog.FolderCal } dialog.dismiss() } + + private fun trashBinRetentionNumOfFiles() { + val dialogBuilder = MaterialDialog.Builder(activity) + dialogBuilder.title( + resources.getString(R.string.trash_bin_retention_num_of_files_title) + ) + val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(requireContext()) + val numOfFiles = sharedPrefs.getInt( + PreferencesConstants.KEY_TRASH_BIN_RETENTION_NUM_OF_FILES, + TrashBinConfig.RETENTION_NUM_OF_FILES + ) + dialogBuilder.inputType(InputType.TYPE_CLASS_NUMBER) + dialogBuilder.input( + "", + "$numOfFiles", + true + ) { _, _ -> } + dialogBuilder.theme( + activity.utilsProvider.appTheme.getMaterialDialogTheme(requireContext()) + ) + dialogBuilder.positiveText(resources.getString(R.string.ok)) + dialogBuilder.negativeText(resources.getString(R.string.cancel)) + dialogBuilder.neutralText(resources.getString(R.string.default_string)) + dialogBuilder.positiveColor(activity.accent) + dialogBuilder.negativeColor(activity.accent) + dialogBuilder.neutralColor(activity.accent) + dialogBuilder.onPositive { dialog, _ -> + val inputText = dialog.inputEditText?.text.toString() + sharedPrefs.edit().putInt( + PreferencesConstants.KEY_TRASH_BIN_RETENTION_NUM_OF_FILES, + inputText.toInt() + ).apply() + } + dialogBuilder.onNeutral { _, _ -> + sharedPrefs.edit().putInt( + PreferencesConstants.KEY_TRASH_BIN_RETENTION_NUM_OF_FILES, + TrashBinConfig.RETENTION_NUM_OF_FILES + ).apply() + } + dialogBuilder.onNegative { dialog, _ -> dialog.cancel() } + dialogBuilder.build().show() + } + + private fun trashBinRetentionDays() { + val dialogBuilder = MaterialDialog.Builder(activity) + dialogBuilder.title( + resources.getString(R.string.trash_bin_retention_days_title) + ) + val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(requireContext()) + val days = sharedPrefs.getInt( + PreferencesConstants.KEY_TRASH_BIN_RETENTION_DAYS, + TrashBinConfig.RETENTION_DAYS_INFINITE + ) + dialogBuilder.inputType(InputType.TYPE_CLASS_NUMBER) + dialogBuilder.input( + "", + "$days", + true + ) { _, _ -> } + dialogBuilder.theme( + activity.utilsProvider.appTheme.getMaterialDialogTheme(requireContext()) + ) + dialogBuilder.positiveText(resources.getString(R.string.ok)) + dialogBuilder.negativeText(resources.getString(R.string.cancel)) + dialogBuilder.neutralText(resources.getString(R.string.default_string)) + dialogBuilder.positiveColor(activity.accent) + dialogBuilder.negativeColor(activity.accent) + dialogBuilder.neutralColor(activity.accent) + dialogBuilder.onPositive { dialog, _ -> + val inputText = dialog.inputEditText?.text.toString() + sharedPrefs.edit().putInt( + PreferencesConstants.KEY_TRASH_BIN_RETENTION_DAYS, + inputText.toInt() + ).apply() + } + dialogBuilder.onNeutral { _, _ -> + sharedPrefs.edit().putInt( + PreferencesConstants.KEY_TRASH_BIN_RETENTION_DAYS, + TrashBinConfig.RETENTION_DAYS_INFINITE + ).apply() + } + dialogBuilder.onNegative { dialog, _ -> dialog.cancel() } + dialogBuilder.build().show() + } + + private fun trashBinRetentionBytes() { + val dialogBuilder = MaterialDialog.Builder(activity) + dialogBuilder.title( + resources.getString(R.string.trash_bin_retention_bytes_title) + ) + val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(requireContext()) + val bytes = sharedPrefs.getLong( + PreferencesConstants.KEY_TRASH_BIN_RETENTION_BYTES, + TrashBinConfig.RETENTION_BYTES_INFINITE + ) + dialogBuilder.inputType(InputType.TYPE_CLASS_NUMBER) + dialogBuilder.input( + "", + "$bytes", + true + ) { _, _ -> } + dialogBuilder.theme( + activity.utilsProvider.appTheme.getMaterialDialogTheme(requireContext()) + ) + dialogBuilder.positiveText(resources.getString(R.string.ok)) + dialogBuilder.negativeText(resources.getString(R.string.cancel)) + dialogBuilder.neutralText(resources.getString(R.string.default_string)) + dialogBuilder.positiveColor(activity.accent) + dialogBuilder.negativeColor(activity.accent) + dialogBuilder.neutralColor(activity.accent) + dialogBuilder.onPositive { dialog, _ -> + val inputText = dialog.inputEditText?.text.toString() + sharedPrefs.edit().putLong( + PreferencesConstants.KEY_TRASH_BIN_RETENTION_BYTES, + inputText.toLong() + ).apply() + } + dialogBuilder.onNeutral { _, _ -> + sharedPrefs.edit().putLong( + PreferencesConstants.KEY_TRASH_BIN_RETENTION_BYTES, + TrashBinConfig.RETENTION_BYTES_INFINITE + ).apply() + } + dialogBuilder.onNegative { dialog, _ -> dialog.cancel() } + dialogBuilder.build().show() + } + + private fun trashBinCleanupInterval() { + val dialogBuilder = MaterialDialog.Builder(activity) + dialogBuilder.title( + resources.getString(R.string.trash_bin_cleanup_interval_title) + ) + val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(requireContext()) + val intervalHours = sharedPrefs.getInt( + PreferencesConstants.KEY_TRASH_BIN_CLEANUP_INTERVAL_HOURS, + TrashBinConfig.INTERVAL_CLEANUP_HOURS + ) + dialogBuilder.inputType(InputType.TYPE_CLASS_NUMBER) + dialogBuilder.input( + "", + "$intervalHours", + true + ) { _, _ -> } + dialogBuilder.theme( + activity.utilsProvider.appTheme.getMaterialDialogTheme(requireContext()) + ) + dialogBuilder.positiveText(resources.getString(R.string.ok)) + dialogBuilder.negativeText(resources.getString(R.string.cancel)) + dialogBuilder.neutralText(resources.getString(R.string.default_string)) + dialogBuilder.positiveColor(activity.accent) + dialogBuilder.negativeColor(activity.accent) + dialogBuilder.neutralColor(activity.accent) + dialogBuilder.onPositive { dialog, _ -> + val inputText = dialog.inputEditText?.text.toString() + sharedPrefs.edit().putInt( + PreferencesConstants.KEY_TRASH_BIN_CLEANUP_INTERVAL_HOURS, + inputText.toInt() + ).apply() + } + dialogBuilder.onNeutral { _, _ -> + sharedPrefs.edit().putInt( + PreferencesConstants.KEY_TRASH_BIN_CLEANUP_INTERVAL_HOURS, + TrashBinConfig.INTERVAL_CLEANUP_HOURS + ).apply() + } + dialogBuilder.onNegative { dialog, _ -> dialog.cancel() } + dialogBuilder.build().show() + } } diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/PreferencesConstants.kt b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/PreferencesConstants.kt index 43797a30c9..6190482466 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/PreferencesConstants.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/preferencefragments/PreferencesConstants.kt @@ -76,6 +76,10 @@ object PreferencesConstants { const val PREFERENCE_TEXTEDITOR_NEWSTACK = "texteditor_newstack" const val PREFERENCE_DELETE_CONFIRMATION = "delete_confirmation" const val PREFERENCE_DISABLE_PLAYER_INTENT_FILTERS = "disable_player_intent_filters" + const val PREFERENCE_TRASH_BIN_RETENTION_NUM_OF_FILES = "retention_num_of_files" + const val PREFERENCE_TRASH_BIN_RETENTION_DAYS = "retention_days" + const val PREFERENCE_TRASH_BIN_RETENTION_BYTES = "retention_bytes" + const val PREFERENCE_TRASH_BIN_CLEANUP_INTERVAL = "cleanup_interval" // security_prefs.xml const val PREFERENCE_CRYPT_FINGERPRINT = "crypt_fingerprint" @@ -111,6 +115,7 @@ object PreferencesConstants { const val KEY_TRASH_BIN_RETENTION_DAYS = "trash_bin_retention_days" const val KEY_TRASH_BIN_RETENTION_BYTES = "trash_bin_retention_bytes" const val KEY_TRASH_BIN_RETENTION_NUM_OF_FILES = "trash_bin_retention_num_of_files" + const val KEY_TRASH_BIN_CLEANUP_INTERVAL_HOURS = "trash_bin_cleanup_interval_hours" const val DEFAULT_PREFERENCE_DELETE_CONFIRMATION = true } diff --git a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/BottomBar.java b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/BottomBar.java index 5eaae51180..0a72ee7ade 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/BottomBar.java +++ b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/BottomBar.java @@ -177,7 +177,9 @@ public boolean onSingleTapConfirmed(MotionEvent e) { final MainFragment mainFragment = mainActivity.getCurrentMainFragment(); Objects.requireNonNull(mainFragment); if (mainFragment.getMainFragmentViewModel() != null - && OpenMode.CUSTOM != mainFragment.getMainFragmentViewModel().getOpenMode()) { + && OpenMode.CUSTOM != mainFragment.getMainFragmentViewModel().getOpenMode() + && OpenMode.TRASH_BIN + != mainFragment.getMainFragmentViewModel().getOpenMode()) { FileUtils.crossfade(buttons, pathLayout); timer.cancel(); timer.start(); @@ -382,6 +384,7 @@ public void updatePath( newPath = mainActivityHelper.parseOTGPath(news); break; case CUSTOM: + case TRASH_BIN: newPath = mainActivityHelper.getIntegralNames(news); break; case DROPBOX: diff --git a/app/src/main/java/com/amaze/filemanager/ui/views/drawer/Drawer.java b/app/src/main/java/com/amaze/filemanager/ui/views/drawer/Drawer.java index d770d31d59..44b8b85794 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/views/drawer/Drawer.java +++ b/app/src/main/java/com/amaze/filemanager/ui/views/drawer/Drawer.java @@ -589,15 +589,16 @@ public void refreshDrawer() { R.drawable.ic_round_analytics_24, null); - // initially load trash bin items with "7" but ones listed they're referred as @link{OpenMode.TRASH_BIN} + // initially load trash bin items with "7" but ones listed they're referred as + // @link{OpenMode.TRASH_BIN} addNewItem( - menu, - LASTGROUP, - order++, - R.string.trasbin_bin, - new MenuMetadata("7"), - R.drawable.round_delete_outline_24, - null); + menu, + LASTGROUP, + order++, + R.string.trash_bin, + new MenuMetadata("7"), + R.drawable.round_delete_outline_24, + null); addNewItem( menu, 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 03f626a3f1..8b85c335c1 100644 --- a/app/src/main/java/com/amaze/filemanager/utils/MainActivityActionMode.kt +++ b/app/src/main/java/com/amaze/filemanager/utils/MainActivityActionMode.kt @@ -130,8 +130,9 @@ class MainActivityActionMode(private val mainActivityReference: WeakReference { + GeneralDialogCreation.restoreFilesDialog( + mainActivity, + mainActivity, + checkedItems, + mainActivity.utilsProvider.appTheme + ) + true + } R.id.share -> { if (checkedItems.size > 100) Toast.makeText( mainActivity, diff --git a/app/src/main/java/com/amaze/filemanager/utils/MainActivityHelper.java b/app/src/main/java/com/amaze/filemanager/utils/MainActivityHelper.java index 4261f6f174..1f462111df 100644 --- a/app/src/main/java/com/amaze/filemanager/utils/MainActivityHelper.java +++ b/app/src/main/java/com/amaze/filemanager/utils/MainActivityHelper.java @@ -296,6 +296,9 @@ public String getIntegralNames(String path) { case 6: newPath = mainActivity.getString(R.string.recent); break; + case 7: + newPath = mainActivity.getString(R.string.trash_bin); + break; } return newPath; } diff --git a/app/src/main/res/drawable/round_restore_from_trash_24.xml b/app/src/main/res/drawable/round_restore_from_trash_24.xml new file mode 100644 index 0000000000..038ef7ef64 --- /dev/null +++ b/app/src/main/res/drawable/round_restore_from_trash_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout-v16/dialog_delete.xml b/app/src/main/res/layout-v16/dialog_delete.xml index 2f8056c893..d547a1326d 100644 --- a/app/src/main/res/layout-v16/dialog_delete.xml +++ b/app/src/main/res/layout-v16/dialog_delete.xml @@ -8,9 +8,10 @@ android:orientation="vertical" > @@ -65,4 +66,14 @@ tools:text="Total: 6.00 MB" > + diff --git a/app/src/main/res/layout/dialog_delete.xml b/app/src/main/res/layout/dialog_delete.xml index 469fc16546..7e2d065c64 100644 --- a/app/src/main/res/layout/dialog_delete.xml +++ b/app/src/main/res/layout/dialog_delete.xml @@ -8,9 +8,10 @@ android:orientation="vertical" > diff --git a/app/src/main/res/menu/contextual.xml b/app/src/main/res/menu/contextual.xml index 8a497495a5..79537e433c 100644 --- a/app/src/main/res/menu/contextual.xml +++ b/app/src/main/res/menu/contextual.xml @@ -36,8 +36,8 @@ app:showAsAction="always" /> diff --git a/app/src/main/res/menu/item_extras.xml b/app/src/main/res/menu/item_extras.xml index d2d2d1201e..6011fdbe67 100644 --- a/app/src/main/res/menu/item_extras.xml +++ b/app/src/main/res/menu/item_extras.xml @@ -25,6 +25,11 @@ android:id="@+id/delete" android:title="@string/delete" /> + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c99e399c90..290e6f34c7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -77,6 +77,8 @@ Directories Files Deleting the items below will permanently remove them from your device and they cannot be recovered. + Are you sure you want to delete following items? + Are you sure you want to restore following items? Delete items permanently? Deleting Do you want to set current path as home for this tab? @@ -821,10 +823,18 @@ You only need to do this once, until the next time you select a new location for Checking for conflicts Destination and source folder shouldn\'t match to overwrite. No available browser - Trash Bin + Trash Bin Delete Permanently Restore Restore Files Restoring… + Retention by number of files + Maximum files trash bin can store + Retention by days + Maximum days for which trash bin can store a file + Retention by data size + Maximum data (in MBs) trash bin can store + Cleanup interval + Trigger auto-cleanup interval (hours) diff --git a/app/src/main/res/xml/behavior_prefs.xml b/app/src/main/res/xml/behavior_prefs.xml index 875ca5686b..980f26e466 100644 --- a/app/src/main/res/xml/behavior_prefs.xml +++ b/app/src/main/res/xml/behavior_prefs.xml @@ -48,6 +48,32 @@ app:key="extractpath" app:summary="@string/archive_summary" app:title="@string/archive_extract_folder"> + + + + + + Date: Wed, 22 Nov 2023 19:14:00 +0530 Subject: [PATCH 4/7] 971: Fix trash bin restore when parent file not present; fix directory icon in trash bin --- .../adapters/data/LayoutElementParcelable.java | 2 +- .../asynchronous/asynctasks/LoadFilesListTask.java | 1 + .../com/amaze/filemanager/filesystem/HybridFile.java | 2 +- .../ui/activities/MainActivityViewModel.kt | 11 +++++++++++ 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/amaze/filemanager/adapters/data/LayoutElementParcelable.java b/app/src/main/java/com/amaze/filemanager/adapters/data/LayoutElementParcelable.java index c47e6c2254..a186ba688a 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/data/LayoutElementParcelable.java +++ b/app/src/main/java/com/amaze/filemanager/adapters/data/LayoutElementParcelable.java @@ -48,7 +48,7 @@ public class LayoutElementParcelable implements Parcelable { public final String permissions; public final String symlink; public String size; - public final boolean isDirectory; + public boolean isDirectory; public long date, longSize; public String dateModification; public final boolean header; diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/LoadFilesListTask.java b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/LoadFilesListTask.java index 67997f3fc2..db0cea2dae 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/LoadFilesListTask.java +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/LoadFilesListTask.java @@ -594,6 +594,7 @@ else if (cursor.getCount() > 0 && cursor.moveToFirst()) { element.longSize = trashBinFile.getSizeBytes(); element.size = Formatter.formatFileSize(context, trashBinFile.getSizeBytes()); element.dateModification = Utils.getDate(context, trashBinFile.getDeleteTime() * 1000); + element.isDirectory = trashBinFile.isDirectory(); deletedFiles.add(element); } } 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 6471230700..230031889b 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/HybridFile.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/HybridFile.java @@ -1644,7 +1644,7 @@ public LayoutElementParcelable generateLayoutElement(@NonNull Context c, boolean 0, true, file.lastModified() + "", - false, + file.isDirectory(), showThumbs, mode); } else { diff --git a/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivityViewModel.kt b/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivityViewModel.kt index 66235ae0ea..9cde46c553 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivityViewModel.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivityViewModel.kt @@ -28,6 +28,7 @@ import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import androidx.preference.PreferenceManager +import com.amaze.filemanager.R import com.amaze.filemanager.adapters.data.LayoutElementParcelable import com.amaze.filemanager.application.AppConfig import com.amaze.filemanager.fileoperations.filesystem.OpenMode @@ -238,6 +239,16 @@ class MainActivityViewModel(val applicationContext: Application) : override fun invoke(source: String, dest: String): Boolean { val sourceFile = File(source) val destFile = File(dest) + if (destFile.exists()) { + AppConfig.toast( + applicationContext, + applicationContext.getString(R.string.fileexist) + ) + return false + } + if (destFile.parentFile != null && !destFile.parentFile!!.exists()) { + destFile.parentFile?.mkdirs() + } if (!sourceFile.renameTo(destFile)) { return false } From 8dbfaec8bec8f206e873ee36a59e3cdfaa6d7e82 Mon Sep 17 00:00:00 2001 From: Raymond Lai Date: Thu, 23 Nov 2023 23:40:04 +0800 Subject: [PATCH 5/7] Fix DeleteTask failing delete SMB files In most file manager implementations, including desktop ones, remote files are to be deleted directly even when put to recycle bin. --- .../filemanager/asynchronous/asynctasks/DeleteTask.java | 7 ++++++- .../asynchronous/asynctasks/SmbDeleteTaskTest.kt | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) 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 51d2d811f7..bc77c13c45 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 @@ -184,7 +184,12 @@ private boolean doDeleteFile(@NonNull HybridFileParcelable file) throws Exceptio } default: try { - if (!doDeletePermanently) { + /* SMB and SFTP (or any remote files that may support in the future) should not be + * supported by recycle bin. - TranceLove + */ + if (!doDeletePermanently + && !OpenMode.SMB.equals(file.getMode()) + && !OpenMode.SFTP.equals(file.getMode())) { return file.moveToBin(applicationContext); } return file.delete(applicationContext, rootMode); diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/SmbDeleteTaskTest.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/SmbDeleteTaskTest.kt index d440d38285..123a6c65f4 100644 --- a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/SmbDeleteTaskTest.kt +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/SmbDeleteTaskTest.kt @@ -40,7 +40,7 @@ class SmbDeleteTaskTest : AbstractDeleteTaskTestBase() { } /** - * Test case to verify delete SSH file failure scenario. + * Test case to verify delete SMB file failure scenario. * * @see AbstractDeleteTaskTestBase.doTestDeleteFileAccessDenied */ From bd77530d9aa621c29b7efe743041d31533e9101b Mon Sep 17 00:00:00 2001 From: Raymond Lai Date: Thu, 23 Nov 2023 23:58:26 +0800 Subject: [PATCH 6/7] Fix Codacy complaints Also a condition inside DeleteTask, that delete SSH files should not trigger mediascanner as well. --- .../filemanager/asynchronous/asynctasks/DeleteTask.java | 2 +- .../filemanager/ui/activities/MainActivityViewModel.kt | 9 +++++++++ .../filemanager/ui/dialogs/GeneralDialogCreation.java | 8 ++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) 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 bc77c13c45..0b187e65f6 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 @@ -113,7 +113,7 @@ protected final AsyncTaskResult doInBackground( } // delete file from media database - if (!file.isSmb()) + if (!file.isSmb() && !file.isSftp()) MediaConnectionUtils.scanFile( applicationContext, files.toArray(new HybridFile[files.size()])); diff --git a/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivityViewModel.kt b/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivityViewModel.kt index 9cde46c553..9d66f0141e 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivityViewModel.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivityViewModel.kt @@ -185,6 +185,9 @@ class MainActivityViewModel(val applicationContext: Application) : return mutableLiveData } + /** + * TODO: Documentation + */ fun moveToBinLightWeight(mediaFileInfoList: List) { viewModelScope.launch(Dispatchers.IO) { val trashBinFilesList = mediaFileInfoList.map { @@ -221,6 +224,9 @@ class MainActivityViewModel(val applicationContext: Application) : } } + /** + * Restore files from trash bin + */ fun restoreFromBin(mediaFileInfoList: List) { viewModelScope.launch(Dispatchers.IO) { LOG.info("Restoring media files from bin $mediaFileInfoList") @@ -269,6 +275,9 @@ class MainActivityViewModel(val applicationContext: Application) : } } + /** + * TODO: Documentation + */ fun progressTrashBinFilesLiveData(): MutableLiveData?> { if (trashBinFilesLiveData == null) { trashBinFilesLiveData = MutableLiveData() diff --git a/app/src/main/java/com/amaze/filemanager/ui/dialogs/GeneralDialogCreation.java b/app/src/main/java/com/amaze/filemanager/ui/dialogs/GeneralDialogCreation.java index bb60ab0917..35695d1cdb 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/dialogs/GeneralDialogCreation.java +++ b/app/src/main/java/com/amaze/filemanager/ui/dialogs/GeneralDialogCreation.java @@ -416,6 +416,14 @@ private void updateViews( } } + /** + * Displays a dialog prompting user to restore files in trash bin. + * + * @param context + * @param mainActivity + * @param positions + * @param appTheme + */ @SuppressWarnings("ConstantConditions") public static void restoreFilesDialog( @NonNull final Context context, From 8a786eb0ae8f0f0c46731e00bdac30755c3ba4b0 Mon Sep 17 00:00:00 2001 From: Raymond Lai Date: Fri, 24 Nov 2023 00:05:22 +0800 Subject: [PATCH 7/7] Fix Codacy complaints on code complexity --- .../com/amaze/filemanager/ui/dialogs/GeneralDialogCreation.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/amaze/filemanager/ui/dialogs/GeneralDialogCreation.java b/app/src/main/java/com/amaze/filemanager/ui/dialogs/GeneralDialogCreation.java index 35695d1cdb..dab0f4c33a 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/dialogs/GeneralDialogCreation.java +++ b/app/src/main/java/com/amaze/filemanager/ui/dialogs/GeneralDialogCreation.java @@ -424,7 +424,7 @@ private void updateViews( * @param positions * @param appTheme */ - @SuppressWarnings("ConstantConditions") + @SuppressWarnings({"ConstantConditions", "PMD.NPathComplexity"}) public static void restoreFilesDialog( @NonNull final Context context, @NonNull final MainActivity mainActivity,