diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/ListElementsSearch.kt b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/ListElementsSearch.kt new file mode 100644 index 0000000000..4a9d5c81a7 --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/ListElementsSearch.kt @@ -0,0 +1,49 @@ +/* + * 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.adapters.data.LayoutElementParcelable +import kotlinx.coroutines.isActive +import kotlin.coroutines.coroutineContext + +class ListElementsSearch( + query: String, + path: String, + searchParameters: SearchParameters, + private val items: List, +) : FileSearch(query, path, searchParameters) { + override suspend fun search(filter: SearchFilter) { + for (item in items) { + if (!coroutineContext.isActive) { + break + } + + if (item.isBack || item.header) { + continue + } + + val resultRange = filter.searchFilter(item.title) + if (resultRange != null) { + publishProgress(item.generateBaseFile(), resultRange) + } + } + } +} 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 069e2b95cf..31de2b1ec3 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 @@ -34,7 +34,9 @@ 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.FileSearch import com.amaze.filemanager.asynchronous.asynctasks.searchfilesystem.IndexedSearch +import com.amaze.filemanager.asynchronous.asynctasks.searchfilesystem.ListElementsSearch import com.amaze.filemanager.asynchronous.asynctasks.searchfilesystem.SearchParameters import com.amaze.filemanager.asynchronous.asynctasks.searchfilesystem.SearchResult import com.amaze.filemanager.asynchronous.asynctasks.searchfilesystem.searchParametersFromBoolean @@ -117,18 +119,30 @@ class MainActivityViewModel(val applicationContext: Application) : query: String, ): LiveData> { val searchParameters = createSearchParameters(mainActivity) - val path = mainActivity.currentMainFragment?.currentPath ?: "" + val openMode = + mainActivity.currentMainFragment?.mainFragmentViewModel?.openMode ?: OpenMode.FILE - val basicSearch = BasicSearch(query, path, searchParameters, this.applicationContext) + val fileSearch: FileSearch = + if (openMode == OpenMode.CUSTOM || openMode == OpenMode.TRASH_BIN) { + ListElementsSearch( + query, + path, + searchParameters, + mainActivity.currentMainFragment?.mainFragmentViewModel?.listElements + ?: emptyList(), + ) + } else { + BasicSearch(query, path, searchParameters, this.applicationContext) + } lastSearchJob = viewModelScope.launch(Dispatchers.IO) { - basicSearch.search() + fileSearch.search() } - lastSearchLiveData = basicSearch.foundFilesLiveData - return basicSearch.foundFilesLiveData + lastSearchLiveData = fileSearch.foundFilesLiveData + return fileSearch.foundFilesLiveData } /** 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 a1f808dbe0..87185cc027 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 @@ -33,6 +33,7 @@ import com.amaze.filemanager.adapters.SearchRecyclerViewAdapter; import com.amaze.filemanager.asynchronous.asynctasks.searchfilesystem.SearchResult; import com.amaze.filemanager.asynchronous.asynctasks.searchfilesystem.SearchResultListSorter; +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; @@ -272,9 +273,18 @@ private void basicSearch(String s) { searchResultsHintTV.setVisibility(View.VISIBLE); searchResultsSortButton.setVisibility(View.VISIBLE); searchResultsSortHintTV.setVisibility(View.VISIBLE); - deepSearchContainer.setVisibility(View.VISIBLE); - searchMode = 1; - deepSearchButton.setText(mainActivity.getString(R.string.try_indexed_search)); + + OpenMode openMode = + mainActivity.getCurrentMainFragment().getMainFragmentViewModel().getOpenMode(); + + if (openMode == OpenMode.CUSTOM || openMode == OpenMode.TRASH_BIN) { + deepSearchContainer.setVisibility(View.GONE); + searchMode = 0; + } else { + deepSearchContainer.setVisibility(View.VISIBLE); + searchMode = 1; + deepSearchButton.setText(mainActivity.getString(R.string.try_indexed_search)); + } mainActivity .getCurrentMainFragment() diff --git a/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/ListElementsSearchTest.kt b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/ListElementsSearchTest.kt new file mode 100644 index 0000000000..8456a42c1b --- /dev/null +++ b/app/src/test/java/com/amaze/filemanager/asynchronous/asynctasks/searchfilesystem/ListElementsSearchTest.kt @@ -0,0 +1,153 @@ +/* + * 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.arch.core.executor.testing.InstantTaskExecutorRule +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 kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.util.EnumSet + +@RunWith(AndroidJUnit4::class) +class ListElementsSearchTest { + private companion object { + const val QUERY = "report" + } + + @get:Rule + val rule = InstantTaskExecutorRule() + + /** + * If the visible item title matches the query, the item should be added to + * [FileSearch.foundFilesLiveData] + */ + @Test + fun testSimpleSearchMatch() { + val search = + ListElementsSearch( + QUERY, + "/", + EnumSet.noneOf(SearchParameter::class.java), + listOf( + layoutElement("Annual Report.pdf", "/drawer/Annual Report.pdf"), + layoutElement("Vacation Photo.jpg", "/drawer/Vacation Photo.jpg"), + ), + ) + + search.foundFilesLiveData.observeForever { actualResults -> + Assert.assertNotNull(actualResults) + Assert.assertEquals(listOf("Annual Report.pdf"), actualResults.map { it.file.getName() }) + Assert.assertEquals(listOf("/drawer/Annual Report.pdf"), actualResults.map { it.file.getPath() }) + Assert.assertEquals(listOf(7..12), actualResults.map { it.matchRange }) + } + + runTest { + search.search() + } + } + + /** + * Back and header items should not be added to + * [FileSearch.foundFilesLiveData] + */ + @Test + fun testSkipsBackAndHeaderItems() { + val search = + ListElementsSearch( + QUERY, + "/", + EnumSet.noneOf(SearchParameter::class.java), + listOf( + layoutElement("Report Back", "/drawer/back", isBack = true), + layoutElement("Report Header", "/drawer/header", header = true), + layoutElement("Report File.txt", "/drawer/Report File.txt"), + ), + ) + + search.foundFilesLiveData.observeForever { actualResults -> + Assert.assertNotNull(actualResults) + Assert.assertEquals(listOf("Report File.txt"), actualResults.map { it.file.getName() }) + Assert.assertEquals(listOf("/drawer/Report File.txt"), actualResults.map { it.file.getPath() }) + } + + runTest { + search.search() + } + } + + /** + * If no visible item title matches the query, no results should be published to + * [FileSearch.foundFilesLiveData] + */ + @Test + fun testNoMatchDoesNotPublishResults() { + var observerCalled = false + + val search = + ListElementsSearch( + QUERY, + "/", + EnumSet.noneOf(SearchParameter::class.java), + listOf( + layoutElement("Vacation Photo.jpg", "/drawer/Vacation Photo.jpg"), + layoutElement("Budget 2025.xlsx", "/drawer/Budget 2025.xlsx"), + ), + ) + + search.foundFilesLiveData.observeForever { + observerCalled = true + } + + runTest { + search.search() + } + + Assert.assertFalse(observerCalled) + } + + private fun layoutElement( + title: String, + path: String, + isBack: Boolean = false, + header: Boolean = false, + ): LayoutElementParcelable = + LayoutElementParcelable( + ApplicationProvider.getApplicationContext(), + isBack, + title, + path, + "", + "", + "", + 0, + header, + "", + false, + false, + OpenMode.FILE, + ) +}