diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/language/LanguageRobot.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/language/LanguageRobot.kt index acbc23d3f..edbbaaa2f 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/language/LanguageRobot.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/language/LanguageRobot.kt @@ -31,6 +31,7 @@ import org.kiwix.kiwixmobile.BaseRobot import org.kiwix.kiwixmobile.Findable.ViewId import org.kiwix.kiwixmobile.R import org.kiwix.kiwixmobile.core.page.SEARCH_ICON_TESTING_TAG +import org.kiwix.kiwixmobile.core.search.SEARCH_FIELD_TESTING_TAG import org.kiwix.kiwixmobile.language.composables.LANGUAGE_ITEM_CHECKBOX_TESTING_TAG import org.kiwix.kiwixmobile.nav.destination.library.online.LANGUAGE_MENU_ICON_TESTING_TAG import org.kiwix.kiwixmobile.testutils.TestUtils diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/search/SearchFragmentTest.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/search/SearchFragmentTest.kt index 6a718ff71..de409f415 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/search/SearchFragmentTest.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/search/SearchFragmentTest.kt @@ -18,6 +18,7 @@ package org.kiwix.kiwixmobile.search import android.os.Build +import androidx.compose.ui.test.junit4.createComposeRule import androidx.core.content.edit import androidx.core.net.toUri import androidx.lifecycle.Lifecycle @@ -25,7 +26,6 @@ import androidx.navigation.fragment.NavHostFragment import androidx.preference.PreferenceManager import androidx.test.core.app.ActivityScenario import androidx.test.espresso.accessibility.AccessibilityChecks -import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.withContentDescription import androidx.test.internal.runner.junit4.statement.UiThreadStatement import androidx.test.platform.app.InstrumentationRegistry @@ -47,11 +47,11 @@ import org.junit.Rule import org.junit.Test import org.kiwix.kiwixmobile.BaseActivityTest import org.kiwix.kiwixmobile.R -import org.kiwix.kiwixmobile.core.R.id import org.kiwix.kiwixmobile.core.search.SearchFragment import org.kiwix.kiwixmobile.core.search.viewmodel.Action import org.kiwix.kiwixmobile.core.utils.LanguageUtils.Companion.handleLocaleChange import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil +import org.kiwix.kiwixmobile.core.utils.TestingUtils.COMPOSE_TEST_RULE_ORDER import org.kiwix.kiwixmobile.core.utils.TestingUtils.RETRY_RULE_ORDER import org.kiwix.kiwixmobile.main.KiwixMainActivity import org.kiwix.kiwixmobile.nav.destination.library.local.LocalLibraryFragmentDirections.actionNavigationLibraryToNavigationReader @@ -73,6 +73,9 @@ class SearchFragmentTest : BaseActivityTest() { @JvmField val retryRule = RetryRule() + @get:Rule(order = COMPOSE_TEST_RULE_ORDER) + val composeTestRule = createComposeRule() + private lateinit var kiwixMainActivity: KiwixMainActivity private lateinit var uiDevice: UiDevice private lateinit var downloadingZimFile: File @@ -115,10 +118,6 @@ class SearchFragmentTest : BaseActivityTest() { setRunChecksFromRootView(true) setSuppressingResultMatcher( anyOf( - allOf( - matchesCheck(TouchTargetSizeCheck::class.java), - matchesViews(ViewMatchers.withId(id.menu_searchintext)) - ), allOf( matchesCheck(TouchTargetSizeCheck::class.java), matchesViews( @@ -142,28 +141,32 @@ class SearchFragmentTest : BaseActivityTest() { search { checkZimFileSearchSuccessful(R.id.readerFragment) } openSearchWithQuery("Android", testZimFile) search { - clickOnSearchItemInSearchList() + clickOnSearchItemInSearchList(composeTestRule) checkZimFileSearchSuccessful(R.id.readerFragment) } openSearchWithQuery(zimFile = testZimFile) search { // test with fast typing/deleting - searchWithFrequentlyTypedWords(searchUnitTestingQuery) - assertSearchSuccessful(searchUnitTestResult) - deleteSearchedQueryFrequently(searchUnitTestingQuery, uiDevice) + searchWithFrequentlyTypedWords(searchUnitTestingQuery, composeTestRule = composeTestRule) + assertSearchSuccessful(searchUnitTestResult, composeTestRule) + deleteSearchedQueryFrequently( + searchUnitTestingQuery, + uiDevice, + composeTestRule = composeTestRule + ) // test with a short delay typing/deleting to // properly test the cancelling of previously searching task - searchWithFrequentlyTypedWords(searchUnitTestingQuery, 50) - assertSearchSuccessful(searchUnitTestResult) - deleteSearchedQueryFrequently(searchUnitTestingQuery, uiDevice, 50) + searchWithFrequentlyTypedWords(searchUnitTestingQuery, 50, composeTestRule) + assertSearchSuccessful(searchUnitTestResult, composeTestRule) + deleteSearchedQueryFrequently(searchUnitTestingQuery, uiDevice, 50, composeTestRule) // test with a long delay typing/deleting to // properly execute the search query letter by letter - searchWithFrequentlyTypedWords(searchUnitTestingQuery, 300) - assertSearchSuccessful(searchUnitTestResult) - deleteSearchedQueryFrequently(searchUnitTestingQuery, uiDevice, 300) + searchWithFrequentlyTypedWords(searchUnitTestingQuery, 300, composeTestRule) + assertSearchSuccessful(searchUnitTestResult, composeTestRule) + deleteSearchedQueryFrequently(searchUnitTestingQuery, uiDevice, 300, composeTestRule) // to close the keyboard pressBack() // go to reader screen @@ -190,39 +193,46 @@ class SearchFragmentTest : BaseActivityTest() { openSearchWithQuery(zimFile = downloadingZimFile) search { // test with fast typing/deleting - searchWithFrequentlyTypedWords(searchQueryForDownloadedZimFile) - assertSearchSuccessful(searchResultForDownloadedZimFile) - deleteSearchedQueryFrequently(searchQueryForDownloadedZimFile, uiDevice) + searchWithFrequentlyTypedWords( + searchQueryForDownloadedZimFile, + composeTestRule = composeTestRule + ) + assertSearchSuccessful(searchResultForDownloadedZimFile, composeTestRule) + deleteSearchedQueryFrequently( + searchQueryForDownloadedZimFile, + uiDevice, + composeTestRule = composeTestRule + ) // test with a short delay typing/deleting to // properly test the cancelling of previously searching task - searchWithFrequentlyTypedWords(searchQueryForDownloadedZimFile, 50) - assertSearchSuccessful(searchResultForDownloadedZimFile) - deleteSearchedQueryFrequently(searchQueryForDownloadedZimFile, uiDevice, 50) + searchWithFrequentlyTypedWords(searchQueryForDownloadedZimFile, 50, composeTestRule) + assertSearchSuccessful(searchResultForDownloadedZimFile, composeTestRule) + deleteSearchedQueryFrequently(searchQueryForDownloadedZimFile, uiDevice, 50, composeTestRule) // test with a long delay typing/deleting to // properly execute the search query letter by letter - searchWithFrequentlyTypedWords(searchQueryForDownloadedZimFile, 300) - assertSearchSuccessful(searchResultForDownloadedZimFile) - deleteSearchedQueryFrequently(searchQueryForDownloadedZimFile, uiDevice, 300) + searchWithFrequentlyTypedWords(searchQueryForDownloadedZimFile, 300, composeTestRule) + assertSearchSuccessful(searchResultForDownloadedZimFile, composeTestRule) + deleteSearchedQueryFrequently(searchQueryForDownloadedZimFile, uiDevice, 300, composeTestRule) // open the reader fragment for next text case. - openKiwixReaderFragmentWithFile(downloadingZimFile) + clickOnNavigationIcon(composeTestRule) } // Added test for checking the crash scenario where the application was crashing when we // frequently searched for article, and clicked on the searched item. search { // test by searching 10 article and clicking on them - searchAndClickOnArticle(searchQueryForDownloadedZimFile) - searchAndClickOnArticle("A Song") - searchAndClickOnArticle("The Ra") - searchAndClickOnArticle("The Ge") - searchAndClickOnArticle("Wish") - searchAndClickOnArticle("WIFI") - searchAndClickOnArticle("Woman") - searchAndClickOnArticle("Big Ba") - searchAndClickOnArticle("My Wor") - searchAndClickOnArticle("100") + searchAndClickOnArticle(searchQueryForDownloadedZimFile, composeTestRule) + searchAndClickOnArticle("A Song", composeTestRule) + searchAndClickOnArticle("The Ra", composeTestRule) + searchAndClickOnArticle("The Ge", composeTestRule) + searchAndClickOnArticle("Wish", composeTestRule) + searchAndClickOnArticle("WIFI", composeTestRule) + searchAndClickOnArticle("Woman", composeTestRule) + searchAndClickOnArticle("Big Ba", composeTestRule) + searchAndClickOnArticle("My Wor", composeTestRule) + searchAndClickOnArticle("100", composeTestRule) assertArticleLoaded() } removeTemporaryZimFilesToFreeUpDeviceStorage() diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/search/SearchRobot.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/search/SearchRobot.kt index 750da7f9f..ba2509ed8 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/search/SearchRobot.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/search/SearchRobot.kt @@ -19,28 +19,33 @@ package org.kiwix.kiwixmobile.search import android.view.KeyEvent -import androidx.recyclerview.widget.RecyclerView +import androidx.compose.ui.test.assert +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.ComposeContentTestRule +import androidx.compose.ui.test.onAllNodesWithTag +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextClearance +import androidx.compose.ui.test.performTextInput import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.action.ViewActions.clearText import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.action.ViewActions.typeText -import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition -import androidx.test.espresso.matcher.ViewMatchers.hasDescendant import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.espresso.web.sugar.Web.onWebView import androidx.test.espresso.web.webdriver.DriverAtoms.findElement import androidx.test.espresso.web.webdriver.Locator import androidx.test.uiautomator.UiDevice import applyWithViewHierarchyPrinting import com.adevinta.android.barista.interaction.BaristaSleepInteractions -import com.adevinta.android.barista.internal.matcher.HelperMatchers.atPosition import org.kiwix.kiwixmobile.BaseRobot import org.kiwix.kiwixmobile.Findable.ViewId import org.kiwix.kiwixmobile.core.R +import org.kiwix.kiwixmobile.core.search.SEARCH_FIELD_TESTING_TAG +import org.kiwix.kiwixmobile.core.search.SEARCH_ITEM_TESTING_TAG +import org.kiwix.kiwixmobile.core.ui.components.NAVIGATION_ICON_TESTING_TAG import org.kiwix.kiwixmobile.testutils.TestUtils +import org.kiwix.kiwixmobile.testutils.TestUtils.TEST_PAUSE_MS import org.kiwix.kiwixmobile.testutils.TestUtils.testFlakyView +import org.kiwix.kiwixmobile.testutils.TestUtils.waitUntilTimeout fun search(func: SearchRobot.() -> Unit) = SearchRobot().applyWithViewHierarchyPrinting(func) @@ -50,15 +55,11 @@ class SearchRobot : BaseRobot() { val searchQueryForDownloadedZimFile = "A Fool" val searchResultForDownloadedZimFile = "A Fool for You" - fun clickOnSearchItemInSearchList() { - BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS_FOR_SEARCH_TEST.toLong()) - isVisible(ViewId(R.id.search_list)) - onView(withId(R.id.search_list)).perform( - actionOnItemAtPosition( - 0, - click() - ) - ) + fun clickOnSearchItemInSearchList(composeTestRule: ComposeContentTestRule) { + composeTestRule.apply { + waitUntilTimeout() + onAllNodesWithTag(SEARCH_ITEM_TESTING_TAG)[0].performClick() + } } fun checkZimFileSearchSuccessful(readerFragment: Int) { @@ -66,31 +67,47 @@ class SearchRobot : BaseRobot() { isVisible(ViewId(readerFragment)) } - fun searchWithFrequentlyTypedWords(query: String, wait: Long = 0L) { + fun searchWithFrequentlyTypedWords( + query: String, + wait: Long = 0L, + composeTestRule: ComposeContentTestRule + ) { testFlakyView({ - val searchView = onView(withId(androidx.appcompat.R.id.search_src_text)) - for (char in query) { - searchView.perform(typeText(char.toString())) - if (wait != 0L) { - BaristaSleepInteractions.sleep(wait) + composeTestRule.apply { + waitUntilTimeout() + val searchView = onNodeWithTag(SEARCH_FIELD_TESTING_TAG) + searchView.performTextInput("") + for (char in query) { + searchView.performTextInput(char.toString()) + if (wait != 0L) { + waitUntilTimeout(wait) + } } } }) } - fun assertSearchSuccessful(searchResult: String) { - BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS_FOR_SEARCH_TEST.toLong()) - val recyclerViewId = R.id.search_list - - onView(withId(recyclerViewId)).check( - matches( - atPosition(0, hasDescendant(withText(searchResult))) + fun assertSearchSuccessful(searchResult: String, composeTestRule: ComposeContentTestRule) { + composeTestRule.apply { + waitUntil( + timeoutMillis = TEST_PAUSE_MS.toLong(), + condition = { + onAllNodesWithTag(SEARCH_ITEM_TESTING_TAG) + .fetchSemanticsNodes().isNotEmpty() + } ) - ) + onAllNodesWithTag(SEARCH_ITEM_TESTING_TAG)[0] + .assert(hasText(searchResult)) + } } - fun deleteSearchedQueryFrequently(textToDelete: String, uiDevice: UiDevice, wait: Long = 0L) { - for (i in textToDelete.indices) { + fun deleteSearchedQueryFrequently( + textToDelete: String, + uiDevice: UiDevice, + wait: Long = 0L, + composeTestRule: ComposeContentTestRule + ) { + repeat(textToDelete.length) { uiDevice.pressKeyCode(KeyEvent.KEYCODE_DEL) if (wait != 0L) { BaristaSleepInteractions.sleep(wait) @@ -98,20 +115,22 @@ class SearchRobot : BaseRobot() { } // clear search query if any remains due to any condition not to affect any other test scenario - val searchView = onView(withId(androidx.appcompat.R.id.search_src_text)) - searchView.perform(clearText()) + composeTestRule.onNodeWithTag(SEARCH_FIELD_TESTING_TAG).performTextClearance() + } + + fun clickOnNavigationIcon(composeTestRule: ComposeContentTestRule) { + composeTestRule.onNodeWithTag(NAVIGATION_ICON_TESTING_TAG).performClick() } private fun openSearchScreen() { testFlakyView({ onView(withId(R.id.menu_search)).perform(click()) }) } - fun searchAndClickOnArticle(searchString: String) { - // wait a bit to properly load the ZIM file in the reader - BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS_FOR_SEARCH_TEST.toLong()) + fun searchAndClickOnArticle(searchString: String, composeTestRule: ComposeContentTestRule) { openSearchScreen() - searchWithFrequentlyTypedWords(searchString) - clickOnSearchItemInSearchList() + searchWithFrequentlyTypedWords(searchString, composeTestRule = composeTestRule) + clickOnSearchItemInSearchList(composeTestRule) + checkZimFileSearchSuccessful(org.kiwix.kiwixmobile.R.id.readerFragment) } fun assertArticleLoaded() { diff --git a/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageFragment.kt b/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageFragment.kt index 84963aacf..66a5a427b 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageFragment.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageFragment.kt @@ -47,7 +47,6 @@ import org.kiwix.kiwixmobile.language.viewmodel.LanguageViewModel import javax.inject.Inject const val SAVE_ICON_TESTING_TAG = "saveLanguages" -const val SEARCH_FIELD_TESTING_TAG = "searchField" class LanguageFragment : BaseFragment() { private val languageViewModel by lazy { viewModel(viewModelFactory) } diff --git a/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageScreen.kt b/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageScreen.kt index 73f0abf04..8edc61fb1 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageScreen.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageScreen.kt @@ -39,6 +39,7 @@ import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.dp import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.extensions.CollectSideEffectWithActivity +import org.kiwix.kiwixmobile.core.search.SEARCH_FIELD_TESTING_TAG import org.kiwix.kiwixmobile.core.ui.components.ContentLoadingProgressBar import org.kiwix.kiwixmobile.core.ui.components.KiwixAppBar import org.kiwix.kiwixmobile.core.ui.components.KiwixSearchView diff --git a/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/online/OnlineLibraryScreen.kt b/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/online/OnlineLibraryScreen.kt index 03d246b70..2ffd3c68a 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/online/OnlineLibraryScreen.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/online/OnlineLibraryScreen.kt @@ -51,7 +51,6 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTag import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp import org.kiwix.kiwixmobile.core.R.string import org.kiwix.kiwixmobile.core.extensions.hideKeyboardOnLazyColumnScroll import org.kiwix.kiwixmobile.core.ui.components.ContentLoadingProgressBar @@ -72,6 +71,7 @@ import org.kiwix.kiwixmobile.core.utils.ComposeDimens.EIGHT_DP import org.kiwix.kiwixmobile.core.utils.ComposeDimens.FOUR_DP import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SIXTEEN_DP import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SIX_DP +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.THREE_DP import org.kiwix.kiwixmobile.nav.destination.library.local.rememberScrollBehavior import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem.DividerItem @@ -258,7 +258,7 @@ private fun ShowFetchingLibraryLayout(message: String) { ) { ContentLoadingProgressBar( modifier = Modifier.size(DOWNLOADING_LIBRARY_PROGRESSBAR_SIZE), - circularProgressBarStockWidth = 3.dp, + circularProgressBarStockWidth = THREE_DP, progressBarTrackColor = cardContainerColor ) Text( diff --git a/app/src/main/res/navigation/kiwix_nav_graph.xml b/app/src/main/res/navigation/kiwix_nav_graph.xml index cd31a7d78..52b2f8703 100644 --- a/app/src/main/res/navigation/kiwix_nav_graph.xml +++ b/app/src/main/res/navigation/kiwix_nav_graph.xml @@ -132,8 +132,7 @@ + android:label="SearchFragment"> (viewModelFactory) } - private var searchAdapter: SearchAdapter? = null - private var isDataLoading = false + private var isDataLoading = mutableStateOf(false) private var renderingJob: Job? = null + /** + * Represents the state of the FIND_IN_PAGE menu item. + * + * A [Pair] containing: + * - [Boolean]: Whether the menu item is enabled (clickable). + * - [Boolean]: Whether the menu item is visible. + */ + private var findInPageMenuItem = mutableStateOf(false to false) + private var composeView: ComposeView? = null + private val searchScreenState = mutableStateOf( + SearchScreenState( + searchList = emptyList(), + isLoading = true, + shouldShowLoadingMoreProgressBar = false, + searchText = "", + onSearchViewClearClick = { onSearchClear() }, + onSearchViewValueChange = { onSearchValueChanged(it) }, + onItemClick = { onItemClick(it) }, + onNewTabIconClick = { onItemClickNewTab(it) }, + onItemLongClick = { + searchViewModel.actions.trySend(OnItemLongClick(it)).isSuccess + }, + navigationIcon = { + NavigationIcon(onClick = { requireActivity().onBackPressedDispatcher.onBackPressed() }) + }, + onLoadMore = { loadMoreSearchResult() }, + onKeyboardSubmitButtonClick = { + getSearchListItemForQuery(it)?.let(::onItemClick) + } + ) + ) + override fun inject(baseActivity: BaseActivity) { baseActivity.cachedComponent.inject(this) } @@ -111,80 +116,87 @@ class SearchFragment : BaseFragment() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - fragmentSearchBinding = FragmentSearchBinding.inflate(inflater, container, false) - setupMenu() - return fragmentSearchBinding?.root + ): View? = ComposeView(requireContext()).also { + composeView = it } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - searchAdapter = SearchAdapter( - RecentSearchDelegate(::onItemClick, ::onItemClickNewTab) { - searchViewModel.actions.trySend(OnItemLongClick(it)).isSuccess - }, - ZimSearchResultDelegate(::onItemClick, ::onItemClickNewTab) - ) - setupToolbar(view) - fragmentSearchBinding?.searchList?.run { - adapter = searchAdapter - layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false) - setHasFixedSize(true) - // Add scroll listener to detect when the last item is reached - addOnScrollListener(object : RecyclerView.OnScrollListener() { - override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { - super.onScrolled(recyclerView, dx, dy) - - val layoutManager = recyclerView.layoutManager as LinearLayoutManager - val totalItemCount = layoutManager.itemCount - val lastVisibleItem = layoutManager.findLastVisibleItemPosition() - // Check if the user is about to reach the last item - if (!isDataLoading && - totalItemCount <= lastVisibleItem + VISIBLE_ITEMS_THRESHOLD - LOADING_ITEMS_BEFORE - ) { - // Load more data when the last item is almost visible - loadMoreSearchResult() - } - } - }) - } - viewLifecycleOwner.lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { - searchViewModel.effects.collect { it.invokeWith(this@SearchFragment.coreMainActivity) } + composeView?.apply { + setContent { + SearchScreen( + searchScreenState.value, + actionMenuItems(), + isDataLoading.value + ) + DialogHost(dialogShower as AlertDialogShower) } } - handleBackPress() - fragmentSearchBinding?.root?.addView( - requireContext().getDialogHostComposeView(dialogShower as AlertDialogShower) - ) searchViewModel.setAlertDialogShower(dialogShower as AlertDialogShower) + observeViewModelData() + handleSearchArgument() + handleBackPress() + } + + private fun handleSearchArgument() { + val searchStringFromArguments = arguments?.getString(NAV_ARG_SEARCH_STRING) + if (searchStringFromArguments != null) { + onSearchValueChanged(searchStringFromArguments) + } + val argsCopy = Bundle(arguments) + searchViewModel.actions.trySend(Action.CreatedWithArguments(argsCopy)).isSuccess + arguments?.remove(EXTRA_IS_WIDGET_VOICE) + } + + private fun observeViewModelData() { + viewLifecycleOwner.lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + launch { + searchViewModel.effects.collect { effect -> + effect.invokeWith(this@SearchFragment.coreMainActivity) + } + } + + launch { + searchViewModel.state.collect { state -> + render(state) + } + } + } + } + searchViewModel.voiceSearchResult.observe(viewLifecycleOwner) { searchTerm -> + searchTerm?.let { + onSearchValueChanged(it) + } + } } /** * Loads more search results and appends them to the existing search results list in the RecyclerView. * This function is typically triggered when the RecyclerView is near about its last item. */ - @SuppressLint("CheckResult") private fun loadMoreSearchResult() { - if (isDataLoading) return - isDataLoading = true - val safeStartIndex = searchAdapter?.itemCount ?: 0 + if (isDataLoading.value) return + isDataLoading.value = true + val searchList = searchScreenState.value.searchList // Show a loading indicator while data is being loaded - fragmentSearchBinding?.loadingMoreDataIndicator?.isShowing(true) + searchScreenState.update { copy(shouldShowLoadingMoreProgressBar = true) } lifecycleScope.launch { // Request more search results from the ViewModel, providing the start // index and existing results - searchViewModel.loadMoreSearchResults(safeStartIndex, searchAdapter?.items) + searchViewModel.loadMoreSearchResults(searchList.size, searchList) .let { searchResults -> // Hide the loading indicator when data loading is complete - fragmentSearchBinding?.loadingMoreDataIndicator?.isShowing(false) + searchScreenState.update { copy(shouldShowLoadingMoreProgressBar = false) } // Update data loading status based on the received search results - isDataLoading = when { + isDataLoading.value = when { searchResults == null -> true searchResults.isEmpty() -> false else -> { // Append the new search results to the existing list - searchAdapter?.addData(searchResults) + searchScreenState.update { + copy(searchList = searchScreenState.value.searchList + searchResults) + } false } } @@ -203,34 +215,13 @@ class SearchFragment : BaseFragment() { ) } - @Suppress("UnnecessarySafeCall") - private fun setupToolbar(view: View) { - view.post { - with(activity as? CoreMainActivity) { - this?.setSupportActionBar(view.findViewById(R.id.toolbar)) - this?.supportActionBar?.apply { - setHomeButtonEnabled(true) - title = getString(R.string.menu_search_in_text) - } - } - } - } - override fun onDestroyView() { super.onDestroyView() renderingJob?.cancel() renderingJob = null activity?.intent?.action = null - searchView?.setOnQueryTextListener(null) - searchView = null - searchInTextMenuItem = null - findInPageTextView = null - searchMenuItem?.setOnActionExpandListener(null) - searchMenuItem = null - fragmentSearchBinding?.searchList?.adapter = null - searchAdapter = null - fragmentSearchBinding?.root?.removeAllViews() - fragmentSearchBinding = null + composeView?.disposeComposition() + composeView = null } private fun goBack() { @@ -238,84 +229,48 @@ class SearchFragment : BaseFragment() { findNavController().popBackStack(readerFragmentResId, false) } - private fun setupMenu() { - (requireActivity() as MenuHost).addMenuProvider( - object : MenuProvider { - override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { - menuInflater.inflate(R.menu.menu_search, menu) - searchMenuItem = menu.findItem(R.id.menu_search) - searchMenuItem?.expandActionView() - searchView = searchMenuItem?.actionView as SearchView - searchView?.apply { - setUpSearchView(requireActivity()) - searchView?.setOnQueryTextListener( - SimpleTextListener { query, isSubmit -> - if (query.isNotEmpty()) { - setIsPageSearchEnabled(true) - when { - isSubmit -> { - // if user press the search/enter button on keyboard, - // try to open the article if present - getSearchListItemForQuery(query)?.let(::onItemClick) - } - - else -> searchViewModel.actions.trySend(Filter(query)).isSuccess - } - } else { - setIsPageSearchEnabled(false) - } - } - ) - } - - searchMenuItem?.setOnActionExpandListener( - object : OnActionExpandListener { - override fun onMenuItemActionExpand(item: MenuItem) = false - - override fun onMenuItemActionCollapse(item: MenuItem): Boolean { - searchViewModel.actions.trySend(ExitedSearch).isSuccess - return false - } - } - ) - searchInTextMenuItem = menu.findItem(R.id.menu_searchintext) - findInPageTextView = - searchInTextMenuItem?.actionView?.findViewById(R.id.find_in_page_text_view) - searchInTextMenuItem?.actionView?.setOnClickListener { - searchViewModel.actions.trySend(ClickedSearchInText).isSuccess - } - viewLifecycleOwner.lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { - searchViewModel.state.collect { render(it) } - } - } - val searchStringFromArguments = arguments?.getString(NAV_ARG_SEARCH_STRING) - if (searchStringFromArguments != null) { - searchView?.setQuery(searchStringFromArguments, false) - } - searchViewModel.actions.trySend(Action.CreatedWithArguments(arguments)).isSuccess - arguments?.remove(EXTRA_IS_WIDGET_VOICE) - searchViewModel.voiceSearchResult.observe(viewLifecycleOwner) { searchTerm -> - searchTerm?.let { - searchView?.setQuery(it, false) - } - } - } - - override fun onMenuItemSelected(menuItem: MenuItem) = true - }, - viewLifecycleOwner, - Lifecycle.State.RESUMED - ) - } - private fun getSearchListItemForQuery(query: String): SearchListItem? = - searchAdapter?.items?.firstOrNull { + searchScreenState.value.searchList.firstOrNull { it.value.equals(query, ignoreCase = true) } + private fun onSearchClear() { + searchScreenState.update { copy(searchText = "") } + setIsPageSearchEnabled("") + searchEntryForSearchTerm("") + } + + private fun onSearchValueChanged(searchText: String) { + searchScreenState.update { copy(searchText = searchText) } + setIsPageSearchEnabled(searchText) + searchEntryForSearchTerm(searchText) + } + + private fun searchEntryForSearchTerm(searchText: String) { + searchViewModel.actions.trySend(Filter(searchText)).isSuccess + } + + private fun actionMenuItems() = listOfNotNull( + // Check if the `FIND_IN_PAGE` is visible or not. + // If visible then show it in menu. + if (findInPageMenuItem.value.second) { + ActionMenuItem( + contentDescription = R.string.menu_search_in_text, + onClick = { + searchViewModel.actions.trySend(ClickedSearchInText).isSuccess + }, + testingTag = FIND_IN_PAGE_TESTING_TAG, + iconButtonText = R.string.menu_search_in_text, + isEnabled = findInPageMenuItem.value.first + ) + } else { + // If `FIND_IN_PAGE` is not visible return null so that it will not show on the menu item. + null + } + ) + @Suppress("InjectDispatcher") - suspend fun render(state: SearchState) { + private suspend fun render(state: SearchState) { renderingJob?.apply { // cancel the children job. Since we are getting the result on IO thread // with `withContext` that is child for this job @@ -334,11 +289,10 @@ class SearchFragment : BaseFragment() { if (!isVisible) { return } - isDataLoading = false - searchInTextMenuItem?.actionView?.isVisible = state.searchOrigin == FromWebView - setIsPageSearchEnabled(state.searchTerm.isNotBlank()) - - fragmentSearchBinding?.searchLoadingIndicator?.isShowing(true) + isDataLoading.value = false + findInPageMenuItem.value = findInPageMenuItem.value.first to (state.searchOrigin == FromWebView) + setIsPageSearchEnabled(state.searchTerm) + searchScreenState.update { copy(isLoading = true) } renderingJob = lifecycleScope.launch { try { @@ -347,10 +301,11 @@ class SearchFragment : BaseFragment() { state.getVisibleResults(0, coroutineContext[Job]) } - fragmentSearchBinding?.searchLoadingIndicator?.isShowing(false) + searchScreenState.update { copy(isLoading = false) } searchResult?.let { - fragmentSearchBinding?.searchNoResults?.isVisible = it.isEmpty() - searchAdapter?.items = it + searchScreenState.update { + copy(searchList = it) + } } } catch (ignore: CancellationException) { Log.e("SEARCH_RESULT", "Cancelled the previous job ${ignore.message}") @@ -360,19 +315,13 @@ class SearchFragment : BaseFragment() { "Error in getting searched result\nOriginal exception ${ignore.message}" ) } finally { - fragmentSearchBinding?.searchLoadingIndicator?.isShowing(false) + searchScreenState.update { copy(isLoading = false) } } } } - private fun setIsPageSearchEnabled(isEnabled: Boolean) { - searchInTextMenuItem?.actionView?.isEnabled = isEnabled - findInPageTextView?.alpha = - if (isEnabled) { - ENABLED_SEARCH_IN_TEXT_OPACITY - } else { - DISABLED_SEARCH_IN_TEXT_OPACITY - } + private fun setIsPageSearchEnabled(searchText: String) { + findInPageMenuItem.value = searchText.isNotBlank() to findInPageMenuItem.value.second } private fun onItemClick(it: SearchListItem) { @@ -388,14 +337,12 @@ class SearchFragment : BaseFragment() { @Suppress("DEPRECATION") override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) - searchViewModel.actions.trySend(ActivityResultReceived(requestCode, resultCode, data)).isSuccess - } -} - -private fun ContentLoadingProgressBar.isShowing(show: Boolean) { - if (show) { - show() - } else { - hide() + searchViewModel.actions.trySend( + ActivityResultReceived( + requestCode, + resultCode, + data + ) + ).isSuccess } } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/search/adapter/SearchListItem.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/search/SearchListItem.kt similarity index 91% rename from core/src/main/java/org/kiwix/kiwixmobile/core/search/adapter/SearchListItem.kt rename to core/src/main/java/org/kiwix/kiwixmobile/core/search/SearchListItem.kt index 84777e941..2d9c558c4 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/search/adapter/SearchListItem.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/search/SearchListItem.kt @@ -1,6 +1,6 @@ /* * Kiwix Android - * Copyright (c) 2020 Kiwix + * Copyright (c) 2025 Kiwix * This program 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 @@ -16,7 +16,7 @@ * */ -package org.kiwix.kiwixmobile.core.search.adapter +package org.kiwix.kiwixmobile.core.search sealed class SearchListItem { abstract val value: String diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/search/SearchScreen.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/search/SearchScreen.kt new file mode 100644 index 000000000..658442fb7 --- /dev/null +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/search/SearchScreen.kt @@ -0,0 +1,264 @@ +/* + * Kiwix Android + * Copyright (c) 2025 Kiwix + * This program 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 org.kiwix.kiwixmobile.core.search + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTag +import androidx.compose.ui.text.style.TextAlign +import org.kiwix.kiwixmobile.core.R +import org.kiwix.kiwixmobile.core.ui.components.ContentLoadingProgressBar +import org.kiwix.kiwixmobile.core.ui.components.KiwixAppBar +import org.kiwix.kiwixmobile.core.ui.components.KiwixSearchView +import org.kiwix.kiwixmobile.core.ui.models.ActionMenuItem +import org.kiwix.kiwixmobile.core.ui.theme.KiwixTheme +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.EIGHT_DP +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.FOUR_DP +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.LOAD_MORE_PROGRESS_BAR_SIZE +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.MINIMUM_HEIGHT_OF_SEARCH_ITEM +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SEARCH_ITEM_TEXT_SIZE +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SIXTEEN_DP +import org.kiwix.kiwixmobile.core.utils.ComposeDimens.THREE_DP + +const val SEARCH_FIELD_TESTING_TAG = "searchFieldTestingTag" +const val NO_SEARCH_RESULT_TESTING_TAG = "noSearchResultTestingTag" +const val FIND_IN_PAGE_TESTING_TAG = "findInPageTestingTag" +const val SEARCH_ITEM_TESTING_TAG = "searchItemTestingTag" +const val LOADING_ITEMS_BEFORE = 3 + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SearchScreen( + searchScreenState: SearchScreenState, + actionMenuItemList: List, + isLoadingMoreResult: Boolean +) { + val lazyListState = rememberLazyListState() + KiwixTheme { + Scaffold( + topBar = { + KiwixAppBar( + titleId = R.string.empty_string, + navigationIcon = searchScreenState.navigationIcon, + actionMenuItems = actionMenuItemList, + searchBar = { + KiwixSearchView( + value = searchScreenState.searchText, + searchViewTextFiledTestTag = SEARCH_FIELD_TESTING_TAG, + onValueChange = searchScreenState.onSearchViewValueChange, + onClearClick = searchScreenState.onSearchViewClearClick, + modifier = Modifier, + onKeyboardSubmitButtonClick = searchScreenState.onKeyboardSubmitButtonClick + ) + } + ) + } + ) { innerPadding -> + SearchScreenContent(searchScreenState, innerPadding, lazyListState) + } + } + InfiniteListHandler( + listState = lazyListState, + isLoadingMoreResult = isLoadingMoreResult, + onLoadMore = searchScreenState.onLoadMore + ) +} + +@Composable +private fun SearchScreenContent( + searchScreenState: SearchScreenState, + innerPadding: PaddingValues, + lazyListState: LazyListState +) { + val progressBarTrackColor = MaterialTheme.colorScheme.background + Box( + modifier = Modifier + .fillMaxSize() + .padding( + top = innerPadding.calculateTopPadding(), + start = innerPadding.calculateStartPadding(LocalLayoutDirection.current), + end = innerPadding.calculateEndPadding(LocalLayoutDirection.current) + ), + contentAlignment = Alignment.Center + ) { + if (searchScreenState.searchList.isEmpty()) { + NoSearchResultView() + } else { + LazyColumn( + modifier = Modifier + .fillMaxSize(), + state = lazyListState + ) { + items(searchScreenState.searchList) { item -> + SearchListItem( + searchListItem = item, + onItemClick = { searchScreenState.onItemClick(item) }, + onNewTabIconClick = { searchScreenState.onNewTabIconClick(item) }, + onItemLongClick = if (item is SearchListItem.RecentSearchListItem) { + { searchScreenState.onItemLongClick(item) } + } else { + null + } + ) + } + showLoadMoreProgressBar(searchScreenState, progressBarTrackColor) + } + } + ShowLoadingProgressBar(searchScreenState.isLoading, progressBarTrackColor) + } +} + +private fun LazyListScope.showLoadMoreProgressBar( + searchScreenState: SearchScreenState, + progressBarTrackColor: Color +) { + if (searchScreenState.shouldShowLoadingMoreProgressBar) { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(SIXTEEN_DP), + contentAlignment = Alignment.Center + ) { + ContentLoadingProgressBar( + modifier = Modifier.size(LOAD_MORE_PROGRESS_BAR_SIZE), + circularProgressBarStockWidth = THREE_DP, + progressBarTrackColor = progressBarTrackColor + ) + } + } + } +} + +@Composable +private fun ShowLoadingProgressBar(isLoading: Boolean, progressBarTrackColor: Color) { + if (isLoading) { + Box( + modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + ContentLoadingProgressBar(progressBarTrackColor = progressBarTrackColor) + } + } +} + +@Composable +private fun NoSearchResultView() { + Text( + text = stringResource(R.string.no_results), + textAlign = TextAlign.Center, + modifier = Modifier + .padding(horizontal = FOUR_DP) + .semantics { testTag = NO_SEARCH_RESULT_TESTING_TAG } + ) +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun SearchListItem( + searchListItem: SearchListItem, + onNewTabIconClick: (SearchListItem) -> Unit, + onItemClick: (SearchListItem) -> Unit, + onItemLongClick: ((SearchListItem) -> Unit)? = null +) { + Row( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = MINIMUM_HEIGHT_OF_SEARCH_ITEM), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = searchListItem.value, + modifier = Modifier + .weight(1f) + .padding(horizontal = EIGHT_DP) + .combinedClickable( + onClick = { onItemClick(searchListItem) }, + onLongClick = { onItemLongClick?.invoke(searchListItem) } + ) + .semantics { testTag = SEARCH_ITEM_TESTING_TAG }, + fontSize = SEARCH_ITEM_TEXT_SIZE, + ) + + IconButton( + onClick = { onNewTabIconClick(searchListItem) }, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_open_in_new_24dp), + contentDescription = stringResource(id = R.string.search_open_in_new_tab), + ) + } + } +} + +@Composable +fun InfiniteListHandler( + listState: LazyListState, + buffer: Int = LOADING_ITEMS_BEFORE, + isLoadingMoreResult: Boolean, + onLoadMore: () -> Unit +) { + val shouldLoadMore = remember { + derivedStateOf { + val lastVisibleItemIndex = listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0 + val totalItemCount = listState.layoutInfo.totalItemsCount + !isLoadingMoreResult && lastVisibleItemIndex >= totalItemCount - buffer + } + } + + LaunchedEffect(shouldLoadMore) { + snapshotFlow { shouldLoadMore.value }.collect { load -> + if (load) onLoadMore() + } + } +} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/search/SearchScreenState.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/search/SearchScreenState.kt new file mode 100644 index 000000000..9b9e77eca --- /dev/null +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/search/SearchScreenState.kt @@ -0,0 +1,72 @@ +/* + * Kiwix Android + * Copyright (c) 2025 Kiwix + * This program 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 org.kiwix.kiwixmobile.core.search + +import androidx.compose.runtime.Composable + +data class SearchScreenState( + /** + * Manages the search screen list state. + */ + val searchList: List, + /** + * Manages the showing of loading progress at the initial. + */ + val isLoading: Boolean, + /** + * Manages the showing of load more progress at the end of search list. + */ + val shouldShowLoadingMoreProgressBar: Boolean, + /** + * Handles the calling for more items. + */ + val onLoadMore: () -> Unit, + /** + * Stores the searchView text, and displayed it inside the searchView. + */ + val searchText: String, + /** + * Handles the click on searchView's close button. + */ + val onSearchViewClearClick: () -> Unit, + /** + * Handles the changing of searchView values. + */ + val onSearchViewValueChange: (String) -> Unit, + /** + * Handles the item click on searchItem + */ + val onItemClick: (SearchListItem) -> Unit, + /** + * Handles the long click on searchItem. + */ + val onItemLongClick: (SearchListItem) -> Unit, + /** + * Handles the newTabIcon click. + */ + val onNewTabIconClick: (SearchListItem) -> Unit, + /** + * Handles the Keyboard submit button click. + */ + val onKeyboardSubmitButtonClick: (String) -> Unit, + /** + * Manages the navigationIcon shown in the toolbar. + */ + val navigationIcon: @Composable() () -> Unit +) diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/search/adapter/SearchAdapter.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/search/adapter/SearchAdapter.kt deleted file mode 100644 index 2c981c8ef..000000000 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/search/adapter/SearchAdapter.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Kiwix Android - * Copyright (c) 2020 Kiwix - * This program 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 org.kiwix.kiwixmobile.core.search.adapter - -import org.kiwix.kiwixmobile.core.base.adapter.AdapterDelegate -import org.kiwix.kiwixmobile.core.base.adapter.BaseDelegateAdapter - -class SearchAdapter( - vararg delegates: AdapterDelegate -) : BaseDelegateAdapter(*delegates) { - override fun getIdFor(item: SearchListItem) = item.value.hashCode().toLong() -} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/search/adapter/SearchDelegate.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/search/adapter/SearchDelegate.kt deleted file mode 100644 index 947302d08..000000000 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/search/adapter/SearchDelegate.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Kiwix Android - * Copyright (c) 2020 Kiwix - * This program 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 org.kiwix.kiwixmobile.core.search.adapter - -import android.view.ViewGroup -import org.kiwix.kiwixmobile.core.base.adapter.AbsDelegateAdapter -import org.kiwix.kiwixmobile.core.databinding.ListItemSearchBinding -import org.kiwix.kiwixmobile.core.extensions.ViewGroupExtensions.viewBinding -import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.RecentSearchListItem -import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.ZimSearchResultListItem -import org.kiwix.kiwixmobile.core.search.adapter.SearchViewHolder.RecentSearchViewHolder -import org.kiwix.kiwixmobile.core.search.adapter.SearchViewHolder.ZimSearchResultViewHolder - -sealed class SearchDelegate> : - AbsDelegateAdapter { - class RecentSearchDelegate( - private val onClickListener: (SearchListItem) -> Unit, - private val onClickListenerNewTab: (SearchListItem) -> Unit, - private val onLongClickListener: (SearchListItem) -> Unit - ) : SearchDelegate() { - override val itemClass = RecentSearchListItem::class.java - - override fun createViewHolder(parent: ViewGroup) = - RecentSearchViewHolder( - parent.viewBinding(ListItemSearchBinding::inflate, false), - onClickListener, - onClickListenerNewTab, - onLongClickListener - ) - } - - class ZimSearchResultDelegate( - private val onClickListener: (SearchListItem) -> Unit, - private val onClickListenerNewTab: (SearchListItem) -> Unit - ) : SearchDelegate() { - override val itemClass = ZimSearchResultListItem::class.java - - override fun createViewHolder(parent: ViewGroup) = - ZimSearchResultViewHolder( - parent.viewBinding(ListItemSearchBinding::inflate, false), - onClickListener, - onClickListenerNewTab - ) - } -} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/search/adapter/SearchViewHolder.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/search/adapter/SearchViewHolder.kt deleted file mode 100644 index c2af51565..000000000 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/search/adapter/SearchViewHolder.kt +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Kiwix Android - * Copyright (c) 2020 Kiwix - * This program 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 org.kiwix.kiwixmobile.core.search.adapter - -import android.view.View -import org.kiwix.kiwixmobile.core.base.adapter.BaseViewHolder -import org.kiwix.kiwixmobile.core.databinding.ListItemSearchBinding -import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.RecentSearchListItem -import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.ZimSearchResultListItem - -sealed class SearchViewHolder(containerView: View) : - BaseViewHolder(containerView) { - class RecentSearchViewHolder( - private val listItemSearchBinding: ListItemSearchBinding, - private val onClickListener: (SearchListItem) -> Unit, - private val onClickListenerNewTab: (SearchListItem) -> Unit, - private val onLongClickListener: (SearchListItem) -> Unit - ) : SearchViewHolder(listItemSearchBinding.root) { - override fun bind(item: RecentSearchListItem) { - containerView.setOnClickListener { onClickListener(item) } - containerView.setOnLongClickListener { - onLongClickListener(item) - true - } - listItemSearchBinding.listItemSearchNewTabButton.setOnClickListener { - onClickListenerNewTab( - item - ) - } - listItemSearchBinding.listItemSearchText.text = item.value - } - } - - class ZimSearchResultViewHolder( - private val listItemSearchBinding: ListItemSearchBinding, - private val onClickListener: (SearchListItem) -> Unit, - private val onClickListenerNewTab: (SearchListItem) -> Unit - ) : SearchViewHolder(listItemSearchBinding.root) { - override fun bind(item: ZimSearchResultListItem) { - containerView.setOnClickListener { onClickListener(item) } - listItemSearchBinding.listItemSearchNewTabButton.setOnClickListener { - onClickListenerNewTab( - item - ) - } - listItemSearchBinding.listItemSearchText.text = item.value - } - } -} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/Action.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/Action.kt index 1a06f1fb1..98e09fe43 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/Action.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/Action.kt @@ -20,7 +20,7 @@ package org.kiwix.kiwixmobile.core.search.viewmodel import android.content.Intent import android.os.Bundle -import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem +import org.kiwix.kiwixmobile.core.search.SearchListItem sealed class Action { object ExitedSearch : Action() diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchState.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchState.kt index 53ba5cb1c..ea7074752 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchState.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchState.kt @@ -21,7 +21,7 @@ package org.kiwix.kiwixmobile.core.search.viewmodel import kotlinx.coroutines.Job import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.yield -import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem +import org.kiwix.kiwixmobile.core.search.SearchListItem import org.kiwix.kiwixmobile.core.utils.files.Log data class SearchState( diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchViewModel.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchViewModel.kt index dc9c8a89c..4ce1d1214 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchViewModel.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchViewModel.kt @@ -36,7 +36,7 @@ import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.base.SideEffect import org.kiwix.kiwixmobile.core.dao.RecentSearchRoomDao import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer -import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem +import org.kiwix.kiwixmobile.core.search.SearchListItem import org.kiwix.kiwixmobile.core.search.viewmodel.Action.ActivityResultReceived import org.kiwix.kiwixmobile.core.search.viewmodel.Action.ClickedSearchInText import org.kiwix.kiwixmobile.core.search.viewmodel.Action.ConfirmedDelete @@ -84,7 +84,7 @@ class SearchViewModel @Inject constructor( FromWebView ) val state: MutableStateFlow = MutableStateFlow(initialState) - private val _effects = Channel>() + private val _effects = Channel>(Channel.UNLIMITED) val effects = _effects.receiveAsFlow() val actions = Channel(Channel.UNLIMITED) private val filter = MutableStateFlow("") diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/DeleteRecentSearch.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/DeleteRecentSearch.kt index e27ed00c1..a1a0edb12 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/DeleteRecentSearch.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/DeleteRecentSearch.kt @@ -24,7 +24,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.kiwix.kiwixmobile.core.base.SideEffect import org.kiwix.kiwixmobile.core.dao.RecentSearchRoomDao -import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem +import org.kiwix.kiwixmobile.core.search.SearchListItem @Suppress("InjectDispatcher") data class DeleteRecentSearch( diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/OpenSearchItem.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/OpenSearchItem.kt index ed25f9720..6f63a8337 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/OpenSearchItem.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/OpenSearchItem.kt @@ -27,7 +27,7 @@ import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.setNavigationRes import org.kiwix.kiwixmobile.core.main.CoreMainActivity import org.kiwix.kiwixmobile.core.main.SEARCH_ITEM_TITLE_KEY import org.kiwix.kiwixmobile.core.reader.addContentPrefix -import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem +import org.kiwix.kiwixmobile.core.search.SearchListItem import org.kiwix.kiwixmobile.core.utils.TAG_FILE_SEARCHED data class OpenSearchItem( diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/SaveSearchToRecents.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/SaveSearchToRecents.kt index 3d26a5d08..83e13c0fc 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/SaveSearchToRecents.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/SaveSearchToRecents.kt @@ -25,7 +25,7 @@ import kotlinx.coroutines.launch import org.kiwix.kiwixmobile.core.base.SideEffect import org.kiwix.kiwixmobile.core.dao.RecentSearchRoomDao import org.kiwix.kiwixmobile.core.reader.addContentPrefix -import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem +import org.kiwix.kiwixmobile.core.search.SearchListItem @Suppress("InjectDispatcher") data class SaveSearchToRecents( diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/ShowDeleteSearchDialog.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/ShowDeleteSearchDialog.kt index 4d771fa7b..a357c26f2 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/ShowDeleteSearchDialog.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/ShowDeleteSearchDialog.kt @@ -22,7 +22,7 @@ import androidx.appcompat.app.AppCompatActivity import kotlinx.coroutines.channels.Channel import org.kiwix.kiwixmobile.core.base.SideEffect import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.cachedComponent -import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem +import org.kiwix.kiwixmobile.core.search.SearchListItem import org.kiwix.kiwixmobile.core.search.viewmodel.Action import org.kiwix.kiwixmobile.core.utils.dialog.DialogShower import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog.DeleteSearch diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/StartSpeechInput.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/StartSpeechInput.kt index 177ffea9f..d35e8e649 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/StartSpeechInput.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/StartSpeechInput.kt @@ -46,7 +46,7 @@ data class StartSpeechInput(private val actions: Channel) : SideEffect) { Row { actionMenuItems.forEach { menuItem -> - IconButton( - enabled = menuItem.isEnabled, - onClick = menuItem.onClick, - modifier = menuItem.modifier.testTag(menuItem.testingTag) - ) { - Icon( - painter = menuItem.icon.toPainter(), - contentDescription = stringResource(menuItem.contentDescription), - tint = if (menuItem.isEnabled) menuItem.iconTint else Color.Gray - ) + val modifier = menuItem.modifier.testTag(menuItem.testingTag) + // If icon is not null show the icon. + menuItem.icon?.let { + IconButton( + enabled = menuItem.isEnabled, + onClick = menuItem.onClick, + modifier = modifier + ) { + Icon( + painter = it.toPainter(), + contentDescription = stringResource(menuItem.contentDescription), + tint = if (menuItem.isEnabled) menuItem.iconTint else Color.Gray + ) + } + } ?: run { + // Else show the textView button in menuItem. + TextButton( + enabled = menuItem.isEnabled, + onClick = menuItem.onClick, + modifier = modifier + ) { + Text( + text = stringResource(id = menuItem.iconButtonText).uppercase(), + color = if (menuItem.isEnabled) Color.White else Color.Gray, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } } } } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/KiwixSearchView.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/KiwixSearchView.kt index cace7b3b0..b71248743 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/KiwixSearchView.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/KiwixSearchView.kt @@ -18,6 +18,8 @@ package org.kiwix.kiwixmobile.core.ui.components +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Text @@ -30,10 +32,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.ImeAction import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.utils.ComposeDimens @@ -45,8 +49,10 @@ fun KiwixSearchView( searchViewTextFiledTestTag: String = "", clearButtonTestTag: String = "", onValueChange: (String) -> Unit, - onClearClick: () -> Unit + onClearClick: () -> Unit, + onKeyboardSubmitButtonClick: (String) -> Unit = {} ) { + val keyboardController = LocalSoftwareKeyboardController.current val colors = TextFieldDefaults.colors( focusedIndicatorColor = Color.Transparent, unfocusedIndicatorColor = Color.Transparent, @@ -89,6 +95,15 @@ fun KiwixSearchView( ) } } - } + }, + keyboardOptions = KeyboardOptions.Default.copy( + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onDone = { + keyboardController?.hide() + onKeyboardSubmitButtonClick.invoke(value) + } + ) ) } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/NavigationIcon.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/NavigationIcon.kt index c652ca8ed..6ab8fde87 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/NavigationIcon.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/ui/components/NavigationIcon.kt @@ -24,13 +24,18 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTag import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.ui.models.IconItem import org.kiwix.kiwixmobile.core.ui.models.toPainter import org.kiwix.kiwixmobile.core.ui.theme.White +const val NAVIGATION_ICON_TESTING_TAG = "navigationIconTestingTag" + /** * A composable function that renders a navigation icon, which can be either a vector * or drawable image. @@ -47,9 +52,10 @@ fun NavigationIcon( iconItem: IconItem = IconItem.Vector(Icons.AutoMirrored.Filled.ArrowBack), onClick: () -> Unit, @StringRes contentDescription: Int = R.string.toolbar_back_button_content_description, - iconTint: Color = White + iconTint: Color = White, + testingTag: String = NAVIGATION_ICON_TESTING_TAG ) { - IconButton(onClick = onClick) { + IconButton(onClick = onClick, modifier = Modifier.semantics { testTag = testingTag }) { Icon( painter = iconItem.toPainter(), contentDescription = stringResource(contentDescription), diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/ui/models/ActionMenuItem.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/ui/models/ActionMenuItem.kt index 45ad8eec1..b367aa273 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/ui/models/ActionMenuItem.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/ui/models/ActionMenuItem.kt @@ -21,14 +21,16 @@ package org.kiwix.kiwixmobile.core.ui.models import androidx.annotation.StringRes import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.ui.theme.White data class ActionMenuItem( - val icon: IconItem, + val icon: IconItem? = null, @StringRes val contentDescription: Int, val onClick: () -> Unit, val iconTint: Color = White, val isEnabled: Boolean = true, + @StringRes val iconButtonText: Int = R.string.empty_string, val testingTag: String, val modifier: Modifier = Modifier ) diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/ComposeDimens.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/ComposeDimens.kt index 2c236895d..f6e9c4224 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/ComposeDimens.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/ComposeDimens.kt @@ -35,8 +35,8 @@ object ComposeDimens { // Error screen dimens val CRASH_IMAGE_SIZE = 70.dp - // KiwixAppBar(Toolbar) height - val KIWIX_APP_BAR_HEIGHT = 56.dp + // KiwixAppBar(Toolbar) dimens + val ACTION_MENU_TEXTVIEW_BUTTON_PADDING = 13.dp // Padding & Margins val SIXTY_DP = 60.dp @@ -51,6 +51,7 @@ object ComposeDimens { val SIX_DP = 6.dp val FIVE_DP = 5.dp val FOUR_DP = 4.dp + val THREE_DP = 3.dp val TWO_DP = 2.dp val ONE_DP = 1.dp val SEVENTY_DP = 70.dp @@ -169,4 +170,9 @@ object ComposeDimens { val DOWNLOADING_LIBRARY_MESSAGE_TEXT_SIZE = 8.sp val DOWNLOADING_LIBRARY_PROGRESSBAR_SIZE = 30.dp const val ONLINE_BOOK_DISABLED_COLOR_ALPHA = 0.5F + + // Search screen dimens + val MINIMUM_HEIGHT_OF_SEARCH_ITEM = 64.dp + val SEARCH_ITEM_TEXT_SIZE = 16.sp + val LOAD_MORE_PROGRESS_BAR_SIZE = 40.dp } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/SimpleRecyclerViewScrollListener.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/SimpleRecyclerViewScrollListener.kt deleted file mode 100644 index 7c4d307d9..000000000 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/SimpleRecyclerViewScrollListener.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Kiwix Android - * Copyright (c) 2021 Kiwix - * This program 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 org.kiwix.kiwixmobile.core.utils - -import androidx.recyclerview.widget.RecyclerView - -class SimpleRecyclerViewScrollListener( - private val onLayoutScrollListener: (RecyclerView, Int) -> Unit -) : - RecyclerView.OnScrollListener() { - override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { - super.onScrollStateChanged(recyclerView, newState) - onLayoutScrollListener( - recyclerView, - newState - ) - } - - override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { - super.onScrolled(recyclerView, dx, dy) - val currentScrollPosition = recyclerView.computeVerticalScrollOffset() - - if (currentScrollPosition > previousScrollPosition) { - onLayoutScrollListener( - recyclerView, - SCROLL_DOWN - ) - } else if (currentScrollPosition < previousScrollPosition) { - onLayoutScrollListener( - recyclerView, - SCROLL_UP - ) - } - - previousScrollPosition = currentScrollPosition - } - - private var previousScrollPosition = 0 - - companion object { - const val SCROLL_DOWN = 2000 - const val SCROLL_UP = 2001 - } -} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/SimpleTextListener.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/SimpleTextListener.kt deleted file mode 100644 index b94abcf24..000000000 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/SimpleTextListener.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Kiwix Android - * Copyright (c) 2020 Kiwix - * This program 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 org.kiwix.kiwixmobile.core.utils - -import androidx.appcompat.widget.SearchView.OnQueryTextListener - -class SimpleTextListener(private val onQueryTextChangeAction: (String, Boolean) -> Unit) : - OnQueryTextListener { - override fun onQueryTextSubmit(s: String): Boolean { - onQueryTextChangeAction.invoke(s, true) - return true - } - - override fun onQueryTextChange(s: String): Boolean { - onQueryTextChangeAction.invoke(s, false) - return true - } -} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/zim_manager/TagsView.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/zim_manager/TagsView.kt deleted file mode 100644 index b1a675516..000000000 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/zim_manager/TagsView.kt +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Kiwix Android - * Copyright (c) 2019 Kiwix - * This program 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 org.kiwix.kiwixmobile.core.zim_manager - -import android.content.Context -import android.util.AttributeSet -import android.view.LayoutInflater -import com.google.android.material.chip.Chip -import com.google.android.material.chip.ChipGroup -import org.kiwix.kiwixmobile.core.databinding.TagContentBinding -import org.kiwix.kiwixmobile.core.zim_manager.KiwixTag.Companion.YesNoValueTag -import org.kiwix.kiwixmobile.core.zim_manager.KiwixTag.Companion.YesNoValueTag.DetailsTag -import org.kiwix.kiwixmobile.core.zim_manager.KiwixTag.Companion.YesNoValueTag.PicturesTag -import org.kiwix.kiwixmobile.core.zim_manager.KiwixTag.Companion.YesNoValueTag.VideoTag -import org.kiwix.kiwixmobile.core.zim_manager.KiwixTag.TagValue.YES - -class TagsView(context: Context, attrs: AttributeSet) : ChipGroup(context, attrs) { - private var tagContentBinding: TagContentBinding? = null - - init { - tagContentBinding = TagContentBinding.inflate(LayoutInflater.from(context), this) - } - - override fun onDetachedFromWindow() { - super.onDetachedFromWindow() - tagContentBinding = null - } - - fun render(tags: List) { - tagContentBinding?.tagPicture?.selectBy(tags.isYesOrNotDefined()) - tagContentBinding?.tagVideo?.selectBy(tags.isYesOrNotDefined()) - val shortTextIsSelected = tags.isDefinedAndNo() - tagContentBinding?.tagTextOnly?.selectBy( - tags.isDefinedAndNo() && - tags.isDefinedAndNo() && - !shortTextIsSelected - ) - tagContentBinding?.tagShortText?.selectBy(shortTextIsSelected) - } - - private inline fun List.isYesOrNotDefined() = - isYes() || !isDefined() - - private inline fun List.isDefinedAndNo() = - isDefined() && !isYes() - - private inline fun List.isYes() = - filterIsInstance().getOrNull(0)?.value == YES - - private inline fun List.isDefined() = - filterIsInstance().isNotEmpty() - - private fun Chip.selectBy(criteria: Boolean) { - isChecked = criteria - isEnabled = criteria - visibility = if (criteria) VISIBLE else GONE - } -} diff --git a/core/src/main/res/layout/action_searchintext.xml b/core/src/main/res/layout/action_searchintext.xml deleted file mode 100644 index d3005a440..000000000 --- a/core/src/main/res/layout/action_searchintext.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - diff --git a/core/src/main/res/layout/fragment_search.xml b/core/src/main/res/layout/fragment_search.xml deleted file mode 100644 index 708aa6111..000000000 --- a/core/src/main/res/layout/fragment_search.xml +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/core/src/main/res/layout/layout_toolbar.xml b/core/src/main/res/layout/layout_toolbar.xml index 1d414057f..ab4b33c49 100644 --- a/core/src/main/res/layout/layout_toolbar.xml +++ b/core/src/main/res/layout/layout_toolbar.xml @@ -7,8 +7,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" app:popupTheme="@style/KiwixTheme" - android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar" - tools:showIn="@layout/fragment_search"> + android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar"> - - - - - - - - - diff --git a/core/src/main/res/layout/tag_content.xml b/core/src/main/res/layout/tag_content.xml deleted file mode 100644 index 9b68460b0..000000000 --- a/core/src/main/res/layout/tag_content.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - diff --git a/core/src/main/res/menu/menu_page.xml b/core/src/main/res/menu/menu_page.xml deleted file mode 100644 index ef083bc2f..000000000 --- a/core/src/main/res/menu/menu_page.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - diff --git a/core/src/main/res/menu/menu_search.xml b/core/src/main/res/menu/menu_search.xml deleted file mode 100644 index 1108a0b06..000000000 --- a/core/src/main/res/menu/menu_search.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - diff --git a/core/src/test/java/org/kiwix/kiwixmobile/core/dao/NewRecentSearchDaoTest.kt b/core/src/test/java/org/kiwix/kiwixmobile/core/dao/NewRecentSearchDaoTest.kt index b754cc349..6a524beac 100644 --- a/core/src/test/java/org/kiwix/kiwixmobile/core/dao/NewRecentSearchDaoTest.kt +++ b/core/src/test/java/org/kiwix/kiwixmobile/core/dao/NewRecentSearchDaoTest.kt @@ -30,7 +30,7 @@ import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.kiwix.kiwixmobile.core.dao.entities.RecentSearchEntity import org.kiwix.kiwixmobile.core.dao.entities.RecentSearchEntity_ -import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.RecentSearchListItem +import org.kiwix.kiwixmobile.core.search.SearchListItem.RecentSearchListItem import org.kiwix.kiwixmobile.core.search.viewmodel.test import org.kiwix.sharedFunctions.recentSearchEntity diff --git a/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchStateTest.kt b/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchStateTest.kt index 9aca9dabe..a50e390bd 100644 --- a/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchStateTest.kt +++ b/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchStateTest.kt @@ -27,8 +27,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test -import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem -import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.RecentSearchListItem +import org.kiwix.kiwixmobile.core.search.SearchListItem.RecentSearchListItem import org.kiwix.kiwixmobile.core.search.viewmodel.SearchOrigin.FromWebView internal class SearchStateTest { @@ -123,8 +122,8 @@ internal class SearchStateTest { val searchResultsWithTerm = SearchResultsWithTerm(searchTerm, suggestionSearchWrapper, mockk()) val searchState = SearchState(searchTerm, searchResultsWithTerm, emptyList(), FromWebView) - var list: List? = emptyList() - var list1: List? = emptyList() + var list: List? = emptyList() + var list1: List? = emptyList() val job = launch(Dispatchers.IO) { delay(1000) diff --git a/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchViewModelTest.kt b/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchViewModelTest.kt index 85bf1de35..6b46be41f 100644 --- a/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchViewModelTest.kt +++ b/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchViewModelTest.kt @@ -50,7 +50,7 @@ import org.kiwix.kiwixmobile.core.base.SideEffect import org.kiwix.kiwixmobile.core.dao.RecentSearchRoomDao import org.kiwix.kiwixmobile.core.reader.ZimFileReader import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer -import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.RecentSearchListItem +import org.kiwix.kiwixmobile.core.search.SearchListItem.RecentSearchListItem import org.kiwix.kiwixmobile.core.search.viewmodel.Action.ActivityResultReceived import org.kiwix.kiwixmobile.core.search.viewmodel.Action.ClickedSearchInText import org.kiwix.kiwixmobile.core.search.viewmodel.Action.ConfirmedDelete diff --git a/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/DeleteRecentSearchTest.kt b/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/DeleteRecentSearchTest.kt index 2597b0d12..70ec55c2f 100644 --- a/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/DeleteRecentSearchTest.kt +++ b/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/DeleteRecentSearchTest.kt @@ -27,8 +27,8 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.Test import org.kiwix.kiwixmobile.core.dao.RecentSearchRoomDao -import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem -import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.RecentSearchListItem +import org.kiwix.kiwixmobile.core.search.SearchListItem +import org.kiwix.kiwixmobile.core.search.SearchListItem.RecentSearchListItem internal class DeleteRecentSearchTest { @Test diff --git a/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/OpenSearchItemTest.kt b/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/OpenSearchItemTest.kt index 6c3cb80e4..f943978b4 100644 --- a/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/OpenSearchItemTest.kt +++ b/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/OpenSearchItemTest.kt @@ -28,7 +28,7 @@ import org.junit.jupiter.api.Test import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.setNavigationResultOnCurrent import org.kiwix.kiwixmobile.core.main.CoreMainActivity import org.kiwix.kiwixmobile.core.reader.ZimFileReader -import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.RecentSearchListItem +import org.kiwix.kiwixmobile.core.search.SearchListItem.RecentSearchListItem import org.kiwix.kiwixmobile.core.utils.TAG_FILE_SEARCHED import org.kiwix.kiwixmobile.core.utils.TAG_FILE_SEARCHED_NEW_TAB diff --git a/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/SaveSearchToRecentsTest.kt b/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/SaveSearchToRecentsTest.kt index 974a3dbd1..0220293aa 100644 --- a/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/SaveSearchToRecentsTest.kt +++ b/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/SaveSearchToRecentsTest.kt @@ -29,7 +29,7 @@ import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.Test import org.kiwix.kiwixmobile.core.dao.RecentSearchRoomDao import org.kiwix.kiwixmobile.core.reader.ZimFileReader -import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.RecentSearchListItem +import org.kiwix.kiwixmobile.core.search.SearchListItem.RecentSearchListItem internal class SaveSearchToRecentsTest { private val newRecentSearchDao: RecentSearchRoomDao = mockk() diff --git a/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/ShowDeleteSearchDialogTest.kt b/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/ShowDeleteSearchDialogTest.kt index 65729ab26..0a1ace17d 100644 --- a/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/ShowDeleteSearchDialogTest.kt +++ b/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/ShowDeleteSearchDialogTest.kt @@ -25,7 +25,7 @@ import io.mockk.verify import kotlinx.coroutines.channels.Channel import org.junit.jupiter.api.Test import org.kiwix.kiwixmobile.core.main.CoreMainActivity -import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.RecentSearchListItem +import org.kiwix.kiwixmobile.core.search.SearchListItem.RecentSearchListItem import org.kiwix.kiwixmobile.core.search.viewmodel.Action import org.kiwix.kiwixmobile.core.search.viewmodel.Action.ConfirmedDelete import org.kiwix.kiwixmobile.core.utils.dialog.DialogShower diff --git a/custom/src/androidTest/java/org/kiwix/kiwixmobile/custom/search/SearchFragmentTestForCustomApp.kt b/custom/src/androidTest/java/org/kiwix/kiwixmobile/custom/search/SearchFragmentTestForCustomApp.kt index a9a3f1d9a..d44009ea1 100644 --- a/custom/src/androidTest/java/org/kiwix/kiwixmobile/custom/search/SearchFragmentTestForCustomApp.kt +++ b/custom/src/androidTest/java/org/kiwix/kiwixmobile/custom/search/SearchFragmentTestForCustomApp.kt @@ -21,6 +21,7 @@ package org.kiwix.kiwixmobile.custom.search import android.Manifest import android.content.Context import android.content.res.AssetFileDescriptor +import androidx.compose.ui.test.junit4.createComposeRule import androidx.core.content.edit import androidx.lifecycle.Lifecycle import androidx.navigation.fragment.NavHostFragment @@ -52,6 +53,8 @@ import org.kiwix.kiwixmobile.core.search.SearchFragment import org.kiwix.kiwixmobile.core.search.viewmodel.Action import org.kiwix.kiwixmobile.core.utils.LanguageUtils import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil +import org.kiwix.kiwixmobile.core.utils.TestingUtils.COMPOSE_TEST_RULE_ORDER +import org.kiwix.kiwixmobile.core.utils.TestingUtils.RETRY_RULE_ORDER import org.kiwix.kiwixmobile.custom.main.CustomMainActivity import org.kiwix.kiwixmobile.custom.main.CustomReaderFragment import org.kiwix.kiwixmobile.custom.testutils.RetryRule @@ -80,10 +83,13 @@ class SearchFragmentTestForCustomApp { InstrumentationRegistry.getInstrumentation().targetContext.applicationContext } - @Rule + @Rule(order = RETRY_RULE_ORDER) @JvmField var retryRule = RetryRule() + @get:Rule(order = COMPOSE_TEST_RULE_ORDER) + val composeTestRule = createComposeRule() + private lateinit var customMainActivity: CustomMainActivity private lateinit var uiDevice: UiDevice private lateinit var downloadingZimFile: File @@ -151,24 +157,24 @@ class SearchFragmentTestForCustomApp { openZimFileInReader(zimFile = downloadingZimFile) openSearchWithQuery() val searchTerm = "gard" - val searchedItem = "Gardanta Spirito" + val searchedItem = "Gardanta Spirito - Андивионский Научный Альянс" search { // test with fast typing/deleting - searchWithFrequentlyTypedWords(searchTerm) - assertSearchSuccessful(searchedItem) - deleteSearchedQueryFrequently(searchTerm, uiDevice) + searchWithFrequentlyTypedWords(searchTerm, composeTestRule = composeTestRule) + assertSearchSuccessful(searchedItem, composeTestRule) + deleteSearchedQueryFrequently(searchTerm, uiDevice, composeTestRule = composeTestRule) // test with a short delay typing/deleting to // properly test the cancelling of previously searching task - searchWithFrequentlyTypedWords(searchTerm, 50) - assertSearchSuccessful(searchedItem) - deleteSearchedQueryFrequently(searchTerm, uiDevice, 50) + searchWithFrequentlyTypedWords(searchTerm, 50, composeTestRule) + assertSearchSuccessful(searchedItem, composeTestRule) + deleteSearchedQueryFrequently(searchTerm, uiDevice, 50, composeTestRule) // test with a long delay typing/deleting to // properly execute the search query letter by letter - searchWithFrequentlyTypedWords(searchTerm, 300) - assertSearchSuccessful(searchedItem) - deleteSearchedQueryFrequently(searchTerm, uiDevice, 300) + searchWithFrequentlyTypedWords(searchTerm, 300, composeTestRule) + assertSearchSuccessful(searchedItem, composeTestRule) + deleteSearchedQueryFrequently(searchTerm, uiDevice, 300, composeTestRule) // to close the keyboard pressBack() // go to reader screen @@ -179,16 +185,16 @@ class SearchFragmentTestForCustomApp { // frequently searched for article, and clicked on the searched item. search { // test by searching 10 article and clicking on them - searchAndClickOnArticle(searchTerm) - searchAndClickOnArticle("eilum") - searchAndClickOnArticle("page") - searchAndClickOnArticle("list") - searchAndClickOnArticle("ladder") - searchAndClickOnArticle("welc") - searchAndClickOnArticle("js") - searchAndClickOnArticle("hizo") - searchAndClickOnArticle("fad") - searchAndClickOnArticle("forum") + searchAndClickOnArticle(searchTerm, composeTestRule) + searchAndClickOnArticle("eilum", composeTestRule) + searchAndClickOnArticle("page", composeTestRule) + searchAndClickOnArticle("list", composeTestRule) + searchAndClickOnArticle("ladder", composeTestRule) + searchAndClickOnArticle("welc", composeTestRule) + searchAndClickOnArticle("js", composeTestRule) + searchAndClickOnArticle("hizo", composeTestRule) + searchAndClickOnArticle("fad", composeTestRule) + searchAndClickOnArticle("forum", composeTestRule) assertArticleLoaded() } } diff --git a/custom/src/androidTest/java/org/kiwix/kiwixmobile/custom/search/SearchRobot.kt b/custom/src/androidTest/java/org/kiwix/kiwixmobile/custom/search/SearchRobot.kt index a266d0d07..6f0964bcb 100644 --- a/custom/src/androidTest/java/org/kiwix/kiwixmobile/custom/search/SearchRobot.kt +++ b/custom/src/androidTest/java/org/kiwix/kiwixmobile/custom/search/SearchRobot.kt @@ -19,14 +19,19 @@ package org.kiwix.kiwixmobile.custom.search import android.view.KeyEvent +import androidx.compose.ui.test.assert +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.ComposeContentTestRule +import androidx.compose.ui.test.onAllNodesWithTag +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextClearance +import androidx.compose.ui.test.performTextInput import androidx.core.view.GravityCompat -import androidx.recyclerview.widget.RecyclerView import androidx.test.espresso.Espresso import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.assertion.ViewAssertions -import androidx.test.espresso.contrib.RecyclerViewActions import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.espresso.web.assertion.WebViewAssertions.webMatches @@ -40,56 +45,68 @@ import androidx.test.espresso.web.webdriver.Locator import androidx.test.uiautomator.UiDevice import com.adevinta.android.barista.interaction.BaristaDrawerInteractions.openDrawerWithGravity import com.adevinta.android.barista.interaction.BaristaSleepInteractions -import com.adevinta.android.barista.internal.matcher.HelperMatchers import org.hamcrest.CoreMatchers.containsString import org.kiwix.kiwixmobile.core.R +import org.kiwix.kiwixmobile.core.search.SEARCH_FIELD_TESTING_TAG +import org.kiwix.kiwixmobile.core.search.SEARCH_ITEM_TESTING_TAG import org.kiwix.kiwixmobile.custom.R.id import org.kiwix.kiwixmobile.custom.testutils.TestUtils +import org.kiwix.kiwixmobile.custom.testutils.TestUtils.TEST_PAUSE_MS import org.kiwix.kiwixmobile.custom.testutils.TestUtils.testFlakyView +import org.kiwix.kiwixmobile.custom.testutils.TestUtils.waitUntilTimeout fun search(searchRobot: SearchRobot.() -> Unit) = SearchRobot().searchRobot() class SearchRobot { - fun searchWithFrequentlyTypedWords(query: String, wait: Long = 0L) { + fun searchWithFrequentlyTypedWords( + query: String, + wait: Long = 0L, + composeTestRule: ComposeContentTestRule + ) { testFlakyView({ - val searchView = Espresso.onView(ViewMatchers.withId(androidx.appcompat.R.id.search_src_text)) - searchView.perform(ViewActions.clearText()) - for (char in query) { - searchView.perform(ViewActions.typeText(char.toString())) - if (wait != 0L) { - BaristaSleepInteractions.sleep(wait) + composeTestRule.apply { + waitUntilTimeout() + val searchView = onNodeWithTag(SEARCH_FIELD_TESTING_TAG) + searchView.performTextInput("") + for (char in query) { + searchView.performTextInput(char.toString()) + if (wait != 0L) { + waitUntilTimeout(wait) + } } } }) } - fun assertSearchSuccessful(searchResult: String) { - testFlakyView({ - BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS_FOR_SEARCH_TEST.toLong()) - Espresso.onView(ViewMatchers.withId(R.id.search_list)).check( - ViewAssertions.matches( - HelperMatchers.atPosition( - 0, - ViewMatchers.hasDescendant(ViewMatchers.withSubstring(searchResult)) - ) - ) + fun assertSearchSuccessful(searchResult: String, composeTestRule: ComposeContentTestRule) { + composeTestRule.apply { + waitUntil( + timeoutMillis = TEST_PAUSE_MS.toLong(), + condition = { + onAllNodesWithTag(SEARCH_ITEM_TESTING_TAG) + .fetchSemanticsNodes().isNotEmpty() + } ) - }) + onAllNodesWithTag(SEARCH_ITEM_TESTING_TAG)[0] + .assert(hasText(searchResult)) + } } - fun deleteSearchedQueryFrequently(textToDelete: String, uiDevice: UiDevice, wait: Long = 0L) { - testFlakyView({ - for (i in textToDelete.indices) { - uiDevice.pressKeyCode(KeyEvent.KEYCODE_DEL) - if (wait != 0L) { - BaristaSleepInteractions.sleep(wait) - } + fun deleteSearchedQueryFrequently( + textToDelete: String, + uiDevice: UiDevice, + wait: Long = 0L, + composeTestRule: ComposeContentTestRule + ) { + repeat(textToDelete.length) { + uiDevice.pressKeyCode(KeyEvent.KEYCODE_DEL) + if (wait != 0L) { + BaristaSleepInteractions.sleep(wait) } + } - // clear search query if any remains due to any condition not to affect any other test scenario - val searchView = Espresso.onView(ViewMatchers.withId(androidx.appcompat.R.id.search_src_text)) - searchView.perform(ViewActions.clearText()) - }) + // clear search query if any remains due to any condition not to affect any other test scenario + composeTestRule.onNodeWithTag(SEARCH_FIELD_TESTING_TAG).performTextClearance() } private fun openSearchScreen() { @@ -99,26 +116,21 @@ class SearchRobot { }) } - fun searchAndClickOnArticle(searchString: String) { - // Wait a bit to properly load the ZIM file in the reader. - BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS_FOR_SEARCH_TEST.toLong()) + fun searchAndClickOnArticle(searchString: String, composeTestRule: ComposeContentTestRule) { + // wait a bit to properly load the ZIM file in the reader + composeTestRule.waitUntilTimeout() openSearchScreen() // Wait a bit to properly visible the search screen. - BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS_FOR_SEARCH_TEST.toLong()) - searchWithFrequentlyTypedWords(searchString) - clickOnSearchItemInSearchList() + composeTestRule.waitUntilTimeout() + searchWithFrequentlyTypedWords(searchString, composeTestRule = composeTestRule) + clickOnSearchItemInSearchList(composeTestRule) } - private fun clickOnSearchItemInSearchList() { - testFlakyView({ - BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS_FOR_SEARCH_TEST.toLong()) - Espresso.onView(ViewMatchers.withId(R.id.search_list)).perform( - RecyclerViewActions.actionOnItemAtPosition( - 0, - ViewActions.click() - ) - ) - }) + private fun clickOnSearchItemInSearchList(composeTestRule: ComposeContentTestRule) { + composeTestRule.apply { + waitUntilTimeout() + onAllNodesWithTag(SEARCH_ITEM_TESTING_TAG)[0].performClick() + } } fun assertArticleLoaded() { diff --git a/custom/src/androidTest/java/org/kiwix/kiwixmobile/custom/testutils/TestUtils.kt b/custom/src/androidTest/java/org/kiwix/kiwixmobile/custom/testutils/TestUtils.kt index de9df8ac3..671d823d3 100644 --- a/custom/src/androidTest/java/org/kiwix/kiwixmobile/custom/testutils/TestUtils.kt +++ b/custom/src/androidTest/java/org/kiwix/kiwixmobile/custom/testutils/TestUtils.kt @@ -21,12 +21,15 @@ package org.kiwix.kiwixmobile.custom.testutils import android.content.Context import android.content.ContextWrapper import android.content.Intent +import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.test.uiautomator.By import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiObject import androidx.test.uiautomator.UiSelector import org.kiwix.kiwixmobile.core.utils.files.Log import java.io.File +import java.util.Timer +import java.util.TimerTask object TestUtils { private const val TAG = "TESTUTILS" @@ -90,6 +93,31 @@ object TestUtils { } } + fun ComposeContentTestRule.waitUntilTimeout(timeoutMillis: Long = TEST_PAUSE_MS.toLong()) { + AsyncTimer.start(timeoutMillis) + waitUntil( + condition = { AsyncTimer.expired }, + timeoutMillis = timeoutMillis + 1000 + ) + } + + object AsyncTimer { + var expired = false + fun start(delay: Long = 1000) { + expired = false + val timerTask = TimerTaskImpl { + expired = true + } + Timer().schedule(timerTask, delay) + } + } + + class TimerTaskImpl(private val runnable: Runnable) : TimerTask() { + override fun run() { + runnable.run() + } + } + @JvmStatic fun deleteTemporaryFilesOfTestCases(context: Context) { context.getExternalFilesDirs(null).filterNotNull()