mirror of
https://github.com/kiwix/kiwix-android.git
synced 2025-09-11 16:37:18 -04:00
#2082 Very slow search function - replace RX with Flow
This commit is contained in:
parent
222c0627a1
commit
19bd06a460
@ -6,7 +6,7 @@ buildscript {
|
|||||||
dependencies {
|
dependencies {
|
||||||
classpath(Libs.com_android_tools_build_gradle)
|
classpath(Libs.com_android_tools_build_gradle)
|
||||||
classpath(Libs.kotlin_gradle_plugin)
|
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
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import kotlin.String
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generated by https://github.com/jmfayard/buildSrcVersions
|
* Generated by https://github.com/jmfayard/buildSrcVersions
|
||||||
*
|
*
|
||||||
@ -5,18 +7,18 @@
|
|||||||
* `$ ./gradlew buildSrcVersions`
|
* `$ ./gradlew buildSrcVersions`
|
||||||
*/
|
*/
|
||||||
object Libs {
|
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 =
|
const val kotlinx_coroutines_test: String = "org.jetbrains.kotlinx:kotlinx-coroutines-test:" +
|
||||||
"androidx.navigation:navigation-fragment-ktx:${Versions.navigation}"
|
Versions.org_jetbrains_kotlinx_kotlinx_coroutines
|
||||||
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}"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* https://developer.android.com/testing
|
* https://developer.android.com/testing
|
||||||
@ -106,6 +108,30 @@ object Libs {
|
|||||||
const val kotlin_stdlib_jdk7: String = "org.jetbrains.kotlin:kotlin-stdlib-jdk7:" +
|
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
|
* https://github.com/google/dagger
|
||||||
*/
|
*/
|
||||||
@ -260,9 +286,6 @@ object Libs {
|
|||||||
"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
|
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
|
* http://jcp.org/en/jsr/detail?id=250
|
||||||
*/
|
*/
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import kotlin.String
|
||||||
import org.gradle.plugin.use.PluginDependenciesSpec
|
import org.gradle.plugin.use.PluginDependenciesSpec
|
||||||
import org.gradle.plugin.use.PluginDependencySpec
|
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.
|
* YOU are responsible for updating manually the dependency version.
|
||||||
*/
|
*/
|
||||||
object Versions {
|
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_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_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"
|
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 de_fayard_buildsrcversions_gradle_plugin: String = "0.7.0"
|
||||||
|
|
||||||
const val com_github_triplet_play_gradle_plugin: String = "2.8.0"
|
const val com_github_triplet_play_gradle_plugin: String = "2.8.0" // available: "3.0.0"
|
||||||
|
|
||||||
const val multidex_instrumentation: String = "2.0.0"
|
|
||||||
|
|
||||||
const val javax_annotation_api: String = "1.3.2"
|
const val javax_annotation_api: String = "1.3.2"
|
||||||
|
|
||||||
@ -49,17 +52,17 @@ object Versions {
|
|||||||
|
|
||||||
const val leakcanary_android: String = "2.4"
|
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 collection_ktx: String = "1.1.0"
|
||||||
|
|
||||||
const val preference_ktx: String = "1.1.1"
|
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"
|
const val core_testing: String = "2.1.0"
|
||||||
|
|
||||||
@ -85,34 +88,32 @@ object Versions {
|
|||||||
|
|
||||||
const val kiwixlib: String = "9.4.0"
|
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 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 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 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 aapt2: String = "4.0.1-6197926"
|
||||||
|
|
||||||
const val junit: String = "1.1.1"
|
const val junit: String = "1.1.1" // available: "1.1.2"
|
||||||
|
|
||||||
const val navigation: String = "2.3.0"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Current version: "6.2"
|
* Current version: "6.2"
|
||||||
* See issue 19: How to update Gradle itself?
|
* See issue 19: How to update Gradle itself?
|
||||||
* https://github.com/jmfayard/buildSrcVersions/issues/19
|
* https://github.com/jmfayard/buildSrcVersions/issues/19
|
||||||
*/
|
*/
|
||||||
const val gradleLatestVersion: String = "6.5.1"
|
const val gradleLatestVersion: String = "6.6.1"
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -161,9 +161,9 @@ class AllProjectConfigurer {
|
|||||||
implementation(Libs.constraintlayout)
|
implementation(Libs.constraintlayout)
|
||||||
implementation(Libs.multidex)
|
implementation(Libs.multidex)
|
||||||
// navigation
|
// navigation
|
||||||
implementation(Libs.navigation_kotlin_fragment)
|
implementation(Libs.navigation_fragment_ktx)
|
||||||
implementation(Libs.navigation_kotlin_ui)
|
implementation(Libs.navigation_ui_ktx)
|
||||||
androidTestImplementation(Libs.navigation_kotlin_testing)
|
androidTestImplementation(Libs.navigation_testing)
|
||||||
implementation(Libs.okhttp)
|
implementation(Libs.okhttp)
|
||||||
implementation(Libs.logging_interceptor)
|
implementation(Libs.logging_interceptor)
|
||||||
implementation(Libs.retrofit)
|
implementation(Libs.retrofit)
|
||||||
|
@ -61,4 +61,6 @@ dependencies {
|
|||||||
implementation(Libs.objectbox_kotlin)
|
implementation(Libs.objectbox_kotlin)
|
||||||
implementation(Libs.objectbox_rxjava)
|
implementation(Libs.objectbox_rxjava)
|
||||||
implementation(Libs.webkit)
|
implementation(Libs.webkit)
|
||||||
|
testImplementation(Libs.kotlinx_coroutines_test)
|
||||||
|
implementation(Libs.kotlinx_coroutines_android)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* Kiwix Android
|
||||||
|
* Copyright (c) 2020 Kiwix <android.kiwix.org>
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 <T> buildCallbackFlow(query: Query<T>) =
|
||||||
|
callbackFlow<List<T>> {
|
||||||
|
val subscription = query.subscribe()
|
||||||
|
.observer { sendBlocking(it) }
|
||||||
|
awaitClose(subscription::cancel)
|
||||||
|
}
|
||||||
|
}
|
@ -19,14 +19,20 @@ package org.kiwix.kiwixmobile.core.dao
|
|||||||
|
|
||||||
import io.objectbox.Box
|
import io.objectbox.Box
|
||||||
import io.objectbox.kotlin.query
|
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.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.data.local.entity.RecentSearch
|
||||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.RecentSearchListItem
|
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.RecentSearchListItem
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class NewRecentSearchDao @Inject constructor(private val box: Box<RecentSearchEntity>) {
|
class NewRecentSearchDao @Inject constructor(
|
||||||
fun recentSearches(zimId: String?) = box.asFlowable(
|
private val box: Box<RecentSearchEntity>,
|
||||||
|
private val flowBuilder: FlowBuilder
|
||||||
|
) {
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
fun recentSearches(zimId: String?) = flowBuilder.buildCallbackFlow(
|
||||||
box.query {
|
box.query {
|
||||||
equal(RecentSearchEntity_.zimId, zimId ?: "")
|
equal(RecentSearchEntity_.zimId, zimId ?: "")
|
||||||
orderDesc(RecentSearchEntity_.id)
|
orderDesc(RecentSearchEntity_.id)
|
||||||
|
@ -23,6 +23,7 @@ import dagger.Provides
|
|||||||
import io.objectbox.BoxStore
|
import io.objectbox.BoxStore
|
||||||
import io.objectbox.kotlin.boxFor
|
import io.objectbox.kotlin.boxFor
|
||||||
import org.kiwix.kiwixmobile.core.dao.FetchDownloadDao
|
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.HistoryDao
|
||||||
import org.kiwix.kiwixmobile.core.dao.NewBookDao
|
import org.kiwix.kiwixmobile.core.dao.NewBookDao
|
||||||
import org.kiwix.kiwixmobile.core.dao.NewBookmarksDao
|
import org.kiwix.kiwixmobile.core.dao.NewBookmarksDao
|
||||||
@ -57,8 +58,10 @@ open class DatabaseModule {
|
|||||||
@Provides @Singleton fun providesNewBookmarksDao(boxStore: BoxStore): NewBookmarksDao =
|
@Provides @Singleton fun providesNewBookmarksDao(boxStore: BoxStore): NewBookmarksDao =
|
||||||
NewBookmarksDao(boxStore.boxFor())
|
NewBookmarksDao(boxStore.boxFor())
|
||||||
|
|
||||||
@Provides @Singleton fun providesNewRecentSearchDao(boxStore: BoxStore): NewRecentSearchDao =
|
@Provides @Singleton fun providesNewRecentSearchDao(
|
||||||
NewRecentSearchDao(boxStore.boxFor())
|
boxStore: BoxStore,
|
||||||
|
flowBuilder: FlowBuilder
|
||||||
|
): NewRecentSearchDao = NewRecentSearchDao(boxStore.boxFor(), flowBuilder)
|
||||||
|
|
||||||
@Provides @Singleton fun providesFetchDownloadDao(
|
@Provides @Singleton fun providesFetchDownloadDao(
|
||||||
boxStore: BoxStore,
|
boxStore: BoxStore,
|
||||||
|
@ -24,21 +24,23 @@ import android.view.Menu
|
|||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.MenuItem.OnActionExpandListener
|
import android.view.MenuItem.OnActionExpandListener
|
||||||
import androidx.appcompat.widget.SearchView
|
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.ViewModelProvider
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import io.reactivex.disposables.CompositeDisposable
|
import kotlinx.android.synthetic.main.activity_search.searchLoadingIndicator
|
||||||
import kotlinx.android.synthetic.main.activity_search.searchViewAnimator
|
import kotlinx.android.synthetic.main.activity_search.searchNoResults
|
||||||
import kotlinx.android.synthetic.main.activity_search.search_list
|
import kotlinx.android.synthetic.main.activity_search.search_list
|
||||||
import kotlinx.android.synthetic.main.layout_toolbar.toolbar
|
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
|
||||||
import org.kiwix.kiwixmobile.core.R.id
|
import org.kiwix.kiwixmobile.core.R.id
|
||||||
import org.kiwix.kiwixmobile.core.base.BaseActivity
|
import org.kiwix.kiwixmobile.core.base.BaseActivity
|
||||||
import org.kiwix.kiwixmobile.core.di.components.CoreComponent
|
import org.kiwix.kiwixmobile.core.di.components.CoreComponent
|
||||||
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.coreActivityComponent
|
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.coreActivityComponent
|
||||||
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.viewModel
|
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.SearchAdapter
|
||||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchDelegate.RecentSearchDelegate
|
import org.kiwix.kiwixmobile.core.search.adapter.SearchDelegate.RecentSearchDelegate
|
||||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchDelegate.ZimSearchResultDelegate
|
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.ExitedSearch
|
||||||
import org.kiwix.kiwixmobile.core.search.viewmodel.Action.Filter
|
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.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.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.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.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 org.kiwix.kiwixmobile.core.utils.SimpleTextListener
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ -68,7 +68,6 @@ class SearchActivity : BaseActivity() {
|
|||||||
private lateinit var searchInTextMenuItem: MenuItem
|
private lateinit var searchInTextMenuItem: MenuItem
|
||||||
|
|
||||||
private val searchViewModel by lazy { viewModel<SearchViewModel>(viewModelFactory) }
|
private val searchViewModel by lazy { viewModel<SearchViewModel>(viewModelFactory) }
|
||||||
private val compositeDisposable = CompositeDisposable()
|
|
||||||
private val searchAdapter: SearchAdapter by lazy {
|
private val searchAdapter: SearchAdapter by lazy {
|
||||||
SearchAdapter(
|
SearchAdapter(
|
||||||
RecentSearchDelegate(::onItemClick, ::onItemClickNewTab) {
|
RecentSearchDelegate(::onItemClick, ::onItemClickNewTab) {
|
||||||
@ -93,12 +92,9 @@ class SearchActivity : BaseActivity() {
|
|||||||
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
|
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
|
||||||
setHasFixedSize(true)
|
setHasFixedSize(true)
|
||||||
}
|
}
|
||||||
compositeDisposable.add(searchViewModel.effects.subscribe { it.invokeWith(this) })
|
lifecycleScope.launchWhenCreated {
|
||||||
|
searchViewModel.effects.collect { it.invokeWith(this@SearchActivity) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
compositeDisposable.clear()
|
|
||||||
super.onDestroy()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun finish() {
|
override fun finish() {
|
||||||
@ -127,29 +123,19 @@ class SearchActivity : BaseActivity() {
|
|||||||
searchViewModel.actions.offer(ClickedSearchInText)
|
searchViewModel.actions.offer(ClickedSearchInText)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
lifecycleScope.launchWhenCreated {
|
||||||
searchViewModel.state.observe(this, Observer(::render))
|
searchViewModel.state.collect { render(it) }
|
||||||
|
}
|
||||||
searchViewModel.actions.offer(CreatedWithIntent(intent))
|
searchViewModel.actions.offer(CreatedWithIntent(intent))
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun render(state: State) {
|
private fun render(state: SearchState) {
|
||||||
searchInTextMenuItem.isVisible = state.searchOrigin == FromWebView
|
searchInTextMenuItem.isVisible = state.searchOrigin == FromWebView
|
||||||
when (state) {
|
searchInTextMenuItem.isEnabled = state.searchTerm.isNotBlank()
|
||||||
is Results -> {
|
searchLoadingIndicator.isShowing(state.isLoading)
|
||||||
searchViewAnimator.setDistinctDisplayedChild(0)
|
searchNoResults.isVisible = state.visibleResults.isEmpty()
|
||||||
searchAdapter.items = state.values
|
searchAdapter.items = state.visibleResults
|
||||||
render(state.searchString)
|
|
||||||
}
|
|
||||||
is NoResults -> {
|
|
||||||
searchViewAnimator.setDistinctDisplayedChild(1)
|
|
||||||
render(state.searchString)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun render(searchString: String) {
|
|
||||||
searchInTextMenuItem.isEnabled = searchString.isNotBlank()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onItemClick(it: SearchListItem) {
|
private fun onItemClick(it: SearchListItem) {
|
||||||
@ -165,3 +151,11 @@ class SearchActivity : BaseActivity() {
|
|||||||
searchViewModel.actions.offer(ActivityResultReceived(requestCode, resultCode, data))
|
searchViewModel.actions.offer(ActivityResultReceived(requestCode, resultCode, data))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun ContentLoadingProgressBar.isShowing(show: Boolean) {
|
||||||
|
if (show) {
|
||||||
|
show()
|
||||||
|
} else {
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -18,33 +18,49 @@
|
|||||||
|
|
||||||
package org.kiwix.kiwixmobile.core.search.viewmodel
|
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.ZimFileReader
|
||||||
import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer
|
|
||||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem
|
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem
|
||||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.ZimSearchResultListItem
|
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.ZimSearchResultListItem
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
interface SearchResultGenerator {
|
interface SearchResultGenerator {
|
||||||
fun generateSearchResults(searchTerm: String): List<SearchListItem>
|
suspend fun generateSearchResults(
|
||||||
|
searchTerm: String,
|
||||||
|
zimFileReader: ZimFileReader?
|
||||||
|
): List<SearchListItem>
|
||||||
}
|
}
|
||||||
|
|
||||||
class ZimSearchResultGenerator @Inject constructor(
|
class ZimSearchResultGenerator @Inject constructor() : SearchResultGenerator {
|
||||||
private val zimReaderContainer: ZimReaderContainer
|
|
||||||
) : SearchResultGenerator {
|
|
||||||
override fun generateSearchResults(searchTerm: String) =
|
|
||||||
if (searchTerm.isNotEmpty()) readResultsFromZim(searchTerm, zimReaderContainer.copyReader())
|
|
||||||
else emptyList()
|
|
||||||
|
|
||||||
private fun readResultsFromZim(
|
override suspend fun generateSearchResults(searchTerm: String, zimFileReader: ZimFileReader?) =
|
||||||
it: String,
|
withContext(Dispatchers.IO) {
|
||||||
|
if (searchTerm.isNotEmpty()) readResultsFromZim(searchTerm, zimFileReader)
|
||||||
|
else emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun readResultsFromZim(
|
||||||
|
searchTerm: String,
|
||||||
reader: ZimFileReader?
|
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 {
|
private suspend fun suggestionResults(reader: ZimFileReader?) = createList {
|
||||||
reader?.getNextSuggestion()?.let { ZimSearchResultListItem(it.title) }
|
yield()
|
||||||
|
reader?.getNextSuggestion()
|
||||||
|
?.let { ZimSearchResultListItem(it.title) }
|
||||||
}
|
}
|
||||||
.distinct()
|
.distinct()
|
||||||
.toList()
|
.toList()
|
||||||
.also { reader?.dispose() }
|
|
||||||
|
private suspend fun <T> createList(readSearchResult: suspend () -> T?): List<T> {
|
||||||
|
return mutableListOf<T>().apply {
|
||||||
|
while (true) readSearchResult()?.let(::add) ?: break
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,21 +20,21 @@ package org.kiwix.kiwixmobile.core.search.viewmodel
|
|||||||
|
|
||||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem
|
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem
|
||||||
|
|
||||||
|
data class SearchState(
|
||||||
|
val searchTerm: String,
|
||||||
|
val searchResultsWithTerm: SearchResultsWithTerm,
|
||||||
|
val recentResults: List<SearchListItem.RecentSearchListItem>,
|
||||||
|
val searchOrigin: SearchOrigin
|
||||||
|
) {
|
||||||
|
val visibleResults = when {
|
||||||
|
searchTerm.isNotEmpty() -> searchResultsWithTerm.results
|
||||||
|
else -> recentResults
|
||||||
|
}
|
||||||
|
|
||||||
|
val isLoading = searchTerm != searchResultsWithTerm.searchTerm
|
||||||
|
}
|
||||||
|
|
||||||
enum class SearchOrigin {
|
enum class SearchOrigin {
|
||||||
FromWebView,
|
FromWebView,
|
||||||
FromTabView
|
FromTabView
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class State {
|
|
||||||
abstract val searchString: String
|
|
||||||
abstract val searchOrigin: SearchOrigin
|
|
||||||
|
|
||||||
data class Results(
|
|
||||||
override val searchString: String,
|
|
||||||
val values: List<SearchListItem>,
|
|
||||||
override val searchOrigin: SearchOrigin
|
|
||||||
) : State()
|
|
||||||
|
|
||||||
data class NoResults(override val searchString: String, override val searchOrigin: SearchOrigin) :
|
|
||||||
State()
|
|
||||||
}
|
|
@ -18,15 +18,20 @@
|
|||||||
|
|
||||||
package org.kiwix.kiwixmobile.core.search.viewmodel
|
package org.kiwix.kiwixmobile.core.search.viewmodel
|
||||||
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import io.reactivex.Flowable
|
import androidx.lifecycle.viewModelScope
|
||||||
import io.reactivex.disposables.CompositeDisposable
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import io.reactivex.disposables.Disposable
|
import kotlinx.coroutines.channels.Channel
|
||||||
import io.reactivex.functions.Function4
|
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
|
||||||
import io.reactivex.processors.BehaviorProcessor
|
import kotlinx.coroutines.channels.consumeEach
|
||||||
import io.reactivex.processors.PublishProcessor
|
import kotlinx.coroutines.channels.sendBlocking
|
||||||
import io.reactivex.schedulers.Schedulers
|
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.R
|
||||||
import org.kiwix.kiwixmobile.core.base.SideEffect
|
import org.kiwix.kiwixmobile.core.base.SideEffect
|
||||||
import org.kiwix.kiwixmobile.core.dao.NewRecentSearchDao
|
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.ScreenWasStartedFrom
|
||||||
import org.kiwix.kiwixmobile.core.search.viewmodel.Action.StartSpeechInputFailed
|
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.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.DeleteRecentSearch
|
||||||
import org.kiwix.kiwixmobile.core.search.viewmodel.effects.FinishActivity
|
import org.kiwix.kiwixmobile.core.search.viewmodel.effects.FinishActivity
|
||||||
import org.kiwix.kiwixmobile.core.search.viewmodel.effects.OpenSearchItem
|
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.ShowDeleteSearchDialog
|
||||||
import org.kiwix.kiwixmobile.core.search.viewmodel.effects.ShowToast
|
import org.kiwix.kiwixmobile.core.search.viewmodel.effects.ShowToast
|
||||||
import org.kiwix.kiwixmobile.core.search.viewmodel.effects.StartSpeechInput
|
import org.kiwix.kiwixmobile.core.search.viewmodel.effects.StartSpeechInput
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
private const val DEBOUNCE_MS = 500L
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
|
||||||
class SearchViewModel @Inject constructor(
|
class SearchViewModel @Inject constructor(
|
||||||
private val recentSearchDao: NewRecentSearchDao,
|
private val recentSearchDao: NewRecentSearchDao,
|
||||||
private val zimReaderContainer: ZimReaderContainer,
|
private val zimReaderContainer: ZimReaderContainer,
|
||||||
private val searchResultGenerator: SearchResultGenerator
|
private val searchResultGenerator: SearchResultGenerator
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
val state = MutableLiveData<State>().apply { value = NoResults("", FromWebView) }
|
private val initialState: SearchState =
|
||||||
val effects = PublishProcessor.create<SideEffect<*>>()
|
SearchState("", SearchResultsWithTerm("", emptyList()), emptyList(), FromWebView)
|
||||||
val actions = PublishProcessor.create<Action>()
|
val state: MutableStateFlow<SearchState> = MutableStateFlow(initialState)
|
||||||
private val filter = BehaviorProcessor.createDefault("")
|
private val _effects = Channel<SideEffect<*>>()
|
||||||
private val searchOrigin = BehaviorProcessor.createDefault(FromWebView)
|
val effects = _effects.receiveAsFlow()
|
||||||
private val searchResults = PublishProcessor.create<List<SearchListItem>>()
|
val actions = Channel<Action>(Channel.UNLIMITED)
|
||||||
|
private val filter = ConflatedBroadcastChannel("")
|
||||||
private val compositeDisposable = CompositeDisposable()
|
private val searchOrigin = ConflatedBroadcastChannel(FromWebView)
|
||||||
private var searchTask: Disposable? = null
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
compositeDisposable.addAll(
|
viewModelScope.launch { reducer() }
|
||||||
viewStateReducer(),
|
viewModelScope.launch { actionMapper() }
|
||||||
actionMapper(),
|
|
||||||
searchResultEventsFromZimReader()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
private suspend fun reducer() {
|
||||||
compositeDisposable.clear()
|
combine(
|
||||||
super.onCleared()
|
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) {
|
when (it) {
|
||||||
ExitedSearch -> effects.offer(FinishActivity)
|
ExitedSearch -> _effects.offer(FinishActivity)
|
||||||
is OnItemClick -> saveSearchAndOpenItem(it.searchListItem, false)
|
is OnItemClick -> saveSearchAndOpenItem(it.searchListItem, false)
|
||||||
is OnOpenInNewTabClick -> saveSearchAndOpenItem(it.searchListItem, true)
|
is OnOpenInNewTabClick -> saveSearchAndOpenItem(it.searchListItem, true)
|
||||||
is OnItemLongClick -> showDeleteDialog(it)
|
is OnItemLongClick -> showDeleteDialog(it)
|
||||||
is Filter -> filter.offer(it.term)
|
is Filter -> filter.sendBlocking(it.term)
|
||||||
ClickedSearchInText -> searchPreviousScreenWhenStateIsValid()
|
ClickedSearchInText -> searchPreviousScreenWhenStateIsValid()
|
||||||
is ConfirmedDelete -> deleteItemAndShowToast(it)
|
is ConfirmedDelete -> deleteItemAndShowToast(it)
|
||||||
is CreatedWithIntent -> effects.offer(SearchIntentProcessing(it.intent, actions))
|
is CreatedWithIntent -> _effects.offer(SearchIntentProcessing(it.intent, actions))
|
||||||
ReceivedPromptForSpeechInput -> effects.offer(StartSpeechInput(actions))
|
ReceivedPromptForSpeechInput -> _effects.offer(StartSpeechInput(actions))
|
||||||
StartSpeechInputFailed -> effects.offer(ShowToast(R.string.speech_not_supported))
|
StartSpeechInputFailed -> _effects.offer(ShowToast(R.string.speech_not_supported))
|
||||||
is ActivityResultReceived ->
|
is ActivityResultReceived ->
|
||||||
effects.offer(ProcessActivityResult(it.requestCode, it.resultCode, it.data, actions))
|
_effects.offer(ProcessActivityResult(it.requestCode, it.resultCode, it.data, actions))
|
||||||
is ScreenWasStartedFrom -> searchOrigin.offer(it.searchOrigin)
|
is ScreenWasStartedFrom -> searchOrigin.sendBlocking(it.searchOrigin)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.subscribe(
|
|
||||||
{},
|
|
||||||
Throwable::printStackTrace
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun deleteItemAndShowToast(it: ConfirmedDelete) {
|
private fun deleteItemAndShowToast(it: ConfirmedDelete) {
|
||||||
effects.offer(DeleteRecentSearch(it.searchListItem, recentSearchDao))
|
_effects.offer(DeleteRecentSearch(it.searchListItem, recentSearchDao))
|
||||||
effects.offer(ShowToast(R.string.delete_specific_search_toast))
|
_effects.offer(ShowToast(R.string.delete_specific_search_toast))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun searchPreviousScreenWhenStateIsValid(): Any =
|
private fun searchPreviousScreenWhenStateIsValid(): Any =
|
||||||
effects.offer(SearchInPreviousScreen(state.value!!.searchString))
|
_effects.offer(SearchInPreviousScreen(state.value.searchTerm))
|
||||||
|
|
||||||
private fun showDeleteDialog(longClick: OnItemLongClick) {
|
private fun showDeleteDialog(longClick: OnItemLongClick) {
|
||||||
effects.offer(ShowDeleteSearchDialog(longClick.searchListItem, actions))
|
_effects.offer(ShowDeleteSearchDialog(longClick.searchListItem, actions))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveSearchAndOpenItem(searchListItem: SearchListItem, openInNewTab: Boolean) {
|
private fun saveSearchAndOpenItem(searchListItem: SearchListItem, openInNewTab: Boolean) {
|
||||||
effects.offer(
|
_effects.offer(SaveSearchToRecents(recentSearchDao, searchListItem, zimReaderContainer.id))
|
||||||
SaveSearchToRecents(recentSearchDao, searchListItem, zimReaderContainer.id)
|
_effects.offer(OpenSearchItem(searchListItem, openInNewTab))
|
||||||
)
|
}
|
||||||
effects.offer(
|
|
||||||
OpenSearchItem(searchListItem, openInNewTab)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun viewStateReducer() =
|
data class SearchResultsWithTerm(val searchTerm: String, val results: List<SearchListItem>)
|
||||||
Flowable.combineLatest(
|
|
||||||
recentSearchDao.recentSearches(zimReaderContainer.id),
|
|
||||||
searchResults,
|
|
||||||
filter,
|
|
||||||
searchOrigin,
|
|
||||||
Function4(this::reduce)
|
|
||||||
).subscribe(state::postValue, Throwable::printStackTrace)
|
|
||||||
|
|
||||||
private fun reduce(
|
|
||||||
recentSearchResults: List<SearchListItem>,
|
|
||||||
zimSearchResults: List<SearchListItem>,
|
|
||||||
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
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -22,7 +22,7 @@ import android.app.Activity
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.speech.RecognizerIntent
|
import android.speech.RecognizerIntent
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
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.base.SideEffect
|
||||||
import org.kiwix.kiwixmobile.core.search.viewmodel.Action
|
import org.kiwix.kiwixmobile.core.search.viewmodel.Action
|
||||||
import org.kiwix.kiwixmobile.core.search.viewmodel.Action.Filter
|
import org.kiwix.kiwixmobile.core.search.viewmodel.Action.Filter
|
||||||
@ -31,7 +31,7 @@ data class ProcessActivityResult(
|
|||||||
private val requestCode: Int,
|
private val requestCode: Int,
|
||||||
private val resultCode: Int,
|
private val resultCode: Int,
|
||||||
private val data: Intent?,
|
private val data: Intent?,
|
||||||
private val actions: PublishProcessor<Action>
|
private val actions: Channel<Action>
|
||||||
) : SideEffect<Unit> {
|
) : SideEffect<Unit> {
|
||||||
override fun invokeWith(activity: AppCompatActivity) {
|
override fun invokeWith(activity: AppCompatActivity) {
|
||||||
if (requestCode == StartSpeechInput.REQ_CODE_SPEECH_INPUT &&
|
if (requestCode == StartSpeechInput.REQ_CODE_SPEECH_INPUT &&
|
||||||
|
@ -22,7 +22,7 @@ import android.annotation.TargetApi
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build.VERSION_CODES
|
import android.os.Build.VERSION_CODES
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
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.base.SideEffect
|
||||||
import org.kiwix.kiwixmobile.core.search.viewmodel.Action
|
import org.kiwix.kiwixmobile.core.search.viewmodel.Action
|
||||||
import org.kiwix.kiwixmobile.core.search.viewmodel.Action.Filter
|
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(
|
data class SearchIntentProcessing(
|
||||||
private val intent: Intent?,
|
private val intent: Intent?,
|
||||||
private val actions: PublishProcessor<Action>
|
private val actions: Channel<Action>
|
||||||
) : SideEffect<Unit> {
|
) : SideEffect<Unit> {
|
||||||
@TargetApi(VERSION_CODES.M)
|
@TargetApi(VERSION_CODES.M)
|
||||||
override fun invokeWith(activity: AppCompatActivity) {
|
override fun invokeWith(activity: AppCompatActivity) {
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
package org.kiwix.kiwixmobile.core.search.viewmodel.effects
|
package org.kiwix.kiwixmobile.core.search.viewmodel.effects
|
||||||
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
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.base.SideEffect
|
||||||
import org.kiwix.kiwixmobile.core.search.SearchActivity
|
import org.kiwix.kiwixmobile.core.search.SearchActivity
|
||||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem
|
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem
|
||||||
@ -30,7 +30,7 @@ import javax.inject.Inject
|
|||||||
|
|
||||||
data class ShowDeleteSearchDialog(
|
data class ShowDeleteSearchDialog(
|
||||||
private val searchListItem: SearchListItem,
|
private val searchListItem: SearchListItem,
|
||||||
private val actions: PublishProcessor<Action>
|
private val actions: Channel<Action>
|
||||||
) : SideEffect<Unit> {
|
) : SideEffect<Unit> {
|
||||||
|
|
||||||
@Inject lateinit var dialogShower: DialogShower
|
@Inject lateinit var dialogShower: DialogShower
|
||||||
|
@ -22,14 +22,14 @@ import android.content.ActivityNotFoundException
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.speech.RecognizerIntent
|
import android.speech.RecognizerIntent
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
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.R
|
||||||
import org.kiwix.kiwixmobile.core.base.SideEffect
|
import org.kiwix.kiwixmobile.core.base.SideEffect
|
||||||
import org.kiwix.kiwixmobile.core.search.viewmodel.Action
|
import org.kiwix.kiwixmobile.core.search.viewmodel.Action
|
||||||
import org.kiwix.kiwixmobile.core.search.viewmodel.Action.StartSpeechInputFailed
|
import org.kiwix.kiwixmobile.core.search.viewmodel.Action.StartSpeechInputFailed
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
data class StartSpeechInput(private val actions: PublishProcessor<Action>) : SideEffect<Unit> {
|
data class StartSpeechInput(private val actions: Channel<Action>) : SideEffect<Unit> {
|
||||||
|
|
||||||
override fun invokeWith(activity: AppCompatActivity) {
|
override fun invokeWith(activity: AppCompatActivity) {
|
||||||
try {
|
try {
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:animateLayoutChanges="true"
|
||||||
android:fitsSystemWindows="true"
|
android:fitsSystemWindows="true"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
@ -16,26 +18,28 @@
|
|||||||
<include layout="@layout/layout_toolbar" />
|
<include layout="@layout/layout_toolbar" />
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
<ViewAnimator
|
|
||||||
android:id="@+id/searchViewAnimator"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:animateLayoutChanges="true">
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/search_list"
|
android:id="@+id/search_list"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent" />
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
android:id="@+id/searchNoResults"
|
||||||
style="@style/no_content_text"
|
style="@style/no_content_text"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:text="@string/no_results" />
|
android:text="@string/no_results" />
|
||||||
|
|
||||||
</ViewAnimator>
|
<androidx.core.widget.ContentLoadingProgressBar
|
||||||
|
android:id="@+id/searchLoadingIndicator"
|
||||||
</LinearLayout>
|
style="?android:attr/progressBarStyleLarge"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center" />
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
|
@ -20,60 +20,72 @@ package org.kiwix.kiwixmobile.core.dao
|
|||||||
|
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.mockkStatic
|
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
import io.objectbox.Box
|
import io.objectbox.Box
|
||||||
import io.objectbox.query.Query
|
import io.objectbox.query.Query
|
||||||
import io.objectbox.query.QueryBuilder
|
import io.objectbox.query.QueryBuilder
|
||||||
import io.objectbox.rx.RxQuery
|
import kotlinx.coroutines.flow.Flow
|
||||||
import io.reactivex.Observable
|
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.Nested
|
||||||
import org.junit.jupiter.api.Test
|
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.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.data.local.entity.RecentSearch
|
||||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.RecentSearchListItem
|
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.RecentSearchListItem
|
||||||
|
import org.kiwix.kiwixmobile.core.search.viewmodel.test
|
||||||
import org.kiwix.sharedFunctions.recentSearchEntity
|
import org.kiwix.sharedFunctions.recentSearchEntity
|
||||||
|
|
||||||
internal class NewRecentSearchDaoTest {
|
internal class NewRecentSearchDaoTest {
|
||||||
|
|
||||||
private val box: Box<RecentSearchEntity> = mockk(relaxed = true)
|
private val box: Box<RecentSearchEntity> = mockk(relaxed = true)
|
||||||
private val newRecentSearchDao = NewRecentSearchDao(box)
|
private val flowBuilder: FlowBuilder = mockk()
|
||||||
|
private val newRecentSearchDao = NewRecentSearchDao(box, flowBuilder)
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
inner class RecentSearchTests {
|
inner class RecentSearchTests {
|
||||||
@Test
|
@Test
|
||||||
fun `recentSearches searches by Id passed`() {
|
fun `recentSearches searches by Id passed`() = runBlockingTest {
|
||||||
val zimId = "id"
|
val zimId = "id"
|
||||||
val queryResult = listOf<RecentSearchEntity>(recentSearchEntity())
|
val queryResult = listOf<RecentSearchEntity>(recentSearchEntity())
|
||||||
expectFromRecentSearches(queryResult, zimId)
|
expectFromRecentSearches(queryResult, zimId)
|
||||||
newRecentSearchDao.recentSearches(zimId).test()
|
newRecentSearchDao.recentSearches(zimId)
|
||||||
|
.test(this)
|
||||||
.assertValues(queryResult.map { RecentSearchListItem(it.searchTerm) })
|
.assertValues(queryResult.map { RecentSearchListItem(it.searchTerm) })
|
||||||
|
.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `recentSearches searches with blank Id if null passed`() {
|
fun `recentSearches searches with blank Id if null passed`() = runBlockingTest {
|
||||||
val queryResult = listOf<RecentSearchEntity>(recentSearchEntity())
|
val queryResult = listOf<RecentSearchEntity>(recentSearchEntity())
|
||||||
expectFromRecentSearches(queryResult, "")
|
expectFromRecentSearches(queryResult, "")
|
||||||
newRecentSearchDao.recentSearches(null).test()
|
newRecentSearchDao.recentSearches(null)
|
||||||
|
.test(this)
|
||||||
.assertValues(queryResult.map { RecentSearchListItem(it.searchTerm) })
|
.assertValues(queryResult.map { RecentSearchListItem(it.searchTerm) })
|
||||||
|
.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `recentSearches searches returns distinct entities by searchTerm`() {
|
fun `recentSearches searches returns distinct entities by searchTerm`() = runBlockingTest {
|
||||||
val queryResult = listOf<RecentSearchEntity>(recentSearchEntity(), recentSearchEntity())
|
val queryResult = listOf<RecentSearchEntity>(recentSearchEntity(), recentSearchEntity())
|
||||||
expectFromRecentSearches(queryResult, "")
|
expectFromRecentSearches(queryResult, "")
|
||||||
newRecentSearchDao.recentSearches("").test()
|
newRecentSearchDao.recentSearches("")
|
||||||
|
.test(this)
|
||||||
.assertValues(queryResult.take(1).map { RecentSearchListItem(it.searchTerm) })
|
.assertValues(queryResult.take(1).map { RecentSearchListItem(it.searchTerm) })
|
||||||
|
.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `recentSearches searches returns a limitedNumber of entities`() {
|
fun `recentSearches searches returns a limitedNumber of entities`() = runBlockingTest {
|
||||||
val searchResults: List<RecentSearchEntity> =
|
val searchResults: List<RecentSearchEntity> =
|
||||||
(0..200).map { recentSearchEntity(searchTerm = "$it") }
|
(0..200).map { recentSearchEntity(searchTerm = "$it") }
|
||||||
expectFromRecentSearches(searchResults, "")
|
expectFromRecentSearches(searchResults, "")
|
||||||
newRecentSearchDao.recentSearches("").test()
|
newRecentSearchDao.recentSearches("")
|
||||||
|
.test(this)
|
||||||
.assertValue { it.size == 100 }
|
.assertValue { it.size == 100 }
|
||||||
|
.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun expectFromRecentSearches(queryResult: List<RecentSearchEntity>, zimId: String) {
|
private fun expectFromRecentSearches(queryResult: List<RecentSearchEntity>, zimId: String) {
|
||||||
@ -83,8 +95,7 @@ internal class NewRecentSearchDaoTest {
|
|||||||
every { queryBuilder.orderDesc(RecentSearchEntity_.id) } returns queryBuilder
|
every { queryBuilder.orderDesc(RecentSearchEntity_.id) } returns queryBuilder
|
||||||
val query = mockk<Query<RecentSearchEntity>>()
|
val query = mockk<Query<RecentSearchEntity>>()
|
||||||
every { queryBuilder.build() } returns query
|
every { queryBuilder.build() } returns query
|
||||||
mockkStatic(RxQuery::class)
|
every { flowBuilder.buildCallbackFlow(query) } returns flowOf(queryResult)
|
||||||
every { RxQuery.observable(query) } returns Observable.just(queryResult)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,3 +134,13 @@ internal class NewRecentSearchDaoTest {
|
|||||||
verify { box.put(listOf(recentSearchEntity(searchTerm = term, zimId = id))) }
|
verify { box.put(listOf(recentSearchEntity(searchTerm = term, zimId = id))) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun <T> Flow<T>.assertValue(function: (T) -> Boolean) {
|
||||||
|
val value = toList().last()
|
||||||
|
val result = function(value)
|
||||||
|
assertThat(result).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend inline fun <T> Flow<T>.assertValues(expectedValues: List<T>) {
|
||||||
|
assertThat(toList()).containsExactlyElementsOf(expectedValues)
|
||||||
|
}
|
||||||
|
@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
* Kiwix Android
|
||||||
|
* Copyright (c) 2020 Kiwix <android.kiwix.org>
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
@ -19,21 +19,34 @@
|
|||||||
package org.kiwix.kiwixmobile.core.search.viewmodel
|
package org.kiwix.kiwixmobile.core.search.viewmodel
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.jraska.livedata.test
|
|
||||||
import io.mockk.clearAllMocks
|
import io.mockk.clearAllMocks
|
||||||
|
import io.mockk.coEvery
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.reactivex.processors.PublishProcessor
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import io.reactivex.schedulers.TestScheduler
|
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.AfterAll
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Nested
|
import org.junit.jupiter.api.Nested
|
||||||
import org.junit.jupiter.api.Test
|
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
|
||||||
import org.kiwix.kiwixmobile.core.R.string
|
import org.kiwix.kiwixmobile.core.R.string
|
||||||
import org.kiwix.kiwixmobile.core.base.SideEffect
|
import org.kiwix.kiwixmobile.core.base.SideEffect
|
||||||
import org.kiwix.kiwixmobile.core.dao.NewRecentSearchDao
|
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.reader.ZimReaderContainer
|
||||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.RecentSearchListItem
|
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.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.ReceivedPromptForSpeechInput
|
||||||
import org.kiwix.kiwixmobile.core.search.viewmodel.Action.ScreenWasStartedFrom
|
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.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.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.DeleteRecentSearch
|
||||||
import org.kiwix.kiwixmobile.core.search.viewmodel.effects.FinishActivity
|
import org.kiwix.kiwixmobile.core.search.viewmodel.effects.FinishActivity
|
||||||
import org.kiwix.kiwixmobile.core.search.viewmodel.effects.OpenSearchItem
|
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.ShowDeleteSearchDialog
|
||||||
import org.kiwix.kiwixmobile.core.search.viewmodel.effects.ShowToast
|
import org.kiwix.kiwixmobile.core.search.viewmodel.effects.ShowToast
|
||||||
import org.kiwix.kiwixmobile.core.search.viewmodel.effects.StartSpeechInput
|
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 {
|
internal class SearchViewModelTest {
|
||||||
private val recentSearchDao: NewRecentSearchDao = mockk()
|
private val recentSearchDao: NewRecentSearchDao = mockk()
|
||||||
private val zimReaderContainer: ZimReaderContainer = mockk()
|
private val zimReaderContainer: ZimReaderContainer = mockk()
|
||||||
private val searchResultGenerator: SearchResultGenerator = mockk()
|
private val searchResultGenerator: SearchResultGenerator = mockk()
|
||||||
|
private val zimFileReader: ZimFileReader = mockk()
|
||||||
|
private val testDispatcher = TestCoroutineDispatcher()
|
||||||
|
|
||||||
lateinit var viewModel: SearchViewModel
|
lateinit var viewModel: SearchViewModel
|
||||||
|
|
||||||
private val testScheduler = TestScheduler()
|
|
||||||
|
|
||||||
init {
|
|
||||||
setScheduler(testScheduler)
|
|
||||||
}
|
|
||||||
|
|
||||||
@AfterAll
|
@AfterAll
|
||||||
fun teardown() {
|
fun teardown() {
|
||||||
resetSchedulers()
|
Dispatchers.resetMain()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val recentsFromDb: PublishProcessor<List<RecentSearchListItem>> =
|
private lateinit var recentsFromDb: Channel<List<RecentSearchListItem>>
|
||||||
PublishProcessor.create()
|
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun init() {
|
fun init() {
|
||||||
|
Dispatchers.resetMain()
|
||||||
|
Dispatchers.setMain(testDispatcher)
|
||||||
clearAllMocks()
|
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 { zimReaderContainer.id } returns "id"
|
||||||
every { recentSearchDao.recentSearches("id") } returns recentsFromDb
|
every { recentSearchDao.recentSearches("id") } returns recentsFromDb.consumeAsFlow()
|
||||||
viewModel = SearchViewModel(recentSearchDao, zimReaderContainer, searchResultGenerator)
|
viewModel = SearchViewModel(recentSearchDao, zimReaderContainer, searchResultGenerator)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
inner class StateTests {
|
inner class StateTests {
|
||||||
@Test
|
@Test
|
||||||
fun `initial state is Initialising`() {
|
fun `initial state is Initialising`() = runBlockingTest {
|
||||||
viewModel.state.test().assertValue(NoResults("", FromWebView))
|
viewModel.state.test(this).assertValue(
|
||||||
|
SearchState("", SearchResultsWithTerm("", emptyList()), emptyList(), FromWebView)
|
||||||
|
).finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `non empty search term with search results shows Results`() {
|
fun `SearchState combines sources from inputs`() = runBlockingTest {
|
||||||
val item = ZimSearchResultListItem("")
|
val item = ZimSearchResultListItem("")
|
||||||
val searchTerm = "searchTerm"
|
val searchTerm = "searchTerm"
|
||||||
val searchOrigin = FromWebView
|
val searchOrigin = FromWebView
|
||||||
|
viewModel.state.test(this)
|
||||||
|
.also {
|
||||||
emissionOf(
|
emissionOf(
|
||||||
searchTerm = searchTerm,
|
searchTerm = searchTerm,
|
||||||
searchResults = listOf(item),
|
searchResults = listOf(item),
|
||||||
databaseResults = listOf(RecentSearchListItem("")),
|
databaseResults = listOf(RecentSearchListItem("")),
|
||||||
searchOrigin = searchOrigin
|
searchOrigin = searchOrigin
|
||||||
)
|
)
|
||||||
resultsIn(Results(searchTerm, listOf(item), searchOrigin))
|
|
||||||
}
|
}
|
||||||
|
.assertValue(
|
||||||
@Test
|
SearchState(
|
||||||
fun `non empty search string with no search results is NoResults`() {
|
searchTerm,
|
||||||
emissionOf(
|
SearchResultsWithTerm(searchTerm, listOf(item)),
|
||||||
searchTerm = "a",
|
listOf(RecentSearchListItem("")),
|
||||||
searchResults = emptyList(),
|
searchOrigin
|
||||||
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))
|
.finish()
|
||||||
}
|
|
||||||
|
|
||||||
@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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
inner class ActionMapping {
|
inner class ActionMapping {
|
||||||
@Test
|
@Test
|
||||||
fun `ExitedSearch offers Finish`() {
|
fun `ExitedSearch offers Finish`() = runBlockingTest {
|
||||||
actionResultsInEffects(ExitedSearch, FinishActivity)
|
actionResultsInEffects(ExitedSearch, FinishActivity)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `OnItemClick offers Saves and Opens`() {
|
fun `OnItemClick offers Saves and Opens`() = runBlockingTest {
|
||||||
val searchListItem = RecentSearchListItem("")
|
val searchListItem = RecentSearchListItem("")
|
||||||
actionResultsInEffects(
|
actionResultsInEffects(
|
||||||
OnItemClick(searchListItem),
|
OnItemClick(searchListItem),
|
||||||
@ -232,7 +159,7 @@ internal class SearchViewModelTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `OnOpenInNewTabClick offers Saves and Opens in new tab`() {
|
fun `OnOpenInNewTabClick offers Saves and Opens in new tab`() = runBlockingTest {
|
||||||
val searchListItem = RecentSearchListItem("")
|
val searchListItem = RecentSearchListItem("")
|
||||||
actionResultsInEffects(
|
actionResultsInEffects(
|
||||||
OnOpenInNewTabClick(searchListItem),
|
OnOpenInNewTabClick(searchListItem),
|
||||||
@ -242,7 +169,7 @@ internal class SearchViewModelTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `OnItemLongClick offers Saves and Opens`() {
|
fun `OnItemLongClick offers Saves and Opens`() = runBlockingTest {
|
||||||
val searchListItem = RecentSearchListItem("")
|
val searchListItem = RecentSearchListItem("")
|
||||||
actionResultsInEffects(
|
actionResultsInEffects(
|
||||||
OnItemLongClick(searchListItem),
|
OnItemLongClick(searchListItem),
|
||||||
@ -251,12 +178,12 @@ internal class SearchViewModelTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ClickedSearchInText offers SearchInPreviousScreen`() {
|
fun `ClickedSearchInText offers SearchInPreviousScreen`() = runBlockingTest {
|
||||||
actionResultsInEffects(ClickedSearchInText, SearchInPreviousScreen(""))
|
actionResultsInEffects(ClickedSearchInText, SearchInPreviousScreen(""))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ConfirmedDelete offers Delete and Toast`() {
|
fun `ConfirmedDelete offers Delete and Toast`() = runBlockingTest {
|
||||||
val searchListItem = RecentSearchListItem("")
|
val searchListItem = RecentSearchListItem("")
|
||||||
actionResultsInEffects(
|
actionResultsInEffects(
|
||||||
ConfirmedDelete(searchListItem),
|
ConfirmedDelete(searchListItem),
|
||||||
@ -266,7 +193,7 @@ internal class SearchViewModelTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `CreatedWithIntent offers SearchIntentProcessing`() {
|
fun `CreatedWithIntent offers SearchIntentProcessing`() = runBlockingTest {
|
||||||
val intent = mockk<Intent>()
|
val intent = mockk<Intent>()
|
||||||
actionResultsInEffects(
|
actionResultsInEffects(
|
||||||
CreatedWithIntent(intent),
|
CreatedWithIntent(intent),
|
||||||
@ -275,7 +202,7 @@ internal class SearchViewModelTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ReceivedPromptForSpeechInput offers SearchIntentProcessing`() {
|
fun `ReceivedPromptForSpeechInput offers SearchIntentProcessing`() = runBlockingTest {
|
||||||
actionResultsInEffects(
|
actionResultsInEffects(
|
||||||
ReceivedPromptForSpeechInput,
|
ReceivedPromptForSpeechInput,
|
||||||
StartSpeechInput(viewModel.actions)
|
StartSpeechInput(viewModel.actions)
|
||||||
@ -283,7 +210,7 @@ internal class SearchViewModelTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `StartSpeechInputFailed offers ShowToast`() {
|
fun `StartSpeechInputFailed offers ShowToast`() = runBlockingTest {
|
||||||
actionResultsInEffects(
|
actionResultsInEffects(
|
||||||
StartSpeechInputFailed,
|
StartSpeechInputFailed,
|
||||||
ShowToast(string.speech_not_supported)
|
ShowToast(string.speech_not_supported)
|
||||||
@ -291,40 +218,71 @@ internal class SearchViewModelTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `ActivityResultReceived offers ProcessActivityResult`() {
|
fun `ActivityResultReceived offers ProcessActivityResult`() = runBlockingTest {
|
||||||
actionResultsInEffects(
|
actionResultsInEffects(
|
||||||
ActivityResultReceived(0, 1, null),
|
ActivityResultReceived(0, 1, null),
|
||||||
ProcessActivityResult(0, 1, null, viewModel.actions)
|
ProcessActivityResult(0, 1, null, viewModel.actions)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun actionResultsInEffects(
|
private fun TestCoroutineScope.actionResultsInEffects(
|
||||||
action: Action,
|
action: Action,
|
||||||
vararg effects: SideEffect<*>
|
vararg effects: SideEffect<*>
|
||||||
) {
|
) {
|
||||||
viewModel.effects
|
viewModel.effects
|
||||||
.test()
|
.test(this)
|
||||||
.also { viewModel.actions.offer(action) }
|
.also { viewModel.actions.offer(action) }
|
||||||
.assertValues(*effects)
|
.assertValues(*effects)
|
||||||
|
.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resultsIn(st: State) {
|
private fun TestCoroutineScope.emissionOf(
|
||||||
viewModel.state.test()
|
|
||||||
.also { testScheduler.advanceTimeBy(100, MILLISECONDS) }
|
|
||||||
.assertValue(st)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun emissionOf(
|
|
||||||
searchTerm: String,
|
searchTerm: String,
|
||||||
searchResults: List<ZimSearchResultListItem>,
|
searchResults: List<ZimSearchResultListItem>,
|
||||||
databaseResults: List<RecentSearchListItem>,
|
databaseResults: List<RecentSearchListItem>,
|
||||||
searchOrigin: SearchOrigin
|
searchOrigin: SearchOrigin
|
||||||
) {
|
) {
|
||||||
every { searchResultGenerator.generateSearchResults(searchTerm) } returns searchResults
|
|
||||||
|
coEvery {
|
||||||
|
searchResultGenerator.generateSearchResults(searchTerm, zimFileReader)
|
||||||
|
} returns searchResults
|
||||||
viewModel.actions.offer(Filter(searchTerm))
|
viewModel.actions.offer(Filter(searchTerm))
|
||||||
recentsFromDb.offer(databaseResults)
|
recentsFromDb.offer(databaseResults)
|
||||||
viewModel.actions.offer(ScreenWasStartedFrom(searchOrigin))
|
viewModel.actions.offer(ScreenWasStartedFrom(searchOrigin))
|
||||||
testScheduler.advanceTimeBy(500, MILLISECONDS)
|
advanceTimeBy(500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> Flow<T>.test(scope: CoroutineScope) = TestObserver(scope, this)
|
||||||
|
|
||||||
|
class TestObserver<T>(
|
||||||
|
scope: CoroutineScope,
|
||||||
|
flow: Flow<T>
|
||||||
|
) {
|
||||||
|
private val values = mutableListOf<T>()
|
||||||
|
private val job: Job = scope.launch {
|
||||||
|
flow.collect {
|
||||||
|
values.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assertValues(vararg values: T): TestObserver<T> {
|
||||||
|
assertThat(values.toList()).containsExactlyElementsOf(this.values)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assertValue(value: T): TestObserver<T> {
|
||||||
|
assertThat(values.last()).isEqualTo(value)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun finish() {
|
||||||
|
job.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun assertValue(value: (T) -> Boolean): TestObserver<T> {
|
||||||
|
assertThat(values.last()).satisfies { value(it) }
|
||||||
|
return this
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,34 +21,27 @@ package org.kiwix.kiwixmobile.core.search.viewmodel
|
|||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.jupiter.api.BeforeEach
|
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.kiwix.kiwixmobile.core.reader.ZimFileReader
|
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.SearchSuggestion
|
||||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.ZimSearchResultListItem
|
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.ZimSearchResultListItem
|
||||||
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
|
|
||||||
|
|
||||||
internal class ZimSearchResultGeneratorTest {
|
internal class ZimSearchResultGeneratorTest {
|
||||||
|
|
||||||
private val sharedPreferenceUtil: SharedPreferenceUtil = mockk()
|
|
||||||
private val zimReaderContainer: ZimReaderContainer = mockk()
|
|
||||||
private val zimFileReader: ZimFileReader = mockk()
|
private val zimFileReader: ZimFileReader = mockk()
|
||||||
|
|
||||||
private val zimSearchResultGenerator: ZimSearchResultGenerator =
|
private val zimSearchResultGenerator: ZimSearchResultGenerator =
|
||||||
ZimSearchResultGenerator(zimReaderContainer)
|
ZimSearchResultGenerator()
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
internal fun setUp() {
|
|
||||||
every { zimReaderContainer.copyReader() } returns zimFileReader
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
internal fun `empty search term returns empty list`() {
|
internal fun `empty search term returns empty list`() {
|
||||||
assertThat(zimSearchResultGenerator.generateSearchResults(""))
|
runBlocking {
|
||||||
|
assertThat(zimSearchResultGenerator.generateSearchResults("", zimFileReader))
|
||||||
.isEqualTo(emptyList<ZimSearchResultListItem>())
|
.isEqualTo(emptyList<ZimSearchResultListItem>())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
internal fun `suggestion results are distinct`() {
|
internal fun `suggestion results are distinct`() {
|
||||||
@ -58,11 +51,12 @@ internal class ZimSearchResultGeneratorTest {
|
|||||||
every { zimFileReader.searchSuggestions(" ", 200) } returns true
|
every { zimFileReader.searchSuggestions(" ", 200) } returns true
|
||||||
every { zimFileReader.getNextSuggestion() } returnsMany listOf(item, item, null)
|
every { zimFileReader.getNextSuggestion() } returnsMany listOf(item, item, null)
|
||||||
every { item.title } returns validTitle
|
every { item.title } returns validTitle
|
||||||
assertThat(zimSearchResultGenerator.generateSearchResults(searchTerm))
|
runBlocking {
|
||||||
|
assertThat(zimSearchResultGenerator.generateSearchResults(searchTerm, zimFileReader))
|
||||||
.isEqualTo(listOf(ZimSearchResultListItem(validTitle)))
|
.isEqualTo(listOf(ZimSearchResultListItem(validTitle)))
|
||||||
verify {
|
verify {
|
||||||
zimFileReader.searchSuggestions(searchTerm, 200)
|
zimFileReader.searchSuggestions(searchTerm, 200)
|
||||||
zimFileReader.dispose()
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ import io.mockk.clearAllMocks
|
|||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.verify
|
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.BeforeEach
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.kiwix.kiwixmobile.core.search.viewmodel.Action
|
import org.kiwix.kiwixmobile.core.search.viewmodel.Action
|
||||||
@ -37,7 +37,7 @@ internal class ProcessActivityResultTest {
|
|||||||
|
|
||||||
private val activity: AppCompatActivity = mockk()
|
private val activity: AppCompatActivity = mockk()
|
||||||
private val data = mockk<Intent>()
|
private val data = mockk<Intent>()
|
||||||
private val actions = mockk<PublishProcessor<Action>>(relaxed = true)
|
private val actions = mockk<Channel<Action>>(relaxed = true)
|
||||||
private val successfulResult = ProcessActivityResult(
|
private val successfulResult = ProcessActivityResult(
|
||||||
StartSpeechInput.REQ_CODE_SPEECH_INPUT,
|
StartSpeechInput.REQ_CODE_SPEECH_INPUT,
|
||||||
Activity.RESULT_OK,
|
Activity.RESULT_OK,
|
||||||
|
@ -26,7 +26,7 @@ import io.mockk.every
|
|||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
import io.mockk.verifySequence
|
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.BeforeEach
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.kiwix.kiwixmobile.core.search.viewmodel.Action
|
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.Action.ScreenWasStartedFrom
|
||||||
import org.kiwix.kiwixmobile.core.search.viewmodel.SearchOrigin.FromTabView
|
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.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_IS_WIDGET_VOICE
|
||||||
|
import org.kiwix.kiwixmobile.core.utils.EXTRA_SEARCH
|
||||||
|
import org.kiwix.kiwixmobile.core.utils.TAG_FROM_TAB_SWITCHER
|
||||||
|
|
||||||
internal class SearchIntentProcessingTest {
|
internal class SearchIntentProcessingTest {
|
||||||
|
|
||||||
private val actions: PublishProcessor<Action> = mockk(relaxed = true)
|
private val actions: Channel<Action> = mockk(relaxed = true)
|
||||||
|
|
||||||
private val activity: AppCompatActivity = mockk()
|
private val activity: AppCompatActivity = mockk()
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ import io.mockk.every
|
|||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.slot
|
import io.mockk.slot
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
import io.reactivex.processors.PublishProcessor
|
import kotlinx.coroutines.channels.Channel
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.kiwix.kiwixmobile.core.search.SearchActivity
|
import org.kiwix.kiwixmobile.core.search.SearchActivity
|
||||||
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.RecentSearchListItem
|
import org.kiwix.kiwixmobile.core.search.adapter.SearchListItem.RecentSearchListItem
|
||||||
@ -35,7 +35,7 @@ internal class ShowDeleteSearchDialogTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `invoke with shows dialog that offers ConfirmedDelete action`() {
|
fun `invoke with shows dialog that offers ConfirmedDelete action`() {
|
||||||
val actions = mockk<PublishProcessor<Action>>(relaxed = true)
|
val actions = mockk<Channel<Action>>(relaxed = true)
|
||||||
val searchListItem = RecentSearchListItem("")
|
val searchListItem = RecentSearchListItem("")
|
||||||
val activity = mockk<SearchActivity>()
|
val activity = mockk<SearchActivity>()
|
||||||
val showDeleteSearchDialog = ShowDeleteSearchDialog(searchListItem, actions)
|
val showDeleteSearchDialog = ShowDeleteSearchDialog(searchListItem, actions)
|
||||||
|
@ -26,7 +26,7 @@ import io.mockk.every
|
|||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.mockkConstructor
|
import io.mockk.mockkConstructor
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
import io.reactivex.processors.PublishProcessor
|
import kotlinx.coroutines.channels.Channel
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.kiwix.kiwixmobile.core.R
|
import org.kiwix.kiwixmobile.core.R
|
||||||
import org.kiwix.kiwixmobile.core.search.viewmodel.Action
|
import org.kiwix.kiwixmobile.core.search.viewmodel.Action
|
||||||
@ -35,7 +35,7 @@ import java.util.Locale
|
|||||||
|
|
||||||
internal class StartSpeechInputTest {
|
internal class StartSpeechInputTest {
|
||||||
|
|
||||||
private val actions = mockk<PublishProcessor<Action>>(relaxed = true)
|
private val actions = mockk<Channel<Action>>(relaxed = true)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when invoke with throws exception offer StartSpeechInputFailed action`() {
|
fun `when invoke with throws exception offer StartSpeechInputFailed action`() {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user