mirror of
https://github.com/kiwix/kiwix-android.git
synced 2025-08-03 02:36:24 -04:00
Merge branch 'macgills/2.5-kotlin' into macgills/2.5-coverage
This commit is contained in:
commit
afb933ac6a
@ -1,3 +1,7 @@
|
||||
2.5
|
||||
NEW: Downloads are now using the DownloadManager
|
||||
NEW: Downloads/Device/Library completely rewritten
|
||||
|
||||
2.4
|
||||
FIX: External SD card problems
|
||||
FIX: Some UI translation
|
||||
|
10
README.md
10
README.md
@ -25,12 +25,12 @@ We utilize different build variants (flavours) to build various different versio
|
||||
- [Retrofit](http://square.github.io/retrofit/) - Retrofit turns your REST API into a Java interface
|
||||
- [OkHttp](https://github.com/square/okhttp) - An HTTP+SPDY client for Android and Java applications
|
||||
- [Butterknife](http://jakewharton.github.io/butterknife/) - View "injection" library for Android
|
||||
- [Mockito](https://github.com/mockito/mockito) - Most popular Mocking framework for unit tests written in Java
|
||||
- [Guava](https://github.com/google/guava) - Collections, caching, primitives support, concurrency libraries, common annotations, string processing, I/O, and so forth.
|
||||
- [Apache](https://github.com/apache/commons-io) - The Apache Commons IO library contains utility classes, stream implementations, file filters, file comparators, endian transformation classes, and much more.
|
||||
- [Mockito](https://github.com/mockito/mockito) - Most popular Mocking framework for unit tests written in Java
|
||||
- [RxJava](https://github.com/ReactiveX/RxJava) - Reactive Extensions for the JVM – a library for composing asynchronous and event-based programs using observable sequences for the Java VM.
|
||||
|
||||
|
||||
- [ObjectBox] (https://github.com/objectbox/objectbox-java) - Reactive NoSQL Databse to replace SquiDb
|
||||
- [MockK] (https://github.com/mockk/mockk) - Kotlin mocking library that allows mocking of final classes by default.
|
||||
- [JUnit5] (https://github.com/junit-team/junit5/) - The next generation of JUnit
|
||||
- [AssertJ] (https://github.com/joel-costigliola/assertj-core) - Fluent assertions for test code
|
||||
|
||||
## Contributing
|
||||
|
||||
|
@ -116,11 +116,9 @@ dependencies {
|
||||
implementation "io.reactivex.rxjava2:rxjava:$rxJavaVersion"
|
||||
|
||||
// JUnit
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'junit:junit:4.12'
|
||||
|
||||
// Mockito
|
||||
testImplementation "org.mockito:mockito-core:2.24.5"
|
||||
androidTestImplementation "org.mockito:mockito-android:2.24.5"
|
||||
|
||||
// Leak canary
|
||||
@ -143,6 +141,14 @@ dependencies {
|
||||
implementation "android.arch.lifecycle:extensions:1.1.1"
|
||||
implementation "io.objectbox:objectbox-kotlin:$objectboxVersion"
|
||||
implementation "io.objectbox:objectbox-rxjava:$objectboxVersion"
|
||||
|
||||
testImplementation "org.junit.jupiter:junit-jupiter:5.4.2"
|
||||
testImplementation "io.mockk:mockk:1.9"
|
||||
testImplementation "org.assertj:assertj-core:3.11.1"
|
||||
//update this with androidx
|
||||
testImplementation 'com.jraska.livedata:testing-ktx:0.2.1'
|
||||
testImplementation 'android.arch.core:core-testing:1.1.1'
|
||||
|
||||
}
|
||||
|
||||
// Set custom app import directory
|
||||
@ -227,6 +233,7 @@ android {
|
||||
warning 'StringFormatInvalid'
|
||||
}
|
||||
testOptions {
|
||||
unitTests.returnDefaultValues = true
|
||||
unitTests.all {
|
||||
testLogging {
|
||||
events "passed", "skipped", "failed", "standardOut", "standardError"
|
||||
|
@ -25,6 +25,7 @@ import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import dagger.android.AndroidInjectionModule;
|
||||
import javax.inject.Singleton;
|
||||
import org.kiwix.kiwixmobile.downloader.model.UriToFileConverter;
|
||||
import org.kiwix.kiwixmobile.utils.BookUtils;
|
||||
|
||||
@Module(includes = {
|
||||
@ -52,4 +53,9 @@ public class ApplicationModule {
|
||||
BookUtils provideBookUtils() {
|
||||
return new BookUtils();
|
||||
}
|
||||
|
||||
@Provides @Singleton
|
||||
UriToFileConverter provideUriToFIleCOnverter() {
|
||||
return new UriToFileConverter.Impl();
|
||||
}
|
||||
}
|
||||
|
@ -6,8 +6,8 @@ import java.io.File
|
||||
|
||||
data class BookOnDisk(
|
||||
val databaseId: Long? = null,
|
||||
val book: Book,
|
||||
val file: File
|
||||
val book: Book = Book().apply { id = "" },
|
||||
val file: File = File("")
|
||||
) {
|
||||
constructor(bookOnDiskEntity: BookOnDiskEntity) : this(
|
||||
bookOnDiskEntity.id,
|
||||
|
@ -17,12 +17,11 @@
|
||||
*/
|
||||
package org.kiwix.kiwixmobile.downloader.model
|
||||
|
||||
import android.view.WindowId
|
||||
import org.kiwix.kiwixmobile.database.newdb.entities.DownloadEntity
|
||||
import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity
|
||||
import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity.Book
|
||||
|
||||
data class DownloadModel(
|
||||
val databaseId: Long? = null,
|
||||
val downloadId: Long,
|
||||
val book: LibraryNetworkEntity.Book
|
||||
val downloadId: Long = 0,
|
||||
val book: LibraryNetworkEntity.Book = Book().apply { id = "" }
|
||||
)
|
||||
|
@ -55,21 +55,22 @@ import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity.Book
|
||||
import java.io.File
|
||||
|
||||
class DownloadStatus(
|
||||
val downloadId: Long,
|
||||
val title: String,
|
||||
val description: String,
|
||||
val state: DownloadState,
|
||||
val bytesDownloadedSoFar: Long,
|
||||
val totalSizeBytes: Long,
|
||||
val lastModified: String,
|
||||
val localUri: String?,
|
||||
val mediaProviderUri: String?,
|
||||
val mediaType: String?,
|
||||
val uri: String?,
|
||||
val book: Book
|
||||
val downloadId: Long = 0L,
|
||||
val title: String = "",
|
||||
val description: String = "",
|
||||
val state: DownloadState = DownloadState.Pending,
|
||||
val bytesDownloadedSoFar: Long = 0,
|
||||
val totalSizeBytes: Long = 0,
|
||||
val lastModified: String = "",
|
||||
val localUri: String? = null,
|
||||
val mediaProviderUri: String? = null,
|
||||
val mediaType: String? = null,
|
||||
val uri: String? = null,
|
||||
val book: Book = Book().apply { id = "" }
|
||||
) {
|
||||
|
||||
fun toBookOnDisk() = BookOnDisk(book = book, file = File(Uri.parse(localUri).path))
|
||||
fun toBookOnDisk(uriToFileConverter:UriToFileConverter) =
|
||||
BookOnDisk(book = book, file = uriToFileConverter.convert(localUri))
|
||||
|
||||
constructor(
|
||||
cursor: Cursor,
|
||||
@ -90,6 +91,11 @@ class DownloadStatus(
|
||||
)
|
||||
}
|
||||
|
||||
interface UriToFileConverter {
|
||||
fun convert(uriString: String?) = File(Uri.parse(uriString).path)
|
||||
class Impl:UriToFileConverter{}
|
||||
}
|
||||
|
||||
sealed class DownloadState(val stringId: Int) {
|
||||
companion object {
|
||||
fun from(
|
||||
|
@ -3,8 +3,8 @@ package org.kiwix.kiwixmobile.zim_manager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.ConnectivityManager
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.processors.BehaviorProcessor
|
||||
import io.reactivex.processors.PublishProcessor
|
||||
import org.kiwix.kiwixmobile.extensions.networkState
|
||||
import javax.inject.Inject
|
||||
|
||||
@ -13,14 +13,14 @@ class ConnectivityBroadcastReceiver @Inject constructor(private val connectivity
|
||||
|
||||
override val action: String = ConnectivityManager.CONNECTIVITY_ACTION
|
||||
|
||||
val networkStates =
|
||||
BehaviorProcessor.createDefault(connectivityManager.networkState)
|
||||
private val _networkStates = BehaviorProcessor.createDefault(connectivityManager.networkState)
|
||||
val networkStates: Flowable<NetworkState> = _networkStates
|
||||
|
||||
override fun onIntentWithActionReceived(
|
||||
context: Context,
|
||||
intent: Intent
|
||||
) {
|
||||
networkStates.onNext(connectivityManager.networkState)
|
||||
_networkStates.onNext(connectivityManager.networkState)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package org.kiwix.kiwixmobile.zim_manager
|
||||
|
||||
import android.content.Context
|
||||
import org.kiwix.kiwixmobile.zim_manager.library_view.adapter.Language
|
||||
import javax.inject.Inject
|
||||
|
||||
class DefaultLanguageProvider @Inject constructor(private val context: Context) {
|
||||
fun provide() = Language(
|
||||
context.resources.configuration.locale.isO3Language,
|
||||
true,
|
||||
1
|
||||
)
|
||||
}
|
@ -21,6 +21,7 @@ package org.kiwix.kiwixmobile.zim_manager
|
||||
import android.app.Application
|
||||
import android.arch.lifecycle.MutableLiveData
|
||||
import android.arch.lifecycle.ViewModel
|
||||
import android.support.annotation.VisibleForTesting
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.disposables.Disposable
|
||||
@ -39,6 +40,7 @@ import org.kiwix.kiwixmobile.downloader.model.DownloadItem
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadModel
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadState.Successful
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadStatus
|
||||
import org.kiwix.kiwixmobile.downloader.model.UriToFileConverter
|
||||
import org.kiwix.kiwixmobile.extensions.calculateSearchMatches
|
||||
import org.kiwix.kiwixmobile.extensions.registerReceiver
|
||||
import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity
|
||||
@ -71,7 +73,9 @@ class ZimManageViewModel @Inject constructor(
|
||||
private val context: Application,
|
||||
private val connectivityBroadcastReceiver: ConnectivityBroadcastReceiver,
|
||||
private val bookUtils: BookUtils,
|
||||
private val fat32Checker: Fat32Checker
|
||||
private val fat32Checker: Fat32Checker,
|
||||
private val uriToFileConverter: UriToFileConverter,
|
||||
private val defaultLanguageProvider: DefaultLanguageProvider
|
||||
) : ViewModel() {
|
||||
|
||||
val libraryItems: MutableLiveData<List<LibraryListItem>> = MutableLiveData()
|
||||
@ -94,6 +98,11 @@ class ZimManageViewModel @Inject constructor(
|
||||
context.registerReceiver(connectivityBroadcastReceiver)
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun onClearedExposed() {
|
||||
onCleared()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
compositeDisposable.clear()
|
||||
context.unregisterReceiver(connectivityBroadcastReceiver)
|
||||
@ -105,16 +114,17 @@ class ZimManageViewModel @Inject constructor(
|
||||
val downloadStatuses = downloadStatuses(downloads)
|
||||
val booksFromDao = books()
|
||||
val networkLibrary = PublishProcessor.create<LibraryNetworkEntity>()
|
||||
val languages = languageDao.languages()
|
||||
return arrayOf(
|
||||
updateDownloadItems(downloadStatuses),
|
||||
removeCompletedDownloadsFromDb(downloadStatuses),
|
||||
removeNonExistingDownloadsFromDb(downloadStatuses, downloads),
|
||||
updateBookItems(booksFromDao),
|
||||
checkFileSystemForBooksOnRequest(booksFromDao),
|
||||
updateLibraryItems(booksFromDao, downloads, networkLibrary),
|
||||
updateLanguagesInDao(networkLibrary),
|
||||
updateLibraryItems(booksFromDao, downloads, networkLibrary, languages),
|
||||
updateLanguagesInDao(networkLibrary, languages),
|
||||
updateNetworkStates(),
|
||||
updateLanguageItemsForDialog(),
|
||||
updateLanguageItemsForDialog(languages),
|
||||
requestsAndConnectivtyChangesToLibraryRequests(networkLibrary)
|
||||
)
|
||||
}
|
||||
@ -155,6 +165,7 @@ class ZimManageViewModel @Inject constructor(
|
||||
)
|
||||
.buffer(3, SECONDS)
|
||||
.map(this::downloadIdsWithNoStatusesOverBufferPeriod)
|
||||
.filter { it.isNotEmpty() }
|
||||
.subscribe(
|
||||
{
|
||||
downloadDao.delete(*it.toLongArray())
|
||||
@ -184,13 +195,15 @@ class ZimManageViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateLanguageItemsForDialog() = requestLanguagesDialog
|
||||
.withLatestFrom(languageDao.languages(),
|
||||
BiFunction<Unit, List<Language>, List<Language>> { _, languages -> languages })
|
||||
.subscribe(
|
||||
languageItems::postValue,
|
||||
Throwable::printStackTrace
|
||||
)
|
||||
private fun updateLanguageItemsForDialog(languages: Flowable<List<Language>>) =
|
||||
requestLanguagesDialog
|
||||
.withLatestFrom(
|
||||
languages,
|
||||
BiFunction<Unit, List<Language>, List<Language>> { _, languages -> languages })
|
||||
.subscribe(
|
||||
languageItems::postValue,
|
||||
Throwable::printStackTrace
|
||||
)
|
||||
|
||||
private fun updateNetworkStates() =
|
||||
connectivityBroadcastReceiver.networkStates.subscribe(
|
||||
@ -200,11 +213,12 @@ class ZimManageViewModel @Inject constructor(
|
||||
private fun updateLibraryItems(
|
||||
booksFromDao: Flowable<List<BookOnDisk>>,
|
||||
downloads: Flowable<List<DownloadModel>>,
|
||||
library: Flowable<LibraryNetworkEntity>
|
||||
library: Flowable<LibraryNetworkEntity>,
|
||||
languages: Flowable<List<Language>>
|
||||
) = Flowable.combineLatest(
|
||||
booksFromDao,
|
||||
downloads,
|
||||
languageDao.languages().filter { it.isNotEmpty() },
|
||||
languages.filter { it.isNotEmpty() },
|
||||
library,
|
||||
requestFiltering
|
||||
.doOnNext { libraryListIsRefreshing.postValue(true) }
|
||||
@ -221,15 +235,17 @@ class ZimManageViewModel @Inject constructor(
|
||||
)
|
||||
|
||||
private fun updateLanguagesInDao(
|
||||
library: Flowable<LibraryNetworkEntity>
|
||||
library: Flowable<LibraryNetworkEntity>,
|
||||
languages: Flowable<List<Language>>
|
||||
) = library
|
||||
.subscribeOn(Schedulers.io())
|
||||
.map { it.books }
|
||||
.withLatestFrom(
|
||||
languageDao.languages(),
|
||||
languages,
|
||||
BiFunction(this::combineToLanguageList)
|
||||
)
|
||||
.map { it.sortedBy(Language::language) }
|
||||
.filter { it.isNotEmpty() }
|
||||
.subscribe(
|
||||
languageDao::insert,
|
||||
Throwable::printStackTrace
|
||||
@ -278,11 +294,7 @@ class ZimManageViewModel @Inject constructor(
|
||||
|
||||
private fun defaultLanguage() =
|
||||
listOf(
|
||||
Language(
|
||||
context.resources.configuration.locale.isO3Language,
|
||||
true,
|
||||
1
|
||||
)
|
||||
defaultLanguageProvider.provide()
|
||||
)
|
||||
|
||||
private fun languageIsActive(
|
||||
@ -411,7 +423,8 @@ class ZimManageViewModel @Inject constructor(
|
||||
.filter { it.isNotEmpty() }
|
||||
.subscribe(
|
||||
{
|
||||
bookDao.insert(it.map { downloadStatus -> downloadStatus.toBookOnDisk() })
|
||||
bookDao.insert(
|
||||
it.map { downloadStatus -> downloadStatus.toBookOnDisk(uriToFileConverter) })
|
||||
downloadDao.delete(
|
||||
*it.map { status -> status.downloadId }.toLongArray()
|
||||
)
|
||||
|
@ -18,7 +18,7 @@ class StorageObserver @Inject constructor(
|
||||
private val downloadDao: NewDownloadDao
|
||||
) {
|
||||
|
||||
private val _booksOnFileSystem = PublishProcessor.create<Collection<BookOnDisk>>()
|
||||
private val _booksOnFileSystem = PublishProcessor.create<List<BookOnDisk>>()
|
||||
val booksOnFileSystem = _booksOnFileSystem.distinctUntilChanged()
|
||||
.doOnSubscribe {
|
||||
downloadDao.downloads()
|
||||
@ -37,7 +37,7 @@ class StorageObserver @Inject constructor(
|
||||
}
|
||||
|
||||
override fun onScanCompleted() {
|
||||
_booksOnFileSystem.onNext(foundBooks)
|
||||
_booksOnFileSystem.onNext(foundBooks.toList())
|
||||
|
||||
}
|
||||
}).scan(sharedPreferenceUtil.prefStorage)
|
||||
|
@ -2,22 +2,32 @@ package org.kiwix.kiwixmobile.zim_manager.library_view.adapter
|
||||
|
||||
import java.util.Locale
|
||||
|
||||
class Language constructor(
|
||||
locale: Locale,
|
||||
data class Language constructor(
|
||||
var active: Boolean,
|
||||
var occurencesOfLanguage: Int,
|
||||
var language: String = locale.displayLanguage,
|
||||
var languageLocalized: String = locale.getDisplayLanguage(locale),
|
||||
var languageCode: String = locale.isO3Language,
|
||||
var languageCodeISO2: String = locale.language
|
||||
var language: String,
|
||||
var languageLocalized: String,
|
||||
var languageCode: String,
|
||||
var languageCodeISO2: String
|
||||
) {
|
||||
constructor(
|
||||
locale: Locale,
|
||||
active: Boolean,
|
||||
occurrencesOfLanguage: Int
|
||||
) : this(
|
||||
active,
|
||||
occurrencesOfLanguage,
|
||||
locale.displayLanguage,
|
||||
locale.getDisplayLanguage(locale),
|
||||
locale.isO3Language,
|
||||
locale.language
|
||||
)
|
||||
|
||||
constructor(
|
||||
languageCode: String,
|
||||
active: Boolean,
|
||||
occurrencesOfLanguage: Int
|
||||
) : this(Locale(languageCode), active, occurrencesOfLanguage) {
|
||||
}
|
||||
) : this(Locale(languageCode), active, occurrencesOfLanguage)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return (other as Language).language == language && other.active == active
|
||||
|
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Kiwix Android
|
||||
* Copyright (C) 2018 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
|
||||
|
||||
import android.arch.core.executor.ArchTaskExecutor
|
||||
import android.arch.core.executor.TaskExecutor
|
||||
import org.junit.jupiter.api.extension.AfterEachCallback
|
||||
import org.junit.jupiter.api.extension.BeforeEachCallback
|
||||
import org.junit.jupiter.api.extension.ExtensionContext
|
||||
|
||||
class InstantExecutorExtension : BeforeEachCallback, AfterEachCallback {
|
||||
|
||||
override fun beforeEach(context: ExtensionContext?) {
|
||||
ArchTaskExecutor.getInstance()
|
||||
.setDelegate(object : TaskExecutor() {
|
||||
override fun executeOnDiskIO(runnable: Runnable) = runnable.run()
|
||||
|
||||
override fun postToMainThread(runnable: Runnable) = runnable.run()
|
||||
|
||||
override fun isMainThread(): Boolean = true
|
||||
})
|
||||
}
|
||||
|
||||
override fun afterEach(context: ExtensionContext?) {
|
||||
ArchTaskExecutor.getInstance().setDelegate(null)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,503 @@
|
||||
/*
|
||||
* Kiwix Android
|
||||
* Copyright (C) 2018 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.zim_manager
|
||||
|
||||
import android.app.Application
|
||||
import com.jraska.livedata.test
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import io.reactivex.Scheduler
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.android.plugins.RxAndroidPlugins
|
||||
import io.reactivex.plugins.RxJavaPlugins
|
||||
import io.reactivex.processors.PublishProcessor
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import io.reactivex.schedulers.TestScheduler
|
||||
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.InstantExecutorExtension
|
||||
import org.kiwix.kiwixmobile.R
|
||||
import org.kiwix.kiwixmobile.database.newdb.dao.NewBookDao
|
||||
import org.kiwix.kiwixmobile.database.newdb.dao.NewDownloadDao
|
||||
import org.kiwix.kiwixmobile.database.newdb.dao.NewLanguagesDao
|
||||
import org.kiwix.kiwixmobile.downloader.Downloader
|
||||
import org.kiwix.kiwixmobile.downloader.model.BookOnDisk
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadItem
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadModel
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadState
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadStatus
|
||||
import org.kiwix.kiwixmobile.downloader.model.UriToFileConverter
|
||||
import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity
|
||||
import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity.Book
|
||||
import org.kiwix.kiwixmobile.network.KiwixService
|
||||
import org.kiwix.kiwixmobile.utils.BookUtils
|
||||
import org.kiwix.kiwixmobile.zim_manager.Fat32Checker.FileSystemState
|
||||
import org.kiwix.kiwixmobile.zim_manager.Fat32Checker.FileSystemState.CanWrite4GbFile
|
||||
import org.kiwix.kiwixmobile.zim_manager.Fat32Checker.FileSystemState.CannotWrite4GbFile
|
||||
import org.kiwix.kiwixmobile.zim_manager.NetworkState.CONNECTED
|
||||
import org.kiwix.kiwixmobile.zim_manager.NetworkState.NOT_CONNECTED
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.StorageObserver
|
||||
import org.kiwix.kiwixmobile.zim_manager.library_view.adapter.Language
|
||||
import org.kiwix.kiwixmobile.zim_manager.library_view.adapter.LibraryListItem
|
||||
import java.io.File
|
||||
import java.util.LinkedList
|
||||
import java.util.concurrent.TimeUnit.MILLISECONDS
|
||||
import java.util.concurrent.TimeUnit.SECONDS
|
||||
|
||||
@ExtendWith(InstantExecutorExtension::class)
|
||||
class ZimManageViewModelTest {
|
||||
|
||||
private val newDownloadDao: NewDownloadDao = mockk()
|
||||
private val newBookDao: NewBookDao = mockk()
|
||||
private val newLanguagesDao: NewLanguagesDao = mockk()
|
||||
private val downloader: Downloader = mockk()
|
||||
private val storageObserver: StorageObserver = mockk()
|
||||
private val kiwixService: KiwixService = mockk()
|
||||
private val application: Application = mockk()
|
||||
private val connectivityBroadcastReceiver: ConnectivityBroadcastReceiver = mockk()
|
||||
private val bookUtils: BookUtils = mockk()
|
||||
private val fat32Checker: Fat32Checker = mockk()
|
||||
private val uriToFileConverter: UriToFileConverter = mockk()
|
||||
lateinit var viewModel: ZimManageViewModel
|
||||
|
||||
private val downloads: PublishProcessor<List<DownloadModel>> = PublishProcessor.create()
|
||||
private val booksOnFileSystem: PublishProcessor<List<BookOnDisk>> = PublishProcessor.create()
|
||||
private val books: PublishProcessor<List<BookOnDisk>> = PublishProcessor.create()
|
||||
private val languages: PublishProcessor<List<Language>> = PublishProcessor.create()
|
||||
private val fileSystemStates: PublishProcessor<FileSystemState> = PublishProcessor.create()
|
||||
private val networkStates: PublishProcessor<NetworkState> = PublishProcessor.create()
|
||||
|
||||
private val testScheduler = TestScheduler()
|
||||
|
||||
init {
|
||||
setScheduler(testScheduler)
|
||||
}
|
||||
|
||||
private fun setScheduler(replacementScheduler: Scheduler) {
|
||||
RxJavaPlugins.setIoSchedulerHandler { scheduler -> replacementScheduler }
|
||||
RxJavaPlugins.setComputationSchedulerHandler { scheduler -> replacementScheduler }
|
||||
RxJavaPlugins.setNewThreadSchedulerHandler { scheduler -> replacementScheduler }
|
||||
RxAndroidPlugins.setInitMainThreadSchedulerHandler { scheduler -> Schedulers.trampoline() }
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
fun teardown() {
|
||||
RxJavaPlugins.reset()
|
||||
RxAndroidPlugins.reset();
|
||||
}
|
||||
|
||||
private val defaultLanguageProvider: DefaultLanguageProvider = mockk()
|
||||
|
||||
@BeforeEach
|
||||
fun init() {
|
||||
clearMocks(
|
||||
newDownloadDao, newBookDao, newLanguagesDao, downloader,
|
||||
storageObserver, kiwixService, application, connectivityBroadcastReceiver, bookUtils,
|
||||
fat32Checker, uriToFileConverter, defaultLanguageProvider
|
||||
)
|
||||
every { connectivityBroadcastReceiver.action } returns "test"
|
||||
every { newDownloadDao.downloads() } returns downloads
|
||||
every { newBookDao.books() } returns books
|
||||
every { storageObserver.booksOnFileSystem } returns booksOnFileSystem
|
||||
every { newLanguagesDao.languages() } returns languages
|
||||
every { fat32Checker.fileSystemStates } returns fileSystemStates
|
||||
every { connectivityBroadcastReceiver.networkStates } returns networkStates
|
||||
every { application.registerReceiver(any(), any()) } returns mockk()
|
||||
viewModel = ZimManageViewModel(
|
||||
newDownloadDao, newBookDao, newLanguagesDao, downloader,
|
||||
storageObserver, kiwixService, application, connectivityBroadcastReceiver, bookUtils,
|
||||
fat32Checker, uriToFileConverter, defaultLanguageProvider
|
||||
)
|
||||
testScheduler.triggerActions()
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class Context {
|
||||
@Test
|
||||
fun `registers broadcastReceiver in init`() {
|
||||
verify {
|
||||
application.registerReceiver(connectivityBroadcastReceiver, any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unregisters broadcastReceiver in onCleared`() {
|
||||
every { application.unregisterReceiver(any()) } returns mockk()
|
||||
viewModel.onClearedExposed()
|
||||
verify {
|
||||
application.unregisterReceiver(connectivityBroadcastReceiver)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class Downloads {
|
||||
@Test
|
||||
fun `on emission from database query and render downloads`() {
|
||||
val expectedStatus = DownloadStatus()
|
||||
expectStatusWith(listOf(expectedStatus))
|
||||
viewModel.downloadItems
|
||||
.test()
|
||||
.assertValue(listOf(DownloadItem(expectedStatus)))
|
||||
.dispose()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on emission of successful status create a book and delete the download`() {
|
||||
every { uriToFileConverter.convert(any()) } returns File("test")
|
||||
val expectedStatus = DownloadStatus(
|
||||
downloadId = 10L,
|
||||
state = DownloadState.Successful
|
||||
)
|
||||
expectStatusWith(listOf(expectedStatus))
|
||||
val element = expectedStatus.toBookOnDisk(uriToFileConverter)
|
||||
verify {
|
||||
newBookDao.insert(listOf(element))
|
||||
newDownloadDao.delete(10L)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `if statuses don't have a matching Id for download in db over 3 secs then delete`() {
|
||||
expectStatusWith(
|
||||
listOf(DownloadStatus(downloadId = 1)),
|
||||
listOf(DownloadModel(downloadId = 1), DownloadModel(downloadId = 3))
|
||||
)
|
||||
testScheduler.advanceTimeBy(3, SECONDS)
|
||||
testScheduler.triggerActions()
|
||||
verify {
|
||||
newDownloadDao.delete(3)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `if statuses do have a matching Id for download in db over 3 secs then don't delete`() {
|
||||
expectStatusWith(
|
||||
listOf(DownloadStatus(downloadId = 1)),
|
||||
listOf(DownloadModel(downloadId = 1))
|
||||
)
|
||||
testScheduler.advanceTimeBy(3, SECONDS)
|
||||
testScheduler.triggerActions()
|
||||
verify(exactly = 0) {
|
||||
newDownloadDao.delete(any())
|
||||
}
|
||||
}
|
||||
|
||||
private fun expectStatusWith(
|
||||
expectedStatuses: List<DownloadStatus>,
|
||||
expectedDownloads: List<DownloadModel> = listOf(
|
||||
DownloadModel()
|
||||
)
|
||||
) {
|
||||
val downloadList = expectedDownloads
|
||||
every { downloader.queryStatus(downloadList) } returns expectedStatuses
|
||||
downloads.offer(downloadList)
|
||||
testScheduler.triggerActions()
|
||||
testScheduler.advanceTimeBy(1, SECONDS)
|
||||
testScheduler.triggerActions()
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class Books {
|
||||
@Test
|
||||
fun `emissions from DB sorted by title and observed`() {
|
||||
books.onNext(
|
||||
listOf(
|
||||
BookOnDisk().apply { book.title = "z" },
|
||||
BookOnDisk().apply { book.title = "a" }
|
||||
)
|
||||
)
|
||||
testScheduler.triggerActions()
|
||||
viewModel.bookItems.test()
|
||||
.assertValue(
|
||||
listOf(
|
||||
BookOnDisk().apply { book.title = "a" },
|
||||
BookOnDisk().apply { book.title = "z" }
|
||||
)
|
||||
)
|
||||
.dispose()
|
||||
}
|
||||
|
||||
@Test
|
||||
// @Disabled("WithLatestFrom is not calling BiFunction removeBooksAlreadyInDao - revisit")
|
||||
fun `books found on filesystem are filtered by books already in db`() {
|
||||
val expectedBook = BookOnDisk().apply { book.id = "1" }
|
||||
val bookToRemove = BookOnDisk().apply { book.id = "2" }
|
||||
testScheduler.triggerActions()
|
||||
viewModel.requestFileSystemCheck.onNext(Unit)
|
||||
testScheduler.triggerActions()
|
||||
books.onNext(listOf(bookToRemove))
|
||||
testScheduler.triggerActions()
|
||||
booksOnFileSystem.onNext(
|
||||
listOf(
|
||||
expectedBook,
|
||||
expectedBook,
|
||||
bookToRemove
|
||||
)
|
||||
)
|
||||
testScheduler.triggerActions()
|
||||
verify {
|
||||
newBookDao.insert(listOf(expectedBook))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
inner class Lanuages {
|
||||
|
||||
@Test
|
||||
fun `a network request with no result and an empty language db triggers an activation of the default locale`() {
|
||||
val expectedLanguage = Language(
|
||||
active = true,
|
||||
occurencesOfLanguage = 1,
|
||||
language = "eng",
|
||||
languageLocalized = "englocal",
|
||||
languageCode = "ENG",
|
||||
languageCodeISO2 = "en"
|
||||
)
|
||||
expectNetworkDbAndDefault(
|
||||
listOf(),
|
||||
listOf(),
|
||||
expectedLanguage
|
||||
)
|
||||
verify { newLanguagesDao.insert(listOf(expectedLanguage)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a network request with no result and a non empty language db does not trigger anything`() {
|
||||
expectNetworkDbAndDefault(
|
||||
listOf(),
|
||||
listOf(
|
||||
Language(
|
||||
active = true,
|
||||
occurencesOfLanguage = 1,
|
||||
language = "eng",
|
||||
languageLocalized = "englocal",
|
||||
languageCode = "ENG",
|
||||
languageCodeISO2 = "en"
|
||||
)
|
||||
),
|
||||
Language(true, 1, "", "", "", "")
|
||||
)
|
||||
verify(exactly = 0) { newLanguagesDao.insert(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a network request with a result and an empty language db triggers an activation of the default locale with the result of the web request`() {
|
||||
val defaultLanguage = Language(
|
||||
active = true,
|
||||
occurencesOfLanguage = 1,
|
||||
language = "English",
|
||||
languageLocalized = "English",
|
||||
languageCode = "eng",
|
||||
languageCodeISO2 = "eng"
|
||||
)
|
||||
expectNetworkDbAndDefault(
|
||||
listOf(
|
||||
Book().apply { language = "eng" },
|
||||
Book().apply { language = "eng" },
|
||||
Book().apply { language = "fra" }
|
||||
),
|
||||
listOf(),
|
||||
defaultLanguage)
|
||||
verify {
|
||||
newLanguagesDao.insert(
|
||||
listOf(
|
||||
defaultLanguage.copy(occurencesOfLanguage = 2),
|
||||
Language(
|
||||
active = false,
|
||||
occurencesOfLanguage = 1,
|
||||
language = "fra",
|
||||
languageLocalized = "",
|
||||
languageCode = "",
|
||||
languageCodeISO2 = ""
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a network request with a result and a non empty language db triggers an activation of the result of the web request with the db`() {
|
||||
val dbLanguage = Language(
|
||||
active = true,
|
||||
occurencesOfLanguage = 1,
|
||||
language = "English",
|
||||
languageLocalized = "English",
|
||||
languageCode = "eng",
|
||||
languageCodeISO2 = "eng"
|
||||
)
|
||||
expectNetworkDbAndDefault(
|
||||
listOf(
|
||||
Book().apply { language = "eng" },
|
||||
Book().apply { language = "eng" },
|
||||
Book().apply { language = "fra" }
|
||||
),
|
||||
listOf(dbLanguage),
|
||||
Language(true, 1, "", "", "", "")
|
||||
)
|
||||
verify {
|
||||
newLanguagesDao.insert(
|
||||
listOf(
|
||||
dbLanguage.copy(occurencesOfLanguage = 2),
|
||||
Language(
|
||||
active = false,
|
||||
occurencesOfLanguage = 1,
|
||||
language = "fra",
|
||||
languageLocalized = "",
|
||||
languageCode = "",
|
||||
languageCodeISO2 = ""
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun expectNetworkDbAndDefault(
|
||||
networkBooks: List<Book>,
|
||||
dbBooks: List<Language>,
|
||||
defaultLanguage: Language
|
||||
) {
|
||||
every { kiwixService.library } returns Single.just(
|
||||
LibraryNetworkEntity().apply {
|
||||
book = LinkedList(networkBooks)
|
||||
})
|
||||
val defaultLanguage = defaultLanguage
|
||||
every { defaultLanguageProvider.provide() } returns defaultLanguage
|
||||
languages.onNext(dbBooks)
|
||||
testScheduler.triggerActions()
|
||||
networkStates.onNext(CONNECTED)
|
||||
testScheduler.triggerActions()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `network states observed`() {
|
||||
networkStates.offer(NOT_CONNECTED)
|
||||
viewModel.networkStates.test()
|
||||
.assertValue(NOT_CONNECTED)
|
||||
.dispose()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `language items for dialog observed`() {
|
||||
val expectedValue = listOf(Language(true, 1, "e", "e", "e", "e"))
|
||||
testScheduler.triggerActions()
|
||||
languages.onNext(expectedValue)
|
||||
testScheduler.triggerActions()
|
||||
viewModel.requestLanguagesDialog.onNext(Unit)
|
||||
testScheduler.triggerActions()
|
||||
viewModel.languageItems.test()
|
||||
.assertValue(expectedValue)
|
||||
.dispose()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `library update removes from sources`() {
|
||||
every { application.getString(R.string.your_languages) } returns "1"
|
||||
every { application.getString(R.string.other_languages) } returns "2"
|
||||
val bookAlreadyOnDisk = Book().apply {
|
||||
id = "0"
|
||||
url = ""
|
||||
}
|
||||
val bookDownloading = Book().apply {
|
||||
id = "1"
|
||||
url = ""
|
||||
}
|
||||
val bookWithStackExchange = Book().apply {
|
||||
id = "2"
|
||||
url = "blahblah/stack_exchange/"
|
||||
}
|
||||
val bookWithActiveLanguage = Book().apply {
|
||||
id = "3"
|
||||
language = "activeLanguage"
|
||||
url = ""
|
||||
}
|
||||
val bookWithInactiveLanguage = Book().apply {
|
||||
id = "3"
|
||||
language = "inactiveLanguage"
|
||||
url = ""
|
||||
}
|
||||
every { kiwixService.library } returns Single.just(
|
||||
LibraryNetworkEntity().apply {
|
||||
book = LinkedList(
|
||||
listOf(
|
||||
bookAlreadyOnDisk,
|
||||
bookDownloading,
|
||||
bookWithStackExchange,
|
||||
bookWithActiveLanguage,
|
||||
bookWithInactiveLanguage
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
networkStates.onNext(CONNECTED)
|
||||
downloads.onNext(listOf(DownloadModel(book = bookDownloading)))
|
||||
books.onNext(listOf(BookOnDisk(book = bookAlreadyOnDisk)))
|
||||
languages.onNext(
|
||||
listOf(
|
||||
Language(true, 1, "", "", "activeLanguage", ""),
|
||||
Language(false, 1, "", "", "inactiveLanguage", "")
|
||||
)
|
||||
)
|
||||
fileSystemStates.onNext(CanWrite4GbFile)
|
||||
testScheduler.advanceTimeBy(500, MILLISECONDS)
|
||||
testScheduler.triggerActions()
|
||||
viewModel.libraryItems.test()
|
||||
.assertValue(
|
||||
listOf(
|
||||
LibraryListItem.DividerItem(Long.MAX_VALUE, "1"),
|
||||
LibraryListItem.BookItem(bookWithActiveLanguage),
|
||||
LibraryListItem.DividerItem(Long.MIN_VALUE, "2"),
|
||||
LibraryListItem.BookItem(bookWithInactiveLanguage)
|
||||
)
|
||||
)
|
||||
.dispose()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `library filters out files over 4GB if file system state says to`() {
|
||||
val bookOver4Gb = Book().apply {
|
||||
id = "0"
|
||||
url = ""
|
||||
size = "${Fat32Checker.FOUR_GIGABYTES_IN_KILOBYTES + 1}"
|
||||
}
|
||||
every { kiwixService.library } returns Single.just(
|
||||
LibraryNetworkEntity().apply {
|
||||
book = LinkedList(listOf(bookOver4Gb))
|
||||
}
|
||||
)
|
||||
networkStates.onNext(CONNECTED)
|
||||
downloads.onNext(listOf())
|
||||
books.onNext(listOf())
|
||||
languages.onNext(listOf(Language(true, 1, "", "", "activeLanguage", "")))
|
||||
fileSystemStates.onNext(CannotWrite4GbFile)
|
||||
testScheduler.advanceTimeBy(500, MILLISECONDS)
|
||||
testScheduler.triggerActions()
|
||||
viewModel.libraryItems.test()
|
||||
.assertValue(listOf())
|
||||
.dispose()
|
||||
}
|
||||
}
|
1
app/src/test/resources/io/mockk/settings.properties
Normal file
1
app/src/test/resources/io/mockk/settings.properties
Normal file
@ -0,0 +1 @@
|
||||
relaxUnitFun=true
|
1
app/src/test/resources/junit-platform.properties
Normal file
1
app/src/test/resources/junit-platform.properties
Normal file
@ -0,0 +1 @@
|
||||
junit.jupiter.testinstance.lifecycle.default = per_class
|
Loading…
x
Reference in New Issue
Block a user