mirror of
https://github.com/kiwix/kiwix-android.git
synced 2025-08-03 02:36:24 -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 {
|
||||
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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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.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<RecentSearchEntity>) {
|
||||
fun recentSearches(zimId: String?) = box.asFlowable(
|
||||
class NewRecentSearchDao @Inject constructor(
|
||||
private val box: Box<RecentSearchEntity>,
|
||||
private val flowBuilder: FlowBuilder
|
||||
) {
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun recentSearches(zimId: String?) = flowBuilder.buildCallbackFlow(
|
||||
box.query {
|
||||
equal(RecentSearchEntity_.zimId, zimId ?: "")
|
||||
orderDesc(RecentSearchEntity_.id)
|
||||
|
@ -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,
|
||||
|
@ -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<SearchViewModel>(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()
|
||||
}
|
||||
}
|
||||
|
@ -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<SearchListItem>
|
||||
suspend fun generateSearchResults(
|
||||
searchTerm: String,
|
||||
zimFileReader: ZimFileReader?
|
||||
): List<SearchListItem>
|
||||
}
|
||||
|
||||
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 <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
|
||||
|
||||
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 {
|
||||
FromWebView,
|
||||
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
|
||||
|
||||
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<State>().apply { value = NoResults("", FromWebView) }
|
||||
val effects = PublishProcessor.create<SideEffect<*>>()
|
||||
val actions = PublishProcessor.create<Action>()
|
||||
private val filter = BehaviorProcessor.createDefault("")
|
||||
private val searchOrigin = BehaviorProcessor.createDefault(FromWebView)
|
||||
private val searchResults = PublishProcessor.create<List<SearchListItem>>()
|
||||
|
||||
private val compositeDisposable = CompositeDisposable()
|
||||
private var searchTask: Disposable? = null
|
||||
private val initialState: SearchState =
|
||||
SearchState("", SearchResultsWithTerm("", emptyList()), emptyList(), FromWebView)
|
||||
val state: MutableStateFlow<SearchState> = MutableStateFlow(initialState)
|
||||
private val _effects = Channel<SideEffect<*>>()
|
||||
val effects = _effects.receiveAsFlow()
|
||||
val actions = Channel<Action>(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<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
|
||||
}
|
||||
)
|
||||
_effects.offer(SaveSearchToRecents(recentSearchDao, searchListItem, zimReaderContainer.id))
|
||||
_effects.offer(OpenSearchItem(searchListItem, openInNewTab))
|
||||
}
|
||||
}
|
||||
|
||||
data class SearchResultsWithTerm(val searchTerm: String, val results: List<SearchListItem>)
|
||||
|
@ -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<Action>
|
||||
private val actions: Channel<Action>
|
||||
) : SideEffect<Unit> {
|
||||
override fun invokeWith(activity: AppCompatActivity) {
|
||||
if (requestCode == StartSpeechInput.REQ_CODE_SPEECH_INPUT &&
|
||||
|
@ -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<Action>
|
||||
private val actions: Channel<Action>
|
||||
) : SideEffect<Unit> {
|
||||
@TargetApi(VERSION_CODES.M)
|
||||
override fun invokeWith(activity: AppCompatActivity) {
|
||||
|
@ -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<Action>
|
||||
private val actions: Channel<Action>
|
||||
) : SideEffect<Unit> {
|
||||
|
||||
@Inject lateinit var dialogShower: DialogShower
|
||||
|
@ -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<Action>) : SideEffect<Unit> {
|
||||
data class StartSpeechInput(private val actions: Channel<Action>) : SideEffect<Unit> {
|
||||
|
||||
override fun invokeWith(activity: AppCompatActivity) {
|
||||
try {
|
||||
|
@ -1,6 +1,8 @@
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:animateLayoutChanges="true"
|
||||
android:fitsSystemWindows="true"
|
||||
android:orientation="vertical">
|
||||
|
||||
@ -16,26 +18,28 @@
|
||||
<include layout="@layout/layout_toolbar" />
|
||||
</RelativeLayout>
|
||||
|
||||
<ViewAnimator
|
||||
android:id="@+id/searchViewAnimator"
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/search_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:animateLayoutChanges="true">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/search_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<TextView
|
||||
style="@style/no_content_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/no_results" />
|
||||
|
||||
</ViewAnimator>
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/searchNoResults"
|
||||
style="@style/no_content_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/no_results" />
|
||||
|
||||
<androidx.core.widget.ContentLoadingProgressBar
|
||||
android:id="@+id/searchLoadingIndicator"
|
||||
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>
|
||||
|
@ -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<RecentSearchEntity> = 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>(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>(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(), 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<RecentSearchEntity> =
|
||||
(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<RecentSearchEntity>, zimId: String) {
|
||||
@ -83,8 +95,7 @@ internal class NewRecentSearchDaoTest {
|
||||
every { queryBuilder.orderDesc(RecentSearchEntity_.id) } returns queryBuilder
|
||||
val query = mockk<Query<RecentSearchEntity>>()
|
||||
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 <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
|
||||
|
||||
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<List<RecentSearchListItem>> =
|
||||
PublishProcessor.create()
|
||||
private lateinit var recentsFromDb: Channel<List<RecentSearchListItem>>
|
||||
|
||||
@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<Intent>()
|
||||
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<ZimSearchResultListItem>,
|
||||
databaseResults: List<RecentSearchListItem>,
|
||||
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 <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,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<ZimSearchResultListItem>())
|
||||
runBlocking {
|
||||
assertThat(zimSearchResultGenerator.generateSearchResults("", zimFileReader))
|
||||
.isEqualTo(emptyList<ZimSearchResultListItem>())
|
||||
}
|
||||
}
|
||||
|
||||
@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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<Intent>()
|
||||
private val actions = mockk<PublishProcessor<Action>>(relaxed = true)
|
||||
private val actions = mockk<Channel<Action>>(relaxed = true)
|
||||
private val successfulResult = ProcessActivityResult(
|
||||
StartSpeechInput.REQ_CODE_SPEECH_INPUT,
|
||||
Activity.RESULT_OK,
|
||||
|
@ -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<Action> = mockk(relaxed = true)
|
||||
private val actions: Channel<Action> = mockk(relaxed = true)
|
||||
|
||||
private val activity: AppCompatActivity = mockk()
|
||||
|
||||
|
@ -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<PublishProcessor<Action>>(relaxed = true)
|
||||
val actions = mockk<Channel<Action>>(relaxed = true)
|
||||
val searchListItem = RecentSearchListItem("")
|
||||
val activity = mockk<SearchActivity>()
|
||||
val showDeleteSearchDialog = ShowDeleteSearchDialog(searchListItem, actions)
|
||||
|
@ -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<PublishProcessor<Action>>(relaxed = true)
|
||||
private val actions = mockk<Channel<Action>>(relaxed = true)
|
||||
|
||||
@Test
|
||||
fun `when invoke with throws exception offer StartSpeechInputFailed action`() {
|
||||
|
Loading…
x
Reference in New Issue
Block a user