diff --git a/README.md b/README.md index 6b18d24f5c..22427ec04e 100644 --- a/README.md +++ b/README.md @@ -85,9 +85,10 @@ We strongly recommend using apk signed by us (either Play Store version or from ### License: Copyright (C) 2014-2018 Arpit Khurana - Copyright (C) 2014-2021 Vishal Nehra - Copyright (C) 2017-2021 Emmanuel Messulam - Copyright (C) 2018-2021 Raymond Lai + Copyright (C) 2014-2023 Vishal Nehra + Copyright (C) 2017-2023 Emmanuel Messulam + Copyright (C) 2018-2023 Raymond Lai + Copyright (C) 2019-2023 Vishnu Sanal T This file is part of Amaze File Manager. Amaze File Manager is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/app/build.gradle b/app/build.gradle index 2495576180..8caf2dd347 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -151,6 +151,8 @@ dependencies { testImplementation "org.jsoup:jsoup:$jsoupVersion" testImplementation "androidx.room:room-migration:$roomVersion" testImplementation "io.mockk:mockk:$mockkVersion" + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinxCoroutineTestVersion" + testImplementation "androidx.arch.core:core-testing:$androidXArchCoreTestVersion" kaptTest "com.google.auto.service:auto-service:1.0-rc4" androidTestImplementation "junit:junit:$junitVersion"//tests the app logic diff --git a/app/src/main/java/com/amaze/filemanager/adapters/SearchRecyclerViewAdapter.kt b/app/src/main/java/com/amaze/filemanager/adapters/SearchRecyclerViewAdapter.kt index d9ea4c87ce..7859f68a06 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/SearchRecyclerViewAdapter.kt +++ b/app/src/main/java/com/amaze/filemanager/adapters/SearchRecyclerViewAdapter.kt @@ -21,6 +21,9 @@ package com.amaze.filemanager.adapters import android.content.Context +import android.text.Spannable +import android.text.SpannableString +import android.text.style.ForegroundColorSpan import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -31,27 +34,30 @@ import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import com.amaze.filemanager.R import com.amaze.filemanager.application.AppConfig -import com.amaze.filemanager.filesystem.HybridFileParcelable +import com.amaze.filemanager.asynchronous.asynctasks.searchfilesystem.SearchResult import com.amaze.filemanager.ui.activities.MainActivity import com.amaze.filemanager.ui.colors.ColorPreference import java.util.Random class SearchRecyclerViewAdapter : - ListAdapter( + ListAdapter( - object : DiffUtil.ItemCallback() { + object : DiffUtil.ItemCallback() { override fun areItemsTheSame( - oldItem: HybridFileParcelable, - newItem: HybridFileParcelable + oldItem: SearchResult, + newItem: SearchResult ): Boolean { - return oldItem.path == newItem.path && oldItem.name == newItem.name + return oldItem.file.path == newItem.file.path && + oldItem.file.name == newItem.file.name } override fun areContentsTheSame( - oldItem: HybridFileParcelable, - newItem: HybridFileParcelable + oldItem: SearchResult, + newItem: SearchResult ): Boolean { - return oldItem.path == newItem.path && oldItem.name == newItem.name + return oldItem.file.path == newItem.file.path && + oldItem.file.name == newItem.file.name && + oldItem.matchRange == newItem.matchRange } } ) { @@ -62,17 +68,25 @@ class SearchRecyclerViewAdapter : } override fun onBindViewHolder(holder: SearchRecyclerViewAdapter.ViewHolder, position: Int) { - val item = getItem(position) - - holder.fileNameTV.text = item.name - holder.filePathTV.text = item.path.substring(0, item.path.lastIndexOf("/")) - - holder.colorView.setBackgroundColor(getRandomColor(holder.colorView.context)) + val (file, matchResult) = getItem(position) val colorPreference = (AppConfig.getInstance().mainActivityContext as MainActivity).currentColorPreference - if (item.isDirectory) { + val fileName = SpannableString(file.name) + fileName.setSpan( + ForegroundColorSpan(colorPreference.accent), + matchResult.first, + matchResult.last + 1, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + + holder.fileNameTV.text = fileName + holder.filePathTV.text = file.path.substring(0, file.path.lastIndexOf("/")) + + holder.colorView.setBackgroundColor(getRandomColor(holder.colorView.context)) + + if (file.isDirectory) { holder.colorView.setBackgroundColor(colorPreference.primaryFirstTab) } else { holder.colorView.setBackgroundColor(colorPreference.accent) @@ -93,16 +107,16 @@ class SearchRecyclerViewAdapter : view.setOnClickListener { - val item = getItem(adapterPosition) + val (file, _) = getItem(adapterPosition) - if (!item.isDirectory) { - item.openFile( + if (!file.isDirectory) { + file.openFile( AppConfig.getInstance().mainActivityContext as MainActivity?, false ) } else { (AppConfig.getInstance().mainActivityContext as MainActivity?) - ?.goToMain(item.path) + ?.goToMain(file.path) } (AppConfig.getInstance().mainActivityContext as MainActivity?) diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/BasicSearch.kt b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/BasicSearch.kt new file mode 100644 index 0000000000..09c568164e --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/BasicSearch.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2014-2024 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.asynchronous.asynctasks.searchfilesystem + +import android.content.Context +import com.amaze.filemanager.filesystem.HybridFileParcelable +import com.amaze.filemanager.filesystem.root.ListFilesCommand.listFiles + +class BasicSearch( + query: String, + path: String, + searchParameters: SearchParameters, + context: Context +) : FileSearch(query, path, searchParameters) { + private val applicationContext = context.applicationContext + + override suspend fun search(filter: SearchFilter) { + listFiles( + path, + SearchParameter.ROOT in searchParameters, + SearchParameter.SHOW_HIDDEN_FILES in searchParameters, + { } + ) { hybridFileParcelable: HybridFileParcelable -> + if (SearchParameter.SHOW_HIDDEN_FILES in searchParameters || + !hybridFileParcelable.isHidden + ) { + val resultRange = + filter.searchFilter(hybridFileParcelable.getName(applicationContext)) + if (resultRange != null) { + publishProgress(hybridFileParcelable, resultRange) + } + } + } + } +} diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/DeepSearch.kt b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/DeepSearch.kt new file mode 100644 index 0000000000..43ba6a0259 --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/DeepSearch.kt @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2014-2024 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.asynchronous.asynctasks.searchfilesystem + +import android.content.Context +import com.amaze.filemanager.fileoperations.filesystem.OpenMode +import com.amaze.filemanager.filesystem.HybridFile +import kotlinx.coroutines.isActive +import org.slf4j.LoggerFactory +import kotlin.coroutines.coroutineContext + +class DeepSearch( + query: String, + path: String, + searchParameters: SearchParameters, + context: Context, + private val openMode: OpenMode +) : FileSearch(query, path, searchParameters) { + private val LOG = LoggerFactory.getLogger(DeepSearch::class.java) + + private val applicationContext: Context + + init { + applicationContext = context.applicationContext + } + + /** + * Search for occurrences of a given text in file names and publish the result + * + * @param directory the current path + */ + override suspend fun search(filter: SearchFilter) { + val directory = HybridFile(openMode, path) + if (directory.isSmb) return + + if (directory.isDirectory(applicationContext)) { + // you have permission to read this directory + val worklist = ArrayDeque() + worklist.add(directory) + while (coroutineContext.isActive && worklist.isNotEmpty()) { + val nextFile = worklist.removeFirst() + nextFile.forEachChildrenFile( + applicationContext, + SearchParameter.ROOT in searchParameters + ) { file -> + if (!file.isHidden || SearchParameter.SHOW_HIDDEN_FILES in searchParameters) { + val resultRange = filter.searchFilter(file.getName(applicationContext)) + if (resultRange != null) { + publishProgress(file, resultRange) + } + if (file.isDirectory(applicationContext)) { + worklist.add(file) + } + } + } + } + } else { + LOG.warn("Cannot search " + directory.path + ": Permission Denied") + } + } +} diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/FileSearch.kt b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/FileSearch.kt new file mode 100644 index 0000000000..c866ce81f9 --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/FileSearch.kt @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2014-2024 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.asynchronous.asynctasks.searchfilesystem + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.amaze.filemanager.filesystem.HybridFileParcelable +import java.util.Locale +import java.util.regex.Pattern + +abstract class FileSearch( + protected val query: String, + protected val path: String, + protected val searchParameters: SearchParameters +) { + private val mutableFoundFilesLiveData: MutableLiveData> = + MutableLiveData() + val foundFilesLiveData: LiveData> = mutableFoundFilesLiveData + private val foundFilesList: MutableList = mutableListOf() + + /** + * Search for files, whose names match [query], starting from [path] and add them to + * [foundFilesLiveData] + */ + suspend fun search() { + if (SearchParameter.REGEX !in searchParameters) { + // regex not turned on so we use simpleFilter + this.search(simpleFilter(query)) + } else { + if (SearchParameter.REGEX_MATCHES !in searchParameters) { + // only regex turned on so we use regexFilter + this.search(regexFilter(query)) + } else { + // regex turned on and names must match pattern so use regexMatchFilter + this.search(regexMatchFilter(query)) + } + } + } + + /** + * Search for files, whose names fulfill [filter], starting from [path] and add them to + * [foundFilesLiveData]. + */ + protected abstract suspend fun search(filter: SearchFilter) + + /** + * Add [file] to list of found files and post it to [foundFilesLiveData] + */ + protected fun publishProgress( + file: HybridFileParcelable, + matchRange: MatchRange + ) { + foundFilesList.add(SearchResult(file, matchRange)) + mutableFoundFilesLiveData.postValue(foundFilesList) + } + + private fun simpleFilter(query: String): SearchFilter = + SearchFilter { fileName -> + // check case-insensitively if query is contained in fileName + val start = fileName.lowercase(Locale.getDefault()).indexOf( + query.lowercase( + Locale.getDefault() + ) + ) + if (start >= 0) { + start until start + query.length + } else { + null + } + } + + private fun regexFilter(query: String): SearchFilter { + val pattern = regexPattern(query) + return SearchFilter { fileName -> + // check case-insensitively if the pattern compiled from query can be found in fileName + val matcher = pattern.matcher(fileName) + if (matcher.find()) { + matcher.start() until matcher.end() + } else { + null + } + } + } + + private fun regexMatchFilter(query: String): SearchFilter { + val pattern = regexPattern(query) + return SearchFilter { fileName -> + // check case-insensitively if the pattern compiled from query matches fileName + if (pattern.matcher(fileName).matches()) { + fileName.indices + } else { + null + } + } + } + + private fun regexPattern(query: String): Pattern = + // compiles the given query into a Pattern + Pattern.compile( + bashRegexToJava(query), + Pattern.CASE_INSENSITIVE + ) + + /** + * method converts bash style regular expression to java. See [Pattern] + * + * @return converted string + */ + private fun bashRegexToJava(originalString: String): String { + val stringBuilder = StringBuilder() + for (i in originalString.indices) { + when (originalString[i].toString() + "") { + "*" -> stringBuilder.append("\\w*") + "?" -> stringBuilder.append("\\w") + else -> stringBuilder.append(originalString[i]) + } + } + return stringBuilder.toString() + } + + fun interface SearchFilter { + /** + * If the file with the given [fileName] fulfills some predicate, returns the part that fulfills the predicate. + * Otherwise returns null. + */ + fun searchFilter(fileName: String): MatchRange? + } +} diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/IndexedSearch.kt b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/IndexedSearch.kt new file mode 100644 index 0000000000..aac138128f --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/IndexedSearch.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2014-2024 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.asynchronous.asynctasks.searchfilesystem + +import android.database.Cursor +import android.provider.MediaStore +import com.amaze.filemanager.filesystem.RootHelper +import kotlinx.coroutines.isActive +import java.io.File +import kotlin.coroutines.coroutineContext + +class IndexedSearch( + query: String, + path: String, + searchParameters: SearchParameters, + private val cursor: Cursor +) : FileSearch(query, path, searchParameters) { + override suspend fun search(filter: SearchFilter) { + if (cursor.count > 0 && cursor.moveToFirst()) { + do { + val nextPath = + cursor.getString( + cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATA) + ) + val displayName = + cursor.getString( + cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DISPLAY_NAME) + ) + if (nextPath != null && displayName != null && nextPath.contains(path)) { + val resultRange = filter.searchFilter(displayName) + if (resultRange != null) { + val hybridFileParcelable = + RootHelper.generateBaseFile( + File(nextPath), + SearchParameter.SHOW_HIDDEN_FILES in searchParameters + ) + if (hybridFileParcelable != null) { + publishProgress(hybridFileParcelable, resultRange) + } + } + } + } while (cursor.moveToNext() && coroutineContext.isActive) + } + + cursor.close() + } +} diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/SearchAsyncTask.java b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/SearchAsyncTask.java deleted file mode 100644 index ebfacc5aa9..0000000000 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/SearchAsyncTask.java +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Copyright (C) 2014-2020 Arpit Khurana , Vishal Nehra , - * Emmanuel Messulam, Raymond Lai and Contributors. - * - * This file is part of Amaze File Manager. - * - * Amaze File Manager is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.amaze.filemanager.asynchronous.asynctasks.searchfilesystem; - -import static com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_SHOW_HIDDENFILES; - -import java.util.regex.Pattern; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.amaze.filemanager.asynchronous.asynctasks.StatefulAsyncTask; -import com.amaze.filemanager.fileoperations.filesystem.OpenMode; -import com.amaze.filemanager.filesystem.HybridFile; -import com.amaze.filemanager.filesystem.HybridFileParcelable; -import com.amaze.filemanager.ui.fragments.SearchWorkerFragment; - -import android.content.Context; -import android.os.AsyncTask; - -import androidx.preference.PreferenceManager; - -public class SearchAsyncTask extends AsyncTask - implements StatefulAsyncTask { - - private final Logger LOG = LoggerFactory.getLogger(SearchAsyncTask.class); - - /** This necessarily leaks the context */ - private final Context applicationContext; - - private SearchWorkerFragment.HelperCallbacks callbacks; - private final String input; - private final boolean rootMode; - private final boolean isRegexEnabled; - private final boolean isMatchesEnabled; - private final HybridFile file; - - public SearchAsyncTask( - Context context, - String input, - OpenMode openMode, - boolean root, - boolean regex, - boolean matches, - String path) { - this.applicationContext = context.getApplicationContext(); - this.input = input; - rootMode = root; - isRegexEnabled = regex; - isMatchesEnabled = matches; - - this.file = new HybridFile(openMode, path); - } - - @Override - protected void onPreExecute() { - /* - * Note that we need to check if the callbacks are null in each - * method in case they are invoked after the Activity's and - * Fragment's onDestroy() method have been called. - */ - if (callbacks != null) { - callbacks.onPreExecute(input); - } - } - - // callbacks not checked for null because of possibility of - // race conditions b/w worker thread main thread - @Override - protected Void doInBackground(Void... params) { - if (file.isSmb()) return null; - - // level 1 - // if regex or not - if (!isRegexEnabled) { - search(file, input); - } else { - // compile the regular expression in the input - Pattern pattern = Pattern.compile(bashRegexToJava(input)); - // level 2 - if (!isMatchesEnabled) searchRegExFind(file, pattern); - else searchRegExMatch(file, pattern); - } - return null; - } - - @Override - public void onPostExecute(Void c) { - if (callbacks != null) { - callbacks.onPostExecute(input); - } - } - - @Override - protected void onCancelled() { - if (callbacks != null) callbacks.onCancelled(); - } - - @Override - public void onProgressUpdate(HybridFileParcelable... val) { - if (!isCancelled() && callbacks != null) { - callbacks.onProgressUpdate(val[0], input); - } - } - - @Override - public void setCallback(SearchWorkerFragment.HelperCallbacks helperCallbacks) { - this.callbacks = helperCallbacks; - } - - /** - * Recursively search for occurrences of a given text in file names and publish the result - * - * @param directory the current path - */ - private void search(HybridFile directory, final SearchFilter filter) { - if (directory.isDirectory( - applicationContext)) { // do you have permission to read this directory? - directory.forEachChildrenFile( - applicationContext, - rootMode, - file -> { - boolean showHiddenFiles = - PreferenceManager.getDefaultSharedPreferences(applicationContext) - .getBoolean(PREFERENCE_SHOW_HIDDENFILES, false); - - if ((!isCancelled() && (showHiddenFiles || !file.isHidden()))) { - if (filter.searchFilter(file.getName(applicationContext))) { - publishProgress(file); - } - if (file.isDirectory() && !isCancelled()) { - search(file, filter); - } - } - }); - } else { - LOG.warn("Cannot search " + directory.getPath() + ": Permission Denied"); - } - } - - /** - * Recursively search for occurrences of a given text in file names and publish the result - * - * @param file the current path - * @param query the searched text - */ - private void search(HybridFile file, final String query) { - search(file, fileName -> fileName.toLowerCase().contains(query.toLowerCase())); - } - - /** - * Recursively find a java regex pattern {@link Pattern} in the file names and publish the result - * - * @param file the current file - * @param pattern the compiled java regex - */ - private void searchRegExFind(HybridFile file, final Pattern pattern) { - search(file, fileName -> pattern.matcher(fileName).find()); - } - - /** - * Recursively match a java regex pattern {@link Pattern} with the file names and publish the - * result - * - * @param file the current file - * @param pattern the compiled java regex - */ - private void searchRegExMatch(HybridFile file, final Pattern pattern) { - search(file, fileName -> pattern.matcher(fileName).matches()); - } - - /** - * method converts bash style regular expression to java. See {@link Pattern} - * - * @return converted string - */ - private String bashRegexToJava(String originalString) { - StringBuilder stringBuilder = new StringBuilder(); - - for (int i = 0; i < originalString.length(); i++) { - switch (originalString.charAt(i) + "") { - case "*": - stringBuilder.append("\\w*"); - break; - case "?": - stringBuilder.append("\\w"); - break; - default: - stringBuilder.append(originalString.charAt(i)); - break; - } - } - - return stringBuilder.toString(); - } - - public interface SearchFilter { - boolean searchFilter(String fileName); - } -} diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/SearchParameter.kt b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/SearchParameter.kt new file mode 100644 index 0000000000..8c22765f91 --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/SearchParameter.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2014-2024 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.asynchronous.asynctasks.searchfilesystem + +enum class SearchParameter { + ROOT, + REGEX, + REGEX_MATCHES, + SHOW_HIDDEN_FILES; + + /** + * Returns [SearchParameters] containing [this] and [other] + */ + infix fun and(other: SearchParameter): SearchParameters = SearchParameters.of(this, other) + + /** + * Returns [SearchParameters] containing [this] and [other] + */ + operator fun plus(other: SearchParameter): SearchParameters = this and other +} diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/SearchParameters.kt b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/SearchParameters.kt new file mode 100644 index 0000000000..62104b9e69 --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/SearchParameters.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2014-2024 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.asynchronous.asynctasks.searchfilesystem + +import java.util.EnumSet + +typealias SearchParameters = EnumSet + +/** + * Returns [SearchParameters] extended by [other] + */ +infix fun SearchParameters.and(other: SearchParameter): SearchParameters = SearchParameters.of( + other, + *this.toTypedArray() +) + +/** + * Returns [SearchParameters] extended by [other] + */ +operator fun SearchParameters.plus(other: SearchParameter): SearchParameters = this and other + +/** + * Returns [SearchParameters] that reflect the given Booleans + */ +fun searchParametersFromBoolean( + showHiddenFiles: Boolean = false, + isRegexEnabled: Boolean = false, + isRegexMatchesEnabled: Boolean = false, + isRoot: Boolean = false +): SearchParameters { + val searchParameterList = mutableListOf() + + if (showHiddenFiles) searchParameterList.add(SearchParameter.SHOW_HIDDEN_FILES) + if (isRegexEnabled) searchParameterList.add(SearchParameter.REGEX) + if (isRegexMatchesEnabled) searchParameterList.add(SearchParameter.REGEX_MATCHES) + if (isRoot) searchParameterList.add(SearchParameter.ROOT) + + return if (searchParameterList.isEmpty()) { + SearchParameters.noneOf(SearchParameter::class.java) + } else { + SearchParameters.copyOf(searchParameterList) + } +} diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/SortSearchResultCallable.kt b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/SearchResult.kt similarity index 66% rename from app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/SortSearchResultCallable.kt rename to app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/SearchResult.kt index f87788ff59..e9aa24d41d 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/SortSearchResultCallable.kt +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/SearchResult.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014-2021 Arpit Khurana , Vishal Nehra , + * Copyright (C) 2014-2024 Arpit Khurana , Vishal Nehra , * Emmanuel Messulam, Raymond Lai and Contributors. * * This file is part of Amaze File Manager. @@ -20,16 +20,11 @@ package com.amaze.filemanager.asynchronous.asynctasks.searchfilesystem -import com.amaze.filemanager.adapters.data.LayoutElementParcelable -import com.amaze.filemanager.filesystem.files.FileListSorter -import java.util.* -import java.util.concurrent.Callable +import com.amaze.filemanager.filesystem.HybridFileParcelable -class SortSearchResultCallable( - val elements: MutableList, - val sorter: FileListSorter -) : Callable { - override fun call() { - Collections.sort(elements, sorter) - } -} +data class SearchResult(val file: HybridFileParcelable, val matchRange: MatchRange) + +typealias MatchRange = IntProgression + +/** Returns the size of the [MatchRange] which means how many characters were matched */ +fun MatchRange.size(): Int = this.last - this.first diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/SearchResultListSorter.kt b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/SearchResultListSorter.kt new file mode 100644 index 0000000000..517faa68fd --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/SearchResultListSorter.kt @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2014-2024 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.asynchronous.asynctasks.searchfilesystem + +import com.amaze.filemanager.filesystem.files.FileListSorter +import com.amaze.filemanager.filesystem.files.sort.DirSortBy +import com.amaze.filemanager.filesystem.files.sort.SortBy +import com.amaze.filemanager.filesystem.files.sort.SortType +import java.util.Date +import java.util.concurrent.TimeUnit + +class SearchResultListSorter( + private val dirArg: DirSortBy, + private val sortType: SortType, + private val searchTerm: String +) : Comparator { + private val fileListSorter: FileListSorter by lazy { FileListSorter(dirArg, sortType) } + + private val relevanceComparator: Comparator by lazy { + Comparator { o1, o2 -> + val currentTime = Date().time + val comparator = compareBy { (item, matchRange) -> + // the match percentage of the search term in the name + val matchPercentageScore = + matchRange.size().toDouble() / item.getParcelableName().length.toDouble() + + // if the name starts with the search term + val startScore = (matchRange.first == 0).toInt() + + // if the search term is surrounded by separators + // e.g. "my-cat" more relevant than "mysterious" for search term "my" + val wordScore = item.getParcelableName().split('-', '_', '.', ' ').any { + it.contentEquals( + searchTerm, + ignoreCase = true + ) + }.toInt() + + val modificationDate = item.getDate() + // the time difference as minutes + val timeDiff = + TimeUnit.MILLISECONDS.toMinutes(currentTime - modificationDate) + // 30 days as minutes + val relevantModificationPeriod = TimeUnit.DAYS.toMinutes(30) + val timeScore = if (timeDiff < relevantModificationPeriod) { + // if the file was modified within the last 30 days, the recency is normalized + (relevantModificationPeriod - timeDiff) / + relevantModificationPeriod.toDouble() + } else { + // for all older modification time, the recency doesn't change the relevancy + 0.0 + } + + 1.2 * matchPercentageScore + + 0.7 * startScore + + 0.7 * wordScore + + 0.6 * timeScore + } + // Reverts the sorting to make most relevant first + comparator.compare(o1, o2) * -1 + } + } + + private fun Boolean.toInt() = if (this) 1 else 0 + + override fun compare(result1: SearchResult, result2: SearchResult): Int { + return when (sortType.sortBy) { + SortBy.RELEVANCE -> relevanceComparator.compare(result1, result2) + SortBy.SIZE, SortBy.TYPE, SortBy.LAST_MODIFIED, SortBy.NAME -> + fileListSorter.compare(result1.file, result2.file) + } + } +} diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/SortSearchResultTask.kt b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/SortSearchResultTask.kt deleted file mode 100644 index 18d159217d..0000000000 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/SortSearchResultTask.kt +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (C) 2014-2021 Arpit Khurana , Vishal Nehra , - * Emmanuel Messulam, Raymond Lai and Contributors. - * - * This file is part of Amaze File Manager. - * - * Amaze File Manager is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.amaze.filemanager.asynchronous.asynctasks.searchfilesystem - -import com.amaze.filemanager.R -import com.amaze.filemanager.adapters.data.LayoutElementParcelable -import com.amaze.filemanager.asynchronous.asynctasks.Task -import com.amaze.filemanager.filesystem.files.FileListSorter -import com.amaze.filemanager.ui.fragments.MainFragment -import org.slf4j.Logger -import org.slf4j.LoggerFactory - -class SortSearchResultTask( - val elements: MutableList, - val sorter: FileListSorter, - val mainFragment: MainFragment, - val query: String -) : Task { - - private val log: Logger = LoggerFactory.getLogger(SortSearchResultTask::class.java) - - private val task = SortSearchResultCallable(elements, sorter) - - override fun getTask(): SortSearchResultCallable = task - - override fun onError(error: Throwable) { - log.error("Could not sort search results because of exception", error) - } - - override fun onFinish(value: Unit) { - val mainFragmentViewModel = mainFragment.mainFragmentViewModel - - if (mainFragmentViewModel == null) { - log.error( - "Could not show sorted search results because main fragment view model is null" - ) - return - } - - val mainActivity = mainFragment.mainActivity - - if (mainActivity == null) { - log.error("Could not show sorted search results because main activity is null") - return - } - - mainFragment.reloadListElements( - true, - true, - !mainFragmentViewModel.isList - ) // TODO: 7/7/2017 this is really inneffient, use RecycleAdapter's - - // createHeaders() - mainActivity.appbar.bottomBar.setPathText("") - mainActivity - .appbar - .bottomBar.fullPathText = mainActivity.getString(R.string.search_results, query) - mainFragmentViewModel.results = false - } -} diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/handlers/FileHandler.kt b/app/src/main/java/com/amaze/filemanager/asynchronous/handlers/FileHandler.kt index 513bc0e5d6..64069eb375 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/handlers/FileHandler.kt +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/handlers/FileHandler.kt @@ -89,7 +89,6 @@ class FileHandler( // no item left in list, recreate views main.reloadListElements( true, - mainFragmentViewModel.results, !mainFragmentViewModel.isList ) } else { diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/files/FileListSorter.kt b/app/src/main/java/com/amaze/filemanager/filesystem/files/FileListSorter.kt index 9bd93791a2..1b57468a08 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/files/FileListSorter.kt +++ b/app/src/main/java/com/amaze/filemanager/filesystem/files/FileListSorter.kt @@ -26,80 +26,19 @@ import com.amaze.filemanager.filesystem.files.sort.DirSortBy import com.amaze.filemanager.filesystem.files.sort.SortBy import com.amaze.filemanager.filesystem.files.sort.SortType import java.lang.Long -import java.util.Date import java.util.Locale -import java.util.concurrent.TimeUnit /** * [Comparator] implementation to sort [LayoutElementParcelable]s. */ class FileListSorter( dirArg: DirSortBy, - sortType: SortType, - searchTerm: String? + sortType: SortType ) : Comparator { private var dirsOnTop = dirArg private val asc: Int = sortType.sortOrder.sortFactor private val sort: SortBy = sortType.sortBy - private val relevanceComparator: Comparator by lazy { - if (searchTerm == null) { - // no search term given, so every result is equally relevant - Comparator { _, _ -> - 0 - } - } else { - Comparator { o1, o2 -> - val currentTime = Date().time - val comparator = compareBy { item -> - // the match percentage of the search term in the name - val matchPercentageScore = - searchTerm.length.toDouble() / item.getParcelableName().length.toDouble() - - // if the name starts with the search term - val startScore = - item.getParcelableName().startsWith(searchTerm, ignoreCase = true).toInt() - - // if the search term is surrounded by separators - // e.g. "my-cat" more relevant than "mysterious" for search term "my" - val wordScore = item.getParcelableName().split('-', '_', '.', ' ').any { - it.contentEquals( - searchTerm, - ignoreCase = true - ) - }.toInt() - - val modificationDate = item.getDate() - // the time difference as minutes - val timeDiff = - TimeUnit.MILLISECONDS.toMinutes(currentTime - modificationDate) - // 30 days as minutes - val relevantModificationPeriod = TimeUnit.DAYS.toMinutes(30) - val timeScore = if (timeDiff < relevantModificationPeriod) { - // if the file was modified within the last 30 days, the recency is normalized - (relevantModificationPeriod - timeDiff) / - relevantModificationPeriod.toDouble() - } else { - // for all older modification time, the recency doesn't change the relevancy - 0.0 - } - - return@compareBy 1.2 * matchPercentageScore + - 0.7 * startScore + - 0.7 * wordScore + - 0.6 * timeScore - } - // Reverts the sorting to make most relevant first - comparator.compare(o1, o2) * -1 - } - } - } - - /** Constructor for convenience if there is no searchTerm */ - constructor(dirArg: DirSortBy, sortType: SortType) : this(dirArg, sortType, null) - - private fun Boolean.toInt() = if (this) 1 else 0 - private fun isDirectory(path: ComparableParcelable): Boolean { return path.isDirectory() } @@ -178,8 +117,8 @@ class FileListSorter( } } SortBy.RELEVANCE -> { - // sort by relevance to the search query - return asc * relevanceComparator.compare(file1, file2) + // This case should not be called because it is not defined + return 0 } } } diff --git a/app/src/main/java/com/amaze/filemanager/ui/activities/AboutActivity.java b/app/src/main/java/com/amaze/filemanager/ui/activities/AboutActivity.java index 2230accd91..93c5aa2d87 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/activities/AboutActivity.java +++ b/app/src/main/java/com/amaze/filemanager/ui/activities/AboutActivity.java @@ -67,13 +67,14 @@ public class AboutActivity extends ThemedActivity implements View.OnClickListene private AppBarLayout mAppBarLayout; private CollapsingToolbarLayout mCollapsingToolbarLayout; private AppCompatTextView mTitleTextView; - private View mAuthorsDivider, mDeveloper1Divider; + private View mAuthorsDivider, mDeveloper1Divider, mDeveloper2Divider; private Billing billing; private static final String URL_AUTHOR1_GITHUB = "https://github.com/arpitkh96"; private static final String URL_AUTHOR2_GITHUB = "https://github.com/VishalNehra"; private static final String URL_DEVELOPER1_GITHUB = "https://github.com/EmmanuelMess"; private static final String URL_DEVELOPER2_GITHUB = "https://github.com/TranceLove"; + private static final String URL_DEVELOPER3_GITHUB = "https://github.com/VishnuSanal"; private static final String URL_REPO_CHANGELOG = "https://github.com/TeamAmaze/AmazeFileManager/commits/master"; private static final String URL_REPO = "https://github.com/TeamAmaze/AmazeFileManager"; @@ -108,6 +109,7 @@ public void onCreate(@Nullable Bundle savedInstanceState) { mTitleTextView = findViewById(R.id.text_view_title); mAuthorsDivider = findViewById(R.id.view_divider_authors); mDeveloper1Divider = findViewById(R.id.view_divider_developers_1); + mDeveloper2Divider = findViewById(R.id.view_divider_developers_2); mAppBarLayout.setLayoutParams(calculateHeaderViewParams()); @@ -200,6 +202,7 @@ private void switchIcons() { // dark theme mAuthorsDivider.setBackgroundColor(Utils.getColor(this, R.color.divider_dark_card)); mDeveloper1Divider.setBackgroundColor(Utils.getColor(this, R.color.divider_dark_card)); + mDeveloper2Divider.setBackgroundColor(Utils.getColor(this, R.color.divider_dark_card)); } } @@ -281,6 +284,10 @@ public void onClick(View v) { openURL(URL_DEVELOPER2_GITHUB, this); break; + case R.id.text_view_developer_3_github: + openURL(URL_DEVELOPER3_GITHUB, this); + break; + case R.id.relative_layout_translate: openURL(URL_REPO_TRANSLATE, this); break; 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 24ccb65341..ff894315c9 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 @@ -123,7 +123,6 @@ import com.amaze.filemanager.ui.fragments.FtpServerFragment; import com.amaze.filemanager.ui.fragments.MainFragment; import com.amaze.filemanager.ui.fragments.ProcessViewerFragment; -import com.amaze.filemanager.ui.fragments.SearchWorkerFragment; import com.amaze.filemanager.ui.fragments.TabFragment; import com.amaze.filemanager.ui.fragments.data.MainFragmentViewModel; import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants; @@ -213,7 +212,6 @@ public class MainActivity extends PermissionsActivity implements SmbConnectionListener, BookmarkCallback, - SearchWorkerFragment.HelperCallbacks, CloudConnectionCallbacks, LoaderManager.LoaderCallbacks, FolderChooserDialog.FolderCallback, @@ -540,7 +538,7 @@ public void onPermissionGranted() { .subscribe( () -> { if (tabFragment != null) { - tabFragment.refactorDrawerStorages(false); + tabFragment.refactorDrawerStorages(false, false); Fragment main = tabFragment.getFragmentAtIndex(0); if (main != null) ((MainFragment) main).updateTabWithDb(tabHandler.findTab(1)); Fragment main1 = tabFragment.getFragmentAtIndex(1); @@ -950,7 +948,7 @@ public void onBackPressed() { fragmentTransaction.remove(compressedExplorerFragment); fragmentTransaction.commit(); supportInvalidateOptionsMenu(); - floatingActionButton.show(); + showFab(); } } else { compressedExplorerFragment.mActionMode.finish(); @@ -1001,6 +999,16 @@ public void exit() { } public void goToMain(String path) { + goToMain(path, false); + } + + /** + * Sets up the main view with a {@link MainFragment} + * + * @param path The path to which to go in the {@link MainFragment} + * @param hideFab Whether the FAB should be hidden in the new created {@link MainFragment} or not + */ + public void goToMain(String path, boolean hideFab) { FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); // title.setText(R.string.app_name); TabFragment tabFragment = new TabFragment(); @@ -1011,17 +1019,19 @@ public void goToMain(String path) { path = "6"; } } + Bundle b = new Bundle(); if (path != null && path.length() > 0) { - Bundle b = new Bundle(); b.putString("path", path); - tabFragment.setArguments(b); } + // This boolean will be given to the newly created MainFragment + b.putBoolean(MainFragment.BUNDLE_HIDE_FAB, hideFab); + tabFragment.setArguments(b); transaction.replace(R.id.content_frame, tabFragment); // Commit the transaction transaction.addToBackStack("tabt" + 1); transaction.commitAllowingStateLoss(); appbar.setTitle(null); - floatingActionButton.show(); + if (isCompressedOpen && pathInCompressedArchive != null) { openCompressed(pathInCompressedArchive); pathInCompressedArchive = null; @@ -1082,8 +1092,6 @@ public boolean onPrepareOptionsMenu(Menu menu) { .getBottomBar() .updatePath( mainFragment.getCurrentPath(), - mainFragment.getMainFragmentViewModel().getResults(), - MainActivityHelper.SEARCH_TEXT, mainFragment.getMainFragmentViewModel().getOpenMode(), mainFragment.getMainFragmentViewModel().getFolderCount(), mainFragment.getMainFragmentViewModel().getFileCount(), @@ -1531,7 +1539,11 @@ public SpeedDialView getFAB() { } public void showFab() { - showFab(getFAB()); + if (getCurrentMainFragment() != null && getCurrentMainFragment().getHideFab()) { + hideFab(); + } else { + showFab(getFAB()); + } } private void showFab(SpeedDialView fab) { @@ -1711,15 +1723,7 @@ void initialisePreferences() { void initialiseViews() { - appbar = - new AppBar( - this, - getPrefs(), - queue -> { - if (!queue.isEmpty()) { - mainActivityHelper.search(getPrefs(), queue); - } - }); + appbar = new AppBar(this, getPrefs()); appBarLayout = getAppbar().getAppbarLayout(); setSupportActionBar(getAppbar().getToolbar()); @@ -2243,51 +2247,6 @@ public void modify(String oldpath, String oldname, String newPath, String newnam .subscribe(() -> drawer.refreshDrawer()); } - @Override - public void onPreExecute(String query) { - executeWithMainFragment( - mainFragment -> { - mainFragment.mSwipeRefreshLayout.setRefreshing(true); - mainFragment.onSearchPreExecute(query); - return null; - }); - } - - @Override - public void onPostExecute(String query) { - final MainFragment mainFragment = getCurrentMainFragment(); - if (mainFragment == null) { - // TODO cancel search - return; - } - - mainFragment.onSearchCompleted(query); - mainFragment.mSwipeRefreshLayout.setRefreshing(false); - } - - @Override - public void onProgressUpdate(@NonNull HybridFileParcelable hybridFileParcelable, String query) { - final MainFragment mainFragment = getCurrentMainFragment(); - if (mainFragment == null) { - // TODO cancel search - return; - } - - mainFragment.addSearchResult(hybridFileParcelable, query); - } - - @Override - public void onCancelled() { - final MainFragment mainFragment = getCurrentMainFragment(); - if (mainFragment == null) { - return; - } - - mainFragment.reloadListElements( - false, false, !mainFragment.getMainFragmentViewModel().isList()); - mainFragment.mSwipeRefreshLayout.setRefreshing(false); - } - @Override public void addConnection(OpenMode service) { try { 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 9d66f0141e..82c290f4f2 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 @@ -25,26 +25,32 @@ import android.content.Intent import android.provider.MediaStore import androidx.collection.LruCache import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData 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.asynchronous.asynctasks.searchfilesystem.BasicSearch +import com.amaze.filemanager.asynchronous.asynctasks.searchfilesystem.DeepSearch +import com.amaze.filemanager.asynchronous.asynctasks.searchfilesystem.IndexedSearch +import com.amaze.filemanager.asynchronous.asynctasks.searchfilesystem.SearchParameters +import com.amaze.filemanager.asynchronous.asynctasks.searchfilesystem.SearchResult +import com.amaze.filemanager.asynchronous.asynctasks.searchfilesystem.searchParametersFromBoolean 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.MediaConnectionUtils.scanFile -import com.amaze.filemanager.filesystem.root.ListFilesCommand.listFiles +import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_REGEX +import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_REGEX_MATCHES import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_SHOW_HIDDENFILES import com.amaze.trashbin.MoveFilesCallback import com.amaze.trashbin.TrashBinFile import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.launch import org.slf4j.LoggerFactory import java.io.File -import java.util.Locale class MainActivityViewModel(val applicationContext: Application) : AndroidViewModel(applicationContext) { @@ -53,6 +59,14 @@ class MainActivityViewModel(val applicationContext: Application) : var listCache: LruCache> = LruCache(50) var trashBinFilesLiveData: MutableLiveData?>? = null + /** The [LiveData] of the last triggered search */ + var lastSearchLiveData: LiveData> = MutableLiveData(listOf()) + private set + + /** The [Job] of the last triggered search */ + var lastSearchJob: Job? = null + private set + companion object { /** * size of list to be cached for local files @@ -97,37 +111,19 @@ class MainActivityViewModel(val applicationContext: Application) : * Perform basic search: searches on the current directory */ fun basicSearch(mainActivity: MainActivity, query: String): - MutableLiveData> { - val hybridFileParcelables = ArrayList() + LiveData> { + val searchParameters = createSearchParameters(mainActivity) - val mutableLiveData: - MutableLiveData> = - MutableLiveData(hybridFileParcelables) + val path = mainActivity.currentMainFragment?.currentPath ?: "" - val showHiddenFiles = PreferenceManager - .getDefaultSharedPreferences(mainActivity) - .getBoolean(PREFERENCE_SHOW_HIDDENFILES, false) + val basicSearch = BasicSearch(query, path, searchParameters, this.applicationContext) - viewModelScope.launch(Dispatchers.IO) { - listFiles( - mainActivity.currentMainFragment!!.currentPath!!, - mainActivity.isRootExplorer, - showHiddenFiles, - { _: OpenMode? -> null } - ) { hybridFileParcelable: HybridFileParcelable -> - if (hybridFileParcelable.getName(mainActivity) - .lowercase(Locale.getDefault()) - .contains(query.lowercase(Locale.getDefault())) && - (showHiddenFiles || !hybridFileParcelable.isHidden) - ) { - hybridFileParcelables.add(hybridFileParcelable) - - mutableLiveData.postValue(hybridFileParcelables) - } - } + lastSearchJob = viewModelScope.launch(Dispatchers.IO) { + basicSearch.search() } - return mutableLiveData + lastSearchLiveData = basicSearch.foundFilesLiveData + return basicSearch.foundFilesLiveData } /** @@ -136,53 +132,69 @@ class MainActivityViewModel(val applicationContext: Application) : fun indexedSearch( mainActivity: MainActivity, query: String - ): MutableLiveData> { - val list = ArrayList() - - val mutableLiveData: MutableLiveData> = MutableLiveData( - list + ): LiveData> { + val projection = arrayOf( + MediaStore.Files.FileColumns.DATA, + MediaStore.Files.FileColumns.DISPLAY_NAME ) + val cursor = mainActivity + .contentResolver + .query(MediaStore.Files.getContentUri("external"), projection, null, null, null) + ?: return MutableLiveData() - val showHiddenFiles = - PreferenceManager.getDefaultSharedPreferences(mainActivity) - .getBoolean(PREFERENCE_SHOW_HIDDENFILES, false) + val searchParameters = createSearchParameters(mainActivity) - viewModelScope.launch(Dispatchers.IO) { - val projection = arrayOf(MediaStore.Files.FileColumns.DATA) - - val cursor = mainActivity - .contentResolver - .query(MediaStore.Files.getContentUri("external"), projection, null, null, null) - ?: return@launch - - if (cursor.count > 0 && cursor.moveToFirst()) { - do { - val path = - cursor.getString( - cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATA) - ) + val path = mainActivity.currentMainFragment?.currentPath ?: "" - if (path != null && - path.contains(mainActivity.currentMainFragment?.currentPath!!) && - File(path).name.lowercase(Locale.getDefault()).contains( - query.lowercase(Locale.getDefault()) - ) - ) { - val hybridFileParcelable = - RootHelper.generateBaseFile(File(path), showHiddenFiles) + val indexedSearch = IndexedSearch(query, path, searchParameters, cursor) - if (hybridFileParcelable != null) { - list.add(hybridFileParcelable) - mutableLiveData.postValue(list) - } - } - } while (cursor.moveToNext()) - } + lastSearchJob = viewModelScope.launch(Dispatchers.IO) { + indexedSearch.search() + } - cursor.close() + lastSearchLiveData = indexedSearch.foundFilesLiveData + return indexedSearch.foundFilesLiveData + } + + /** + * Perform deep search: search recursively for files matching [query] in the current path + */ + fun deepSearch( + mainActivity: MainActivity, + query: String + ): LiveData> { + val searchParameters = createSearchParameters(mainActivity) + + val path = mainActivity.currentMainFragment?.currentPath ?: "" + val openMode = + mainActivity.currentMainFragment?.mainFragmentViewModel?.openMode ?: OpenMode.FILE + + val context = this.applicationContext + + val deepSearch = DeepSearch( + query, + path, + searchParameters, + context, + openMode + ) + + lastSearchJob = viewModelScope.launch(Dispatchers.IO) { + deepSearch.search() } - return mutableLiveData + lastSearchLiveData = deepSearch.foundFilesLiveData + return deepSearch.foundFilesLiveData + } + + private fun createSearchParameters(mainActivity: MainActivity): SearchParameters { + val sharedPref = PreferenceManager.getDefaultSharedPreferences(mainActivity) + return searchParametersFromBoolean( + showHiddenFiles = sharedPref.getBoolean(PREFERENCE_SHOW_HIDDENFILES, false), + isRegexEnabled = sharedPref.getBoolean(PREFERENCE_REGEX, false), + isRegexMatchesEnabled = sharedPref.getBoolean(PREFERENCE_REGEX_MATCHES, false), + isRoot = mainActivity.isRootExplorer + ) } /** diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/CompressedExplorerFragment.kt b/app/src/main/java/com/amaze/filemanager/ui/fragments/CompressedExplorerFragment.kt index 29412de076..975e1f52de 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/CompressedExplorerFragment.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/CompressedExplorerFragment.kt @@ -663,7 +663,7 @@ class CompressedExplorerFragment : Fragment(), BottomBarButtonPath { requireMainActivity() .getAppbar() .bottomBar - .updatePath(path, false, null, OpenMode.FILE, folder, file, this) + .updatePath(path, OpenMode.FILE, folder, file, this) } } 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 ce9f91edb6..24ad0250e5 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 @@ -50,8 +50,6 @@ import com.amaze.filemanager.application.AppConfig; import com.amaze.filemanager.asynchronous.asynctasks.DeleteTask; import com.amaze.filemanager.asynchronous.asynctasks.LoadFilesListTask; -import com.amaze.filemanager.asynchronous.asynctasks.TaskKt; -import com.amaze.filemanager.asynchronous.asynctasks.searchfilesystem.SortSearchResultTask; import com.amaze.filemanager.asynchronous.handlers.FileHandler; import com.amaze.filemanager.database.SortHandler; import com.amaze.filemanager.database.models.explorer.Tab; @@ -63,7 +61,6 @@ import com.amaze.filemanager.filesystem.SafRootHolder; import com.amaze.filemanager.filesystem.files.CryptUtil; import com.amaze.filemanager.filesystem.files.EncryptDecryptUtils; -import com.amaze.filemanager.filesystem.files.FileListSorter; import com.amaze.filemanager.filesystem.files.FileUtils; import com.amaze.filemanager.filesystem.files.MediaConnectionUtils; import com.amaze.filemanager.ui.ExtensionsKt; @@ -84,7 +81,6 @@ import com.amaze.filemanager.utils.BottomBarButtonPath; import com.amaze.filemanager.utils.DataUtils; import com.amaze.filemanager.utils.GenericExtKt; -import com.amaze.filemanager.utils.MainActivityHelper; import com.amaze.filemanager.utils.OTGUtil; import com.amaze.filemanager.utils.Utils; import com.google.android.material.appbar.AppBarLayout; @@ -147,6 +143,9 @@ public class MainFragment extends Fragment private static final Logger LOG = LoggerFactory.getLogger(MainFragment.class); private static final String KEY_FRAGMENT_MAIN = "main"; + /** Key for boolean in arguments whether to hide the FAB if this {@link MainFragment} is shown */ + public static final String BUNDLE_HIDE_FAB = "hideFab"; + public SwipeRefreshLayout mSwipeRefreshLayout; public RecyclerAdapter adapter; @@ -172,6 +171,8 @@ public class MainFragment extends Fragment private MainFragmentViewModel mainFragmentViewModel; private MainActivityViewModel mainActivityViewModel; + private boolean hideFab = false; + private final ActivityResultLauncher handleDocumentUriForRestrictedDirectories = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), @@ -211,6 +212,9 @@ public void onCreate(Bundle savedInstanceState) { requireMainActivity().getCurrentColorPreference().getPrimaryFirstTab()); mainFragmentViewModel.setPrimaryTwoColor( requireMainActivity().getCurrentColorPreference().getPrimarySecondTab()); + if (getArguments() != null) { + hideFab = getArguments().getBoolean(BUNDLE_HIDE_FAB, false); + } } @Override @@ -375,21 +379,19 @@ public void switchView() { DataUtils.getInstance() .getListOrGridForPath(mainFragmentViewModel.getCurrentPath(), DataUtils.LIST) == DataUtils.GRID; - reloadListElements(false, mainFragmentViewModel.getResults(), isPathLayoutGrid); + reloadListElements(false, isPathLayoutGrid); } private void loadViews() { if (mainFragmentViewModel.getCurrentPath() != null) { - if (mainFragmentViewModel.getListElements().size() == 0 - && !mainFragmentViewModel.getResults()) { + if (mainFragmentViewModel.getListElements().size() == 0) { loadlist( mainFragmentViewModel.getCurrentPath(), true, mainFragmentViewModel.getOpenMode(), false); } else { - reloadListElements( - true, mainFragmentViewModel.getResults(), !mainFragmentViewModel.isList()); + reloadListElements(true, !mainFragmentViewModel.isList()); } } else { loadlist(mainFragmentViewModel.getHome(), true, mainFragmentViewModel.getOpenMode(), false); @@ -444,25 +446,6 @@ public void onListItemClicked( int position, LayoutElementParcelable layoutElementParcelable, AppCompatImageView imageView) { - if (mainFragmentViewModel.getResults()) { - // check to initialize search results - // if search task is been running, cancel it - FragmentManager fragmentManager = requireActivity().getSupportFragmentManager(); - SearchWorkerFragment fragment = - (SearchWorkerFragment) fragmentManager.findFragmentByTag(MainActivity.TAG_ASYNC_HELPER); - if (fragment != null) { - if (fragment.searchAsyncTask.getStatus() == AsyncTask.Status.RUNNING) { - fragment.searchAsyncTask.cancel(true); - } - requireActivity().getSupportFragmentManager().beginTransaction().remove(fragment).commit(); - } - - mainFragmentViewModel.setRetainSearchTask(true); - mainFragmentViewModel.setResults(false); - } else { - mainFragmentViewModel.setRetainSearchTask(false); - MainActivityHelper.SEARCH_TEXT = null; - } if (requireMainActivity().getListItemSelected()) { if (isBackButton) { @@ -693,8 +676,7 @@ else if (actualPath.startsWith("/") boolean isPathLayoutGrid = DataUtils.getInstance().getListOrGridForPath(providedPath, DataUtils.LIST) == DataUtils.GRID; - setListElements( - data.second, back, providedPath, data.first, false, isPathLayoutGrid); + setListElements(data.second, back, providedPath, data.first, isPathLayoutGrid); } else { LOG.warn("Load list operation cancelled"); } @@ -815,13 +797,12 @@ public void setListElements( boolean back, String path, final OpenMode openMode, - boolean results, boolean grid) { if (bitmap != null) { mainFragmentViewModel.setListElements(bitmap); mainFragmentViewModel.setCurrentPath(path); mainFragmentViewModel.setOpenMode(openMode); - reloadListElements(back, results, grid); + reloadListElements(back, grid); } else { // list loading cancelled // TODO: Add support for cancelling list loading @@ -829,9 +810,8 @@ public void setListElements( } } - public void reloadListElements(boolean back, boolean results, boolean grid) { + public void reloadListElements(boolean back, boolean grid) { if (isAdded()) { - mainFragmentViewModel.setResults(results); boolean isOtg = (OTGUtil.PREFIX_OTG + "/").equals(mainFragmentViewModel.getCurrentPath()); if (getBoolean(PREFERENCE_SHOW_GOBACK_BUTTON) @@ -846,12 +826,11 @@ public void reloadListElements(boolean back, boolean results, boolean grid) { .getListElements() .get(0) .size - .equals(getString(R.string.goback))) - && !results) { + .equals(getString(R.string.goback)))) { mainFragmentViewModel.getListElements().add(0, getBackElement()); } - if (mainFragmentViewModel.getListElements().size() == 0 && !results) { + if (mainFragmentViewModel.getListElements().size() == 0) { nofilesview.setVisibility(View.VISIBLE); listView.setVisibility(View.GONE); mSwipeRefreshLayout.setEnabled(false); @@ -1102,139 +1081,80 @@ public void goBack() { if (mainFragmentViewModel.getOpenMode() == OpenMode.CUSTOM || mainFragmentViewModel.getOpenMode() == OpenMode.TRASH_BIN) { loadlist(mainFragmentViewModel.getHome(), false, OpenMode.FILE, false); + setHideFab(false); return; } HybridFile currentFile = new HybridFile(mainFragmentViewModel.getOpenMode(), mainFragmentViewModel.getCurrentPath()); - if (!mainFragmentViewModel.getResults()) { - if (!mainFragmentViewModel.getRetainSearchTask()) { - // normal case - if (requireMainActivity().getListItemSelected()) { - adapter.toggleChecked(false); + if (requireMainActivity().getListItemSelected()) { + adapter.toggleChecked(false); + } else { + setHideFab(false); + if (OpenMode.SMB.equals(mainFragmentViewModel.getOpenMode())) { + if (mainFragmentViewModel.getSmbPath() != null + && !mainFragmentViewModel.getSmbPath().equals(mainFragmentViewModel.getCurrentPath())) { + StringBuilder path = new StringBuilder(currentFile.getSmbFile().getParent()); + if (mainFragmentViewModel.getCurrentPath() != null + && mainFragmentViewModel.getCurrentPath().indexOf('?') > 0) + path.append( + mainFragmentViewModel + .getCurrentPath() + .substring(mainFragmentViewModel.getCurrentPath().indexOf('?'))); + loadlist( + path.toString().replace("%3D", "="), + true, + mainFragmentViewModel.getOpenMode(), + false); + } else loadlist(mainFragmentViewModel.getHome(), false, OpenMode.FILE, false); + } else if (OpenMode.SFTP.equals(mainFragmentViewModel.getOpenMode())) { + if (currentFile.getParent(requireContext()) == null) { + loadlist(mainFragmentViewModel.getHome(), false, OpenMode.FILE, false); + } else if (OpenMode.DOCUMENT_FILE.equals(mainFragmentViewModel.getOpenMode())) { + loadlist(currentFile.getParent(getContext()), true, currentFile.getMode(), false); } else { - if (OpenMode.SMB.equals(mainFragmentViewModel.getOpenMode())) { - if (mainFragmentViewModel.getSmbPath() != null - && !mainFragmentViewModel - .getSmbPath() - .equals(mainFragmentViewModel.getCurrentPath())) { - StringBuilder path = new StringBuilder(currentFile.getSmbFile().getParent()); - if (mainFragmentViewModel.getCurrentPath() != null - && mainFragmentViewModel.getCurrentPath().indexOf('?') > 0) - path.append( - mainFragmentViewModel - .getCurrentPath() - .substring(mainFragmentViewModel.getCurrentPath().indexOf('?'))); - loadlist( - path.toString().replace("%3D", "="), - true, - mainFragmentViewModel.getOpenMode(), - false); - } else loadlist(mainFragmentViewModel.getHome(), false, OpenMode.FILE, false); - } else if (OpenMode.SFTP.equals(mainFragmentViewModel.getOpenMode())) { - if (currentFile.getParent(requireContext()) == null) { - loadlist(mainFragmentViewModel.getHome(), false, OpenMode.FILE, false); - } else if (OpenMode.DOCUMENT_FILE.equals(mainFragmentViewModel.getOpenMode())) { - loadlist(currentFile.getParent(getContext()), true, currentFile.getMode(), false); - } else { - - String parent = currentFile.getParent(getContext()); - - if (parent == null) - parent = - mainFragmentViewModel.getHome(); // fall back by traversing back to home folder - - loadlist(parent, true, mainFragmentViewModel.getOpenMode(), false); - } - } else if (OpenMode.FTP.equals(mainFragmentViewModel.getOpenMode())) { - if (mainFragmentViewModel.getCurrentPath() != null) { - String parent = currentFile.getParent(getContext()); - // Hack. - if (parent != null && parent.contains("://")) { - loadlist(parent, true, mainFragmentViewModel.getOpenMode(), false); - } else { - loadlist(mainFragmentViewModel.getHome(), false, OpenMode.FILE, false); - } - } else { - loadlist(mainFragmentViewModel.getHome(), false, OpenMode.FILE, false); - } - } else if (("/").equals(mainFragmentViewModel.getCurrentPath()) - || (mainFragmentViewModel.getHome() != null - && mainFragmentViewModel.getHome().equals(mainFragmentViewModel.getCurrentPath())) - || mainFragmentViewModel.getIsOnCloudRoot()) { - getMainActivity().exit(); - } else if (OpenMode.DOCUMENT_FILE.equals(mainFragmentViewModel.getOpenMode()) - && !currentFile.getPath().startsWith("content://")) { - if (CollectionsKt.contains( - ANDROID_DEVICE_DATA_DIRS, currentFile.getParent(getContext()))) { - loadlist(currentFile.getParent(getContext()), false, OpenMode.ANDROID_DATA, false); - } else { - loadlist( - currentFile.getParent(getContext()), - true, - mainFragmentViewModel.getOpenMode(), - false); - } - } else if (FileUtils.canGoBack(getContext(), currentFile)) { - loadlist( - currentFile.getParent(getContext()), - true, - mainFragmentViewModel.getOpenMode(), - false); + + String parent = currentFile.getParent(getContext()); + + if (parent == null) + parent = mainFragmentViewModel.getHome(); // fall back by traversing back to home folder + + loadlist(parent, true, mainFragmentViewModel.getOpenMode(), false); + } + } else if (OpenMode.FTP.equals(mainFragmentViewModel.getOpenMode())) { + if (mainFragmentViewModel.getCurrentPath() != null) { + String parent = currentFile.getParent(getContext()); + // Hack. + if (parent != null && parent.contains("://")) { + loadlist(parent, true, mainFragmentViewModel.getOpenMode(), false); } else { - requireMainActivity().exit(); + loadlist(mainFragmentViewModel.getHome(), false, OpenMode.FILE, false); } - } - } else { - // case when we had pressed on an item from search results and wanna go back - // leads to resuming the search task - - if (MainActivityHelper.SEARCH_TEXT != null) { - - // starting the search query again :O - FragmentManager fm = requireMainActivity().getSupportFragmentManager(); - - // getting parent path to resume search from there - String parentPath = - new HybridFile( - mainFragmentViewModel.getOpenMode(), mainFragmentViewModel.getCurrentPath()) - .getParent(getActivity()); - // don't fuckin' remove this line, we need to change - // the path back to parent on back press - mainFragmentViewModel.setCurrentPath(parentPath); - - MainActivityHelper.addSearchFragment( - fm, - new SearchWorkerFragment(), - parentPath, - MainActivityHelper.SEARCH_TEXT, - mainFragmentViewModel.getOpenMode(), - requireMainActivity().isRootExplorer(), - sharedPref.getBoolean(SearchWorkerFragment.KEY_REGEX, false), - sharedPref.getBoolean(SearchWorkerFragment.KEY_REGEX_MATCHES, false)); } else { - loadlist(mainFragmentViewModel.getCurrentPath(), true, OpenMode.UNKNOWN, false); + loadlist(mainFragmentViewModel.getHome(), false, OpenMode.FILE, false); } - mainFragmentViewModel.setRetainSearchTask(false); - } - } else { - // to go back after search list have been popped - FragmentManager fm = requireMainActivity().getSupportFragmentManager(); - SearchWorkerFragment fragment = - (SearchWorkerFragment) fm.findFragmentByTag(MainActivity.TAG_ASYNC_HELPER); - if (fragment != null) { - if (fragment.searchAsyncTask.getStatus() == AsyncTask.Status.RUNNING) { - fragment.searchAsyncTask.cancel(true); + } else if (("/").equals(mainFragmentViewModel.getCurrentPath()) + || (mainFragmentViewModel.getHome() != null + && mainFragmentViewModel.getHome().equals(mainFragmentViewModel.getCurrentPath())) + || mainFragmentViewModel.getIsOnCloudRoot()) { + getMainActivity().exit(); + } else if (OpenMode.DOCUMENT_FILE.equals(mainFragmentViewModel.getOpenMode()) + && !currentFile.getPath().startsWith("content://")) { + if (CollectionsKt.contains(ANDROID_DEVICE_DATA_DIRS, currentFile.getParent(getContext()))) { + loadlist(currentFile.getParent(getContext()), false, OpenMode.ANDROID_DATA, false); + } else { + loadlist( + currentFile.getParent(getContext()), + true, + mainFragmentViewModel.getOpenMode(), + false); } - } - if (mainFragmentViewModel.getCurrentPath() != null) { + } else if (FileUtils.canGoBack(getContext(), currentFile)) { loadlist( - new File(mainFragmentViewModel.getCurrentPath()).getPath(), - true, - OpenMode.UNKNOWN, - false); + currentFile.getParent(getContext()), true, mainFragmentViewModel.getOpenMode(), false); + } else { + requireMainActivity().exit(); } - mainFragmentViewModel.setResults(false); } } @@ -1271,36 +1191,27 @@ public void goBackItemClick() { } HybridFile currentFile = new HybridFile(mainFragmentViewModel.getOpenMode(), mainFragmentViewModel.getCurrentPath()); - if (!mainFragmentViewModel.getResults()) { - if (requireMainActivity().getListItemSelected()) { - adapter.toggleChecked(false); - } else { - if (mainFragmentViewModel.getOpenMode() == OpenMode.SMB) { - if (mainFragmentViewModel.getCurrentPath() != null - && !mainFragmentViewModel - .getCurrentPath() - .equals(mainFragmentViewModel.getSmbPath())) { - StringBuilder path = new StringBuilder(currentFile.getSmbFile().getParent()); - if (mainFragmentViewModel.getCurrentPath().indexOf('?') > 0) - path.append( - mainFragmentViewModel - .getCurrentPath() - .substring(mainFragmentViewModel.getCurrentPath().indexOf('?'))); - loadlist(path.toString(), true, OpenMode.SMB, false); - } else loadlist(mainFragmentViewModel.getHome(), false, OpenMode.FILE, false); - } else if (("/").equals(mainFragmentViewModel.getCurrentPath()) - || mainFragmentViewModel.getIsOnCloudRoot()) { - requireMainActivity().exit(); - } else if (FileUtils.canGoBack(getContext(), currentFile)) { - loadlist( - currentFile.getParent(getContext()), - true, - mainFragmentViewModel.getOpenMode(), - false); - } else requireMainActivity().exit(); - } + if (requireMainActivity().getListItemSelected()) { + adapter.toggleChecked(false); } else { - loadlist(currentFile.getPath(), true, mainFragmentViewModel.getOpenMode(), false); + if (mainFragmentViewModel.getOpenMode() == OpenMode.SMB) { + if (mainFragmentViewModel.getCurrentPath() != null + && !mainFragmentViewModel.getCurrentPath().equals(mainFragmentViewModel.getSmbPath())) { + StringBuilder path = new StringBuilder(currentFile.getSmbFile().getParent()); + if (mainFragmentViewModel.getCurrentPath().indexOf('?') > 0) + path.append( + mainFragmentViewModel + .getCurrentPath() + .substring(mainFragmentViewModel.getCurrentPath().indexOf('?'))); + loadlist(path.toString(), true, OpenMode.SMB, false); + } else loadlist(mainFragmentViewModel.getHome(), false, OpenMode.FILE, false); + } else if (("/").equals(mainFragmentViewModel.getCurrentPath()) + || mainFragmentViewModel.getIsOnCloudRoot()) { + requireMainActivity().exit(); + } else if (FileUtils.canGoBack(getContext(), currentFile)) { + loadlist( + currentFile.getParent(getContext()), true, mainFragmentViewModel.getOpenMode(), false); + } else requireMainActivity().exit(); } } @@ -1405,58 +1316,6 @@ public ArrayList addToSmb( return smbFileList; } - // method to add search result entry to the LIST_ELEMENT arrayList - @Nullable - private LayoutElementParcelable addTo(@NonNull HybridFileParcelable hybridFileParcelable) { - if (DataUtils.getInstance().isFileHidden(hybridFileParcelable.getPath())) { - return null; - } - - if (hybridFileParcelable.isDirectory()) { - LayoutElementParcelable layoutElement = - new LayoutElementParcelable( - requireContext(), - hybridFileParcelable.getPath(), - hybridFileParcelable.getPermission(), - hybridFileParcelable.getLink(), - "", - 0, - true, - hybridFileParcelable.getDate() + "", - true, - getBoolean(PREFERENCE_SHOW_THUMB), - hybridFileParcelable.getMode()); - - mainFragmentViewModel.getListElements().add(layoutElement); - mainFragmentViewModel.setFolderCount(mainFragmentViewModel.getFolderCount() + 1); - return layoutElement; - } else { - long longSize = 0; - String size = ""; - if (hybridFileParcelable.getSize() != -1) { - longSize = hybridFileParcelable.getSize(); - size = Formatter.formatFileSize(getContext(), longSize); - } - - LayoutElementParcelable layoutElement = - new LayoutElementParcelable( - requireContext(), - hybridFileParcelable.getPath(), - hybridFileParcelable.getPermission(), - hybridFileParcelable.getLink(), - size, - longSize, - false, - hybridFileParcelable.getDate() + "", - false, - getBoolean(PREFERENCE_SHOW_THUMB), - hybridFileParcelable.getMode()); - mainFragmentViewModel.getListElements().add(layoutElement); - mainFragmentViewModel.setFileCount(mainFragmentViewModel.getFileCount() + 1); - return layoutElement; - } - } - @Override public void onDestroy() { super.onDestroy(); @@ -1519,65 +1378,6 @@ public void addShortcut(LayoutElementParcelable path) { ShortcutManagerCompat.requestPinShortcut(ctx, info, null); } - // This method is used to implement the modification for the pre Searching - public void onSearchPreExecute(String query) { - requireMainActivity().getAppbar().getBottomBar().setPathText(""); - requireMainActivity() - .getAppbar() - .getBottomBar() - .setFullPathText(getString(R.string.searching, query)); - } - - // adds search results based on result boolean. If false, the adapter is initialised with initial - // values, if true, new values are added to the adapter. - public void addSearchResult(@NonNull HybridFileParcelable hybridFileParcelable, String query) { - if (listView == null) { - return; - } - - // initially clearing the array for new result set - if (!mainFragmentViewModel.getResults()) { - mainFragmentViewModel.getListElements().clear(); - mainFragmentViewModel.setFileCount(0); - mainFragmentViewModel.setFolderCount(0); - } - - // adding new value to LIST_ELEMENTS - @Nullable LayoutElementParcelable layoutElementAdded = addTo(hybridFileParcelable); - if (!requireMainActivity() - .getAppbar() - .getBottomBar() - .getFullPathText() - .contains(getString(R.string.searching))) { - requireMainActivity() - .getAppbar() - .getBottomBar() - .setFullPathText(getString(R.string.searching, query)); - } - if (!mainFragmentViewModel.getResults()) { - reloadListElements(false, true, !mainFragmentViewModel.isList()); - requireMainActivity().getAppbar().getBottomBar().setPathText(""); - } else if (layoutElementAdded != null) { - adapter.addItem(layoutElementAdded); - } - stopAnimation(); - } - - public void onSearchCompleted(final String query) { - final List elements = mainFragmentViewModel.getListElements(); - if (!mainFragmentViewModel.getResults()) { - // no results were found - mainFragmentViewModel.getListElements().clear(); - } - TaskKt.fromTask( - new SortSearchResultTask( - elements, - new FileListSorter( - mainFragmentViewModel.getDsort(), mainFragmentViewModel.getSortType()), - this, - query)); - } - @Override public void onDetach() { super.onDetach(); @@ -1737,4 +1537,14 @@ > requireContext().getResources().getDisplayMetrics().heightPixels) { LOG.warn("Failed to adjust scrollview for tv", e); } } + + /** Whether the FAB should be hidden when this MainFragment is shown */ + public boolean getHideFab() { + return this.hideFab; + } + + /** Set whether the FAB should be hidden when this MainFragment is shown */ + public void setHideFab(boolean hideFab) { + this.hideFab = hideFab; + } } diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/SearchWorkerFragment.java b/app/src/main/java/com/amaze/filemanager/ui/fragments/SearchWorkerFragment.java deleted file mode 100644 index f3d3b2207b..0000000000 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/SearchWorkerFragment.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (C) 2014-2020 Arpit Khurana , Vishal Nehra , - * Emmanuel Messulam, Raymond Lai and Contributors. - * - * This file is part of Amaze File Manager. - * - * Amaze File Manager is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.amaze.filemanager.ui.fragments; - -import com.amaze.filemanager.asynchronous.asynctasks.searchfilesystem.SearchAsyncTask; -import com.amaze.filemanager.fileoperations.filesystem.OpenMode; -import com.amaze.filemanager.filesystem.HybridFileParcelable; - -import android.content.Context; -import android.os.Bundle; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; - -/** - * Worker fragment designed to not be destroyed when the activity holding it is recreated (aka the - * state changes like screen rotation) thus maintaining alive an AsyncTask (SearchTask in this case) - * - *

Created by vishal on 26/2/16 edited by EmmanuelMess. - */ -public class SearchWorkerFragment extends Fragment { - - public static final String KEY_PATH = "path"; - public static final String KEY_INPUT = "input"; - public static final String KEY_OPEN_MODE = "open_mode"; - public static final String KEY_ROOT_MODE = "root_mode"; - public static final String KEY_REGEX = "regex"; - public static final String KEY_REGEX_MATCHES = "matches"; - - public SearchAsyncTask searchAsyncTask; - - private HelperCallbacks callbacks; - - // interface for activity to communicate with asynctask - public interface HelperCallbacks { - void onPreExecute(String query); - - void onPostExecute(String query); - - void onProgressUpdate(@NonNull HybridFileParcelable val, String query); - - void onCancelled(); - } - - @Override - public void onAttach(@NonNull Context context) { - super.onAttach(context); - - // hold instance of activity as there is a change in device configuration - callbacks = (HelperCallbacks) context; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setRetainInstance(true); - String path = getArguments().getString(KEY_PATH); - String input = getArguments().getString(KEY_INPUT); - OpenMode openMode = OpenMode.getOpenMode(getArguments().getInt(KEY_OPEN_MODE)); - boolean rootMode = getArguments().getBoolean(KEY_ROOT_MODE); - boolean isRegexEnabled = getArguments().getBoolean(KEY_REGEX); - boolean isMatchesEnabled = getArguments().getBoolean(KEY_REGEX_MATCHES); - - searchAsyncTask = - new SearchAsyncTask( - requireContext(), input, openMode, rootMode, isRegexEnabled, isMatchesEnabled, path); - searchAsyncTask.setCallback(callbacks); - searchAsyncTask.execute(); - } - - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - searchAsyncTask.setCallback(callbacks); - } - - @Override - public void onDetach() { - super.onDetach(); - - // to avoid activity instance leak while changing activity configurations - callbacks = null; - } -} diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/TabFragment.java b/app/src/main/java/com/amaze/filemanager/ui/fragments/TabFragment.java index 1e7ffe6be9..5c8a2950dc 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/TabFragment.java +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/TabFragment.java @@ -46,7 +46,6 @@ import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants; import com.amaze.filemanager.ui.views.Indicator; import com.amaze.filemanager.utils.DataUtils; -import com.amaze.filemanager.utils.MainActivityHelper; import com.amaze.filemanager.utils.Utils; import android.animation.ArgbEvaluator; @@ -127,8 +126,10 @@ public View onCreateView( viewPager = rootView.findViewById(R.id.pager); + boolean hideFab = false; if (getArguments() != null) { path = getArguments().getString(KEY_PATH); + hideFab = getArguments().getBoolean(MainFragment.BUNDLE_HIDE_FAB); } requireMainActivity().supportInvalidateOptionsMenu(); @@ -139,7 +140,7 @@ public View onCreateView( int lastOpenTab = sharedPrefs.getInt(PREFERENCE_CURRENT_TAB, DEFAULT_CURRENT_TAB); MainActivity.currentTab = lastOpenTab; - refactorDrawerStorages(true); + refactorDrawerStorages(true, hideFab); viewPager.setAdapter(sectionsPagerAdapter); @@ -300,6 +301,9 @@ public void onPageSelected(int p1) { if (ma.getCurrentPath() != null) { requireMainActivity().getDrawer().selectCorrectDrawerItemForPath(ma.getCurrentPath()); updateBottomBar(ma); + // FAB might be hidden in the previous tab + // so we check if it should be shown for the new tab + requireMainActivity().showFab(); } } @@ -332,7 +336,7 @@ public Fragment createFragment(int position) { } private void addNewTab(int num, String path) { - addTab(new Tab(num, path, path), ""); + addTab(new Tab(num, path, path), "", false); } /** @@ -340,8 +344,10 @@ private void addNewTab(int num, String path) { * change paths in database. Calls should implement updating each tab's list for new paths. * * @param addTab whether new tabs should be added to ui or just change values in database + * @param hideFabInCurrentMainFragment whether the FAB should be hidden in the current {@link + * MainFragment} */ - public void refactorDrawerStorages(boolean addTab) { + public void refactorDrawerStorages(boolean addTab, boolean hideFabInCurrentMainFragment) { TabHandler tabHandler = TabHandler.getInstance(); Tab tab1 = tabHandler.findTab(1); Tab tab2 = tabHandler.findTab(2); @@ -367,22 +373,22 @@ public void refactorDrawerStorages(boolean addTab) { } else { if (path != null && path.length() != 0) { if (MainActivity.currentTab == 0) { - addTab(tab1, path); - addTab(tab2, ""); + addTab(tab1, path, hideFabInCurrentMainFragment); + addTab(tab2, "", false); } if (MainActivity.currentTab == 1) { - addTab(tab1, ""); - addTab(tab2, path); + addTab(tab1, "", false); + addTab(tab2, path, hideFabInCurrentMainFragment); } } else { - addTab(tab1, ""); - addTab(tab2, ""); + addTab(tab1, "", false); + addTab(tab2, "", false); } } } - private void addTab(@NonNull Tab tab, String path) { + private void addTab(@NonNull Tab tab, String path, boolean hideFabInTab) { MainFragment main = new MainFragment(); Bundle b = new Bundle(); @@ -395,6 +401,8 @@ private void addTab(@NonNull Tab tab, String path) { b.putString("home", tab.home); b.putInt("no", tab.tabNumber); + // specifies if the constructed MainFragment hides the FAB when it is shown + b.putBoolean(MainFragment.BUNDLE_HIDE_FAB, hideFabInTab); main.setArguments(b); fragments.add(main); sectionsPagerAdapter.notifyDataSetChanged(); @@ -435,8 +443,6 @@ public void updateBottomBar(MainFragment mainFragment) { .getBottomBar() .updatePath( mainFragment.getCurrentPath(), - mainFragment.getMainFragmentViewModel().getResults(), - MainActivityHelper.SEARCH_TEXT, mainFragment.getMainFragmentViewModel().getOpenMode(), mainFragment.getMainFragmentViewModel().getFolderCount(), mainFragment.getMainFragmentViewModel().getFileCount(), diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/data/MainFragmentViewModel.kt b/app/src/main/java/com/amaze/filemanager/ui/fragments/data/MainFragmentViewModel.kt index 71d80bafff..b99139943d 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/data/MainFragmentViewModel.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/data/MainFragmentViewModel.kt @@ -66,8 +66,6 @@ class MainFragmentViewModel : ViewModel() { var home: String? = null - var results: Boolean = false - lateinit var openMode: OpenMode // defines the current visible tab, default either 0 or 1 @@ -88,10 +86,6 @@ class MainFragmentViewModel : ViewModel() { // defines the current visible tab, default either 0 or 1 // private int mCurrentTab; - /*boolean identifying if the search task should be re-run on back press after pressing on - any of the search result*/ - var retainSearchTask = false - /** boolean to identify if the view is a list or grid */ var isList = true var addHeader = false 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 961fee9c54..5a02a269b2 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 @@ -82,6 +82,8 @@ object PreferencesConstants { 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" + const val PREFERENCE_REGEX = "regex" + const val PREFERENCE_REGEX_MATCHES = "matches" // security_prefs.xml const val PREFERENCE_CRYPT_FINGERPRINT = "crypt_fingerprint" diff --git a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/AppBar.java b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/AppBar.java index b090d0df6a..12c17004d2 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/AppBar.java +++ b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/AppBar.java @@ -50,10 +50,9 @@ public class AppBar { private AppBarLayout appbarLayout; - public AppBar( - MainActivity a, SharedPreferences sharedPref, SearchView.SearchListener searchListener) { + public AppBar(MainActivity a, SharedPreferences sharedPref) { toolbar = a.findViewById(R.id.action_bar); - searchView = new SearchView(this, a, searchListener); + searchView = new SearchView(this, a); bottomBar = new BottomBar(this, a); appbarLayout = a.findViewById(R.id.lin); 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 0a72ee7ade..fe127201db 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 @@ -199,8 +199,7 @@ public void onLongPress(MotionEvent e) { final MainFragment mainFragment = mainActivity.getCurrentMainFragment(); Objects.requireNonNull(mainFragment); if (mainActivity.getBoolean(PREFERENCE_CHANGEPATHS) - && ((mainFragment.getMainFragmentViewModel() != null - && !mainFragment.getMainFragmentViewModel().getResults()) + && (mainFragment.getMainFragmentViewModel() != null || buttons.getVisibility() == View.VISIBLE)) { GeneralDialogCreation.showChangePathsDialog( mainActivity, mainActivity.getPrefs()); @@ -363,8 +362,6 @@ public void setVisibility(int visibility) { public void updatePath( @NonNull final String news, - boolean results, - String query, OpenMode openmode, int folderCount, int fileCount, @@ -397,13 +394,7 @@ public void updatePath( newPath = news; } - if (!results) { - pathText.setText(mainActivity.getString(R.string.folderfilecount, folderCount, fileCount)); - } else { - fullPathText.setText(mainActivity.getString(R.string.search_results, query)); - pathText.setText(""); - return; - } + pathText.setText(mainActivity.getString(R.string.folderfilecount, folderCount, fileCount)); final String oldPath = fullPathText.getText().toString(); diff --git a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java index 25f305d007..126779a069 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java +++ b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java @@ -26,17 +26,19 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.concurrent.CancellationException; import com.afollestad.materialdialogs.MaterialDialog; import com.amaze.filemanager.R; import com.amaze.filemanager.adapters.SearchRecyclerViewAdapter; -import com.amaze.filemanager.filesystem.HybridFileParcelable; -import com.amaze.filemanager.filesystem.files.FileListSorter; +import com.amaze.filemanager.asynchronous.asynctasks.searchfilesystem.SearchResult; +import com.amaze.filemanager.asynchronous.asynctasks.searchfilesystem.SearchResultListSorter; import com.amaze.filemanager.filesystem.files.sort.DirSortBy; import com.amaze.filemanager.filesystem.files.sort.SortBy; import com.amaze.filemanager.filesystem.files.sort.SortOrder; import com.amaze.filemanager.filesystem.files.sort.SortType; import com.amaze.filemanager.ui.activities.MainActivity; +import com.amaze.filemanager.ui.activities.MainActivityViewModel; import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants; import com.amaze.filemanager.ui.theme.AppTheme; import com.amaze.filemanager.utils.Utils; @@ -73,9 +75,12 @@ import androidx.core.content.ContextCompat; import androidx.core.content.res.ResourcesCompat; import androidx.core.widget.NestedScrollView; +import androidx.lifecycle.LiveData; import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.RecyclerView; +import kotlinx.coroutines.Job; + /** * SearchView, a simple view to search * @@ -127,7 +132,7 @@ public class SearchView { @SuppressWarnings("ConstantConditions") @SuppressLint("NotifyDataSetChanged") - public SearchView(final AppBar appbar, MainActivity mainActivity, SearchListener searchListener) { + public SearchView(final AppBar appbar, MainActivity mainActivity) { this.mainActivity = mainActivity; this.appbar = appbar; @@ -163,6 +168,9 @@ public SearchView(final AppBar appbar, MainActivity mainActivity, SearchListener clearImageView.setOnClickListener( v -> { + // observers of last search are removed to stop updating the results + cancelLastSearch(); + searchViewEditText.setText(""); clearRecyclerView(); }); @@ -202,6 +210,8 @@ public void afterTextChanged(Editable s) {} v -> { String s = getSearchTerm(); + cancelLastSearch(); + if (searchMode == 1) { saveRecentPreference(s); @@ -223,8 +233,13 @@ public void afterTextChanged(Editable s) {} } else if (searchMode == 2) { - searchListener.onSearch(s); - appbar.getSearchView().hideSearchView(); + mainActivity + .getCurrentMainFragment() + .getMainActivityViewModel() + .deepSearch(mainActivity, s) + .observe( + mainActivity.getCurrentMainFragment().getViewLifecycleOwner(), + hybridFileParcelables -> updateResultList(hybridFileParcelables, s)); deepSearchTV.setVisibility(View.GONE); } @@ -356,9 +371,10 @@ private void resetSearchMode() { * @param newResults The list of results that should be displayed * @param searchTerm The search term that resulted in the search results */ - private void updateResultList(List newResults, String searchTerm) { - ArrayList items = new ArrayList<>(newResults); - Collections.sort(items, new FileListSorter(DirSortBy.NONE_ON_TOP, sortType, searchTerm)); + private void updateResultList(List newResults, String searchTerm) { + ArrayList items = new ArrayList<>(newResults); + Collections.sort( + items, new SearchResultListSorter(DirSortBy.NONE_ON_TOP, sortType, searchTerm)); searchRecyclerViewAdapter.submitList(items); searchRecyclerViewAdapter.notifyDataSetChanged(); } @@ -397,6 +413,7 @@ public void revealSearchView() { } mainActivity.showSmokeScreen(); + mainActivity.hideFab(); animator.setInterpolator(new AccelerateDecelerateInterpolator()); animator.setDuration(600); @@ -459,7 +476,9 @@ private void onSortTypeSelected(MaterialDialog dialog, int index, SortOrder sort this.sortType = new SortType(SortBy.getSortBy(index), sortOrder); dialog.dismiss(); updateSearchResultsSortButtonDisplay(); - updateResultList(searchRecyclerViewAdapter.getCurrentList(), getSearchTerm()); + LiveData> lastSearchLiveData = + mainActivity.getCurrentMainFragment().getMainActivityViewModel().getLastSearchLiveData(); + updateResultList(lastSearchLiveData.getValue(), getSearchTerm()); } private void resetSearchResultsSortButton() { @@ -526,6 +545,7 @@ public void hideSearchView() { // removing background fade view mainActivity.hideSmokeScreen(); + mainActivity.showFab(); animator.setInterpolator(new AccelerateDecelerateInterpolator()); animator.setDuration(600); animator.start(); @@ -594,6 +614,8 @@ private void clearRecyclerView() { searchRecyclerViewAdapter.submitList(new ArrayList<>()); searchRecyclerViewAdapter.notifyDataSetChanged(); + deepSearchTV.setVisibility(View.GONE); + searchResultsHintTV.setVisibility(View.GONE); searchResultsSortHintTV.setVisibility(View.GONE); searchResultsSortButton.setVisibility(View.GONE); @@ -626,7 +648,19 @@ private String getSearchTerm() { return searchViewEditText.getText().toString().trim(); } - public interface SearchListener { - void onSearch(String queue); + private void cancelLastSearch() { + MainActivityViewModel viewModel = + mainActivity.getCurrentMainFragment().getMainActivityViewModel(); + + // remove all observers + viewModel + .getLastSearchLiveData() + .removeObservers(mainActivity.getCurrentMainFragment().getViewLifecycleOwner()); + + // stop the job + Job lastJob = viewModel.getLastSearchJob(); + if (lastJob != null) { + lastJob.cancel(new CancellationException("Search outdated")); + } } } 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 44b8b85794..2f1f604bc8 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 @@ -128,7 +128,7 @@ public class Drawer implements NavigationView.OnNavigationItemSelectedListener { 0; // number of storage available (internal/external/otg etc) private boolean isDrawerLocked = false; private FragmentTransaction pending_fragmentTransaction; - private String pendingPath; + private PendingPath pendingPath; private String firstPath = null, secondPath = null; private DrawerLayout mDrawerLayout; @@ -308,7 +308,7 @@ public void refreshDrawer() { STORAGES_GROUP, order++, "OTG", - new MenuMetadata(file), + new MenuMetadata(file, false), R.drawable.ic_usb_white_24dp, R.drawable.ic_show_chart_black_24dp, Formatter.formatFileSize(mainActivity, freeSpace), @@ -322,7 +322,7 @@ public void refreshDrawer() { STORAGES_GROUP, order++, name, - new MenuMetadata(file), + new MenuMetadata(file, false), icon, R.drawable.ic_show_chart_black_24dp, Formatter.formatFileSize(mainActivity, freeSpace), @@ -344,7 +344,7 @@ public void refreshDrawer() { SERVERS_GROUP, order++, file[0], - new MenuMetadata(file[1]), + new MenuMetadata(file[1], false), R.drawable.ic_settings_remote_white_24dp, R.drawable.ic_edit_24dp); } @@ -363,7 +363,7 @@ public void refreshDrawer() { CLOUDS_GROUP, order++, CloudHandler.CLOUD_NAME_DROPBOX, - new MenuMetadata(CloudHandler.CLOUD_PREFIX_DROPBOX + "/"), + new MenuMetadata(CloudHandler.CLOUD_PREFIX_DROPBOX + "/", false), R.drawable.ic_dropbox_white_24dp, deleteIcon); @@ -377,7 +377,7 @@ public void refreshDrawer() { CLOUDS_GROUP, order++, CloudHandler.CLOUD_NAME_BOX, - new MenuMetadata(CloudHandler.CLOUD_PREFIX_BOX + "/"), + new MenuMetadata(CloudHandler.CLOUD_PREFIX_BOX + "/", false), R.drawable.ic_box_white_24dp, deleteIcon); @@ -391,7 +391,7 @@ public void refreshDrawer() { CLOUDS_GROUP, order++, CloudHandler.CLOUD_NAME_ONE_DRIVE, - new MenuMetadata(CloudHandler.CLOUD_PREFIX_ONE_DRIVE + "/"), + new MenuMetadata(CloudHandler.CLOUD_PREFIX_ONE_DRIVE + "/", false), R.drawable.ic_onedrive_white_24dp, deleteIcon); @@ -405,7 +405,7 @@ public void refreshDrawer() { CLOUDS_GROUP, order++, CloudHandler.CLOUD_NAME_GOOGLE_DRIVE, - new MenuMetadata(CloudHandler.CLOUD_PREFIX_GOOGLE_DRIVE + "/"), + new MenuMetadata(CloudHandler.CLOUD_PREFIX_GOOGLE_DRIVE + "/", false), R.drawable.ic_google_drive_white_24dp, deleteIcon); @@ -430,7 +430,7 @@ public void refreshDrawer() { FOLDERS_GROUP, order++, file[0], - new MenuMetadata(file[1]), + new MenuMetadata(file[1], false), R.drawable.ic_folder_white_24dp, R.drawable.ic_edit_24dp); } @@ -451,7 +451,7 @@ public void refreshDrawer() { QUICKACCESSES_GROUP, order++, R.string.quick, - new MenuMetadata("5"), + new MenuMetadata("5", true), R.drawable.ic_star_white_24dp, null); } @@ -461,7 +461,7 @@ public void refreshDrawer() { QUICKACCESSES_GROUP, order++, R.string.recent, - new MenuMetadata("6"), + new MenuMetadata("6", true), R.drawable.ic_history_white_24dp, null); } @@ -471,7 +471,7 @@ public void refreshDrawer() { QUICKACCESSES_GROUP, order++, R.string.images, - new MenuMetadata("0"), + new MenuMetadata("0", true), R.drawable.ic_photo_library_white_24dp, null); } @@ -481,7 +481,7 @@ public void refreshDrawer() { QUICKACCESSES_GROUP, order++, R.string.videos, - new MenuMetadata("1"), + new MenuMetadata("1", true), R.drawable.ic_video_library_white_24dp, null); } @@ -491,7 +491,7 @@ public void refreshDrawer() { QUICKACCESSES_GROUP, order++, R.string.audio, - new MenuMetadata("2"), + new MenuMetadata("2", true), R.drawable.ic_library_music_white_24dp, null); } @@ -501,7 +501,7 @@ public void refreshDrawer() { QUICKACCESSES_GROUP, order++, R.string.documents, - new MenuMetadata("3"), + new MenuMetadata("3", true), R.drawable.ic_library_books_white_24dp, null); } @@ -511,7 +511,7 @@ public void refreshDrawer() { QUICKACCESSES_GROUP, order++, R.string.apks, - new MenuMetadata("4"), + new MenuMetadata("4", true), R.drawable.ic_apk_library_white_24dp, null); } @@ -596,7 +596,7 @@ public void refreshDrawer() { LASTGROUP, order++, R.string.trash_bin, - new MenuMetadata("7"), + new MenuMetadata("7", true), R.drawable.round_delete_outline_24, null); @@ -778,20 +778,22 @@ public void onDrawerClosed() { } if (pendingPath != null) { - HybridFile hFile = new HybridFile(OpenMode.UNKNOWN, pendingPath); + HybridFile hFile = new HybridFile(OpenMode.UNKNOWN, pendingPath.getPath()); hFile.generateMode(mainActivity); if (hFile.isSimpleFile()) { - FileUtils.openFile(new File(pendingPath), mainActivity, mainActivity.getPrefs()); + FileUtils.openFile(new File(pendingPath.getPath()), mainActivity, mainActivity.getPrefs()); resetPendingPath(); return; } MainFragment mainFragment = mainActivity.getCurrentMainFragment(); if (mainFragment != null) { - mainFragment.loadlist(pendingPath, false, OpenMode.UNKNOWN, false); + mainFragment.loadlist(pendingPath.getPath(), false, OpenMode.UNKNOWN, false); + // Set if the FAB should be hidden when displaying the pendingPath + mainFragment.setHideFab(pendingPath.getHideFabInMainFragment()); resetPendingPath(); } else { - mainActivity.goToMain(pendingPath); + mainActivity.goToMain(pendingPath.getPath(), pendingPath.getHideFabInMainFragment()); resetPendingPath(); return; } @@ -845,7 +847,7 @@ public boolean onNavigationItemSelected(@NonNull MenuItem item) { }); dialog.show(); } else { - pendingPath = meta.path; + pendingPath = new PendingPath(meta.path, meta.hideFabInMainFragment); closeIfNotLocked(); if (isLocked()) { onDrawerClosed(); diff --git a/app/src/main/java/com/amaze/filemanager/ui/views/drawer/MenuMetadata.java b/app/src/main/java/com/amaze/filemanager/ui/views/drawer/MenuMetadata.java index af2b98b8da..bfcc7ee4e4 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/views/drawer/MenuMetadata.java +++ b/app/src/main/java/com/amaze/filemanager/ui/views/drawer/MenuMetadata.java @@ -26,17 +26,20 @@ public final class MenuMetadata { public final int type; public final String path; + public final boolean hideFabInMainFragment; public final OnClickListener onClickListener; - public MenuMetadata(String path) { + public MenuMetadata(String path, boolean hideFabInMainFragment) { this.type = ITEM_ENTRY; this.path = path; + this.hideFabInMainFragment = hideFabInMainFragment; this.onClickListener = null; } public MenuMetadata(OnClickListener onClickListener) { this.type = ITEM_INTENT; this.onClickListener = onClickListener; + this.hideFabInMainFragment = false; this.path = null; } diff --git a/app/src/main/java/com/amaze/filemanager/ui/views/drawer/PendingPath.kt b/app/src/main/java/com/amaze/filemanager/ui/views/drawer/PendingPath.kt new file mode 100644 index 0000000000..ec07212886 --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/ui/views/drawer/PendingPath.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2014-2024 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.ui.views.drawer + +data class PendingPath(val path: String, val hideFabInMainFragment: Boolean) 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 4cadc75af0..2c50641a14 100644 --- a/app/src/main/java/com/amaze/filemanager/utils/MainActivityActionMode.kt +++ b/app/src/main/java/com/amaze/filemanager/utils/MainActivityActionMode.kt @@ -151,48 +151,26 @@ class MainActivityActionMode(private val mainActivityReference: WeakReference - if (!mainFragmentViewModel.results) { - adapter.toggleChecked(false, mainFragmentViewModel.currentPath) - } else adapter.toggleChecked(false) + adapter.toggleChecked(false, mainFragmentViewModel.currentPath) mainActivity .updateViews( ColorDrawable( 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 5a5703da2a..110002bc82 100644 --- a/app/src/main/java/com/amaze/filemanager/utils/MainActivityHelper.java +++ b/app/src/main/java/com/amaze/filemanager/utils/MainActivityHelper.java @@ -62,8 +62,6 @@ import com.amaze.filemanager.ui.activities.MainActivity; import com.amaze.filemanager.ui.dialogs.GeneralDialogCreation; import com.amaze.filemanager.ui.fragments.MainFragment; -import com.amaze.filemanager.ui.fragments.SearchWorkerFragment; -import com.amaze.filemanager.ui.fragments.TabFragment; import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants; import com.amaze.filemanager.ui.views.WarnableTextInputValidator; import com.amaze.filemanager.utils.smb.SmbUtil; @@ -75,10 +73,7 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; -import android.os.AsyncTask; import android.os.Build; -import android.os.Bundle; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.widget.Toast; @@ -89,8 +84,6 @@ import androidx.appcompat.widget.AppCompatImageView; import androidx.appcompat.widget.AppCompatTextView; import androidx.documentfile.provider.DocumentFile; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; import androidx.preference.PreferenceManager; public class MainActivityHelper { @@ -102,12 +95,6 @@ public class MainActivityHelper { private int accentColor; private SpeedDialView.OnActionSelectedListener fabActionListener; - /* - * A static string which saves the last searched query. Used to retain search task after - * user presses back button from pressing on any list item of search results - */ - public static String SEARCH_TEXT; - public MainActivityHelper(MainActivity mainActivity) { this.mainActivity = mainActivity; accentColor = mainActivity.getAccent(); @@ -751,81 +738,4 @@ public String parseCloudPath(OpenMode serviceType, String path) { return path; } } - - /** - * Creates a fragment which will handle the search AsyncTask {@link SearchWorkerFragment} - * - * @param query the text query entered the by user - */ - public void search(SharedPreferences sharedPrefs, String query) { - TabFragment tabFragment = mainActivity.getTabFragment(); - if (tabFragment == null) { - Log.w(getClass().getSimpleName(), "Failed to search: tab fragment not available"); - return; - } - final MainFragment ma = (MainFragment) tabFragment.getCurrentTabFragment(); - if (ma == null || ma.getMainFragmentViewModel() == null) { - Log.w(getClass().getSimpleName(), "Failed to search: main fragment not available"); - return; - } - final String fpath = ma.getCurrentPath(); - - /*SearchTask task = new SearchTask(ma.searchHelper, ma, query); - task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, fpath);*/ - // ma.searchTask = task; - SEARCH_TEXT = query; - FragmentManager fm = mainActivity.getSupportFragmentManager(); - SearchWorkerFragment fragment = - (SearchWorkerFragment) fm.findFragmentByTag(MainActivity.TAG_ASYNC_HELPER); - - if (fragment != null) { - if (fragment.searchAsyncTask.getStatus() == AsyncTask.Status.RUNNING) { - fragment.searchAsyncTask.cancel(true); - } - fm.beginTransaction().remove(fragment).commit(); - } - - addSearchFragment( - fm, - new SearchWorkerFragment(), - fpath, - query, - ma.getMainFragmentViewModel().getOpenMode(), - mainActivity.isRootExplorer(), - sharedPrefs.getBoolean(SearchWorkerFragment.KEY_REGEX, false), - sharedPrefs.getBoolean(SearchWorkerFragment.KEY_REGEX_MATCHES, false)); - } - - /** - * Adds a search fragment that can persist it's state on config change - * - * @param fragmentManager fragmentManager - * @param fragment current fragment - * @param path current path - * @param input query typed by user - * @param openMode defines the file type - * @param rootMode is root enabled - * @param regex is regular expression search enabled - * @param matches is matches enabled for patter matching - */ - public static void addSearchFragment( - @NonNull FragmentManager fragmentManager, - @NonNull Fragment fragment, - @NonNull String path, - @NonNull String input, - @NonNull OpenMode openMode, - boolean rootMode, - boolean regex, - boolean matches) { - Bundle args = new Bundle(); - args.putString(SearchWorkerFragment.KEY_INPUT, input); - args.putString(SearchWorkerFragment.KEY_PATH, path); - args.putInt(SearchWorkerFragment.KEY_OPEN_MODE, openMode.ordinal()); - args.putBoolean(SearchWorkerFragment.KEY_ROOT_MODE, rootMode); - args.putBoolean(SearchWorkerFragment.KEY_REGEX, regex); - args.putBoolean(SearchWorkerFragment.KEY_REGEX_MATCHES, matches); - - fragment.setArguments(args); - fragmentManager.beginTransaction().add(fragment, MainActivity.TAG_ASYNC_HELPER).commit(); - } } diff --git a/app/src/main/java/com/amaze/filemanager/utils/OnFileFound.kt b/app/src/main/java/com/amaze/filemanager/utils/OnFileFound.kt index f593791338..4c4b68fabf 100644 --- a/app/src/main/java/com/amaze/filemanager/utils/OnFileFound.kt +++ b/app/src/main/java/com/amaze/filemanager/utils/OnFileFound.kt @@ -27,7 +27,7 @@ import com.amaze.filemanager.filesystem.HybridFileParcelable * * @author Emmanuel on 21/9/2017, at 15:23. */ -interface OnFileFound { +fun interface OnFileFound { @Suppress("UndocumentedPublicFunction") fun onFileFound(file: HybridFileParcelable) } diff --git a/app/src/main/java/com/amaze/filemanager/utils/Utils.java b/app/src/main/java/com/amaze/filemanager/utils/Utils.java index 0c5d642b35..bc50b16de3 100644 --- a/app/src/main/java/com/amaze/filemanager/utils/Utils.java +++ b/app/src/main/java/com/amaze/filemanager/utils/Utils.java @@ -89,6 +89,7 @@ public class Utils { private static final String DATE_TIME_FORMAT = "%s | %s"; private static final String EMAIL_EMMANUEL = "emmanuelbendavid@gmail.com"; private static final String EMAIL_RAYMOND = "airwave209gt@gmail.com"; + private static final String EMAIL_VISHNU = "t.v.s10123@gmail.com"; private static final String EMAIL_VISHAL = "vishalmeham2@gmail.com"; private static final String URL_TELEGRAM = "https://t.me/AmazeFileManager"; private static final String URL_INSTGRAM = "https://www.instagram.com/teamamaze.xyz/"; @@ -426,7 +427,7 @@ public static void openInstagramURL(Context context) { public static Intent buildEmailIntent(Context context, String text, String supportMail) { Intent emailIntent = new Intent(Intent.ACTION_SEND); String[] aEmailList = {supportMail}; - String[] aEmailCCList = {EMAIL_VISHAL, EMAIL_EMMANUEL, EMAIL_RAYMOND}; + String[] aEmailCCList = {EMAIL_VISHAL, EMAIL_EMMANUEL, EMAIL_RAYMOND, EMAIL_VISHNU}; emailIntent.putExtra(Intent.EXTRA_EMAIL, aEmailList); emailIntent.putExtra(Intent.EXTRA_CC, aEmailCCList); emailIntent.putExtra( diff --git a/app/src/main/res/layout/activity_about.xml b/app/src/main/res/layout/activity_about.xml index 495b763454..eb593f9e3e 100644 --- a/app/src/main/res/layout/activity_about.xml +++ b/app/src/main/res/layout/activity_about.xml @@ -441,6 +441,56 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/translators.xml b/app/src/main/res/values/translators.xml index e83a07f750..2d22fd3812 100644 --- a/app/src/main/res/values/translators.xml +++ b/app/src/main/res/values/translators.xml @@ -47,6 +47,7 @@ Vishal Nehra Emmanuel Messulam Raymond Lai + Vishnu Sanal T Team MoKee CookIcons.co diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/BasicSearchTest.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/BasicSearchTest.kt new file mode 100644 index 0000000000..5a5702e3fa --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/BasicSearchTest.kt @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2014-2024 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.asynchronous.asynctasks.searchfilesystem + +import android.content.Context +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.amaze.filemanager.filesystem.HybridFile +import com.amaze.filemanager.filesystem.HybridFileParcelable +import com.amaze.filemanager.filesystem.root.ListFilesCommand +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.mockkConstructor +import io.mockk.mockkObject +import io.mockk.unmockkAll +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.util.EnumSet + +class BasicSearchTest { + @get:Rule + val rule = InstantTaskExecutorRule() + + @MockK(relaxed = true, relaxUnitFun = true) + lateinit var context: Context + + @MockK(relaxed = true, relaxUnitFun = true) + lateinit var foundFileMock: HybridFileParcelable + + val filePath = "/test/abc.txt" + val fileName = "abc.txt" + + /** Set up all mocks */ + @Before + fun setup() { + MockKAnnotations.init(this, relaxUnitFun = true) + every { context.applicationContext } returns context + + mockkConstructor(HybridFile::class) + every { anyConstructed().isDirectory(any()) } returns true + + mockkObject(ListFilesCommand) + every { ListFilesCommand.listFiles(any(), any(), any(), any(), any()) } answers { + val onFileFoundCallback = it.invocation.args.last() as (HybridFileParcelable) -> Unit + onFileFoundCallback(foundFileMock) + } + + every { foundFileMock.isDirectory(any()) } returns false + every { foundFileMock.isHidden } returns true + every { foundFileMock.path } returns filePath + every { foundFileMock.getName(any()) } returns fileName + } + + /** Clean up all mocks */ + @After + fun cleanup() { + unmockkAll() + } + + /** + * If the file name matches the query, the file should be added to + * [FileSearch.foundFilesLiveData] + */ + @Test + fun testSimpleSearchMatch() { + val basicSearch = BasicSearch( + "ab", + filePath, + EnumSet.of(SearchParameter.SHOW_HIDDEN_FILES), + context + ) + + val expectedMatchRanges = listOf(0..1) + + basicSearch.foundFilesLiveData.observeForever { actualResults -> + Assert.assertNotNull(actualResults) + Assert.assertEquals(listOf(foundFileMock), actualResults.map { it.file }) + Assert.assertEquals(expectedMatchRanges, actualResults.map { it.matchRange }) + } + + runTest { + basicSearch.search() + } + } + + /** + * If the file name does not match the query, the file should not be added to + * [FileSearch.foundFilesLiveData] + */ + @Test + fun testSimpleSearchNotMatch() { + val basicSearch = BasicSearch( + "ba", + filePath, + EnumSet.of(SearchParameter.SHOW_HIDDEN_FILES), + context + ) + + basicSearch.foundFilesLiveData.observeForever { actualResults -> + Assert.assertNotNull(actualResults) + Assert.assertTrue( + listNotEmptyError(actualResults.size), + actualResults.isEmpty() + ) + } + + runTest { + basicSearch.search() + } + } + + /** + * If the match is in the path but not in the name, it should not be added to + * [FileSearch.foundFilesLiveData] + */ + @Test + fun testSearchWithPathMatchButNameNotMatch() { + val basicSearch = BasicSearch( + "test", + filePath, + EnumSet.of(SearchParameter.SHOW_HIDDEN_FILES), + context + ) + + basicSearch.foundFilesLiveData.observeForever { actualResults -> + Assert.assertNotNull(actualResults) + Assert.assertTrue( + listNotEmptyError(actualResults.size), + actualResults.isEmpty() + ) + } + + runTest { + basicSearch.search() + } + } + + /** + * If a file is hidden and hidden files should not be shown, it should not be added to + * [FileSearch.foundFilesLiveData] + */ + @Test + fun testMatchHiddenFile() { + val basicSearch = BasicSearch( + "ab", + filePath, + EnumSet.noneOf(SearchParameter::class.java), + context + ) + + basicSearch.foundFilesLiveData.observeForever { actualResults -> + Assert.assertNotNull(actualResults) + Assert.assertTrue( + listNotEmptyError(actualResults.size), + actualResults.isEmpty() + ) + } + + runTest { + basicSearch.search() + } + } + + private fun listNotEmptyError(size: Int) = + "List was not empty as expected but had $size elements" +} diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/DeepSearchTest.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/DeepSearchTest.kt new file mode 100644 index 0000000000..cf26588e63 --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/DeepSearchTest.kt @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2014-2024 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.asynchronous.asynctasks.searchfilesystem + +import android.content.Context +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.amaze.filemanager.fileoperations.filesystem.OpenMode +import com.amaze.filemanager.filesystem.HybridFile +import com.amaze.filemanager.filesystem.HybridFileParcelable +import com.amaze.filemanager.filesystem.root.ListFilesCommand +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.mockkConstructor +import io.mockk.mockkObject +import io.mockk.unmockkAll +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.util.EnumSet + +class DeepSearchTest { + @get:Rule + val rule = InstantTaskExecutorRule() + + @MockK(relaxed = true, relaxUnitFun = true) + lateinit var context: Context + + @MockK(relaxed = true, relaxUnitFun = true) + lateinit var foundFileMock: HybridFileParcelable + + val filePath = "/test/abc.txt" + val fileName = "abc.txt" + + /** Set up all mocks */ + @Before + fun setup() { + MockKAnnotations.init(this, relaxUnitFun = true) + every { context.applicationContext } returns context + + mockkConstructor(HybridFile::class) + every { anyConstructed().isDirectory(any()) } returns true + + mockkObject(ListFilesCommand) + every { ListFilesCommand.listFiles(any(), any(), any(), any(), any()) } answers { + val onFileFoundCallback = lastArg<(HybridFileParcelable) -> Unit>() + onFileFoundCallback(foundFileMock) + } + + every { foundFileMock.isDirectory(any()) } returns false + every { foundFileMock.isHidden } returns true + every { foundFileMock.path } returns filePath + every { foundFileMock.getName(any()) } returns fileName + } + + /** Clean up all mocks */ + @After + fun cleanup() { + unmockkAll() + } + + /** + * If the file name matches the query, the file should be added to + * [FileSearch.foundFilesLiveData] + */ + @Test + fun testSimpleSearchMatch() { + val deepSearch = DeepSearch( + "ab", + filePath, + EnumSet.of(SearchParameter.SHOW_HIDDEN_FILES), + context, + OpenMode.FILE + ) + + val expectedMatchRanges = listOf(0..1) + + deepSearch.foundFilesLiveData.observeForever { actualResults -> + Assert.assertNotNull(actualResults) + Assert.assertEquals(listOf(foundFileMock), actualResults.map { it.file }) + Assert.assertEquals(expectedMatchRanges, actualResults.map { it.matchRange }) + } + + runTest { + deepSearch.search() + } + } + + /** + * If the file name does not match the query, the file should not be added to + * [FileSearch.foundFilesLiveData] + */ + @Test + fun testSimpleSearchNotMatch() { + val deepSearch = DeepSearch( + "ba", + filePath, + EnumSet.of(SearchParameter.SHOW_HIDDEN_FILES), + context, + OpenMode.FILE + ) + + deepSearch.foundFilesLiveData.observeForever { actualResults -> + Assert.assertNotNull(actualResults) + Assert.assertTrue( + listNotEmptyError(actualResults.size), + actualResults.isEmpty() + ) + } + + runTest { + deepSearch.search() + } + } + + /** + * If the match is in the path but not in the name, it should not be added to + * [FileSearch.foundFilesLiveData] + */ + @Test + fun testSearchWithPathMatchButNameNotMatch() { + val deepSearch = DeepSearch( + "test", + filePath, + EnumSet.of(SearchParameter.SHOW_HIDDEN_FILES), + context, + OpenMode.FILE + ) + + deepSearch.foundFilesLiveData.observeForever { actualResults -> + Assert.assertNotNull(actualResults) + Assert.assertTrue( + listNotEmptyError(actualResults.size), + actualResults.isEmpty() + ) + } + + runTest { + deepSearch.search() + } + } + + /** + * If a file is hidden and hidden files should not be shown, it should not be added to + * [FileSearch.foundFilesLiveData] + */ + @Test + fun testMatchHiddenFile() { + val basicSearch = DeepSearch( + "ab", + filePath, + EnumSet.noneOf(SearchParameter::class.java), + context, + OpenMode.FILE + ) + + basicSearch.foundFilesLiveData.observeForever { actualResults -> + Assert.assertNotNull(actualResults) + Assert.assertTrue( + listNotEmptyError(actualResults.size), + actualResults.isEmpty() + ) + } + + runTest { + basicSearch.search() + } + } + + private fun listNotEmptyError(size: Int) = + "List was not empty as expected but had $size elements" +} diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/FileSearchTest.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/FileSearchTest.kt new file mode 100644 index 0000000000..57a8f61556 --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/FileSearchTest.kt @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2014-2024 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.asynchronous.asynctasks.searchfilesystem + +import kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Test +import java.util.EnumSet + +class FileSearchTest { + private fun getFileSearchMatch( + query: String, + path: String, + searchParameters: SearchParameters + ): FileSearch = object : FileSearch(query, path, searchParameters) { + override suspend fun search( + filter: SearchFilter + ) { + val matchRange = filter.searchFilter(path) + Assert.assertNotNull("Expected $path to match filter", matchRange) + Assert.assertTrue("Start of match range is negative", matchRange!!.first >= 0) + Assert.assertTrue( + "End of match range is larger than length of $path", + matchRange.last < path.length + ) + val expectedRange = 5..9 + Assert.assertEquals( + "Range was not as expected $expectedRange but $matchRange", + expectedRange, + matchRange + ) + } + } + + private fun getFileSearchNotMatch( + query: String, + path: String, + searchParameters: SearchParameters + ): FileSearch = + object : FileSearch(query, path, searchParameters) { + override suspend fun search(filter: SearchFilter) { + val matchRange = filter.searchFilter(path) + Assert.assertNull("Expected $path to not match filter", matchRange) + } + } + + private fun getFileSearchRegexMatches( + query: String, + path: String, + searchParameters: SearchParameters + ): FileSearch = object : FileSearch(query, path, searchParameters) { + override suspend fun search( + filter: SearchFilter + ) { + val matchRange = filter.searchFilter(path) + Assert.assertNotNull("Expected $path to match filter", matchRange) + Assert.assertTrue("Start of match range is negative", matchRange!!.first >= 0) + Assert.assertTrue( + "End of match range is larger than length of $path", + matchRange.last < path.length + ) + val expectedRange = path.indices + Assert.assertEquals( + "Range was not as expected $expectedRange but $matchRange", + expectedRange, + matchRange + ) + } + } + + /** Test the simple filter with a path that matches the query */ + @Test + fun simpleFilterMatchTest() = runTest { + getFileSearchMatch( + "abcde", + "01234ABcDe012", + EnumSet.noneOf(SearchParameter::class.java) + ).search() + } + + /** Test the simple filter with a path that does not match the query */ + @Test + fun simpleFilterNotMatchTest() = runTest { + // There is no "e" + getFileSearchNotMatch( + "abcde", + "01234abcd9012", + EnumSet.noneOf(SearchParameter::class.java) + ).search() + } + + /** Test the regex filter with a path that matches the query. The query contains `*`. */ + @Test + fun regexFilterStarMatchTest() = runTest { + getFileSearchMatch( + "a*e", + "01234ABcDe012", + SearchParameters.of(SearchParameter.REGEX) + ).search() + } + + /** Test the regex filter with a path that does not match the query. The query contains `*`. */ + @Test + fun regexFilterStarNotMatchTest() = runTest { + // There is no "e" + getFileSearchNotMatch( + "a*e", + "01234aBcD9012", + SearchParameters.of(SearchParameter.REGEX) + ).search() + } + + /** Test the regex filter with a path that matches the query. The query contains `?`. */ + @Test + fun regexFilterQuestionMarkMatchTest() = runTest { + getFileSearchMatch( + "a???e", + "01234ABcDe0123", + SearchParameters.of(SearchParameter.REGEX) + ).search() + } + + /** Test the regex filter with a path that does not match the query. The query contains `?`. */ + @Test + fun regexFilterQuestionMarkNotMatchTest() = runTest { + // There is one character missing between "a" and "e" + getFileSearchNotMatch( + "a???e", + "01234ABce9012", + SearchParameters.of(SearchParameter.REGEX) + ).search() + } + + /** + * Test the regex filter with a path that does not match the query + * because `-` is not recognized by `?` or `*`. + */ + @Test + fun regexFilterNotMatchNonWordCharacterTest() = runTest { + getFileSearchNotMatch( + "a?c*e", + "0A-corn search", + SearchParameters.of(SearchParameter.REGEX) + ).search() + } + + /** Test the regex match filter with a path that completely matches the query */ + @Test + fun regexMatchFilterMatchTest() = runTest { + getFileSearchRegexMatches( + "a*e", + "A1234ABcDe0123e", + SearchParameter.REGEX + SearchParameter.REGEX_MATCHES + ).search() + } + + /** Test the regex match filter with a path that does not completely match the query */ + @Test + fun regexMatchFilterNotMatchTest() = runTest { + // Pattern does not match whole name + getFileSearchNotMatch( + "a*e", + "01234ABcDe0123", + SearchParameter.REGEX + SearchParameter.REGEX_MATCHES + ).search() + } +} diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/IndexedSearchTest.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/IndexedSearchTest.kt new file mode 100644 index 0000000000..6f78265f4c --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/IndexedSearchTest.kt @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2014-2024 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.asynchronous.asynctasks.searchfilesystem + +import android.database.Cursor +import android.provider.MediaStore +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.RelaxedMockK +import io.mockk.unmockkAll +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.util.EnumSet + +class IndexedSearchTest { + @get:Rule + val rule = InstantTaskExecutorRule() + + val dataColumn = 0 + val displayNameColumn = 1 + + @RelaxedMockK + lateinit var mockCursor: Cursor + + val filePath = "/test/abc.txt" + val fileName = "abc.txt" + + /** Set up all mocks */ + @Before + fun setup() { + MockKAnnotations.init(this) + every { mockCursor.count } returns 1 + every { mockCursor.moveToFirst() } returns true + every { mockCursor.moveToNext() } returns false + every { + mockCursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATA) + } returns dataColumn + every { + mockCursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DISPLAY_NAME) + } returns displayNameColumn + + every { mockCursor.getString(dataColumn) } returns filePath + every { mockCursor.getString(displayNameColumn) } returns fileName + } + + /** Clean up all mocks */ + @After + fun cleanup() { + unmockkAll() + } + + /** + * If the file name matches the query, the file should be added to + * [FileSearch.foundFilesLiveData] + */ + @Test + fun testSimpleSearchMatch() { + val expectedNames = listOf(fileName) + val expectedPaths = listOf(filePath) + val expectedRanges = listOf(0..1) + + val indexedSearch = IndexedSearch( + "ab", + "/", + EnumSet.noneOf(SearchParameter::class.java), + mockCursor + ) + indexedSearch.foundFilesLiveData.observeForever { actualResults -> + Assert.assertNotNull(actualResults) + Assert.assertEquals(expectedNames, actualResults!!.map { (file, _) -> file.name }) + Assert.assertEquals(expectedPaths, actualResults!!.map { (file, _) -> file.path }) + Assert.assertEquals(expectedRanges, actualResults.map { it.matchRange }) + } + runTest { + indexedSearch.search() + } + } + + /** + * If the file name does not match the query, the file should not be added to + * [FileSearch.foundFilesLiveData] + */ + @Test + fun testSimpleSearchNotMatch() { + val indexedSearch = IndexedSearch( + "ba", + "/", + EnumSet.noneOf(SearchParameter::class.java), + mockCursor + ) + indexedSearch.foundFilesLiveData.observeForever { actualResults -> + Assert.assertNotNull(actualResults) + Assert.assertTrue( + listNotEmptyError(actualResults.size), + actualResults.isEmpty() + ) + } + runTest { + indexedSearch.search() + } + } + + /** + * If the match is in the path but not in the name, it should not be added to + * [FileSearch.foundFilesLiveData] + */ + @Test + fun testSearchWithPathMatchButNameNotMatch() { + val indexedSearch = IndexedSearch( + "te", + "/", + EnumSet.noneOf(SearchParameter::class.java), + mockCursor + ) + indexedSearch.foundFilesLiveData.observeForever { actualResults -> + Assert.assertNotNull(actualResults) + Assert.assertTrue( + listNotEmptyError(actualResults.size), + actualResults.isEmpty() + ) + } + runTest { + indexedSearch.search() + } + } + + private fun listNotEmptyError(size: Int) = + "List was not empty as expected but had $size elements" +} diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/SearchParameterTest.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/SearchParameterTest.kt new file mode 100644 index 0000000000..4ae5630359 --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/SearchParameterTest.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2014-2024 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.asynchronous.asynctasks.searchfilesystem + +import org.junit.Assert +import org.junit.Test +import java.util.EnumSet + +class SearchParameterTest { + + /** Tests [SearchParameter.and] */ + @Test + fun testAnd() { + val expected = EnumSet.of(SearchParameter.ROOT, SearchParameter.REGEX_MATCHES) + val actual = SearchParameter.ROOT and SearchParameter.REGEX_MATCHES + Assert.assertEquals(expected, actual) + } + + /** Tests [SearchParameter.plus] */ + @Test + fun testPlus() { + val expected = EnumSet.of(SearchParameter.ROOT, SearchParameter.REGEX_MATCHES) + val actual = SearchParameter.ROOT + SearchParameter.REGEX_MATCHES + Assert.assertEquals(expected, actual) + } +} diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/SearchParametersTest.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/SearchParametersTest.kt new file mode 100644 index 0000000000..006669d43b --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/SearchParametersTest.kt @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2014-2024 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.asynchronous.asynctasks.searchfilesystem + +import org.junit.Assert +import org.junit.Test +import java.util.EnumSet + +class SearchParametersTest { + + /** Tests [SearchParameters.and] */ + @Test + fun testAnd() { + val expected = EnumSet.of(SearchParameter.SHOW_HIDDEN_FILES, SearchParameter.ROOT) + val actual = SearchParameter.SHOW_HIDDEN_FILES and SearchParameter.ROOT + Assert.assertEquals(expected, actual) + } + + /** Tests [SearchParameters.plus] */ + @Test + fun testPlus() { + val expected = EnumSet.of(SearchParameter.REGEX, SearchParameter.ROOT) + val actual = EnumSet.of(SearchParameter.REGEX) + SearchParameter.ROOT + Assert.assertEquals(expected, actual) + } + + /** Tests [searchParametersFromBoolean] with no flag turned on */ + @Test + fun testSearchParametersFromBooleanWithNone() { + val expected = EnumSet.noneOf(SearchParameter::class.java) + val actual = searchParametersFromBoolean() + Assert.assertEquals(expected, actual) + } + + /** Tests [searchParametersFromBoolean] with one flag turned on */ + @Test + fun testSearchParametersFromBooleanWithOne() { + val expected = EnumSet.of(SearchParameter.ROOT) + val actual = searchParametersFromBoolean(isRoot = true) + Assert.assertEquals(expected, actual) + } + + /** Tests [searchParametersFromBoolean] with two flags turned on */ + @Test + fun testSearchParametersFromBooleanWithTwo() { + val expected = EnumSet.of(SearchParameter.ROOT, SearchParameter.SHOW_HIDDEN_FILES) + val actual = searchParametersFromBoolean(isRoot = true, showHiddenFiles = true) + Assert.assertEquals(expected, actual) + } + + /** Tests [searchParametersFromBoolean] with three flags turned on */ + @Test + fun testSearchParametersFromBooleanWithThree() { + val expected = EnumSet.of( + SearchParameter.ROOT, + SearchParameter.REGEX, + SearchParameter.REGEX_MATCHES + ) + val actual = searchParametersFromBoolean( + isRoot = true, + isRegexEnabled = true, + isRegexMatchesEnabled = true + ) + Assert.assertEquals(expected, actual) + } + + /** Tests [searchParametersFromBoolean] with four flags turned on */ + @Test + fun testSearchParametersFromBooleanWithFour() { + val expected = EnumSet.of( + SearchParameter.ROOT, + SearchParameter.REGEX, + SearchParameter.REGEX_MATCHES, + SearchParameter.SHOW_HIDDEN_FILES + ) + val actual = searchParametersFromBoolean( + isRoot = true, + isRegexEnabled = true, + isRegexMatchesEnabled = true, + showHiddenFiles = true + ) + Assert.assertEquals(expected, actual) + } +} diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/SearchResultListSorterTest.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/SearchResultListSorterTest.kt new file mode 100644 index 0000000000..3ca332cd6f --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/SearchResultListSorterTest.kt @@ -0,0 +1,794 @@ +/* + * Copyright (C) 2014-2024 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.asynchronous.asynctasks.searchfilesystem + +import android.os.Build +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.amaze.filemanager.adapters.data.LayoutElementParcelable +import com.amaze.filemanager.fileoperations.filesystem.OpenMode +import com.amaze.filemanager.filesystem.files.sort.DirSortBy +import com.amaze.filemanager.filesystem.files.sort.SortBy +import com.amaze.filemanager.filesystem.files.sort.SortOrder +import com.amaze.filemanager.filesystem.files.sort.SortType +import com.amaze.filemanager.shadows.ShadowMultiDex +import org.junit.Assert +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config +import java.util.Date +import java.util.concurrent.TimeUnit +import java.util.regex.Pattern + +@RunWith(AndroidJUnit4::class) +@Config( + shadows = [ShadowMultiDex::class], + sdk = [Build.VERSION_CODES.KITKAT, Build.VERSION_CODES.P, Build.VERSION_CODES.R] +) +@Suppress("StringLiteralDuplication", "ComplexMethod", "LongMethod", "LargeClass") +class SearchResultListSorterTest { + + private fun getSimpleMatchRange(searchTerm: String, fileName: String): MatchRange { + val startIndex = fileName.lowercase().indexOf(searchTerm.lowercase()) + return startIndex..(startIndex + searchTerm.length) + } + + private fun getPatternMatchRange(pattern: Pattern, fileName: String): MatchRange { + val matcher = pattern.matcher(fileName) + matcher.find() + return matcher.start()..matcher.end() + } + + /** + * Purpose: when sort is [SortBy.RELEVANCE], if file1 matches the search term more than file2, result is positive + * + * Input: SearchResultListSorter with [DirSortBy.NONE_ON_TOP], [SortBy.RELEVANCE], [SortOrder.ASC] and search term "abc" + * compare(file1,file2) file1 title matches "abc" more than file2 title + * + * Expected: return negative integer + */ + @Test + fun testSortByRelevanceWithFile1MoreMatchThanFile2() { + val searchTerm = "abc" + val title1 = "abc.txt" + val matchRange1 = getSimpleMatchRange(searchTerm, title1) + val title2 = "ABCDE.txt" + val matchRange2 = getSimpleMatchRange(searchTerm, title2) + + val searchResultListSorter = SearchResultListSorter( + DirSortBy.NONE_ON_TOP, + SortType(SortBy.RELEVANCE, SortOrder.ASC), + searchTerm + ) + + val file1 = LayoutElementParcelable( + ApplicationProvider.getApplicationContext(), + title1, + "C:\\AmazeFileManager\\abc", + "user", + "symlink", + "100", + 123L, + true, + "1234", + false, + false, + OpenMode.UNKNOWN + ).generateBaseFile() + + val file2 = LayoutElementParcelable( + ApplicationProvider.getApplicationContext(), + "ABCDE.txt", + "C:\\AmazeFileManager\\ABCDE", + "user", + "symlink", + "101", + 124L, + true, + "1234", + false, + false, + OpenMode.UNKNOWN + ).generateBaseFile() + + Assert.assertEquals( + -1, + searchResultListSorter.compare( + SearchResult(file1, matchRange1), + SearchResult(file2, matchRange2) + ).toLong() + ) + } + + /** + * Purpose: when sort is [SortBy.RELEVANCE], if file1 matches the search term less than file2, result is positive + * + * Input: SearchResultListSorter with [DirSortBy.NONE_ON_TOP], [SortBy.RELEVANCE], [SortOrder.ASC] and search term "abc" + * compare(file1,file2) file1 title matches "abc" less than file2 title + * + * Expected: return positive integer + */ + @Test + fun testSortByRelevanceWithFile1LessMatchThanFile2() { + val searchTerm = "abc" + val title1 = "abcdefg.txt" + val matchRange1 = getSimpleMatchRange(searchTerm, title1) + val title2 = "ABC.txt" + val matchRange2 = getSimpleMatchRange(searchTerm, title2) + val searchResultListSorter = SearchResultListSorter( + DirSortBy.NONE_ON_TOP, + SortType(SortBy.RELEVANCE, SortOrder.ASC), + searchTerm + ) + val file1 = LayoutElementParcelable( + ApplicationProvider.getApplicationContext(), + title1, + "C:\\AmazeFileManager\\abcdefg", + "user", + "symlink", + "100", + 123L, + true, + "1234", + false, + false, + OpenMode.UNKNOWN + ).generateBaseFile() + val file2 = LayoutElementParcelable( + ApplicationProvider.getApplicationContext(), + title2, + "C:\\AmazeFileManager\\ABC", + "user", + "symlink", + "101", + 124L, + true, + "1234", + false, + false, + OpenMode.UNKNOWN + ).generateBaseFile() + Assert.assertEquals( + 1, + searchResultListSorter.compare( + SearchResult(file1, matchRange1), + SearchResult(file2, matchRange2) + ).toLong() + ) + } + + /** + * Purpose: when sort is [SortBy.RELEVANCE], if file1 matches the search term as much as file2 + * and file1 starts with search term, result is negative + * + * Input: SearchResultListSorter with [DirSortBy.NONE_ON_TOP], [SortBy.RELEVANCE], [SortOrder.ASC] and search term "abc" + * compare(file1,file2) file1 title matches "abc" as much as file2 title and file1 starts with "abc" + * + * Expected: return negative integer + */ + @Test + fun testSortByRelevanceWithFile1StartsWithSearchTerm() { + val searchTerm = "abc" + val title1 = "abc.txt" + val matchRange1 = getSimpleMatchRange(searchTerm, title1) + val title2 = "XYZ_ABC" + val matchRange2 = getSimpleMatchRange(searchTerm, title2) + + val searchResultListSorter = SearchResultListSorter( + DirSortBy.NONE_ON_TOP, + SortType(SortBy.RELEVANCE, SortOrder.ASC), + searchTerm + ) + val file1 = LayoutElementParcelable( + ApplicationProvider.getApplicationContext(), + title1, + "C:\\AmazeFileManager\\abc", + "user", + "symlink", + "100", + 123L, + true, + "1234", + false, + false, + OpenMode.UNKNOWN + ).generateBaseFile() + val file2 = LayoutElementParcelable( + ApplicationProvider.getApplicationContext(), + title2, + "C:\\AmazeFileManager\\XYZ_ABC", + "user", + "symlink", + "101", + 124L, + true, + "1234", + false, + false, + OpenMode.UNKNOWN + ).generateBaseFile() + Assert.assertEquals( + -1, + searchResultListSorter.compare( + SearchResult(file1, matchRange1), + SearchResult(file2, matchRange2) + ).toLong() + ) + } + + /** + * Purpose: when sort is [SortBy.RELEVANCE], if file1 matches the search term as much as file2 + * and file2 starts with search term, result is positive + * + * Input: SearchResultListSorter with [DirSortBy.NONE_ON_TOP], [SortBy.RELEVANCE], [SortOrder.ASC] and search term "abc" + * compare(file1,file2) file1 title matches "abc" as much as file2 title and file2 starts with "abc" + * + * Expected: return positive integer + */ + @Test + fun testSortByRelevanceWithFile2StartWithSearchTerm() { + val searchTerm = "abc" + val title1 = "txt-abc" + val matchRange1 = getSimpleMatchRange(searchTerm, title1) + val title2 = "ABC.txt" + val matchRange2 = getSimpleMatchRange(searchTerm, title2) + + val searchResultListSorter = SearchResultListSorter( + DirSortBy.NONE_ON_TOP, + SortType(SortBy.RELEVANCE, SortOrder.ASC), + searchTerm + ) + val file1 = LayoutElementParcelable( + ApplicationProvider.getApplicationContext(), + title1, + "C:\\AmazeFileManager\\txt-abc", + "user", + "symlink", + "100", + 123L, + true, + "1234", + false, + false, + OpenMode.UNKNOWN + ).generateBaseFile() + val file2 = LayoutElementParcelable( + ApplicationProvider.getApplicationContext(), + title2, + "C:\\AmazeFileManager\\ABC", + "user", + "symlink", + "101", + 124L, + true, + "1234", + false, + false, + OpenMode.UNKNOWN + ).generateBaseFile() + Assert.assertEquals( + 1, + searchResultListSorter.compare( + SearchResult(file1, matchRange1), + SearchResult(file2, matchRange2) + ).toLong() + ) + } + + /** + * Purpose: when sort is [SortBy.RELEVANCE], if file1 matches the search term as much as file2, + * both start with search term and file1 contains the search term as a word (surrounded by + * separators), result is negative + * + * Input: SearchResultListSorter with [DirSortBy.NONE_ON_TOP], [SortBy.RELEVANCE], [SortOrder.ASC] and search term "abc" + * compare(file1,file2) file1 title matches "abc" as much as file2 title, both start with "abc" + * and file1 contains "abc" as word (separated by "-") + * + * Expected: return negative integer + */ + @Test + fun testSortByRelevanceWithFile1HasSearchTermAsWord() { + val searchTerm = "abc" + val title1 = "abc-efg.txt" + val matchRange1 = getSimpleMatchRange(searchTerm, title1) + val title2 = "ABCD-FG.txt" + val matchRange2 = getSimpleMatchRange(searchTerm, title2) + + val searchResultListSorter = SearchResultListSorter( + DirSortBy.NONE_ON_TOP, + SortType(SortBy.RELEVANCE, SortOrder.ASC), + searchTerm + ) + val file1 = LayoutElementParcelable( + ApplicationProvider.getApplicationContext(), + title1, + "C:\\AmazeFileManager\\abc-efg", + "user", + "symlink", + "100", + 123L, + true, + "1234", + false, + false, + OpenMode.UNKNOWN + ).generateBaseFile() + val file2 = LayoutElementParcelable( + ApplicationProvider.getApplicationContext(), + title2, + "C:\\AmazeFileManager\\ABCD-FG", + "user", + "symlink", + "101", + 124L, + true, + "1234", + false, + false, + OpenMode.UNKNOWN + ).generateBaseFile() + Assert.assertEquals( + -1, + searchResultListSorter.compare( + SearchResult(file1, matchRange1), + SearchResult(file2, matchRange2) + ).toLong() + ) + } + + /** + * Purpose: when sort is [SortBy.RELEVANCE], if file1 matches the search term as much as file2, + * both start with search term and file2 contains the search term as a word (surrounded by + * separators), result is positive + * + * Input: SearchResultListSorter with [DirSortBy.NONE_ON_TOP], [SortBy.RELEVANCE], [SortOrder.ASC] and search term "abc" + * compare(file1,file2) file1 title matches "abc" as much as file2 title, both start with "abc" + * and file2 contains "abc" as word (separated by "_") + * + * Expected: return positive integer + */ + @Test + fun testSortByRelevanceWithFile2HasSearchTermAsWord() { + val searchTerm = "abc" + val title1 = "abcdefg" + val matchRange1 = getSimpleMatchRange(searchTerm, title1) + val title2 = "ABC_EFG" + val matchRange2 = getSimpleMatchRange(searchTerm, title2) + val searchResultListSorter = SearchResultListSorter( + DirSortBy.NONE_ON_TOP, + SortType(SortBy.RELEVANCE, SortOrder.ASC), + searchTerm + ) + val file1 = LayoutElementParcelable( + ApplicationProvider.getApplicationContext(), + title1, + "C:\\AmazeFileManager\\abcdefg", + "user", + "symlink", + "100", + 123L, + true, + "1234", + false, + false, + OpenMode.UNKNOWN + ).generateBaseFile() + val file2 = LayoutElementParcelable( + ApplicationProvider.getApplicationContext(), + title2, + "C:\\AmazeFileManager\\ABC_EFG", + "user", + "symlink", + "101", + 124L, + true, + "1234", + false, + false, + OpenMode.UNKNOWN + ).generateBaseFile() + Assert.assertEquals( + 1, + searchResultListSorter.compare( + SearchResult(file1, matchRange1), + SearchResult(file2, matchRange2) + ).toLong() + ) + } + + /** + * Purpose: when sort is [SortBy.RELEVANCE], if file1 matches the search term as much as file2, + * both start with search term and file2 contains the search term as a word (surrounded by + * separators), result is positive + * + * Input: SearchResultListSorter with [DirSortBy.NONE_ON_TOP], [SortBy.RELEVANCE], [SortOrder.ASC] and search term "abc" + * compare(file1,file2) file1 title matches "abc" as much as file2 title, both start with "abc" + * and file2 contains "abc" as word (separated by " ") + * + * Expected: return positive integer + */ + @Test + fun testSortByRelevanceWithSpaceWordSeparator() { + val searchTerm = "abc" + val title1 = "abcdefg" + val matchRange1 = getSimpleMatchRange(searchTerm, title1) + val title2 = "ABC EFG" + val matchRange2 = getSimpleMatchRange(searchTerm, title2) + + val searchResultListSorter = SearchResultListSorter( + DirSortBy.NONE_ON_TOP, + SortType(SortBy.RELEVANCE, SortOrder.ASC), + "abc" + ) + val file1 = LayoutElementParcelable( + ApplicationProvider.getApplicationContext(), + title1, + "C:\\AmazeFileManager\\abcdefg", + "user", + "symlink", + "100", + 123L, + true, + "1234", + false, + false, + OpenMode.UNKNOWN + ).generateBaseFile() + val file2 = LayoutElementParcelable( + ApplicationProvider.getApplicationContext(), + title2, + "C:\\AmazeFileManager\\ABC EFG", + "user", + "symlink", + "101", + 124L, + true, + "1234", + false, + false, + OpenMode.UNKNOWN + ).generateBaseFile() + Assert.assertEquals( + 1, + searchResultListSorter.compare( + SearchResult(file1, matchRange1), + SearchResult(file2, matchRange2) + ).toLong() + ) + } + + /** + * Purpose: when sort is [SortBy.RELEVANCE], if file1 matches the search term as much as file2, + * both start with search term and file2 contains the search term as a word (surrounded by + * separators), result is positive + * + * Input: SearchResultListSorter with [DirSortBy.NONE_ON_TOP], [SortBy.RELEVANCE], [SortOrder.ASC] and search term "abc" + * compare(file1,file2) file1 title matches "abc" as much as file2 title, both start with "abc" + * and file2 contains "abc" as word (separated by ".") + * + * Expected: return positive integer + */ + @Test + fun testSortByRelevanceWithDotWordSeparator() { + val searchTerm = "abc" + val title1 = "abcdefg" + val matchRange1 = getSimpleMatchRange(searchTerm, title1) + val title2 = "ABC.EFG" + val matchRange2 = getSimpleMatchRange(searchTerm, title2) + + val searchResultListSorter = SearchResultListSorter( + DirSortBy.NONE_ON_TOP, + SortType(SortBy.RELEVANCE, SortOrder.ASC), + searchTerm + ) + val file1 = LayoutElementParcelable( + ApplicationProvider.getApplicationContext(), + title1, + "C:\\AmazeFileManager\\abcdefg", + "user", + "symlink", + "100", + 123L, + true, + "1234", + false, + false, + OpenMode.UNKNOWN + ).generateBaseFile() + val file2 = LayoutElementParcelable( + ApplicationProvider.getApplicationContext(), + title2, + "C:\\AmazeFileManager\\ABC.EFG", + "user", + "symlink", + "101", + 124L, + true, + "1234", + false, + false, + OpenMode.UNKNOWN + ).generateBaseFile() + Assert.assertEquals( + 1, + searchResultListSorter.compare( + SearchResult(file1, matchRange1), + SearchResult(file2, matchRange2) + ).toLong() + ) + } + + /** + * Purpose: when sort is [SortBy.RELEVANCE], if file1 matches the search term as much as file2, + * both start with search term, both contain the search term as a word and file1 date is more recent, + * result is negative + * + * Input: SearchResultListSorter with [DirSortBy.NONE_ON_TOP], [SortBy.RELEVANCE], [SortOrder.ASC] and search term "abc" + * compare(file1,file2) file1 title matches "abc" as much as file2 title, both start with "abc", + * both contain "abc" as word and file1 date is more recent + * + * Expected: return negative integer + */ + @Test + fun testSortByRelevanceWithFile1MoreRecent() { + val searchTerm = "abc" + val title1 = "abc.efg" + val matchRange1 = getSimpleMatchRange(searchTerm, title1) + val title2 = "ABC_EFG" + val matchRange2 = getSimpleMatchRange(searchTerm, title2) + + val searchResultListSorter = SearchResultListSorter( + DirSortBy.NONE_ON_TOP, + SortType(SortBy.RELEVANCE, SortOrder.ASC), + searchTerm + ) + val currentTime = Date().time + val file1 = LayoutElementParcelable( + ApplicationProvider.getApplicationContext(), + title1, + "C:\\AmazeFileManager\\abc.efg", + "user", + "symlink", + "100", + 123L, + true, + (currentTime - TimeUnit.MINUTES.toMillis(5)).toString(), + false, + false, + OpenMode.UNKNOWN + ).generateBaseFile() + val file2 = LayoutElementParcelable( + ApplicationProvider.getApplicationContext(), + title2, + "C:\\AmazeFileManager\\ABC_EFG", + "user", + "symlink", + "101", + 124L, + true, + (currentTime - TimeUnit.MINUTES.toMillis(10)).toString(), + false, + false, + OpenMode.UNKNOWN + ).generateBaseFile() + Assert.assertEquals( + -1, + searchResultListSorter.compare( + SearchResult(file1, matchRange1), + SearchResult(file2, matchRange2) + ).toLong() + ) + } + + /** + * Purpose: when sort is [SortBy.RELEVANCE], if file1 matches the search term as much as file2, + * both start with search term, both contain the search term as a word and file2 date is more recent, + * result is positive + * + * Input: SearchResultListSorter with [DirSortBy.NONE_ON_TOP], [SortBy.RELEVANCE], [SortOrder.ASC] and search term "abc" + * compare(file1,file2) file1 title matches "abc" as much as file2 title, both start with "abc", + * both contain "abc" as word and file2 date is more recent + * + * Expected: return positive integer + */ + @Test + fun testSortByRelevanceWithFile2MoreRecent() { + val searchTerm = "abc" + val title1 = "abc.efg" + val matchRange1 = getSimpleMatchRange(searchTerm, title1) + val title2 = "ABC_EFG" + val matchRange2 = getSimpleMatchRange(searchTerm, title2) + + val searchResultListSorter = SearchResultListSorter( + DirSortBy.NONE_ON_TOP, + SortType(SortBy.RELEVANCE, SortOrder.ASC), + searchTerm + ) + val currentTime = Date().time + val file1 = LayoutElementParcelable( + ApplicationProvider.getApplicationContext(), + title1, + "C:\\AmazeFileManager\\abc.efg", + "user", + "symlink", + "100", + 123L, + true, + (currentTime - TimeUnit.MINUTES.toMillis(10)).toString(), + false, + false, + OpenMode.UNKNOWN + ).generateBaseFile() + val file2 = LayoutElementParcelable( + ApplicationProvider.getApplicationContext(), + title2, + "C:\\AmazeFileManager\\ABC_EFG", + "user", + "symlink", + "101", + 124L, + true, + (currentTime - TimeUnit.MINUTES.toMillis(5)).toString(), + false, + false, + OpenMode.UNKNOWN + ).generateBaseFile() + Assert.assertEquals( + 1, + searchResultListSorter.compare( + SearchResult(file1, matchRange1), + SearchResult(file2, matchRange2) + ).toLong() + ) + } + + /** + * Purpose: when sort is [SortBy.RELEVANCE], if file1 matches the search term as much as file2, + * both start with search term, both contain the search term as a word and date is same, + * result is zero + * + * Input: SearchResultListSorter with [DirSortBy.NONE_ON_TOP], [SortBy.RELEVANCE], [SortOrder.ASC] and search term "abc" + * compare(file1,file2) file1 title matches "abc" as much as file2 title, both start with "abc", + * both contain "abc" as word and the date of both is the same + * + * Expected: return zero + */ + @Test + fun testSortByRelevanceWithSameRelevance() { + val searchTerm = "abc" + val title1 = "abc.efg" + val matchRange1 = getSimpleMatchRange(searchTerm, title1) + val title2 = "ABC_EFG" + val matchRange2 = getSimpleMatchRange(searchTerm, title2) + + val searchResultListSorter = SearchResultListSorter( + DirSortBy.NONE_ON_TOP, + SortType(SortBy.RELEVANCE, SortOrder.ASC), + searchTerm + ) + val file1 = LayoutElementParcelable( + ApplicationProvider.getApplicationContext(), + title1, + "C:\\AmazeFileManager\\abc.efg", + "user", + "symlink", + "100", + 123L, + true, + "1234", + false, + false, + OpenMode.UNKNOWN + ).generateBaseFile() + val file2 = LayoutElementParcelable( + ApplicationProvider.getApplicationContext(), + title2, + "C:\\AmazeFileManager\\ABC_EFG", + "user", + "symlink", + "101", + 124L, + true, + "1234", + false, + false, + OpenMode.UNKNOWN + ).generateBaseFile() + Assert.assertEquals( + 0, + searchResultListSorter.compare( + SearchResult(file1, matchRange1), + SearchResult(file2, matchRange2) + ).toLong() + ) + } + + /** + * Purpose: when sort is [SortBy.RELEVANCE], if file2 matches the search term more than file1 + * and file2 date is more recent, but file1 starts with search term and contains the + * search term as a word, the result is negative. + * + * Input: SearchResultListSorter with [DirSortBy.NONE_ON_TOP], [SortBy.RELEVANCE], [SortOrder.ASC] and search term "abc" + * compare(file1,file2) file2 title matches "abc" more than file1 title and is more recent both start with "abc", + * both contain "abc" as word and the date of both is the same + * + * Expected: return negative integer + */ + @Test + fun testSortByRelevanceWhole() { + val searchTerm = "abc" + val title1 = "abc.efghij" + val matchRange1 = getSimpleMatchRange(searchTerm, title1) + val title2 = "EFGABC" + val matchRange2 = getSimpleMatchRange(searchTerm, title2) + + val searchResultListSorter = SearchResultListSorter( + DirSortBy.NONE_ON_TOP, + SortType(SortBy.RELEVANCE, SortOrder.ASC), + "abc" + ) + val currentTime = Date().time + + // matches 3/10 + // starts with search term + // contains search as whole word + // modification time is less recent + val file1 = LayoutElementParcelable( + ApplicationProvider.getApplicationContext(), + title1, + "C:\\AmazeFileManager\\abc.efghij", + "user", + "symlink", + "100", + 123L, + true, + (currentTime - TimeUnit.MINUTES.toMillis(10)).toString(), + false, + false, + OpenMode.UNKNOWN + ).generateBaseFile() + // matches 3/6 + // doesn't start with search term + // doesn't contain as whole word + // modification time is more recent + val file2 = LayoutElementParcelable( + ApplicationProvider.getApplicationContext(), + title2, + "C:\\AmazeFileManager\\EFGABC", + "user", + "symlink", + "101", + 124L, + true, + (currentTime - TimeUnit.MINUTES.toMillis(5)).toString(), + false, + false, + OpenMode.UNKNOWN + ).generateBaseFile() + Assert.assertEquals( + -1, + searchResultListSorter.compare( + SearchResult(file1, matchRange1), + SearchResult(file2, matchRange2) + ).toLong() + ) + } +} diff --git a/app/src/test/java/com/amaze/filemanager/filesystem/files/FileListSorterTest.kt b/app/src/test/java/com/amaze/filemanager/filesystem/files/FileListSorterTest.kt index 841231b640..5a4f657bd9 100644 --- a/app/src/test/java/com/amaze/filemanager/filesystem/files/FileListSorterTest.kt +++ b/app/src/test/java/com/amaze/filemanager/filesystem/files/FileListSorterTest.kt @@ -35,8 +35,6 @@ import org.junit.Assert import org.junit.Test import org.junit.runner.RunWith import org.robolectric.annotation.Config -import java.util.Date -import java.util.concurrent.TimeUnit /** * because of test based on mock-up, extension testing isn't tested so, assume all extension is @@ -1013,594 +1011,4 @@ class FileListSorterTest { ) Assert.assertEquals(fileListSorter.compare(file1, file2).toLong(), 0) } - - /** - * Purpose: when sort is [SortBy.RELEVANCE], if file1 matches the search term more than file2, result is positive - * - * Input: FileListSorter with [DirSortBy.NONE_ON_TOP], [SortBy.RELEVANCE], [SortOrder.ASC] and search term "abc" - * compare(file1,file2) file1 title matches "abc" more than file2 title - * - * Expected: return negative integer - */ - @Test - fun testSortByRelevanceWithFile1MoreMatchThanFile2() { - val fileListSorter = FileListSorter( - DirSortBy.NONE_ON_TOP, - SortType(SortBy.RELEVANCE, SortOrder.ASC), - "abc" - ) - val file1 = LayoutElementParcelable( - ApplicationProvider.getApplicationContext(), - "abc.txt", - "C:\\AmazeFileManager\\abc", - "user", - "symlink", - "100", - 123L, - true, - "1234", - false, - false, - OpenMode.UNKNOWN - ) - val file2 = LayoutElementParcelable( - ApplicationProvider.getApplicationContext(), - "ABCDE.txt", - "C:\\AmazeFileManager\\ABCDE", - "user", - "symlink", - "101", - 124L, - true, - "1234", - false, - false, - OpenMode.UNKNOWN - ) - Assert.assertEquals(-1, fileListSorter.compare(file1, file2).toLong()) - } - - /** - * Purpose: when sort is [SortBy.RELEVANCE], if file1 matches the search term less than file2, result is positive - * - * Input: FileListSorter with [DirSortBy.NONE_ON_TOP], [SortBy.RELEVANCE], [SortOrder.ASC] and search term "abc" - * compare(file1,file2) file1 title matches "abc" less than file2 title - * - * Expected: return positive integer - */ - @Test - fun testSortByRelevanceWithFile1LessMatchThanFile2() { - val fileListSorter = FileListSorter( - DirSortBy.NONE_ON_TOP, - SortType(SortBy.RELEVANCE, SortOrder.ASC), - "abc" - ) - val file1 = LayoutElementParcelable( - ApplicationProvider.getApplicationContext(), - "abcdefg.txt", - "C:\\AmazeFileManager\\abcdefg", - "user", - "symlink", - "100", - 123L, - true, - "1234", - false, - false, - OpenMode.UNKNOWN - ) - val file2 = LayoutElementParcelable( - ApplicationProvider.getApplicationContext(), - "ABC.txt", - "C:\\AmazeFileManager\\ABC", - "user", - "symlink", - "101", - 124L, - true, - "1234", - false, - false, - OpenMode.UNKNOWN - ) - Assert.assertEquals(1, fileListSorter.compare(file1, file2).toLong()) - } - - /** - * Purpose: when sort is [SortBy.RELEVANCE], if file1 matches the search term as much as file2 - * and file1 starts with search term, result is negative - * - * Input: FileListSorter with [DirSortBy.NONE_ON_TOP], [SortBy.RELEVANCE], [SortOrder.ASC] and search term "abc" - * compare(file1,file2) file1 title matches "abc" as much as file2 title and file1 starts with "abc" - * - * Expected: return negative integer - */ - @Test - fun testSortByRelevanceWithFile1StartsWithSearchTerm() { - val fileListSorter = FileListSorter( - DirSortBy.NONE_ON_TOP, - SortType(SortBy.RELEVANCE, SortOrder.ASC), - "abc" - ) - val file1 = LayoutElementParcelable( - ApplicationProvider.getApplicationContext(), - "abc.txt", - "C:\\AmazeFileManager\\abc", - "user", - "symlink", - "100", - 123L, - true, - "1234", - false, - false, - OpenMode.UNKNOWN - ) - val file2 = LayoutElementParcelable( - ApplicationProvider.getApplicationContext(), - "XYZ_ABC", - "C:\\AmazeFileManager\\XYZ_ABC", - "user", - "symlink", - "101", - 124L, - true, - "1234", - false, - false, - OpenMode.UNKNOWN - ) - Assert.assertEquals(-1, fileListSorter.compare(file1, file2).toLong()) - } - - /** - * Purpose: when sort is [SortBy.RELEVANCE], if file1 matches the search term as much as file2 - * and file2 starts with search term, result is positive - * - * Input: FileListSorter with [DirSortBy.NONE_ON_TOP], [SortBy.RELEVANCE], [SortOrder.ASC] and search term "abc" - * compare(file1,file2) file1 title matches "abc" as much as file2 title and file2 starts with "abc" - * - * Expected: return positive integer - */ - @Test - fun testSortByRelevanceWithFile2StartWithSearchTerm() { - val fileListSorter = FileListSorter( - DirSortBy.NONE_ON_TOP, - SortType(SortBy.RELEVANCE, SortOrder.ASC), - "abc" - ) - val file1 = LayoutElementParcelable( - ApplicationProvider.getApplicationContext(), - "txt-abc", - "C:\\AmazeFileManager\\txt-abc", - "user", - "symlink", - "100", - 123L, - true, - "1234", - false, - false, - OpenMode.UNKNOWN - ) - val file2 = LayoutElementParcelable( - ApplicationProvider.getApplicationContext(), - "ABC.txt", - "C:\\AmazeFileManager\\ABC", - "user", - "symlink", - "101", - 124L, - true, - "1234", - false, - false, - OpenMode.UNKNOWN - ) - Assert.assertEquals(1, fileListSorter.compare(file1, file2).toLong()) - } - - /** - * Purpose: when sort is [SortBy.RELEVANCE], if file1 matches the search term as much as file2, - * both start with search term and file1 contains the search term as a word (surrounded by - * separators), result is negative - * - * Input: FileListSorter with [DirSortBy.NONE_ON_TOP], [SortBy.RELEVANCE], [SortOrder.ASC] and search term "abc" - * compare(file1,file2) file1 title matches "abc" as much as file2 title, both start with "abc" - * and file1 contains "abc" as word (separated by "-") - * - * Expected: return negative integer - */ - @Test - fun testSortByRelevanceWithFile1HasSearchTermAsWord() { - val fileListSorter = FileListSorter( - DirSortBy.NONE_ON_TOP, - SortType(SortBy.RELEVANCE, SortOrder.ASC), - "abc" - ) - val file1 = LayoutElementParcelable( - ApplicationProvider.getApplicationContext(), - "abc-efg.txt", - "C:\\AmazeFileManager\\abc-efg", - "user", - "symlink", - "100", - 123L, - true, - "1234", - false, - false, - OpenMode.UNKNOWN - ) - val file2 = LayoutElementParcelable( - ApplicationProvider.getApplicationContext(), - "ABCD-FG.txt", - "C:\\AmazeFileManager\\ABCD-FG", - "user", - "symlink", - "101", - 124L, - true, - "1234", - false, - false, - OpenMode.UNKNOWN - ) - Assert.assertEquals(-1, fileListSorter.compare(file1, file2).toLong()) - } - - /** - * Purpose: when sort is [SortBy.RELEVANCE], if file1 matches the search term as much as file2, - * both start with search term and file2 contains the search term as a word (surrounded by - * separators), result is positive - * - * Input: FileListSorter with [DirSortBy.NONE_ON_TOP], [SortBy.RELEVANCE], [SortOrder.ASC] and search term "abc" - * compare(file1,file2) file1 title matches "abc" as much as file2 title, both start with "abc" - * and file2 contains "abc" as word (separated by "_") - * - * Expected: return positive integer - */ - @Test - fun testSortByRelevanceWithFile2HasSearchTermAsWord() { - val fileListSorter = FileListSorter( - DirSortBy.NONE_ON_TOP, - SortType(SortBy.RELEVANCE, SortOrder.ASC), - "abc" - ) - val file1 = LayoutElementParcelable( - ApplicationProvider.getApplicationContext(), - "abcdefg", - "C:\\AmazeFileManager\\abcdefg", - "user", - "symlink", - "100", - 123L, - true, - "1234", - false, - false, - OpenMode.UNKNOWN - ) - val file2 = LayoutElementParcelable( - ApplicationProvider.getApplicationContext(), - "ABC_EFG", - "C:\\AmazeFileManager\\ABC_EFG", - "user", - "symlink", - "101", - 124L, - true, - "1234", - false, - false, - OpenMode.UNKNOWN - ) - Assert.assertEquals(1, fileListSorter.compare(file1, file2).toLong()) - } - - /** - * Purpose: when sort is [SortBy.RELEVANCE], if file1 matches the search term as much as file2, - * both start with search term and file2 contains the search term as a word (surrounded by - * separators), result is positive - * - * Input: FileListSorter with [DirSortBy.NONE_ON_TOP], [SortBy.RELEVANCE], [SortOrder.ASC] and search term "abc" - * compare(file1,file2) file1 title matches "abc" as much as file2 title, both start with "abc" - * and file2 contains "abc" as word (separated by " ") - * - * Expected: return positive integer - */ - @Test - fun testSortByRelevanceWithSpaceWordSeparator() { - val fileListSorter = FileListSorter( - DirSortBy.NONE_ON_TOP, - SortType(SortBy.RELEVANCE, SortOrder.ASC), - "abc" - ) - val file1 = LayoutElementParcelable( - ApplicationProvider.getApplicationContext(), - "abcdefg", - "C:\\AmazeFileManager\\abcdefg", - "user", - "symlink", - "100", - 123L, - true, - "1234", - false, - false, - OpenMode.UNKNOWN - ) - val file2 = LayoutElementParcelable( - ApplicationProvider.getApplicationContext(), - "ABC EFG", - "C:\\AmazeFileManager\\ABC EFG", - "user", - "symlink", - "101", - 124L, - true, - "1234", - false, - false, - OpenMode.UNKNOWN - ) - Assert.assertEquals(1, fileListSorter.compare(file1, file2).toLong()) - } - - /** - * Purpose: when sort is [SortBy.RELEVANCE], if file1 matches the search term as much as file2, - * both start with search term and file2 contains the search term as a word (surrounded by - * separators), result is positive - * - * Input: FileListSorter with [DirSortBy.NONE_ON_TOP], [SortBy.RELEVANCE], [SortOrder.ASC] and search term "abc" - * compare(file1,file2) file1 title matches "abc" as much as file2 title, both start with "abc" - * and file2 contains "abc" as word (separated by ".") - * - * Expected: return positive integer - */ - @Test - fun testSortByRelevanceWithDotWordSeparator() { - val fileListSorter = FileListSorter( - DirSortBy.NONE_ON_TOP, - SortType(SortBy.RELEVANCE, SortOrder.ASC), - "abc" - ) - val file1 = LayoutElementParcelable( - ApplicationProvider.getApplicationContext(), - "abcdefg", - "C:\\AmazeFileManager\\abcdefg", - "user", - "symlink", - "100", - 123L, - true, - "1234", - false, - false, - OpenMode.UNKNOWN - ) - val file2 = LayoutElementParcelable( - ApplicationProvider.getApplicationContext(), - "ABC.EFG", - "C:\\AmazeFileManager\\ABC.EFG", - "user", - "symlink", - "101", - 124L, - true, - "1234", - false, - false, - OpenMode.UNKNOWN - ) - Assert.assertEquals(1, fileListSorter.compare(file1, file2).toLong()) - } - - /** - * Purpose: when sort is [SortBy.RELEVANCE], if file1 matches the search term as much as file2, - * both start with search term, both contain the search term as a word and file1 date is more recent, - * result is negative - * - * Input: FileListSorter with [DirSortBy.NONE_ON_TOP], [SortBy.RELEVANCE], [SortOrder.ASC] and search term "abc" - * compare(file1,file2) file1 title matches "abc" as much as file2 title, both start with "abc", - * both contain "abc" as word and file1 date is more recent - * - * Expected: return negative integer - */ - @Test - fun testSortByRelevanceWithFile1MoreRecent() { - val fileListSorter = FileListSorter( - DirSortBy.NONE_ON_TOP, - SortType(SortBy.RELEVANCE, SortOrder.ASC), - "abc" - ) - val currentTime = Date().time - val file1 = LayoutElementParcelable( - ApplicationProvider.getApplicationContext(), - "abc.efg", - "C:\\AmazeFileManager\\abc.efg", - "user", - "symlink", - "100", - 123L, - true, - (currentTime - TimeUnit.MINUTES.toMillis(5)).toString(), - false, - false, - OpenMode.UNKNOWN - ) - val file2 = LayoutElementParcelable( - ApplicationProvider.getApplicationContext(), - "ABC_EFG", - "C:\\AmazeFileManager\\ABC_EFG", - "user", - "symlink", - "101", - 124L, - true, - (currentTime - TimeUnit.MINUTES.toMillis(10)).toString(), - false, - false, - OpenMode.UNKNOWN - ) - Assert.assertEquals(-1, fileListSorter.compare(file1, file2).toLong()) - } - - /** - * Purpose: when sort is [SortBy.RELEVANCE], if file1 matches the search term as much as file2, - * both start with search term, both contain the search term as a word and file2 date is more recent, - * result is positive - * - * Input: FileListSorter with [DirSortBy.NONE_ON_TOP], [SortBy.RELEVANCE], [SortOrder.ASC] and search term "abc" - * compare(file1,file2) file1 title matches "abc" as much as file2 title, both start with "abc", - * both contain "abc" as word and file2 date is more recent - * - * Expected: return positive integer - */ - @Test - fun testSortByRelevanceWithFile2MoreRecent() { - val fileListSorter = FileListSorter( - DirSortBy.NONE_ON_TOP, - SortType(SortBy.RELEVANCE, SortOrder.ASC), - "abc" - ) - val currentTime = Date().time - val file1 = LayoutElementParcelable( - ApplicationProvider.getApplicationContext(), - "abc.efg", - "C:\\AmazeFileManager\\abc.efg", - "user", - "symlink", - "100", - 123L, - true, - (currentTime - TimeUnit.MINUTES.toMillis(10)).toString(), - false, - false, - OpenMode.UNKNOWN - ) - val file2 = LayoutElementParcelable( - ApplicationProvider.getApplicationContext(), - "ABC_EFG", - "C:\\AmazeFileManager\\ABC_EFG", - "user", - "symlink", - "101", - 124L, - true, - (currentTime - TimeUnit.MINUTES.toMillis(5)).toString(), - false, - false, - OpenMode.UNKNOWN - ) - Assert.assertEquals(1, fileListSorter.compare(file1, file2).toLong()) - } - - /** - * Purpose: when sort is [SortBy.RELEVANCE], if file1 matches the search term as much as file2, - * both start with search term, both contain the search term as a word and date is same, - * result is zero - * - * Input: FileListSorter with [DirSortBy.NONE_ON_TOP], [SortBy.RELEVANCE], [SortOrder.ASC] and search term "abc" - * compare(file1,file2) file1 title matches "abc" as much as file2 title, both start with "abc", - * both contain "abc" as word and the date of both is the same - * - * Expected: return zero - */ - @Test - fun testSortByRelevanceWithSameRelevance() { - val fileListSorter = FileListSorter( - DirSortBy.NONE_ON_TOP, - SortType(SortBy.RELEVANCE, SortOrder.ASC), - "abc" - ) - val file1 = LayoutElementParcelable( - ApplicationProvider.getApplicationContext(), - "abc.efg", - "C:\\AmazeFileManager\\abc.efg", - "user", - "symlink", - "100", - 123L, - true, - "1234", - false, - false, - OpenMode.UNKNOWN - ) - val file2 = LayoutElementParcelable( - ApplicationProvider.getApplicationContext(), - "ABC_EFG", - "C:\\AmazeFileManager\\ABC_EFG", - "user", - "symlink", - "101", - 124L, - true, - "1234", - false, - false, - OpenMode.UNKNOWN - ) - Assert.assertEquals(0, fileListSorter.compare(file1, file2).toLong()) - } - - /** - * Purpose: when sort is [SortBy.RELEVANCE], if file2 matches the search term more than file1 - * and file2 date is more recent, but file1 starts with search term and contains the - * search term as a word, the result is negative. - * - * Input: FileListSorter with [DirSortBy.NONE_ON_TOP], [SortBy.RELEVANCE], [SortOrder.ASC] and search term "abc" - * compare(file1,file2) file2 title matches "abc" more than file1 title and is more recent both start with "abc", - * both contain "abc" as word and the date of both is the same - * - * Expected: return negative integer - */ - @Test - fun testSortByRelevanceWhole() { - val fileListSorter = FileListSorter( - DirSortBy.NONE_ON_TOP, - SortType(SortBy.RELEVANCE, SortOrder.ASC), - "abc" - ) - val currentTime = Date().time - - // matches 3/10 - // starts with search term - // contains search as whole word - // modification time is less recent - val file1 = LayoutElementParcelable( - ApplicationProvider.getApplicationContext(), - "abc.efghij", - "C:\\AmazeFileManager\\abc.efghij", - "user", - "symlink", - "100", - 123L, - true, - (currentTime - TimeUnit.MINUTES.toMillis(10)).toString(), - false, - false, - OpenMode.UNKNOWN - ) - // matches 3/6 - // doesn't start with search term - // doesn't contain as whole word - // modification time is more recent - val file2 = LayoutElementParcelable( - ApplicationProvider.getApplicationContext(), - "EFGABC", - "C:\\AmazeFileManager\\EFGABC", - "user", - "symlink", - "101", - 124L, - true, - (currentTime - TimeUnit.MINUTES.toMillis(5)).toString(), - false, - false, - OpenMode.UNKNOWN - ) - Assert.assertEquals(-1, fileListSorter.compare(file1, file2).toLong()) - } } diff --git a/build.gradle b/build.gradle index 48e78a6d72..515dc45fe0 100644 --- a/build.gradle +++ b/build.gradle @@ -20,6 +20,8 @@ buildscript { androidXTestVersion = "1.5.0" androidXTestRunnerVersion = "1.5.2" androidXTestExtVersion = "1.1.5" + androidXArchCoreTestVersion = "2.2.0" + kotlinxCoroutineTestVersion = "1.7.3" uiAutomatorVersion = "2.2.0" junitVersion = "4.13.2" slf4jVersion = "1.7.25"