diff --git a/build.gradle.kts b/build.gradle.kts index 5358f3c23..e48c76f88 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ buildscript { dependencies { classpath(Libs.com_android_tools_build_gradle) classpath(Libs.kotlin_gradle_plugin) - classpath(Libs.navigation_kotlin_safeargs) + classpath(Libs.navigation_safe_args_gradle_plugin) // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/buildSrc/src/main/kotlin/Libs.kt b/buildSrc/src/main/kotlin/Libs.kt index a2a846900..083903f97 100644 --- a/buildSrc/src/main/kotlin/Libs.kt +++ b/buildSrc/src/main/kotlin/Libs.kt @@ -1,3 +1,5 @@ +import kotlin.String + /** * Generated by https://github.com/jmfayard/buildSrcVersions * @@ -5,54 +7,54 @@ * `$ ./gradlew buildSrcVersions` */ object Libs { + /** + * https://github.com/Kotlin/kotlinx.coroutines + */ + const val kotlinx_coroutines_android: String = + "org.jetbrains.kotlinx:kotlinx-coroutines-android:" + + Versions.org_jetbrains_kotlinx_kotlinx_coroutines /** - * https://developer.android.com/guide/navigation + * https://github.com/Kotlin/kotlinx.coroutines */ - const val navigation_kotlin_fragment: String = - "androidx.navigation:navigation-fragment-ktx:${Versions.navigation}" - const val navigation_kotlin_ui: String = - "androidx.navigation:navigation-ui-ktx:${Versions.navigation}" - const val navigation_kotlin_testing: String = - "androidx.navigation:navigation-testing:${Versions.navigation}" - const val navigation_kotlin_safeargs: String = - "androidx.navigation:navigation-safe-args-gradle-plugin:${Versions.navigation}" + const val kotlinx_coroutines_test: String = "org.jetbrains.kotlinx:kotlinx-coroutines-test:" + + Versions.org_jetbrains_kotlinx_kotlinx_coroutines /** * https://developer.android.com/testing */ const val espresso_contrib: String = "androidx.test.espresso:espresso-contrib:" + - Versions.androidx_test_espresso + Versions.androidx_test_espresso /** * https://developer.android.com/testing */ const val espresso_core: String = "androidx.test.espresso:espresso-core:" + - Versions.androidx_test_espresso + Versions.androidx_test_espresso /** * https://developer.android.com/testing */ const val espresso_intents: String = "androidx.test.espresso:espresso-intents:" + - Versions.androidx_test_espresso + Versions.androidx_test_espresso /** * https://developer.android.com/testing */ const val espresso_web: String = "androidx.test.espresso:espresso-web:" + - Versions.androidx_test_espresso + Versions.androidx_test_espresso /** * https://github.com/square/retrofit */ const val adapter_rxjava2: String = "com.squareup.retrofit2:adapter-rxjava2:" + - Versions.com_squareup_retrofit2 + Versions.com_squareup_retrofit2 /** * https://github.com/square/retrofit */ const val converter_simplexml: String = "com.squareup.retrofit2:converter-simplexml:" + - Versions.com_squareup_retrofit2 + Versions.com_squareup_retrofit2 /** * https://github.com/square/retrofit @@ -63,13 +65,13 @@ object Libs { * https://square.github.io/okhttp/ */ const val logging_interceptor: String = "com.squareup.okhttp3:logging-interceptor:" + - Versions.com_squareup_okhttp3 + Versions.com_squareup_okhttp3 /** * https://square.github.io/okhttp/ */ const val mockwebserver: String = "com.squareup.okhttp3:mockwebserver:" + - Versions.com_squareup_okhttp3 + Versions.com_squareup_okhttp3 /** * https://square.github.io/okhttp/ @@ -80,31 +82,55 @@ object Libs { * https://kotlinlang.org/ */ const val kotlin_android_extensions: String = "org.jetbrains.kotlin:kotlin-android-extensions:" + - Versions.org_jetbrains_kotlin + Versions.org_jetbrains_kotlin /** * https://kotlinlang.org/ */ const val kotlin_android_extensions_runtime: String = - "org.jetbrains.kotlin:kotlin-android-extensions-runtime:" + Versions.org_jetbrains_kotlin + "org.jetbrains.kotlin:kotlin-android-extensions-runtime:" + Versions.org_jetbrains_kotlin /** * https://kotlinlang.org/ */ const val kotlin_annotation_processing_gradle: String = - "org.jetbrains.kotlin:kotlin-annotation-processing-gradle:" + Versions.org_jetbrains_kotlin + "org.jetbrains.kotlin:kotlin-annotation-processing-gradle:" + Versions.org_jetbrains_kotlin /** * https://kotlinlang.org/ */ const val kotlin_gradle_plugin: String = "org.jetbrains.kotlin:kotlin-gradle-plugin:" + - Versions.org_jetbrains_kotlin + Versions.org_jetbrains_kotlin /** * https://kotlinlang.org/ */ const val kotlin_stdlib_jdk7: String = "org.jetbrains.kotlin:kotlin-stdlib-jdk7:" + - Versions.org_jetbrains_kotlin + Versions.org_jetbrains_kotlin + + /** + * https://developer.android.com/topic/libraries/architecture/index.html + */ + const val navigation_fragment_ktx: String = "androidx.navigation:navigation-fragment-ktx:" + + Versions.androidx_navigation + + /** + * https://developer.android.com/topic/libraries/architecture/index.html + */ + const val navigation_safe_args_gradle_plugin: String = + "androidx.navigation:navigation-safe-args-gradle-plugin:" + Versions.androidx_navigation + + /** + * https://developer.android.com/topic/libraries/architecture/index.html + */ + const val navigation_testing: String = "androidx.navigation:navigation-testing:" + + Versions.androidx_navigation + + /** + * https://developer.android.com/topic/libraries/architecture/index.html + */ + const val navigation_ui_ktx: String = "androidx.navigation:navigation-ui-ktx:" + + Versions.androidx_navigation /** * https://github.com/google/dagger @@ -115,19 +141,19 @@ object Libs { * https://github.com/google/dagger */ const val dagger_android: String = "com.google.dagger:dagger-android:" + - Versions.com_google_dagger + Versions.com_google_dagger /** * https://github.com/google/dagger */ const val dagger_android_processor: String = "com.google.dagger:dagger-android-processor:" + - Versions.com_google_dagger + Versions.com_google_dagger /** * https://github.com/google/dagger */ const val dagger_compiler: String = "com.google.dagger:dagger-compiler:" + - Versions.com_google_dagger + Versions.com_google_dagger /** * https://github.com/yahoo/squidb @@ -138,13 +164,13 @@ object Libs { * https://github.com/yahoo/squidb */ const val squidb_annotations: String = "com.yahoo.squidb:squidb-annotations:" + - Versions.com_yahoo_squidb + Versions.com_yahoo_squidb /** * https://github.com/yahoo/squidb */ const val squidb_processor: String = "com.yahoo.squidb:squidb-processor:" + - Versions.com_yahoo_squidb + Versions.com_yahoo_squidb /** * https://github.com/JakeWharton/butterknife/ @@ -155,13 +181,13 @@ object Libs { * https://github.com/JakeWharton/butterknife/ */ const val butterknife_compiler: String = "com.jakewharton:butterknife-compiler:" + - Versions.com_jakewharton + Versions.com_jakewharton /** * https://github.com/JakeWharton/butterknife/ */ const val butterknife_gradle_plugin: String = "com.jakewharton:butterknife-gradle-plugin:" + - Versions.com_jakewharton + Versions.com_jakewharton /** * https://developer.android.com/testing @@ -192,7 +218,7 @@ object Libs { * https://objectbox.io */ const val objectbox_gradle_plugin: String = "io.objectbox:objectbox-gradle-plugin:" + - Versions.io_objectbox + Versions.io_objectbox /** * https://objectbox.io @@ -208,7 +234,7 @@ object Libs { * https://objectbox.io */ const val objectbox_processor: String = "io.objectbox:objectbox-processor:" + - Versions.io_objectbox + Versions.io_objectbox /** * https://objectbox.io @@ -244,45 +270,42 @@ object Libs { * https://developer.android.com/topic/libraries/architecture/index.html */ const val android_arch_lifecycle_extensions: String = "android.arch.lifecycle:extensions:" + - Versions.android_arch_lifecycle_extensions + Versions.android_arch_lifecycle_extensions /** * https://developer.android.com/studio */ const val com_android_tools_build_gradle: String = "com.android.tools.build:gradle:" + - Versions.com_android_tools_build_gradle + Versions.com_android_tools_build_gradle const val de_fayard_buildsrcversions_gradle_plugin: String = - "de.fayard.buildSrcVersions:de.fayard.buildSrcVersions.gradle.plugin:" + + "de.fayard.buildSrcVersions:de.fayard.buildSrcVersions.gradle.plugin:" + Versions.de_fayard_buildsrcversions_gradle_plugin const val com_github_triplet_play_gradle_plugin: String = - "com.github.triplet.play:com.github.triplet.play.gradle.plugin:" + + "com.github.triplet.play:com.github.triplet.play.gradle.plugin:" + Versions.com_github_triplet_play_gradle_plugin - const val multidex_instrumentation: String = "androidx.multidex:multidex-instrumentation:" + - Versions.multidex_instrumentation - /** * http://jcp.org/en/jsr/detail?id=250 */ const val javax_annotation_api: String = "javax.annotation:javax.annotation-api:" + - Versions.javax_annotation_api + Versions.javax_annotation_api const val ink_page_indicator: String = "com.pacioianu.david:ink-page-indicator:" + - Versions.ink_page_indicator + Versions.ink_page_indicator /** * http://github.com/square/leakcanary/ */ const val leakcanary_android: String = "com.squareup.leakcanary:leakcanary-android:" + - Versions.leakcanary_android + Versions.leakcanary_android /** * http://tools.android.com */ const val constraintlayout: String = "androidx.constraintlayout:constraintlayout:" + - Versions.constraintlayout + Versions.constraintlayout /** * http://developer.android.com/tools/extras/support-library.html @@ -300,7 +323,7 @@ object Libs { const val junit_jupiter: String = "org.junit.jupiter:junit-jupiter:" + Versions.junit_jupiter const val xfetch2okhttp: String = "androidx.tonyodev.fetch2okhttp:xfetch2okhttp:" + - Versions.xfetch2okhttp + Versions.xfetch2okhttp /** * http://assertj.org diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 7a8b1f42d..d265a2919 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -1,3 +1,4 @@ +import kotlin.String import org.gradle.plugin.use.PluginDependenciesSpec import org.gradle.plugin.use.PluginDependencySpec @@ -11,23 +12,27 @@ import org.gradle.plugin.use.PluginDependencySpec * YOU are responsible for updating manually the dependency version. */ object Versions { - const val androidx_test_espresso: String = "3.2.0" + const val org_jetbrains_kotlinx_kotlinx_coroutines: String = "1.3.9" + + const val androidx_test_espresso: String = "3.2.0" // available: "3.3.0" const val com_squareup_retrofit2: String = "2.5.0" // available: "2.9.0" - const val com_squareup_okhttp3: String = "3.6.0" // available: "4.8.0" + const val com_squareup_okhttp3: String = "3.6.0" // available: "4.9.0" - const val org_jetbrains_kotlin: String = "1.3.72" + const val org_jetbrains_kotlin: String = "1.3.72" // available: "1.4.10" - const val com_google_dagger: String = "2.28.3" + const val androidx_navigation: String = "2.3.0" + + const val com_google_dagger: String = "2.28.3" // available: "2.29.1" const val com_yahoo_squidb: String = "2.0.0" // available: "3.2.3" - const val com_jakewharton: String = "10.2.2" + const val com_jakewharton: String = "10.2.2" // available: "10.2.3" - const val androidx_test: String = "1.2.0" + const val androidx_test: String = "1.2.0" // available: "1.3.0" - const val io_objectbox: String = "2.7.0" + const val io_objectbox: String = "2.7.0" // available: "2.7.1" const val org_jacoco: String = "0.7.9" @@ -39,9 +44,7 @@ object Versions { const val de_fayard_buildsrcversions_gradle_plugin: String = "0.7.0" - const val com_github_triplet_play_gradle_plugin: String = "2.8.0" - - const val multidex_instrumentation: String = "2.0.0" + const val com_github_triplet_play_gradle_plugin: String = "2.8.0" // available: "3.0.0" const val javax_annotation_api: String = "1.3.2" @@ -49,17 +52,17 @@ object Versions { const val leakcanary_android: String = "2.4" - const val constraintlayout: String = "1.1.3" + const val constraintlayout: String = "1.1.3" // available: "2.0.1" const val collection_ktx: String = "1.1.0" const val preference_ktx: String = "1.1.1" - const val junit_jupiter: String = "5.6.2" + const val junit_jupiter: String = "5.6.2" // available: "5.7.0" - const val xfetch2okhttp: String = "3.1.4" + const val xfetch2okhttp: String = "3.1.4" // available: "3.1.5" - const val assertj_core: String = "3.16.1" + const val assertj_core: String = "3.16.1" // available: "3.17.2" const val core_testing: String = "2.1.0" @@ -85,34 +88,32 @@ object Versions { const val kiwixlib: String = "9.4.0" - const val material: String = "1.2.0" + const val material: String = "1.2.0" // available: "1.2.1" const val multidex: String = "2.0.1" - const val barista: String = "2.7.1" // available: "3.5.0" + const val barista: String = "2.7.1" // available: "3.6.0" - const val xfetch2: String = "3.1.4" + const val xfetch2: String = "3.1.4" // available: "3.1.5" const val jsr305: String = "3.0.2" - const val ktlint: String = "0.37.2" + const val ktlint: String = "0.36.0" // available: "0.39.0" const val rxjava: String = "2.2.19" - const val webkit: String = "1.2.0" + const val webkit: String = "1.2.0" // available: "1.3.0" const val aapt2: String = "4.0.1-6197926" - const val junit: String = "1.1.1" - - const val navigation: String = "2.3.0" + const val junit: String = "1.1.1" // available: "1.1.2" /** * Current version: "6.2" * See issue 19: How to update Gradle itself? * https://github.com/jmfayard/buildSrcVersions/issues/19 */ - const val gradleLatestVersion: String = "6.5.1" + const val gradleLatestVersion: String = "6.6.1" } /** @@ -121,4 +122,4 @@ object Versions { */ val PluginDependenciesSpec.buildSrcVersions: PluginDependencySpec inline get() = - id("de.fayard.buildSrcVersions").version(Versions.de_fayard_buildsrcversions_gradle_plugin) + id("de.fayard.buildSrcVersions").version(Versions.de_fayard_buildsrcversions_gradle_plugin) diff --git a/buildSrc/src/main/kotlin/plugin/AllProjectConfigurer.kt b/buildSrc/src/main/kotlin/plugin/AllProjectConfigurer.kt index c1e23e2e8..a72d95402 100644 --- a/buildSrc/src/main/kotlin/plugin/AllProjectConfigurer.kt +++ b/buildSrc/src/main/kotlin/plugin/AllProjectConfigurer.kt @@ -161,9 +161,9 @@ class AllProjectConfigurer { implementation(Libs.constraintlayout) implementation(Libs.multidex) // navigation - implementation(Libs.navigation_kotlin_fragment) - implementation(Libs.navigation_kotlin_ui) - androidTestImplementation(Libs.navigation_kotlin_testing) + implementation(Libs.navigation_fragment_ktx) + implementation(Libs.navigation_ui_ktx) + androidTestImplementation(Libs.navigation_testing) implementation(Libs.okhttp) implementation(Libs.logging_interceptor) implementation(Libs.retrofit) diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 11d59e9bb..ad35718aa 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -61,4 +61,6 @@ dependencies { implementation(Libs.objectbox_kotlin) implementation(Libs.objectbox_rxjava) implementation(Libs.webkit) + testImplementation(Libs.kotlinx_coroutines_test) + implementation(Libs.kotlinx_coroutines_android) } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/FlowBuilder.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/FlowBuilder.kt new file mode 100644 index 000000000..90a39e982 --- /dev/null +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/FlowBuilder.kt @@ -0,0 +1,36 @@ +/* + * 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.dao + +import io.objectbox.query.Query +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.channels.sendBlocking +import kotlinx.coroutines.flow.callbackFlow +import javax.inject.Inject + +class FlowBuilder @Inject constructor() { + @OptIn(ExperimentalCoroutinesApi::class) + fun buildCallbackFlow(query: Query) = + callbackFlow> { + val subscription = query.subscribe() + .observer { sendBlocking(it) } + awaitClose(subscription::cancel) + } +} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/NewRecentSearchDao.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/NewRecentSearchDao.kt index 85ba019df..31a4d9123 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/NewRecentSearchDao.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/NewRecentSearchDao.kt @@ -19,14 +19,20 @@ package org.kiwix.kiwixmobile.core.dao import io.objectbox.Box import io.objectbox.kotlin.query +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.map import org.kiwix.kiwixmobile.core.dao.entities.RecentSearchEntity import org.kiwix.kiwixmobile.core.dao.entities.RecentSearchEntity_ import org.kiwix.kiwixmobile.core.data.local.entity.RecentSearch import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.RecentSearchListItem import javax.inject.Inject -class NewRecentSearchDao @Inject constructor(private val box: Box) { - fun recentSearches(zimId: String?) = box.asFlowable( +class NewRecentSearchDao @Inject constructor( + private val box: Box, + private val flowBuilder: FlowBuilder +) { + @OptIn(ExperimentalCoroutinesApi::class) + fun recentSearches(zimId: String?) = flowBuilder.buildCallbackFlow( box.query { equal(RecentSearchEntity_.zimId, zimId ?: "") orderDesc(RecentSearchEntity_.id) diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/di/modules/DatabaseModule.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/di/modules/DatabaseModule.kt index d1adf8878..6ccd463fd 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/di/modules/DatabaseModule.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/di/modules/DatabaseModule.kt @@ -23,6 +23,7 @@ import dagger.Provides import io.objectbox.BoxStore import io.objectbox.kotlin.boxFor import org.kiwix.kiwixmobile.core.dao.FetchDownloadDao +import org.kiwix.kiwixmobile.core.dao.FlowBuilder import org.kiwix.kiwixmobile.core.dao.HistoryDao import org.kiwix.kiwixmobile.core.dao.NewBookDao import org.kiwix.kiwixmobile.core.dao.NewBookmarksDao @@ -57,8 +58,10 @@ open class DatabaseModule { @Provides @Singleton fun providesNewBookmarksDao(boxStore: BoxStore): NewBookmarksDao = NewBookmarksDao(boxStore.boxFor()) - @Provides @Singleton fun providesNewRecentSearchDao(boxStore: BoxStore): NewRecentSearchDao = - NewRecentSearchDao(boxStore.boxFor()) + @Provides @Singleton fun providesNewRecentSearchDao( + boxStore: BoxStore, + flowBuilder: FlowBuilder + ): NewRecentSearchDao = NewRecentSearchDao(boxStore.boxFor(), flowBuilder) @Provides @Singleton fun providesFetchDownloadDao( boxStore: BoxStore, diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/search/SearchActivity.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/search/SearchActivity.kt index a50e13b50..1d1a5bbed 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/search/SearchActivity.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/search/SearchActivity.kt @@ -24,21 +24,23 @@ import android.view.Menu import android.view.MenuItem import android.view.MenuItem.OnActionExpandListener import androidx.appcompat.widget.SearchView -import androidx.lifecycle.Observer +import androidx.core.view.isVisible +import androidx.core.widget.ContentLoadingProgressBar import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import io.reactivex.disposables.CompositeDisposable -import kotlinx.android.synthetic.main.activity_search.searchViewAnimator +import kotlinx.android.synthetic.main.activity_search.searchLoadingIndicator +import kotlinx.android.synthetic.main.activity_search.searchNoResults import kotlinx.android.synthetic.main.activity_search.search_list import kotlinx.android.synthetic.main.layout_toolbar.toolbar +import kotlinx.coroutines.flow.collect import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.R.id import org.kiwix.kiwixmobile.core.base.BaseActivity import org.kiwix.kiwixmobile.core.di.components.CoreComponent import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.coreActivityComponent import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.viewModel -import org.kiwix.kiwixmobile.core.extensions.setDistinctDisplayedChild import org.kiwix.kiwixmobile.core.search.adapter.SearchAdapter import org.kiwix.kiwixmobile.core.search.adapter.SearchDelegate.RecentSearchDelegate import org.kiwix.kiwixmobile.core.search.adapter.SearchDelegate.ZimSearchResultDelegate @@ -49,13 +51,11 @@ import org.kiwix.kiwixmobile.core.search.viewmodel.Action.CreatedWithIntent import org.kiwix.kiwixmobile.core.search.viewmodel.Action.ExitedSearch import org.kiwix.kiwixmobile.core.search.viewmodel.Action.Filter import org.kiwix.kiwixmobile.core.search.viewmodel.Action.OnItemClick -import org.kiwix.kiwixmobile.core.search.viewmodel.Action.OnOpenInNewTabClick import org.kiwix.kiwixmobile.core.search.viewmodel.Action.OnItemLongClick +import org.kiwix.kiwixmobile.core.search.viewmodel.Action.OnOpenInNewTabClick import org.kiwix.kiwixmobile.core.search.viewmodel.SearchOrigin.FromWebView +import org.kiwix.kiwixmobile.core.search.viewmodel.SearchState import org.kiwix.kiwixmobile.core.search.viewmodel.SearchViewModel -import org.kiwix.kiwixmobile.core.search.viewmodel.State -import org.kiwix.kiwixmobile.core.search.viewmodel.State.NoResults -import org.kiwix.kiwixmobile.core.search.viewmodel.State.Results import org.kiwix.kiwixmobile.core.utils.SimpleTextListener import javax.inject.Inject @@ -68,7 +68,6 @@ class SearchActivity : BaseActivity() { private lateinit var searchInTextMenuItem: MenuItem private val searchViewModel by lazy { viewModel(viewModelFactory) } - private val compositeDisposable = CompositeDisposable() private val searchAdapter: SearchAdapter by lazy { SearchAdapter( RecentSearchDelegate(::onItemClick, ::onItemClickNewTab) { @@ -93,12 +92,9 @@ class SearchActivity : BaseActivity() { layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false) setHasFixedSize(true) } - compositeDisposable.add(searchViewModel.effects.subscribe { it.invokeWith(this) }) - } - - override fun onDestroy() { - compositeDisposable.clear() - super.onDestroy() + lifecycleScope.launchWhenCreated { + searchViewModel.effects.collect { it.invokeWith(this@SearchActivity) } + } } override fun finish() { @@ -127,29 +123,19 @@ class SearchActivity : BaseActivity() { searchViewModel.actions.offer(ClickedSearchInText) true } - - searchViewModel.state.observe(this, Observer(::render)) + lifecycleScope.launchWhenCreated { + searchViewModel.state.collect { render(it) } + } searchViewModel.actions.offer(CreatedWithIntent(intent)) return true } - private fun render(state: State) { + private fun render(state: SearchState) { searchInTextMenuItem.isVisible = state.searchOrigin == FromWebView - when (state) { - is Results -> { - searchViewAnimator.setDistinctDisplayedChild(0) - searchAdapter.items = state.values - render(state.searchString) - } - is NoResults -> { - searchViewAnimator.setDistinctDisplayedChild(1) - render(state.searchString) - } - } - } - - private fun render(searchString: String) { - searchInTextMenuItem.isEnabled = searchString.isNotBlank() + searchInTextMenuItem.isEnabled = state.searchTerm.isNotBlank() + searchLoadingIndicator.isShowing(state.isLoading) + searchNoResults.isVisible = state.visibleResults.isEmpty() + searchAdapter.items = state.visibleResults } private fun onItemClick(it: SearchListItem) { @@ -165,3 +151,11 @@ class SearchActivity : BaseActivity() { searchViewModel.actions.offer(ActivityResultReceived(requestCode, resultCode, data)) } } + +private fun ContentLoadingProgressBar.isShowing(show: Boolean) { + if (show) { + show() + } else { + hide() + } +} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchResultGenerator.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchResultGenerator.kt index dbf9a59f3..01cbff15d 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchResultGenerator.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchResultGenerator.kt @@ -18,33 +18,49 @@ package org.kiwix.kiwixmobile.core.search.viewmodel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.coroutines.yield import org.kiwix.kiwixmobile.core.reader.ZimFileReader -import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.ZimSearchResultListItem import javax.inject.Inject interface SearchResultGenerator { - fun generateSearchResults(searchTerm: String): List + suspend fun generateSearchResults( + searchTerm: String, + zimFileReader: ZimFileReader? + ): List } -class ZimSearchResultGenerator @Inject constructor( - private val zimReaderContainer: ZimReaderContainer -) : SearchResultGenerator { - override fun generateSearchResults(searchTerm: String) = - if (searchTerm.isNotEmpty()) readResultsFromZim(searchTerm, zimReaderContainer.copyReader()) - else emptyList() +class ZimSearchResultGenerator @Inject constructor() : SearchResultGenerator { - private fun readResultsFromZim( - it: String, + override suspend fun generateSearchResults(searchTerm: String, zimFileReader: ZimFileReader?) = + withContext(Dispatchers.IO) { + if (searchTerm.isNotEmpty()) readResultsFromZim(searchTerm, zimFileReader) + else emptyList() + } + + private suspend fun readResultsFromZim( + searchTerm: String, reader: ZimFileReader? ) = - reader?.searchSuggestions(it, 200).run { suggestionResults(reader) } + reader.also { yield() } + ?.searchSuggestions(searchTerm, 200) + .also { yield() } + .run { suggestionResults(reader) } - private fun suggestionResults(reader: ZimFileReader?) = generateSequence { - reader?.getNextSuggestion()?.let { ZimSearchResultListItem(it.title) } + private suspend fun suggestionResults(reader: ZimFileReader?) = createList { + yield() + reader?.getNextSuggestion() + ?.let { ZimSearchResultListItem(it.title) } } .distinct() .toList() - .also { reader?.dispose() } + + private suspend fun createList(readSearchResult: suspend () -> T?): List { + return mutableListOf().apply { + while (true) readSearchResult()?.let(::add) ?: break + } + } } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/State.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchState.kt similarity index 69% rename from core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/State.kt rename to core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchState.kt index dc7b4b93b..c0df6bb9e 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/State.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchState.kt @@ -20,21 +20,21 @@ package org.kiwix.kiwixmobile.core.search.viewmodel import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem +data class SearchState( + val searchTerm: String, + val searchResultsWithTerm: SearchResultsWithTerm, + val recentResults: List, + val searchOrigin: SearchOrigin +) { + val visibleResults = when { + searchTerm.isNotEmpty() -> searchResultsWithTerm.results + else -> recentResults + } + + val isLoading = searchTerm != searchResultsWithTerm.searchTerm +} + enum class SearchOrigin { FromWebView, FromTabView } - -sealed class State { - abstract val searchString: String - abstract val searchOrigin: SearchOrigin - - data class Results( - override val searchString: String, - val values: List, - override val searchOrigin: SearchOrigin - ) : State() - - data class NoResults(override val searchString: String, override val searchOrigin: SearchOrigin) : - State() -} 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 fb855a923..08afe1e8c 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 @@ -18,15 +18,20 @@ package org.kiwix.kiwixmobile.core.search.viewmodel -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import io.reactivex.Flowable -import io.reactivex.disposables.CompositeDisposable -import io.reactivex.disposables.Disposable -import io.reactivex.functions.Function4 -import io.reactivex.processors.BehaviorProcessor -import io.reactivex.processors.PublishProcessor -import io.reactivex.schedulers.Schedulers +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.ConflatedBroadcastChannel +import kotlinx.coroutines.channels.consumeEach +import kotlinx.coroutines.channels.sendBlocking +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.base.SideEffect import org.kiwix.kiwixmobile.core.dao.NewRecentSearchDao @@ -45,8 +50,6 @@ import org.kiwix.kiwixmobile.core.search.viewmodel.Action.ReceivedPromptForSpeec import org.kiwix.kiwixmobile.core.search.viewmodel.Action.ScreenWasStartedFrom import org.kiwix.kiwixmobile.core.search.viewmodel.Action.StartSpeechInputFailed import org.kiwix.kiwixmobile.core.search.viewmodel.SearchOrigin.FromWebView -import org.kiwix.kiwixmobile.core.search.viewmodel.State.NoResults -import org.kiwix.kiwixmobile.core.search.viewmodel.State.Results import org.kiwix.kiwixmobile.core.search.viewmodel.effects.DeleteRecentSearch import org.kiwix.kiwixmobile.core.search.viewmodel.effects.FinishActivity import org.kiwix.kiwixmobile.core.search.viewmodel.effects.OpenSearchItem @@ -57,118 +60,85 @@ import org.kiwix.kiwixmobile.core.search.viewmodel.effects.SearchIntentProcessin import org.kiwix.kiwixmobile.core.search.viewmodel.effects.ShowDeleteSearchDialog import org.kiwix.kiwixmobile.core.search.viewmodel.effects.ShowToast import org.kiwix.kiwixmobile.core.search.viewmodel.effects.StartSpeechInput -import java.util.concurrent.TimeUnit import javax.inject.Inject -private const val DEBOUNCE_MS = 500L - +@OptIn(ExperimentalCoroutinesApi::class) class SearchViewModel @Inject constructor( private val recentSearchDao: NewRecentSearchDao, private val zimReaderContainer: ZimReaderContainer, private val searchResultGenerator: SearchResultGenerator ) : ViewModel() { - val state = MutableLiveData().apply { value = NoResults("", FromWebView) } - val effects = PublishProcessor.create>() - val actions = PublishProcessor.create() - private val filter = BehaviorProcessor.createDefault("") - private val searchOrigin = BehaviorProcessor.createDefault(FromWebView) - private val searchResults = PublishProcessor.create>() - - private val compositeDisposable = CompositeDisposable() - private var searchTask: Disposable? = null + private val initialState: SearchState = + SearchState("", SearchResultsWithTerm("", emptyList()), emptyList(), FromWebView) + val state: MutableStateFlow = MutableStateFlow(initialState) + private val _effects = Channel>() + val effects = _effects.receiveAsFlow() + val actions = Channel(Channel.UNLIMITED) + private val filter = ConflatedBroadcastChannel("") + private val searchOrigin = ConflatedBroadcastChannel(FromWebView) init { - compositeDisposable.addAll( - viewStateReducer(), - actionMapper(), - searchResultEventsFromZimReader() - ) + viewModelScope.launch { reducer() } + viewModelScope.launch { actionMapper() } } - override fun onCleared() { - compositeDisposable.clear() - super.onCleared() + private suspend fun reducer() { + combine( + filter.asFlow(), + searchResults(), + recentSearchDao.recentSearches(zimReaderContainer.id), + searchOrigin.asFlow() + ) { searchTerm, searchResultsWithTerm, recentResults, searchOrigin -> + SearchState(searchTerm, searchResultsWithTerm, recentResults, searchOrigin) + } + .collect { state.value = it } } - private fun actionMapper() = actions.map { + private fun searchResults() = filter.asFlow() + .mapLatest { + val zimFileReader = zimReaderContainer.copyReader() + try { + SearchResultsWithTerm(it, searchResultGenerator.generateSearchResults(it, zimFileReader)) + } finally { + zimFileReader?.dispose() + } + } + + private suspend fun actionMapper() = actions.consumeEach { when (it) { - ExitedSearch -> effects.offer(FinishActivity) + ExitedSearch -> _effects.offer(FinishActivity) is OnItemClick -> saveSearchAndOpenItem(it.searchListItem, false) is OnOpenInNewTabClick -> saveSearchAndOpenItem(it.searchListItem, true) is OnItemLongClick -> showDeleteDialog(it) - is Filter -> filter.offer(it.term) + is Filter -> filter.sendBlocking(it.term) ClickedSearchInText -> searchPreviousScreenWhenStateIsValid() is ConfirmedDelete -> deleteItemAndShowToast(it) - is CreatedWithIntent -> effects.offer(SearchIntentProcessing(it.intent, actions)) - ReceivedPromptForSpeechInput -> effects.offer(StartSpeechInput(actions)) - StartSpeechInputFailed -> effects.offer(ShowToast(R.string.speech_not_supported)) + is CreatedWithIntent -> _effects.offer(SearchIntentProcessing(it.intent, actions)) + ReceivedPromptForSpeechInput -> _effects.offer(StartSpeechInput(actions)) + StartSpeechInputFailed -> _effects.offer(ShowToast(R.string.speech_not_supported)) is ActivityResultReceived -> - effects.offer(ProcessActivityResult(it.requestCode, it.resultCode, it.data, actions)) - is ScreenWasStartedFrom -> searchOrigin.offer(it.searchOrigin) + _effects.offer(ProcessActivityResult(it.requestCode, it.resultCode, it.data, actions)) + is ScreenWasStartedFrom -> searchOrigin.sendBlocking(it.searchOrigin) } - }.subscribe( - {}, - Throwable::printStackTrace - ) + } private fun deleteItemAndShowToast(it: ConfirmedDelete) { - effects.offer(DeleteRecentSearch(it.searchListItem, recentSearchDao)) - effects.offer(ShowToast(R.string.delete_specific_search_toast)) + _effects.offer(DeleteRecentSearch(it.searchListItem, recentSearchDao)) + _effects.offer(ShowToast(R.string.delete_specific_search_toast)) } private fun searchPreviousScreenWhenStateIsValid(): Any = - effects.offer(SearchInPreviousScreen(state.value!!.searchString)) + _effects.offer(SearchInPreviousScreen(state.value.searchTerm)) private fun showDeleteDialog(longClick: OnItemLongClick) { - effects.offer(ShowDeleteSearchDialog(longClick.searchListItem, actions)) + _effects.offer(ShowDeleteSearchDialog(longClick.searchListItem, actions)) } private fun saveSearchAndOpenItem(searchListItem: SearchListItem, openInNewTab: Boolean) { - effects.offer( - SaveSearchToRecents(recentSearchDao, searchListItem, zimReaderContainer.id) - ) - effects.offer( - OpenSearchItem(searchListItem, openInNewTab) - ) - } - - private fun viewStateReducer() = - Flowable.combineLatest( - recentSearchDao.recentSearches(zimReaderContainer.id), - searchResults, - filter, - searchOrigin, - Function4(this::reduce) - ).subscribe(state::postValue, Throwable::printStackTrace) - - private fun reduce( - recentSearchResults: List, - zimSearchResults: List, - searchString: String, - searchOrigin: SearchOrigin - ) = when { - searchString.isNotEmpty() && zimSearchResults.isNotEmpty() -> - Results(searchString, zimSearchResults, searchOrigin) - searchString.isEmpty() && recentSearchResults.isNotEmpty() -> - Results(searchString, recentSearchResults, searchOrigin) - else -> NoResults(searchString, searchOrigin) - } - - private fun searchResultEventsFromZimReader() = filter - .distinctUntilChanged() - .debounce(DEBOUNCE_MS, TimeUnit.MILLISECONDS) - .subscribe(::performSearch) - - private fun performSearch(searchTerm: String) { - compositeDisposable.add( - Flowable.fromCallable { searchResultGenerator.generateSearchResults(searchTerm) } - .subscribeOn(Schedulers.io()) - .subscribe { searchResults.offer(it) } - .also { - searchTask?.dispose() - searchTask = it - } - ) + _effects.offer(SaveSearchToRecents(recentSearchDao, searchListItem, zimReaderContainer.id)) + _effects.offer(OpenSearchItem(searchListItem, openInNewTab)) } } + +data class SearchResultsWithTerm(val searchTerm: String, val results: List) diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/ProcessActivityResult.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/ProcessActivityResult.kt index 7c67a045a..f027ce171 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/ProcessActivityResult.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/ProcessActivityResult.kt @@ -22,7 +22,7 @@ import android.app.Activity import android.content.Intent import android.speech.RecognizerIntent import androidx.appcompat.app.AppCompatActivity -import io.reactivex.processors.PublishProcessor +import kotlinx.coroutines.channels.Channel import org.kiwix.kiwixmobile.core.base.SideEffect import org.kiwix.kiwixmobile.core.search.viewmodel.Action import org.kiwix.kiwixmobile.core.search.viewmodel.Action.Filter @@ -31,7 +31,7 @@ data class ProcessActivityResult( private val requestCode: Int, private val resultCode: Int, private val data: Intent?, - private val actions: PublishProcessor + private val actions: Channel ) : SideEffect { override fun invokeWith(activity: AppCompatActivity) { if (requestCode == StartSpeechInput.REQ_CODE_SPEECH_INPUT && diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/SearchIntentProcessing.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/SearchIntentProcessing.kt index fa5d6be79..aac7e22df 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/SearchIntentProcessing.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/SearchIntentProcessing.kt @@ -22,7 +22,7 @@ import android.annotation.TargetApi import android.content.Intent import android.os.Build.VERSION_CODES import androidx.appcompat.app.AppCompatActivity -import io.reactivex.processors.PublishProcessor +import kotlinx.coroutines.channels.Channel import org.kiwix.kiwixmobile.core.base.SideEffect import org.kiwix.kiwixmobile.core.search.viewmodel.Action import org.kiwix.kiwixmobile.core.search.viewmodel.Action.Filter @@ -36,7 +36,7 @@ import org.kiwix.kiwixmobile.core.utils.TAG_FROM_TAB_SWITCHER data class SearchIntentProcessing( private val intent: Intent?, - private val actions: PublishProcessor + private val actions: Channel ) : SideEffect { @TargetApi(VERSION_CODES.M) override fun invokeWith(activity: AppCompatActivity) { 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 dd1f676bd..7c2efa551 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 @@ -19,7 +19,7 @@ package org.kiwix.kiwixmobile.core.search.viewmodel.effects import androidx.appcompat.app.AppCompatActivity -import io.reactivex.processors.PublishProcessor +import kotlinx.coroutines.channels.Channel import org.kiwix.kiwixmobile.core.base.SideEffect import org.kiwix.kiwixmobile.core.search.SearchActivity import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem @@ -30,7 +30,7 @@ import javax.inject.Inject data class ShowDeleteSearchDialog( private val searchListItem: SearchListItem, - private val actions: PublishProcessor + private val actions: Channel ) : SideEffect { @Inject lateinit var dialogShower: DialogShower 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 e0dbe7c73..3d4679832 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 @@ -22,14 +22,14 @@ import android.content.ActivityNotFoundException import android.content.Intent import android.speech.RecognizerIntent import androidx.appcompat.app.AppCompatActivity -import io.reactivex.processors.PublishProcessor +import kotlinx.coroutines.channels.Channel import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.base.SideEffect import org.kiwix.kiwixmobile.core.search.viewmodel.Action import org.kiwix.kiwixmobile.core.search.viewmodel.Action.StartSpeechInputFailed import java.util.Locale -data class StartSpeechInput(private val actions: PublishProcessor) : SideEffect { +data class StartSpeechInput(private val actions: Channel) : SideEffect { override fun invokeWith(activity: AppCompatActivity) { try { diff --git a/core/src/main/res/layout/activity_search.xml b/core/src/main/res/layout/activity_search.xml index dfa1d4e39..ac0c5d76b 100644 --- a/core/src/main/res/layout/activity_search.xml +++ b/core/src/main/res/layout/activity_search.xml @@ -1,6 +1,8 @@ @@ -16,26 +18,28 @@ - - - - - - - + android:layout_height="match_parent" /> + + + + 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 b66e21c64..6447d2e96 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 @@ -20,60 +20,72 @@ package org.kiwix.kiwixmobile.core.dao import io.mockk.every import io.mockk.mockk -import io.mockk.mockkStatic import io.mockk.verify import io.objectbox.Box import io.objectbox.query.Query import io.objectbox.query.QueryBuilder -import io.objectbox.rx.RxQuery -import io.reactivex.Observable +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.test.runBlockingTest +import org.assertj.core.api.Assertions.assertThat 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.data.local.entity.RecentSearch import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.RecentSearchListItem +import org.kiwix.kiwixmobile.core.search.viewmodel.test import org.kiwix.sharedFunctions.recentSearchEntity internal class NewRecentSearchDaoTest { private val box: Box = mockk(relaxed = true) - private val newRecentSearchDao = NewRecentSearchDao(box) + private val flowBuilder: FlowBuilder = mockk() + private val newRecentSearchDao = NewRecentSearchDao(box, flowBuilder) @Nested inner class RecentSearchTests { @Test - fun `recentSearches searches by Id passed`() { + fun `recentSearches searches by Id passed`() = runBlockingTest { val zimId = "id" val queryResult = listOf(recentSearchEntity()) expectFromRecentSearches(queryResult, zimId) - newRecentSearchDao.recentSearches(zimId).test() + newRecentSearchDao.recentSearches(zimId) + .test(this) .assertValues(queryResult.map { RecentSearchListItem(it.searchTerm) }) + .finish() } @Test - fun `recentSearches searches with blank Id if null passed`() { + fun `recentSearches searches with blank Id if null passed`() = runBlockingTest { val queryResult = listOf(recentSearchEntity()) expectFromRecentSearches(queryResult, "") - newRecentSearchDao.recentSearches(null).test() + newRecentSearchDao.recentSearches(null) + .test(this) .assertValues(queryResult.map { RecentSearchListItem(it.searchTerm) }) + .finish() } @Test - fun `recentSearches searches returns distinct entities by searchTerm`() { + fun `recentSearches searches returns distinct entities by searchTerm`() = runBlockingTest { val queryResult = listOf(recentSearchEntity(), recentSearchEntity()) expectFromRecentSearches(queryResult, "") - newRecentSearchDao.recentSearches("").test() + newRecentSearchDao.recentSearches("") + .test(this) .assertValues(queryResult.take(1).map { RecentSearchListItem(it.searchTerm) }) + .finish() } @Test - fun `recentSearches searches returns a limitedNumber of entities`() { + fun `recentSearches searches returns a limitedNumber of entities`() = runBlockingTest { val searchResults: List = (0..200).map { recentSearchEntity(searchTerm = "$it") } expectFromRecentSearches(searchResults, "") - newRecentSearchDao.recentSearches("").test() + newRecentSearchDao.recentSearches("") + .test(this) .assertValue { it.size == 100 } + .finish() } private fun expectFromRecentSearches(queryResult: List, zimId: String) { @@ -83,8 +95,7 @@ internal class NewRecentSearchDaoTest { every { queryBuilder.orderDesc(RecentSearchEntity_.id) } returns queryBuilder val query = mockk>() every { queryBuilder.build() } returns query - mockkStatic(RxQuery::class) - every { RxQuery.observable(query) } returns Observable.just(queryResult) + every { flowBuilder.buildCallbackFlow(query) } returns flowOf(queryResult) } } @@ -123,3 +134,13 @@ internal class NewRecentSearchDaoTest { verify { box.put(listOf(recentSearchEntity(searchTerm = term, zimId = id))) } } } + +private suspend fun Flow.assertValue(function: (T) -> Boolean) { + val value = toList().last() + val result = function(value) + assertThat(result).isTrue() +} + +suspend inline fun Flow.assertValues(expectedValues: List) { + assertThat(toList()).containsExactlyElementsOf(expectedValues) +} 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 new file mode 100644 index 000000000..b34b70696 --- /dev/null +++ b/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/SearchStateTest.kt @@ -0,0 +1,79 @@ +/* + * 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.viewmodel + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.RecentSearchListItem +import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.ZimSearchResultListItem +import org.kiwix.kiwixmobile.core.search.viewmodel.SearchOrigin.FromWebView + +internal class SearchStateTest { + + @Test + internal fun `visibleResults use searchResults when searchTerm is not empty`() { + val results = listOf(ZimSearchResultListItem("")) + assertThat( + SearchState( + "notEmpty", + SearchResultsWithTerm("", results), + emptyList(), + FromWebView + ).visibleResults + ).isEqualTo(results) + } + + @Test + internal fun `visibleResults use recentResults when searchTerm is empty`() { + val results = listOf(RecentSearchListItem("")) + assertThat( + SearchState( + "", + SearchResultsWithTerm("", emptyList()), + results, + FromWebView + ).visibleResults + ).isEqualTo(results) + } + + @Test + internal fun `isLoading when searchTerm is not equal to ResultTerm`() { + assertThat( + SearchState( + "", + SearchResultsWithTerm("notEqual", emptyList()), + emptyList(), + FromWebView + ).isLoading + ).isTrue() + } + + @Test + internal fun `is not Loading when searchTerm is equal to ResultTerm`() { + val searchTerm = "equal" + assertThat( + SearchState( + searchTerm, + SearchResultsWithTerm(searchTerm, emptyList()), + emptyList(), + FromWebView + ).isLoading + ).isFalse() + } +} 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 4ef81e2df..b50b8d50b 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 @@ -19,21 +19,34 @@ package org.kiwix.kiwixmobile.core.search.viewmodel import android.content.Intent -import com.jraska.livedata.test import io.mockk.clearAllMocks +import io.mockk.coEvery import io.mockk.every import io.mockk.mockk -import io.reactivex.processors.PublishProcessor -import io.reactivex.schedulers.TestScheduler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.consumeAsFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestCoroutineDispatcher +import kotlinx.coroutines.test.TestCoroutineScope +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.test.setMain +import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.R.string import org.kiwix.kiwixmobile.core.base.SideEffect import org.kiwix.kiwixmobile.core.dao.NewRecentSearchDao +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.adapter.SearchListItem.ZimSearchResultListItem @@ -49,10 +62,7 @@ import org.kiwix.kiwixmobile.core.search.viewmodel.Action.OnOpenInNewTabClick import org.kiwix.kiwixmobile.core.search.viewmodel.Action.ReceivedPromptForSpeechInput import org.kiwix.kiwixmobile.core.search.viewmodel.Action.ScreenWasStartedFrom import org.kiwix.kiwixmobile.core.search.viewmodel.Action.StartSpeechInputFailed -import org.kiwix.kiwixmobile.core.search.viewmodel.SearchOrigin.FromTabView import org.kiwix.kiwixmobile.core.search.viewmodel.SearchOrigin.FromWebView -import org.kiwix.kiwixmobile.core.search.viewmodel.State.NoResults -import org.kiwix.kiwixmobile.core.search.viewmodel.State.Results import org.kiwix.kiwixmobile.core.search.viewmodel.effects.DeleteRecentSearch import org.kiwix.kiwixmobile.core.search.viewmodel.effects.FinishActivity import org.kiwix.kiwixmobile.core.search.viewmodel.effects.OpenSearchItem @@ -63,166 +73,83 @@ import org.kiwix.kiwixmobile.core.search.viewmodel.effects.SearchIntentProcessin import org.kiwix.kiwixmobile.core.search.viewmodel.effects.ShowDeleteSearchDialog import org.kiwix.kiwixmobile.core.search.viewmodel.effects.ShowToast import org.kiwix.kiwixmobile.core.search.viewmodel.effects.StartSpeechInput -import org.kiwix.sharedFunctions.InstantExecutorExtension -import org.kiwix.sharedFunctions.resetSchedulers -import org.kiwix.sharedFunctions.setScheduler -import java.util.concurrent.TimeUnit.MILLISECONDS -@ExtendWith(InstantExecutorExtension::class) +@OptIn(ExperimentalCoroutinesApi::class) internal class SearchViewModelTest { private val recentSearchDao: NewRecentSearchDao = mockk() private val zimReaderContainer: ZimReaderContainer = mockk() private val searchResultGenerator: SearchResultGenerator = mockk() + private val zimFileReader: ZimFileReader = mockk() + private val testDispatcher = TestCoroutineDispatcher() lateinit var viewModel: SearchViewModel - private val testScheduler = TestScheduler() - - init { - setScheduler(testScheduler) - } - @AfterAll fun teardown() { - resetSchedulers() + Dispatchers.resetMain() } - private val recentsFromDb: PublishProcessor> = - PublishProcessor.create() + private lateinit var recentsFromDb: Channel> @BeforeEach fun init() { + Dispatchers.resetMain() + Dispatchers.setMain(testDispatcher) clearAllMocks() + recentsFromDb = Channel(kotlinx.coroutines.channels.Channel.UNLIMITED) + every { zimReaderContainer.copyReader() } returns zimFileReader + coEvery { + searchResultGenerator.generateSearchResults("", zimFileReader) + } returns emptyList() every { zimReaderContainer.id } returns "id" - every { recentSearchDao.recentSearches("id") } returns recentsFromDb + every { recentSearchDao.recentSearches("id") } returns recentsFromDb.consumeAsFlow() viewModel = SearchViewModel(recentSearchDao, zimReaderContainer, searchResultGenerator) } @Nested inner class StateTests { @Test - fun `initial state is Initialising`() { - viewModel.state.test().assertValue(NoResults("", FromWebView)) + fun `initial state is Initialising`() = runBlockingTest { + viewModel.state.test(this).assertValue( + SearchState("", SearchResultsWithTerm("", emptyList()), emptyList(), FromWebView) + ).finish() } @Test - fun `non empty search term with search results shows Results`() { + fun `SearchState combines sources from inputs`() = runBlockingTest { val item = ZimSearchResultListItem("") val searchTerm = "searchTerm" val searchOrigin = FromWebView - emissionOf( - searchTerm = searchTerm, - searchResults = listOf(item), - databaseResults = listOf(RecentSearchListItem("")), - searchOrigin = searchOrigin - ) - resultsIn(Results(searchTerm, listOf(item), searchOrigin)) - } - - @Test - fun `non empty search string with no search results is NoResults`() { - emissionOf( - searchTerm = "a", - searchResults = emptyList(), - databaseResults = listOf(RecentSearchListItem("")), - searchOrigin = FromWebView - ) - resultsIn(NoResults("a", FromWebView)) - } - - @Test - fun `empty search string with database results shows Results`() { - val item = RecentSearchListItem("") - emissionOf( - searchTerm = "", - searchResults = listOf(ZimSearchResultListItem("")), - databaseResults = listOf(item), - searchOrigin = FromWebView - ) - resultsIn(Results("", listOf(item), FromWebView)) - } - - @Test - fun `empty search string with no database results is NoResults`() { - emissionOf( - searchTerm = "", - searchResults = listOf(ZimSearchResultListItem("")), - databaseResults = emptyList(), - searchOrigin = FromWebView - ) - resultsIn(NoResults("", FromWebView)) - } - - @Test - fun `duplicate search terms are ignored`() { - val searchString = "a" - val item = ZimSearchResultListItem("") - emissionOf( - searchTerm = searchString, - searchResults = listOf(item), - databaseResults = emptyList(), - searchOrigin = FromWebView - ) - viewModel.actions.offer(Filter(searchString)) - viewModel.state.test() - .also { testScheduler.advanceTimeBy(100, MILLISECONDS) } - .assertValueHistory( - Results(searchString, listOf(item), FromWebView) + viewModel.state.test(this) + .also { + emissionOf( + searchTerm = searchTerm, + searchResults = listOf(item), + databaseResults = listOf(RecentSearchListItem("")), + searchOrigin = searchOrigin + ) + } + .assertValue( + SearchState( + searchTerm, + SearchResultsWithTerm(searchTerm, listOf(item)), + listOf(RecentSearchListItem("")), + searchOrigin + ) ) - } - - @Test - fun `only latest search term is used`() { - val item = ZimSearchResultListItem("") - emissionOf( - searchTerm = "a", - searchResults = listOf(item), - databaseResults = emptyList(), - searchOrigin = FromWebView - ) - emissionOf( - searchTerm = "b", - searchResults = listOf(item), - databaseResults = emptyList(), - searchOrigin = FromWebView - ) - viewModel.state.test() - .also { testScheduler.advanceTimeBy(100, MILLISECONDS) } - .assertValueHistory(Results("b", listOf(item), FromWebView)) - } - - @Test - fun `webView search origin leads to webView in NoResults`() { - emissionOf( - searchTerm = "", - searchResults = listOf(ZimSearchResultListItem("")), - databaseResults = emptyList(), - searchOrigin = FromWebView - ) - resultsIn(NoResults("", FromWebView)) - } - - @Test - fun `tabView search origin leads to tabView in Results`() { - emissionOf( - searchTerm = "", - searchResults = listOf(ZimSearchResultListItem("")), - databaseResults = emptyList(), - searchOrigin = FromTabView - ) - resultsIn(NoResults("", FromTabView)) + .finish() } } @Nested inner class ActionMapping { @Test - fun `ExitedSearch offers Finish`() { + fun `ExitedSearch offers Finish`() = runBlockingTest { actionResultsInEffects(ExitedSearch, FinishActivity) } @Test - fun `OnItemClick offers Saves and Opens`() { + fun `OnItemClick offers Saves and Opens`() = runBlockingTest { val searchListItem = RecentSearchListItem("") actionResultsInEffects( OnItemClick(searchListItem), @@ -232,7 +159,7 @@ internal class SearchViewModelTest { } @Test - fun `OnOpenInNewTabClick offers Saves and Opens in new tab`() { + fun `OnOpenInNewTabClick offers Saves and Opens in new tab`() = runBlockingTest { val searchListItem = RecentSearchListItem("") actionResultsInEffects( OnOpenInNewTabClick(searchListItem), @@ -242,7 +169,7 @@ internal class SearchViewModelTest { } @Test - fun `OnItemLongClick offers Saves and Opens`() { + fun `OnItemLongClick offers Saves and Opens`() = runBlockingTest { val searchListItem = RecentSearchListItem("") actionResultsInEffects( OnItemLongClick(searchListItem), @@ -251,12 +178,12 @@ internal class SearchViewModelTest { } @Test - fun `ClickedSearchInText offers SearchInPreviousScreen`() { + fun `ClickedSearchInText offers SearchInPreviousScreen`() = runBlockingTest { actionResultsInEffects(ClickedSearchInText, SearchInPreviousScreen("")) } @Test - fun `ConfirmedDelete offers Delete and Toast`() { + fun `ConfirmedDelete offers Delete and Toast`() = runBlockingTest { val searchListItem = RecentSearchListItem("") actionResultsInEffects( ConfirmedDelete(searchListItem), @@ -266,7 +193,7 @@ internal class SearchViewModelTest { } @Test - fun `CreatedWithIntent offers SearchIntentProcessing`() { + fun `CreatedWithIntent offers SearchIntentProcessing`() = runBlockingTest { val intent = mockk() actionResultsInEffects( CreatedWithIntent(intent), @@ -275,7 +202,7 @@ internal class SearchViewModelTest { } @Test - fun `ReceivedPromptForSpeechInput offers SearchIntentProcessing`() { + fun `ReceivedPromptForSpeechInput offers SearchIntentProcessing`() = runBlockingTest { actionResultsInEffects( ReceivedPromptForSpeechInput, StartSpeechInput(viewModel.actions) @@ -283,7 +210,7 @@ internal class SearchViewModelTest { } @Test - fun `StartSpeechInputFailed offers ShowToast`() { + fun `StartSpeechInputFailed offers ShowToast`() = runBlockingTest { actionResultsInEffects( StartSpeechInputFailed, ShowToast(string.speech_not_supported) @@ -291,40 +218,71 @@ internal class SearchViewModelTest { } @Test - fun `ActivityResultReceived offers ProcessActivityResult`() { + fun `ActivityResultReceived offers ProcessActivityResult`() = runBlockingTest { actionResultsInEffects( ActivityResultReceived(0, 1, null), ProcessActivityResult(0, 1, null, viewModel.actions) ) } - private fun actionResultsInEffects( + private fun TestCoroutineScope.actionResultsInEffects( action: Action, vararg effects: SideEffect<*> ) { viewModel.effects - .test() + .test(this) .also { viewModel.actions.offer(action) } .assertValues(*effects) + .finish() } } - private fun resultsIn(st: State) { - viewModel.state.test() - .also { testScheduler.advanceTimeBy(100, MILLISECONDS) } - .assertValue(st) - } - - private fun emissionOf( + private fun TestCoroutineScope.emissionOf( searchTerm: String, searchResults: List, databaseResults: List, searchOrigin: SearchOrigin ) { - every { searchResultGenerator.generateSearchResults(searchTerm) } returns searchResults + + coEvery { + searchResultGenerator.generateSearchResults(searchTerm, zimFileReader) + } returns searchResults viewModel.actions.offer(Filter(searchTerm)) recentsFromDb.offer(databaseResults) viewModel.actions.offer(ScreenWasStartedFrom(searchOrigin)) - testScheduler.advanceTimeBy(500, MILLISECONDS) + advanceTimeBy(500) + } +} + +fun Flow.test(scope: CoroutineScope) = TestObserver(scope, this) + +class TestObserver( + scope: CoroutineScope, + flow: Flow +) { + private val values = mutableListOf() + private val job: Job = scope.launch { + flow.collect { + values.add(it) + } + } + + fun assertValues(vararg values: T): TestObserver { + assertThat(values.toList()).containsExactlyElementsOf(this.values) + return this + } + + fun assertValue(value: T): TestObserver { + assertThat(values.last()).isEqualTo(value) + return this + } + + fun finish() { + job.cancel() + } + + fun assertValue(value: (T) -> Boolean): TestObserver { + assertThat(values.last()).satisfies { value(it) } + return this } } diff --git a/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/ZimSearchResultGeneratorTest.kt b/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/ZimSearchResultGeneratorTest.kt index 39949fca5..dc9a37840 100644 --- a/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/ZimSearchResultGeneratorTest.kt +++ b/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/ZimSearchResultGeneratorTest.kt @@ -21,33 +21,26 @@ package org.kiwix.kiwixmobile.core.search.viewmodel import io.mockk.every import io.mockk.mockk import io.mockk.verify +import kotlinx.coroutines.runBlocking import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.kiwix.kiwixmobile.core.reader.ZimFileReader -import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer import org.kiwix.kiwixmobile.core.search.SearchSuggestion import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.ZimSearchResultListItem -import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil internal class ZimSearchResultGeneratorTest { - private val sharedPreferenceUtil: SharedPreferenceUtil = mockk() - private val zimReaderContainer: ZimReaderContainer = mockk() private val zimFileReader: ZimFileReader = mockk() private val zimSearchResultGenerator: ZimSearchResultGenerator = - ZimSearchResultGenerator(zimReaderContainer) - - @BeforeEach - internal fun setUp() { - every { zimReaderContainer.copyReader() } returns zimFileReader - } + ZimSearchResultGenerator() @Test internal fun `empty search term returns empty list`() { - assertThat(zimSearchResultGenerator.generateSearchResults("")) - .isEqualTo(emptyList()) + runBlocking { + assertThat(zimSearchResultGenerator.generateSearchResults("", zimFileReader)) + .isEqualTo(emptyList()) + } } @Test @@ -58,11 +51,12 @@ internal class ZimSearchResultGeneratorTest { every { zimFileReader.searchSuggestions(" ", 200) } returns true every { zimFileReader.getNextSuggestion() } returnsMany listOf(item, item, null) every { item.title } returns validTitle - assertThat(zimSearchResultGenerator.generateSearchResults(searchTerm)) - .isEqualTo(listOf(ZimSearchResultListItem(validTitle))) - verify { - zimFileReader.searchSuggestions(searchTerm, 200) - zimFileReader.dispose() + runBlocking { + assertThat(zimSearchResultGenerator.generateSearchResults(searchTerm, zimFileReader)) + .isEqualTo(listOf(ZimSearchResultListItem(validTitle))) + verify { + zimFileReader.searchSuggestions(searchTerm, 200) + } } } } diff --git a/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/ProcessActivityResultTest.kt b/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/ProcessActivityResultTest.kt index 6a33ab411..d59236509 100644 --- a/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/ProcessActivityResultTest.kt +++ b/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/ProcessActivityResultTest.kt @@ -27,7 +27,7 @@ import io.mockk.clearAllMocks import io.mockk.every import io.mockk.mockk import io.mockk.verify -import io.reactivex.processors.PublishProcessor +import kotlinx.coroutines.channels.Channel import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.kiwix.kiwixmobile.core.search.viewmodel.Action @@ -37,7 +37,7 @@ internal class ProcessActivityResultTest { private val activity: AppCompatActivity = mockk() private val data = mockk() - private val actions = mockk>(relaxed = true) + private val actions = mockk>(relaxed = true) private val successfulResult = ProcessActivityResult( StartSpeechInput.REQ_CODE_SPEECH_INPUT, Activity.RESULT_OK, diff --git a/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/SearchIntentProcessingTest.kt b/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/SearchIntentProcessingTest.kt index b4d25aa85..96a4a1b2d 100644 --- a/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/SearchIntentProcessingTest.kt +++ b/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/SearchIntentProcessingTest.kt @@ -26,7 +26,7 @@ import io.mockk.every import io.mockk.mockk import io.mockk.verify import io.mockk.verifySequence -import io.reactivex.processors.PublishProcessor +import kotlinx.coroutines.channels.Channel import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.kiwix.kiwixmobile.core.search.viewmodel.Action @@ -35,13 +35,13 @@ import org.kiwix.kiwixmobile.core.search.viewmodel.Action.ReceivedPromptForSpeec import org.kiwix.kiwixmobile.core.search.viewmodel.Action.ScreenWasStartedFrom import org.kiwix.kiwixmobile.core.search.viewmodel.SearchOrigin.FromTabView import org.kiwix.kiwixmobile.core.search.viewmodel.SearchOrigin.FromWebView -import org.kiwix.kiwixmobile.core.utils.TAG_FROM_TAB_SWITCHER -import org.kiwix.kiwixmobile.core.utils.EXTRA_SEARCH import org.kiwix.kiwixmobile.core.utils.EXTRA_IS_WIDGET_VOICE +import org.kiwix.kiwixmobile.core.utils.EXTRA_SEARCH +import org.kiwix.kiwixmobile.core.utils.TAG_FROM_TAB_SWITCHER internal class SearchIntentProcessingTest { - private val actions: PublishProcessor = mockk(relaxed = true) + private val actions: Channel = mockk(relaxed = true) private val activity: AppCompatActivity = 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 b75872298..e94b4fa9b 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 @@ -22,7 +22,7 @@ import io.mockk.every import io.mockk.mockk import io.mockk.slot import io.mockk.verify -import io.reactivex.processors.PublishProcessor +import kotlinx.coroutines.channels.Channel import org.junit.jupiter.api.Test import org.kiwix.kiwixmobile.core.search.SearchActivity import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.RecentSearchListItem @@ -35,7 +35,7 @@ internal class ShowDeleteSearchDialogTest { @Test fun `invoke with shows dialog that offers ConfirmedDelete action`() { - val actions = mockk>(relaxed = true) + val actions = mockk>(relaxed = true) val searchListItem = RecentSearchListItem("") val activity = mockk() val showDeleteSearchDialog = ShowDeleteSearchDialog(searchListItem, actions) diff --git a/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/StartSpeechInputTest.kt b/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/StartSpeechInputTest.kt index b15f26f3c..cfcd5a969 100644 --- a/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/StartSpeechInputTest.kt +++ b/core/src/test/java/org/kiwix/kiwixmobile/core/search/viewmodel/effects/StartSpeechInputTest.kt @@ -26,7 +26,7 @@ import io.mockk.every import io.mockk.mockk import io.mockk.mockkConstructor import io.mockk.verify -import io.reactivex.processors.PublishProcessor +import kotlinx.coroutines.channels.Channel import org.junit.jupiter.api.Test import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.search.viewmodel.Action @@ -35,7 +35,7 @@ import java.util.Locale internal class StartSpeechInputTest { - private val actions = mockk>(relaxed = true) + private val actions = mockk>(relaxed = true) @Test fun `when invoke with throws exception offer StartSpeechInputFailed action`() {