Merge pull request #4396 from kiwix/remove_objectbox_code_from_project

Removed ObjectBox code from the project (except migration-related code).
This commit is contained in:
Kelson 2025-08-20 16:08:00 +02:00 committed by GitHub
commit b827cfe542
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 89 additions and 1489 deletions

View File

@ -17,7 +17,6 @@
<ID>NestedBlockDepth:ReceiverHandShake.kt$ReceiverHandShake$override fun exchangeFileTransferMetadata(inputStream: InputStream, outputStream: OutputStream)</ID>
<ID>PackageNaming:AvailableSpaceCalculator.kt$package
org.kiwix.kiwixmobile.zimManager.libraryView</ID>
<ID>PackageNaming:DefaultLanguageProvider.kt$package org.kiwix.kiwixmobile.zimManager</ID>
<ID>PackageNaming:DeleteFiles.kt$package
org.kiwix.kiwixmobile.zimManager.fileselectView.effects</ID>
<ID>PackageNaming:Fat32Checker.kt$package org.kiwix.kiwixmobile.zimManager</ID>

View File

@ -1,37 +0,0 @@
/*
* 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.trySendBlocking
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 { trySendBlocking(it) }
awaitClose(subscription::cancel)
}
}

View File

@ -1,81 +0,0 @@
/*
* Kiwix Android
* Copyright (c) 2019 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.Box
import io.objectbox.kotlin.query
import io.objectbox.query.QueryBuilder
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import org.kiwix.kiwixmobile.core.dao.entities.HistoryEntity
import org.kiwix.kiwixmobile.core.dao.entities.HistoryEntity_
import org.kiwix.kiwixmobile.core.page.adapter.Page
import org.kiwix.kiwixmobile.core.page.history.adapter.HistoryListItem.HistoryItem
import org.kiwix.kiwixmobile.core.reader.ZimReaderSource.Companion.fromDatabaseValue
import javax.inject.Inject
class HistoryDao @Inject constructor(val box: Box<HistoryEntity>) : PageDao {
fun history(): Flow<List<Page>> =
box.asFlow(
box.query {
orderDesc(HistoryEntity_.timeStamp)
}
).map {
it.map { historyEntity ->
historyEntity.zimFilePath?.let { filePath ->
// set zimReaderSource for previously saved history items
fromDatabaseValue(filePath)?.let { zimReaderSource ->
historyEntity.zimReaderSource = zimReaderSource
}
}
HistoryItem(historyEntity)
}
}
override fun pages(): Flow<List<Page>> = history()
override fun deletePages(pagesToDelete: List<Page>) =
deleteHistory(pagesToDelete as List<HistoryItem>)
fun saveHistory(historyItem: HistoryItem) {
box.store.callInTx {
box
.query {
equal(
HistoryEntity_.historyUrl,
historyItem.historyUrl,
QueryBuilder.StringOrder.CASE_INSENSITIVE
).and()
.equal(
HistoryEntity_.dateString,
historyItem.dateString,
QueryBuilder.StringOrder.CASE_INSENSITIVE
)
}
.remove()
box.put(HistoryEntity(historyItem))
}
}
fun deleteHistory(historyList: List<HistoryItem>) {
box.remove(historyList.map(::HistoryEntity))
}
fun deleteAllHistory() {
box.removeAll()
}
}

View File

@ -212,7 +212,6 @@ class LibkiwixBookOnDisk @Inject constructor(
}
}.onFailure { it.printStackTrace() }
writeBookMarksAndSaveLibraryToFile()
// TODO test when getting books it will not goes to circular dependencies mode.
updateLocalBooksFlow()
}

View File

@ -1,159 +0,0 @@
/*
* Kiwix Android
* Copyright (c) 2019 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.Box
import io.objectbox.kotlin.inValues
import io.objectbox.kotlin.query
import io.objectbox.query.QueryBuilder
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import org.kiwix.kiwixmobile.core.dao.entities.BookOnDiskEntity
import org.kiwix.kiwixmobile.core.dao.entities.BookOnDiskEntity_
import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
import org.kiwix.kiwixmobile.core.reader.ZimReaderSource
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem.BookOnDisk
import javax.inject.Inject
class NewBookDao @Inject constructor(private val box: Box<BookOnDiskEntity>) {
@OptIn(ExperimentalCoroutinesApi::class)
@Suppress("Deprecation")
fun books(dispatcher: CoroutineDispatcher = Dispatchers.IO) =
box.asFlow()
.mapLatest { booksList ->
val updatedBooks = booksList.onEach { bookOnDiskEntity ->
val file = bookOnDiskEntity.file
val zimReaderSource = ZimReaderSource(file)
try {
if (zimReaderSource.canOpenInLibkiwix()) {
bookOnDiskEntity.zimReaderSource = zimReaderSource
}
} catch (_: Exception) {
// Do nothing simply return the bookOnDiskEntity.
}
bookOnDiskEntity
}
removeBooksThatAreInTrashFolder(updatedBooks)
removeBooksThatDoNotExist(updatedBooks.toMutableList())
updatedBooks.mapNotNull { book ->
try {
if (book.zimReaderSource.exists() &&
!isInTrashFolder(book.zimReaderSource.toDatabase())
) {
book
} else {
null
}
} catch (_: Exception) {
null
}
}
}
.map { it.map(::BookOnDisk) }
.flowOn(dispatcher)
@Suppress("Deprecation")
suspend fun getBooks() =
box.all.map { bookOnDiskEntity ->
bookOnDiskEntity.file.let { file ->
// set zimReaderSource for previously saved books
val zimReaderSource = ZimReaderSource(file)
if (zimReaderSource.canOpenInLibkiwix()) {
bookOnDiskEntity.zimReaderSource = zimReaderSource
}
}
BookOnDisk(bookOnDiskEntity)
}
fun insert(booksOnDisk: List<BookOnDisk>) {
box.store.callInTx {
val uniqueBooks = uniqueBooksByFile(booksOnDisk)
removeEntriesWithMatchingIds(uniqueBooks)
box.put(uniqueBooks.distinctBy { it.book.id }.map(::BookOnDiskEntity))
}
}
private fun uniqueBooksByFile(booksOnDisk: List<BookOnDisk>): List<BookOnDisk> {
val booksWithSameFilePath = booksWithSameFilePath(booksOnDisk)
return booksOnDisk.filter { bookOnDisk: BookOnDisk ->
booksWithSameFilePath.none { it.zimReaderSource == bookOnDisk.zimReaderSource }
}
}
@Suppress("Deprecation")
private fun booksWithSameFilePath(booksOnDisk: List<BookOnDisk>) =
box.query {
inValues(
BookOnDiskEntity_.zimReaderSource,
booksOnDisk.map { it.zimReaderSource.toDatabase() }.toTypedArray(),
QueryBuilder.StringOrder.CASE_INSENSITIVE
)
}.find()
.map(::BookOnDisk)
private fun removeEntriesWithMatchingIds(uniqueBooks: List<BookOnDisk>) {
box.query {
inValues(
BookOnDiskEntity_.bookId,
uniqueBooks.map { it.book.id }.toTypedArray(),
QueryBuilder.StringOrder.CASE_INSENSITIVE
)
}
.remove()
}
fun delete(databaseId: Long) {
box.remove(databaseId)
}
@Suppress("UnsafeCallOnNullableType")
fun migrationInsert(books: List<LibkiwixBook>) {
insert(books.map { BookOnDisk(book = it, zimReaderSource = ZimReaderSource(it.file!!)) })
}
private suspend fun removeBooksThatDoNotExist(books: MutableList<BookOnDiskEntity>) {
delete(books.filterNot { it.zimReaderSource.exists() })
}
// Remove the existing books from database which are showing on the library screen.
private fun removeBooksThatAreInTrashFolder(books: List<BookOnDiskEntity>) {
delete(books.filter { isInTrashFolder(it.zimReaderSource.toDatabase()) })
}
// Check if any existing ZIM file showing on the library screen which is inside the trash folder.
private fun isInTrashFolder(filePath: String) =
Regex("/\\.Trash/").containsMatchIn(filePath)
private fun delete(books: List<BookOnDiskEntity>) {
box.remove(books)
}
fun bookMatching(downloadTitle: String) =
box.query {
endsWith(
BookOnDiskEntity_.zimReaderSource,
downloadTitle,
QueryBuilder.StringOrder.CASE_INSENSITIVE
)
}.findFirst()
}

View File

@ -1,116 +0,0 @@
/*
* Kiwix Android
* Copyright (c) 2019 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.Box
import io.objectbox.kotlin.query
import io.objectbox.query.QueryBuilder
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import org.kiwix.kiwixmobile.core.dao.entities.BookmarkEntity
import org.kiwix.kiwixmobile.core.dao.entities.BookmarkEntity_
import org.kiwix.kiwixmobile.core.page.adapter.Page
import org.kiwix.kiwixmobile.core.page.bookmark.adapter.BookmarkItem
import org.kiwix.kiwixmobile.core.reader.ZimFileReader
import org.kiwix.kiwixmobile.core.reader.ZimReaderSource.Companion.fromDatabaseValue
import javax.inject.Inject
class NewBookmarksDao @Inject constructor(val box: Box<BookmarkEntity>) : PageDao {
fun bookmarks(): Flow<List<Page>> =
box.asFlow(
box.query {
order(BookmarkEntity_.bookmarkTitle)
}
).map {
it.map { bookmarkEntity ->
bookmarkEntity.zimFilePath?.let { filePath ->
// set zimReaderSource for previously saved bookmarks
fromDatabaseValue(filePath)?.let { zimReaderSource ->
bookmarkEntity.zimReaderSource = zimReaderSource
}
}
BookmarkItem(bookmarkEntity)
}
}
override fun pages(): Flow<List<Page>> = bookmarks()
override fun deletePages(pagesToDelete: List<Page>) =
deleteBookmarks(pagesToDelete as List<BookmarkItem>)
fun getCurrentZimBookmarksUrl(zimFileReader: ZimFileReader?) =
box.query {
equal(
BookmarkEntity_.zimId,
zimFileReader?.id.orEmpty(),
QueryBuilder.StringOrder.CASE_INSENSITIVE
)
.or()
.equal(
BookmarkEntity_.zimName,
zimFileReader?.name.orEmpty(),
QueryBuilder.StringOrder.CASE_INSENSITIVE
)
order(BookmarkEntity_.bookmarkTitle)
}.property(BookmarkEntity_.bookmarkUrl)
.findStrings()
.toList()
.distinct()
fun bookmarkUrlsForCurrentBook(
zimFileReader: ZimFileReader?,
dispatcher: CoroutineDispatcher = Dispatchers.IO
): Flow<List<String>> =
box.asFlow(
box.query {
equal(
BookmarkEntity_.zimId,
zimFileReader?.id.orEmpty(),
QueryBuilder.StringOrder.CASE_INSENSITIVE
)
.or()
.equal(
BookmarkEntity_.zimName,
zimFileReader?.name.orEmpty(),
QueryBuilder.StringOrder.CASE_INSENSITIVE
)
order(BookmarkEntity_.bookmarkTitle)
}
).map { it.map(BookmarkEntity::bookmarkUrl) }
.flowOn(dispatcher)
fun saveBookmark(bookmarkItem: BookmarkItem) {
box.put(BookmarkEntity(bookmarkItem))
}
fun deleteBookmarks(bookmarks: List<BookmarkItem>) {
box.remove(bookmarks.map(::BookmarkEntity))
}
fun deleteBookmark(bookmarkUrl: String) {
box.query {
equal(
BookmarkEntity_.bookmarkUrl,
bookmarkUrl,
QueryBuilder.StringOrder.CASE_INSENSITIVE
)
}.remove()
}
}

View File

@ -1,52 +0,0 @@
/*
* Kiwix Android
* Copyright (c) 2019 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.Box
import io.objectbox.kotlin.flow
import io.objectbox.kotlin.query
import io.objectbox.query.Query
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import org.kiwix.kiwixmobile.core.dao.entities.LanguageEntity
import org.kiwix.kiwixmobile.core.zim_manager.Language
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class NewLanguagesDao @Inject constructor(private val box: Box<LanguageEntity>) {
fun languages() =
box.asFlow()
.map { it.map(LanguageEntity::toLanguageModel) }
fun insert(languages: List<Language>) {
box.store.callInTx {
box.removeAll()
box.put(languages.map(::LanguageEntity))
}
}
}
@OptIn(ExperimentalCoroutinesApi::class)
fun <T> Box<T>.asFlow(query: Query<T> = query {}): Flow<List<T>> {
return query.flow()
.map { it.toList() }
.distinctUntilChanged()
}

View File

@ -1,82 +0,0 @@
/*
* 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.Box
import io.objectbox.kotlin.query
import io.objectbox.query.QueryBuilder
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import org.kiwix.kiwixmobile.core.dao.entities.NotesEntity
import org.kiwix.kiwixmobile.core.dao.entities.NotesEntity_
import org.kiwix.kiwixmobile.core.page.adapter.Page
import org.kiwix.kiwixmobile.core.page.notes.adapter.NoteListItem
import org.kiwix.kiwixmobile.core.reader.ZimReaderSource.Companion.fromDatabaseValue
import javax.inject.Inject
class NewNoteDao @Inject constructor(val box: Box<NotesEntity>) : PageDao {
fun notes(): Flow<List<Page>> =
box.asFlow(
box.query {
order(NotesEntity_.noteTitle)
}
).map {
it.map { notesEntity ->
notesEntity.zimFilePath?.let { filePath ->
// set zimReaderSource for previously saved notes
fromDatabaseValue(filePath)?.let { zimReaderSource ->
notesEntity.zimReaderSource = zimReaderSource
}
}
NoteListItem(notesEntity)
}
}
override fun pages(): Flow<List<Page>> = notes()
override fun deletePages(pagesToDelete: List<Page>) =
deleteNotes(pagesToDelete as List<NoteListItem>)
fun saveNote(noteItem: NoteListItem) {
box.store.callInTx {
if (doesNotAlreadyExist(noteItem)) {
box.put(NotesEntity(noteItem))
}
}
}
fun deleteNotes(noteList: List<NoteListItem>) {
box.remove(noteList.map(::NotesEntity))
}
fun deleteNote(noteUniqueKey: String) {
box.query {
equal(
NotesEntity_.noteTitle,
noteUniqueKey,
QueryBuilder.StringOrder.CASE_INSENSITIVE
)
}.remove()
}
private fun doesNotAlreadyExist(noteItem: NoteListItem) =
box.query {
equal(NotesEntity_.noteTitle, noteItem.title, QueryBuilder.StringOrder.CASE_INSENSITIVE)
}.count() == 0L
}

View File

@ -1,72 +0,0 @@
/*
* Kiwix Android
* Copyright (c) 2019 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.Box
import io.objectbox.kotlin.query
import io.objectbox.query.QueryBuilder
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.search.SearchListItem.RecentSearchListItem
import javax.inject.Inject
class NewRecentSearchDao @Inject constructor(
private val box: Box<RecentSearchEntity>,
private val flowBuilder: FlowBuilder
) {
fun recentSearches(zimId: String?) =
flowBuilder.buildCallbackFlow(
box.query {
equal(
RecentSearchEntity_.zimId,
zimId.orEmpty(),
QueryBuilder.StringOrder.CASE_INSENSITIVE
)
orderDesc(RecentSearchEntity_.id)
}
).map { searchEntities ->
searchEntities.distinctBy(RecentSearchEntity::searchTerm)
.take(NUM_RECENT_RESULTS)
.map { searchEntity -> RecentSearchListItem(searchEntity.searchTerm, searchEntity.url) }
}
fun saveSearch(title: String, id: String, url: String?) {
box.put(RecentSearchEntity(searchTerm = title, zimId = id, url = url))
}
fun deleteSearchString(searchTerm: String) {
box
.query {
equal(
RecentSearchEntity_.searchTerm,
searchTerm,
QueryBuilder.StringOrder.CASE_INSENSITIVE
)
}
.remove()
}
fun deleteSearchHistory() {
box.removeAll()
}
companion object {
private const val NUM_RECENT_RESULTS = 100
}
}

View File

@ -20,11 +20,11 @@ package org.kiwix.kiwixmobile.core.dao.entities
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.TypeConverter
import androidx.room.TypeConverters
import com.tonyodev.fetch2.Download
import com.tonyodev.fetch2.Error
import com.tonyodev.fetch2.Status
import io.objectbox.annotation.Convert
import io.objectbox.converter.PropertyConverter
import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
@Entity
@ -36,9 +36,9 @@ data class DownloadRoomEntity(
val etaInMilliSeconds: Long = -1L,
val bytesDownloaded: Long = -1L,
val totalSizeOfDownload: Long = -1L,
@Convert(converter = StatusConverter::class, dbType = Int::class)
@TypeConverters(StatusConverter::class)
val status: Status = Status.NONE,
@Convert(converter = ErrorConverter::class, dbType = Int::class)
@TypeConverters(ErrorConverter::class)
val error: Error = Error.NONE,
val progress: Int = -1,
val bookId: String,
@ -104,14 +104,18 @@ data class DownloadRoomEntity(
)
}
class StatusConverter : EnumConverter<Status>() {
override fun convertToEntityProperty(databaseValue: Int) = Status.valueOf(databaseValue)
class StatusConverter {
@TypeConverter
fun convertToEntityProperty(databaseValue: Int): Status = Status.valueOf(databaseValue)
@TypeConverter
fun convertToDatabaseValue(status: Status): Int = status.ordinal
}
class ErrorConverter : EnumConverter<Error>() {
override fun convertToEntityProperty(databaseValue: Int) = Error.valueOf(databaseValue)
}
class ErrorConverter {
@TypeConverter
fun convertToEntityProperty(databaseValue: Int) = Error.valueOf(databaseValue)
abstract class EnumConverter<E : Enum<E>> : PropertyConverter<E, Int> {
override fun convertToDatabaseValue(entityProperty: E): Int = entityProperty.ordinal
@TypeConverter
fun convertToDatabaseValue(error: Error): Int = error.ordinal
}

View File

@ -33,9 +33,11 @@ import org.kiwix.kiwixmobile.core.dao.RecentSearchRoomDao
import org.kiwix.kiwixmobile.core.dao.WebViewHistoryRoomDao
import org.kiwix.kiwixmobile.core.dao.entities.BundleRoomConverter
import org.kiwix.kiwixmobile.core.dao.entities.DownloadRoomEntity
import org.kiwix.kiwixmobile.core.dao.entities.ErrorConverter
import org.kiwix.kiwixmobile.core.dao.entities.HistoryRoomEntity
import org.kiwix.kiwixmobile.core.dao.entities.NotesRoomEntity
import org.kiwix.kiwixmobile.core.dao.entities.RecentSearchRoomEntity
import org.kiwix.kiwixmobile.core.dao.entities.StatusConverter
import org.kiwix.kiwixmobile.core.dao.entities.WebViewHistoryEntity
import org.kiwix.kiwixmobile.core.dao.entities.ZimSourceRoomConverter
@ -48,13 +50,15 @@ import org.kiwix.kiwixmobile.core.dao.entities.ZimSourceRoomConverter
DownloadRoomEntity::class,
WebViewHistoryEntity::class
],
version = 8,
version = 9,
exportSchema = false
)
@TypeConverters(
HistoryRoomDaoCoverts::class,
ZimSourceRoomConverter::class,
BundleRoomConverter::class
BundleRoomConverter::class,
StatusConverter::class,
ErrorConverter::class
)
abstract class KiwixRoomDatabase : RoomDatabase() {
abstract fun recentSearchRoomDao(): RecentSearchRoomDao
@ -78,7 +82,8 @@ abstract class KiwixRoomDatabase : RoomDatabase() {
MIGRATION_4_5,
MIGRATION_5_6,
MIGRATION_6_7,
MIGRATION_7_8
MIGRATION_7_8,
MIGRATION_8_9
)
.build().also { db = it }
}
@ -305,6 +310,60 @@ abstract class KiwixRoomDatabase : RoomDatabase() {
}
}
@Suppress("MagicNumber")
private val MIGRATION_8_9 =
object : Migration(8, 9) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"""
CREATE TABLE IF NOT EXISTS DownloadRoomEntity_new (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
downloadId INTEGER NOT NULL,
file TEXT,
etaInMilliSeconds INTEGER NOT NULL DEFAULT -1,
bytesDownloaded INTEGER NOT NULL DEFAULT -1,
totalSizeOfDownload INTEGER NOT NULL DEFAULT -1,
status INTEGER NOT NULL DEFAULT 0,
error INTEGER NOT NULL DEFAULT 0,
progress INTEGER NOT NULL DEFAULT -1,
bookId TEXT NOT NULL,
title TEXT NOT NULL,
description TEXT,
language TEXT NOT NULL,
creator TEXT NOT NULL,
publisher TEXT NOT NULL,
date TEXT NOT NULL,
url TEXT,
articleCount TEXT,
mediaCount TEXT,
size TEXT NOT NULL,
name TEXT,
favIcon TEXT NOT NULL,
tags TEXT
)
""".trimIndent()
)
db.execSQL(
"""
INSERT INTO DownloadRoomEntity_new (
id, downloadId, file, etaInMilliSeconds, bytesDownloaded,
totalSizeOfDownload, status, error, progress, bookId, title,
description, language, creator, publisher, date, url,
articleCount, mediaCount, size, name, favIcon, tags
)
SELECT id, downloadId, file, etaInMilliSeconds, bytesDownloaded,
totalSizeOfDownload, status, error, progress, bookId, title, description,
language, creator, publisher, date, url, articleCount,
mediaCount, size, name, favIcon, tags
FROM DownloadRoomEntity
""".trimIndent()
)
db.execSQL("DROP TABLE DownloadRoomEntity")
db.execSQL("ALTER TABLE DownloadRoomEntity_new RENAME TO DownloadRoomEntity")
}
}
fun destroyInstance() {
db = null
}

View File

@ -28,15 +28,9 @@ import org.kiwix.kiwixmobile.core.CoreApp
import org.kiwix.kiwixmobile.core.LibkiwixBookFactory
import org.kiwix.kiwixmobile.core.StorageObserver
import org.kiwix.kiwixmobile.core.dao.DownloadRoomDao
import org.kiwix.kiwixmobile.core.dao.HistoryDao
import org.kiwix.kiwixmobile.core.dao.HistoryRoomDao
import org.kiwix.kiwixmobile.core.dao.LibkiwixBookOnDisk
import org.kiwix.kiwixmobile.core.dao.LibkiwixBookmarks
import org.kiwix.kiwixmobile.core.dao.NewBookDao
import org.kiwix.kiwixmobile.core.dao.NewBookmarksDao
import org.kiwix.kiwixmobile.core.dao.NewLanguagesDao
import org.kiwix.kiwixmobile.core.dao.NewNoteDao
import org.kiwix.kiwixmobile.core.dao.NewRecentSearchDao
import org.kiwix.kiwixmobile.core.dao.NotesRoomDao
import org.kiwix.kiwixmobile.core.dao.RecentSearchRoomDao
import org.kiwix.kiwixmobile.core.dao.WebViewHistoryRoomDao
@ -92,13 +86,7 @@ interface CoreComponent {
fun application(): Application
fun bookUtils(): BookUtils
fun dataSource(): DataSource
fun newBookDao(): NewBookDao
fun historyDao(): HistoryDao
fun noteDao(): NewNoteDao
fun newLanguagesDao(): NewLanguagesDao
fun recentSearchDao(): NewRecentSearchDao
fun downloadRoomDao(): DownloadRoomDao
fun newBookmarksDao(): NewBookmarksDao
fun connectivityManager(): ConnectivityManager
fun objectBoxToLibkiwixMigrator(): ObjectBoxToLibkiwixMigrator
fun libkiwixBookmarks(): LibkiwixBookmarks

View File

@ -21,15 +21,7 @@ import android.content.Context
import dagger.Module
import dagger.Provides
import io.objectbox.BoxStore
import io.objectbox.kotlin.boxFor
import org.kiwix.kiwixmobile.core.dao.FlowBuilder
import org.kiwix.kiwixmobile.core.dao.HistoryDao
import org.kiwix.kiwixmobile.core.dao.LibkiwixBookOnDisk
import org.kiwix.kiwixmobile.core.dao.NewBookDao
import org.kiwix.kiwixmobile.core.dao.NewBookmarksDao
import org.kiwix.kiwixmobile.core.dao.NewLanguagesDao
import org.kiwix.kiwixmobile.core.dao.NewNoteDao
import org.kiwix.kiwixmobile.core.dao.NewRecentSearchDao
import org.kiwix.kiwixmobile.core.dao.entities.MyObjectBox
import org.kiwix.kiwixmobile.core.data.KiwixRoomDatabase
import javax.inject.Singleton
@ -49,26 +41,6 @@ open class DatabaseModule {
return boxStore!!
}
@Provides @Singleton fun providesNewBookDao(boxStore: BoxStore): NewBookDao =
NewBookDao(boxStore.boxFor())
@Provides @Singleton fun providesNewLanguagesDao(boxStore: BoxStore): NewLanguagesDao =
NewLanguagesDao(boxStore.boxFor())
@Provides @Singleton fun providesNewHistoryDao(boxStore: BoxStore): HistoryDao =
HistoryDao(boxStore.boxFor())
@Provides @Singleton fun providesNewBookmarksDao(boxStore: BoxStore): NewBookmarksDao =
NewBookmarksDao(boxStore.boxFor())
@Provides @Singleton fun providesNewNoteDao(boxStore: BoxStore): NewNoteDao =
NewNoteDao(boxStore.boxFor())
@Provides @Singleton fun providesNewRecentSearchDao(
boxStore: BoxStore,
flowBuilder: FlowBuilder
): NewRecentSearchDao = NewRecentSearchDao(boxStore.boxFor(), flowBuilder)
@Singleton
@Provides
fun provideYourDatabase(

View File

@ -1,93 +0,0 @@
/*
* Kiwix Android
* Copyright (c) 2021 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.mockk.CapturingSlot
import io.mockk.clearAllMocks
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
import io.mockk.verify
import io.objectbox.Box
import io.objectbox.query.Query
import io.objectbox.query.QueryBuilder
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
import org.kiwix.kiwixmobile.core.dao.entities.HistoryEntity
import org.kiwix.kiwixmobile.core.dao.entities.HistoryEntity_
import org.kiwix.kiwixmobile.core.page.adapter.Page
import org.kiwix.kiwixmobile.core.page.history.adapter.HistoryListItem
import org.kiwix.kiwixmobile.core.page.historyItem
import java.util.concurrent.Callable
internal class HistoryDaoTest {
private val box: Box<HistoryEntity> = mockk(relaxed = true)
private val historyDao = HistoryDao(box)
@AfterEach
fun tearDown() {
clearAllMocks()
}
@Test
fun deletePages() {
val historyItem: HistoryListItem.HistoryItem = historyItem(zimReaderSource = mockk())
val historyItemList: List<HistoryListItem.HistoryItem> = listOf(historyItem)
val pagesToDelete: List<Page> = historyItemList
historyDao.deletePages(pagesToDelete)
verify { historyDao.deleteHistory(historyItemList) }
}
@Test
fun saveHistory() {
val historyItem: HistoryListItem.HistoryItem =
historyItem(
historyUrl = "",
dateString = "",
zimReaderSource = mockk()
)
val slot: CapturingSlot<Callable<Unit>> = slot()
every { box.store.callInTx(capture(slot)) } returns Unit
val queryBuilder: QueryBuilder<HistoryEntity> = mockk()
every { box.query() } returns queryBuilder
every {
queryBuilder.equal(HistoryEntity_.historyUrl, "", QueryBuilder.StringOrder.CASE_INSENSITIVE)
} returns queryBuilder
every { queryBuilder.and() } returns queryBuilder
every {
queryBuilder.equal(
HistoryEntity_.dateString,
"",
QueryBuilder.StringOrder.CASE_INSENSITIVE
)
} returns queryBuilder
val query: Query<HistoryEntity> = mockk(relaxed = true)
every { queryBuilder.build() } returns query
historyDao.saveHistory(historyItem)
slot.captured.call()
verify { query.remove() }
verify { box.put(HistoryEntity(historyItem)) }
}
@Test
fun deleteAllHistory() {
historyDao.deleteAllHistory()
verify { box.removeAll() }
}
}

View File

@ -1,295 +0,0 @@
/*
* 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.mockk.CapturingSlot
import io.mockk.clearAllMocks
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.slot
import io.mockk.verify
import io.objectbox.Box
import io.objectbox.query.Query
import io.objectbox.query.QueryBuilder
import io.objectbox.reactive.SubscriptionBuilder
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.test.runTest
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.kiwix.kiwixmobile.core.dao.entities.BookOnDiskEntity
import org.kiwix.kiwixmobile.core.dao.entities.BookOnDiskEntity_
import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
import org.kiwix.kiwixmobile.core.reader.ZimReaderSource
import org.kiwix.kiwixmobile.core.utils.files.testFlow
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem.BookOnDisk
import org.kiwix.sharedFunctions.bookOnDisk
import org.kiwix.sharedFunctions.bookOnDiskEntity
import org.kiwix.sharedFunctions.libkiwixBook
import java.io.File
import java.util.concurrent.Callable
const val MOCKK_TIMEOUT_FOR_VERIFICATION = 1000L
internal class NewBookDaoTest {
private val box: Box<BookOnDiskEntity> = mockk(relaxed = true)
private val newBookDao = NewBookDao(box)
@BeforeEach
internal fun setUp() {
clearAllMocks()
}
@Nested
inner class BooksTests {
@Test
fun `books emits entities whose file exists`() = flakyTest {
runTest {
val (expectedEntity, _) = expectEmissionOfExistingAndNotExistingBook()
testFlow(
flow = newBookDao.books(),
triggerAction = {},
assert = { assertThat(awaitItem()).contains(BookOnDisk(expectedEntity)) }
)
}
}
@Test
fun `books deletes entities whose file does not exist`() = flakyTest {
runTest {
val (_, deletedEntity) = expectEmissionOfExistingAndNotExistingBook()
testFlow(
flow = newBookDao.books(),
triggerAction = {},
assert = {
coVerify(timeout = MOCKK_TIMEOUT_FOR_VERIFICATION) {
box.remove(listOf(deletedEntity))
}
}
)
}
}
@Test
fun `books removes entities whose files are in the trash folder`() = flakyTest {
runTest {
val (_, _) = expectEmissionOfExistingAndNotExistingBook(true)
testFlow(
flow = newBookDao.books(),
triggerAction = {},
assert = { Assertions.assertEquals(emptyList<BookOnDisk>(), awaitItem()) }
)
}
}
@OptIn(ExperimentalCoroutinesApi::class)
private fun expectEmissionOfExistingAndNotExistingBook(
isInTrashFolder: Boolean = false
): Pair<BookOnDiskEntity, BookOnDiskEntity> {
val query: Query<BookOnDiskEntity> = mockk()
val subscriptionBuilder: SubscriptionBuilder<MutableList<BookOnDiskEntity>> =
mockk(relaxed = true)
every { box.query().build() } returns query
every { query.subscribe() } returns subscriptionBuilder
val zimReaderSourceThatExists = mockk<ZimReaderSource>()
val zimReaderSourceThatDoesNotExist = mockk<ZimReaderSource>()
coEvery { zimReaderSourceThatExists.exists() } returns true
coEvery { zimReaderSourceThatDoesNotExist.exists() } returns false
every {
zimReaderSourceThatExists.toDatabase()
} returns if (isInTrashFolder) "/.Trash/test.zim" else ""
every { zimReaderSourceThatDoesNotExist.toDatabase() } returns ""
val entityThatExists = bookOnDiskEntity(zimReaderSource = zimReaderSourceThatExists)
val entityThatDoesNotExist =
bookOnDiskEntity(zimReaderSource = zimReaderSourceThatDoesNotExist)
mockkStatic(Query::class)
mockBoxAsFlow(box, mutableListOf(entityThatExists, entityThatDoesNotExist))
return entityThatExists to entityThatDoesNotExist
}
}
@Test
fun getBooks() =
runTest {
val entity = bookOnDiskEntity()
every { box.all } returns mutableListOf(entity)
assertThat(newBookDao.getBooks()).isEqualTo(listOf(BookOnDisk(entity)))
}
@Nested
inner class Insertion {
@Test
fun `insert transaction adds books to the box that have distinct ids`() {
val slot: CapturingSlot<Callable<Unit>> = slot()
every { box.store.callInTx(capture(slot)) } returns Unit
val distinctBook: BookOnDisk = bookOnDisk(databaseId = 0, book = libkiwixBook(id = "same"))
newBookDao.insert(
listOf(distinctBook, bookOnDisk(databaseId = 1, book = libkiwixBook(id = "same")))
)
val queryBuilder: QueryBuilder<BookOnDiskEntity> = mockk(relaxed = true)
every { box.query() } returns queryBuilder
every {
queryBuilder.`in`(
BookOnDiskEntity_.zimReaderSource,
arrayOf(distinctBook.zimReaderSource.toDatabase()),
QueryBuilder.StringOrder.CASE_INSENSITIVE
)
} returns queryBuilder
val query: Query<BookOnDiskEntity> = mockk(relaxed = true)
every { queryBuilder.build() } returns query
every {
query.find()
} returns listOf(bookOnDiskEntity(zimReaderSource = ZimReaderSource(File("matches_nothing"))))
slot.captured.call()
verify { box.put(listOf(BookOnDiskEntity(distinctBook))) }
}
@Test
fun `insert transaction does not add books if a book with the same path exists in the box`() {
val slot: CapturingSlot<Callable<Unit>> = slot()
every { box.store.callInTx(capture(slot)) } returns Unit
val distinctBook: BookOnDisk = bookOnDisk()
newBookDao.insert(listOf(distinctBook))
val queryBuilder: QueryBuilder<BookOnDiskEntity> = mockk(relaxed = true)
every { box.query() } returns queryBuilder
every {
queryBuilder.`in`(
BookOnDiskEntity_.zimReaderSource,
arrayOf(distinctBook.zimReaderSource.toDatabase()),
QueryBuilder.StringOrder.CASE_INSENSITIVE
)
} returns queryBuilder
val query: Query<BookOnDiskEntity> = mockk(relaxed = true)
every { queryBuilder.build() } returns query
every {
query.find()
} returns listOf(bookOnDiskEntity(zimReaderSource = distinctBook.zimReaderSource))
slot.captured.call()
verify { box.put(listOf()) }
}
@Test
fun `insert transaction removes books with duplicate ids`() {
val slot: CapturingSlot<Callable<Unit>> = slot()
every { box.store.callInTx(capture(slot)) } returns Unit
val distinctBook: BookOnDisk = bookOnDisk()
newBookDao.insert(listOf(distinctBook))
val queryBuilder: QueryBuilder<BookOnDiskEntity> = mockk()
every { box.query() } returns queryBuilder
every {
queryBuilder.`in`(
BookOnDiskEntity_.zimReaderSource,
arrayOf(distinctBook.zimReaderSource.toDatabase()),
QueryBuilder.StringOrder.CASE_INSENSITIVE
)
} returns queryBuilder
val query: Query<BookOnDiskEntity> = mockk()
every { queryBuilder.build() } returns query
every { query.find() } returns listOf()
every {
queryBuilder.`in`(
BookOnDiskEntity_.bookId,
arrayOf(distinctBook.book.id),
QueryBuilder.StringOrder.CASE_INSENSITIVE
)
} returns queryBuilder
every { query.remove() } returns 0L
slot.captured.call()
verify { box.put(listOf(BookOnDiskEntity(distinctBook))) }
}
}
@Test
fun delete() {
newBookDao.delete(0L)
verify { box.remove(0L) }
}
@Test
fun migrationInsert() {
val book: LibkiwixBook = libkiwixBook()
val slot: CapturingSlot<Callable<Unit>> = slot()
every { box.store.callInTx(capture(slot)) } returns Unit
newBookDao.migrationInsert(listOf(book))
slot.captured.call()
verify {
box.put(
listOf(
BookOnDiskEntity(
BookOnDisk(
book = book,
zimReaderSource = ZimReaderSource(book.file!!)
)
)
)
)
}
}
@Test
fun `bookMatching queries file by title`() {
val downloadTitle = "title"
val queryBuilder: QueryBuilder<BookOnDiskEntity> = mockk()
every { box.query() } returns queryBuilder
every {
queryBuilder.endsWith(
BookOnDiskEntity_.zimReaderSource,
downloadTitle,
QueryBuilder.StringOrder.CASE_INSENSITIVE
)
} returns queryBuilder
val query: Query<BookOnDiskEntity> = mockk()
every { queryBuilder.build() } returns query
val bookOnDiskEntity: BookOnDiskEntity = bookOnDiskEntity()
every { query.findFirst() } returns bookOnDiskEntity
assertThat(newBookDao.bookMatching(downloadTitle)).isEqualTo(bookOnDiskEntity)
}
}
fun <T> mockBoxAsFlow(box: Box<T>, result: List<T>) {
mockkStatic("org.kiwix.kiwixmobile.core.dao.NewLanguagesDaoKt")
every { box.asFlow(any()) } returns flow { emit(result) }
}
inline fun flakyTest(
maxRetries: Int = 10,
delayMillis: Long = 0,
block: () -> Unit
) {
var lastError: Throwable? = null
repeat(maxRetries) { attempt ->
try {
block()
return
} catch (e: Throwable) {
lastError = e
println("Test attempt ${attempt + 1} failed: ${e.message}")
if (delayMillis > 0) Thread.sleep(delayMillis)
}
}
throw lastError ?: AssertionError("Test failed after $maxRetries attempts")
}

View File

@ -1,131 +0,0 @@
/*
* Kiwix Android
* Copyright (c) 2021 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.mockk.every
import io.mockk.mockk
import io.mockk.verify
import io.objectbox.Box
import io.objectbox.query.Query
import io.objectbox.query.QueryBuilder
import org.junit.jupiter.api.Test
import org.kiwix.kiwixmobile.core.dao.entities.BookmarkEntity
import org.kiwix.kiwixmobile.core.dao.entities.BookmarkEntity_
import org.kiwix.kiwixmobile.core.page.adapter.Page
import org.kiwix.kiwixmobile.core.page.bookmark
import org.kiwix.kiwixmobile.core.page.bookmark.adapter.BookmarkItem
import org.kiwix.kiwixmobile.core.reader.ZimFileReader
internal class NewBookmarksDaoTest {
private val box: Box<BookmarkEntity> = mockk(relaxed = true)
private val newBookmarksDao = NewBookmarksDao(box)
@Test
fun deletePages() {
val bookmarkItem: BookmarkItem = bookmark(zimReaderSource = mockk())
val bookmarkItemList: List<BookmarkItem> = listOf(bookmarkItem)
val pagesToDelete: List<Page> = bookmarkItemList
newBookmarksDao.deletePages(pagesToDelete)
verify { newBookmarksDao.deleteBookmarks(bookmarkItemList) }
}
@Test
fun getCurrentZimBookmarksUrl() {
val bookmarkItem: BookmarkItem = mockk(relaxed = true)
val zimFileReader: ZimFileReader? = mockk(relaxed = true)
val queryBuilder: QueryBuilder<BookmarkEntity> = mockk()
every { box.query() } returns queryBuilder
every {
queryBuilder.equal(
BookmarkEntity_.zimId, "", QueryBuilder.StringOrder.CASE_INSENSITIVE
)
} returns queryBuilder
every { queryBuilder.or() } returns queryBuilder
every {
queryBuilder.equal(
BookmarkEntity_.zimName, "", QueryBuilder.StringOrder.CASE_INSENSITIVE
)
} returns queryBuilder
every { queryBuilder.order(BookmarkEntity_.bookmarkTitle) } returns queryBuilder
val query: Query<BookmarkEntity> = mockk(relaxed = true)
every { queryBuilder.build() } returns query
every { bookmarkItem.zimId } returns ""
every { bookmarkItem.zimName } returns ""
every { bookmarkItem.databaseId } returns 0L
newBookmarksDao.getCurrentZimBookmarksUrl(zimFileReader)
every {
query.property(BookmarkEntity_.bookmarkUrl).findStrings().toList().distinct()
} returns listOf("")
verify { box.query() }
}
@Test
fun bookmarkUrlsForCurrentBook() {
val bookmarkItem: BookmarkItem = mockk(relaxed = true)
val zimFileReader: ZimFileReader? = mockk(relaxed = true)
val queryBuilder: QueryBuilder<BookmarkEntity> = mockk()
every { box.query() } returns queryBuilder
every {
queryBuilder.equal(
BookmarkEntity_.zimId,
zimFileReader?.id ?: "", QueryBuilder.StringOrder.CASE_INSENSITIVE
)
} returns queryBuilder
every { queryBuilder.or() } returns queryBuilder
every {
queryBuilder.equal(
BookmarkEntity_.zimName,
zimFileReader?.name ?: "", QueryBuilder.StringOrder.CASE_INSENSITIVE
)
} returns queryBuilder
every { queryBuilder.order(BookmarkEntity_.bookmarkTitle) } returns queryBuilder
val query: Query<BookmarkEntity> = mockk(relaxed = true)
every { queryBuilder.build() } returns query
every { bookmarkItem.zimId } returns ""
every { bookmarkItem.zimName } returns ""
every { bookmarkItem.databaseId } returns 0L
newBookmarksDao.bookmarkUrlsForCurrentBook(zimFileReader)
verify { box.query() }
}
@Test
fun saveBookmark() {
val bookmarkItem: BookmarkItem = bookmark(zimReaderSource = mockk())
newBookmarksDao.saveBookmark(bookmarkItem)
verify { box.put(BookmarkEntity(bookmarkItem)) }
}
@Test
fun deleteBookmark() {
val bookmarkUrl = "bookmarkUrl"
val queryBuilder: QueryBuilder<BookmarkEntity> = mockk(relaxed = true)
every { box.query() } returns queryBuilder
every {
queryBuilder.equal(
BookmarkEntity_.bookmarkUrl,
bookmarkUrl,
QueryBuilder.StringOrder.CASE_INSENSITIVE
)
} returns queryBuilder
val query: Query<BookmarkEntity> = mockk(relaxed = true)
every { queryBuilder.build() } returns query
newBookmarksDao.deleteBookmark(bookmarkUrl)
verify { query.remove() }
}
}

View File

@ -1,65 +0,0 @@
/*
* Kiwix Android
* Copyright (c) 2021 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 org.junit.jupiter.api.Test
import io.mockk.CapturingSlot
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
import io.mockk.verify
import io.objectbox.Box
import org.kiwix.kiwixmobile.core.dao.entities.LanguageEntity
import org.kiwix.kiwixmobile.core.zim_manager.Language
import java.util.Locale
import java.util.concurrent.Callable
internal class NewLanguagesDaoTest {
private val box: Box<LanguageEntity> = mockk(relaxed = true)
private val newLanguagesDao = NewLanguagesDao(box)
@Test
fun insert() {
val id = 0L
val active = false
val occurrencesOfLanguage = 0
val language: Language = mockk()
val slot: CapturingSlot<Callable<Unit>> = slot()
every { box.store.callInTx(capture(slot)) } returns Unit
every { language.id } returns id
every { language.active } returns active
every { language.languageCode } returns Locale.ENGLISH.toString()
every { language.occurencesOfLanguage } returns occurrencesOfLanguage
newLanguagesDao.insert(listOf(language))
slot.captured.call()
verify { box.removeAll() }
verify {
box.put(
listOf(
LanguageEntity(
id = 0L,
locale = Locale.ENGLISH,
active = false,
occurencesOfLanguage = 0
)
)
)
}
}
}

View File

@ -1,88 +0,0 @@
/*
* Kiwix Android
* Copyright (c) 2022 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.mockk.CapturingSlot
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
import io.mockk.verify
import io.objectbox.Box
import io.objectbox.query.Query
import io.objectbox.query.QueryBuilder
import org.junit.jupiter.api.Test
import org.kiwix.kiwixmobile.core.dao.entities.NotesEntity
import org.kiwix.kiwixmobile.core.dao.entities.NotesEntity_
import org.kiwix.kiwixmobile.core.page.adapter.Page
import org.kiwix.kiwixmobile.core.page.note
import org.kiwix.kiwixmobile.core.page.notes.adapter.NoteListItem
import java.util.concurrent.Callable
internal class NewNoteDaoTest {
private val notesBox: Box<NotesEntity> = mockk(relaxed = true)
private val newNotesDao = NewNoteDao(notesBox)
@Test
fun deletePages() {
val notesItem: NoteListItem = note(zimReaderSource = mockk())
val notesItemList: List<NoteListItem> = listOf(notesItem)
val pagesToDelete: List<Page> = notesItemList
newNotesDao.deletePages(pagesToDelete)
verify { newNotesDao.deleteNotes(notesItemList) }
}
@Test
fun deleteNotePage() {
val noteTitle = "abNotesTitle"
val queryBuilder: QueryBuilder<NotesEntity> = mockk(relaxed = true)
every { notesBox.query() } returns queryBuilder
every {
queryBuilder.equal(
NotesEntity_.noteTitle,
noteTitle,
QueryBuilder.StringOrder.CASE_INSENSITIVE
)
} returns queryBuilder
val query: Query<NotesEntity> = mockk(relaxed = true)
every { queryBuilder.build() } returns query
newNotesDao.deleteNote(noteTitle)
verify { query.remove() }
}
@Test
fun saveNotePage() {
val newNote: NoteListItem = note(title = "", zimReaderSource = mockk())
val slot: CapturingSlot<Callable<Unit>> = slot()
every { notesBox.store.callInTx(capture(slot)) } returns Unit
val queryBuilder: QueryBuilder<NotesEntity> = mockk(relaxed = true)
every { notesBox.query() } returns queryBuilder
val query: Query<NotesEntity> = mockk(relaxed = true)
every { queryBuilder.build() } returns query
every {
queryBuilder.equal(
NotesEntity_.noteTitle,
newNote.title,
QueryBuilder.StringOrder.CASE_INSENSITIVE
)
} returns queryBuilder
newNotesDao.saveNote(newNote)
slot.captured.call()
verify { notesBox.put(NotesEntity(newNote)) }
}
}

View File

@ -1,150 +0,0 @@
/*
* 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.mockk.every
import io.mockk.mockk
import io.mockk.verify
import io.objectbox.Box
import io.objectbox.query.Query
import io.objectbox.query.QueryBuilder
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.kiwix.kiwixmobile.core.dao.entities.RecentSearchEntity
import org.kiwix.kiwixmobile.core.dao.entities.RecentSearchEntity_
import org.kiwix.kiwixmobile.core.search.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 flowBuilder: FlowBuilder = mockk()
private val newRecentSearchDao = NewRecentSearchDao(box, flowBuilder)
@Nested
inner class RecentSearchTests {
@Test
fun `recentSearches searches by Id passed`() =
runTest {
val zimId = "id"
val queryResult = listOf<RecentSearchEntity>(recentSearchEntity())
expectFromRecentSearches(queryResult, zimId)
newRecentSearchDao.recentSearches(zimId)
.test(this)
.assertValues(
mutableListOf(queryResult.map { RecentSearchListItem(it.searchTerm, it.url) })
)
.finish()
}
@Test
fun `recentSearches searches with blank Id if null passed`() =
runTest {
val queryResult = listOf<RecentSearchEntity>(recentSearchEntity())
expectFromRecentSearches(queryResult, "")
newRecentSearchDao.recentSearches(null)
.test(this)
.assertValues(
mutableListOf(queryResult.map { RecentSearchListItem(it.searchTerm, it.url) })
)
.finish()
}
@Test
fun `recentSearches searches returns distinct entities by searchTerm`() =
runTest {
val queryResult = listOf<RecentSearchEntity>(recentSearchEntity(), recentSearchEntity())
expectFromRecentSearches(queryResult, "")
newRecentSearchDao.recentSearches("")
.test(this)
.assertValues(
mutableListOf(queryResult.take(1).map { RecentSearchListItem(it.searchTerm, it.url) })
)
.finish()
}
@Test
fun `recentSearches searches returns a limitedNumber of entities`() =
runTest {
val searchResults: List<RecentSearchEntity> =
(0..200).map { recentSearchEntity(searchTerm = "$it") }
expectFromRecentSearches(searchResults, "")
newRecentSearchDao.recentSearches("")
.test(this)
.assertLastValue { it.size == 100 }
.finish()
}
private fun expectFromRecentSearches(queryResult: List<RecentSearchEntity>, zimId: String) {
val queryBuilder = mockk<QueryBuilder<RecentSearchEntity>>()
every { box.query() } returns queryBuilder
every {
queryBuilder.equal(
RecentSearchEntity_.zimId,
zimId,
QueryBuilder.StringOrder.CASE_INSENSITIVE
)
} returns queryBuilder
every { queryBuilder.orderDesc(RecentSearchEntity_.id) } returns queryBuilder
val query = mockk<Query<RecentSearchEntity>>()
every { queryBuilder.build() } returns query
every { flowBuilder.buildCallbackFlow(query) } returns flowOf(queryResult)
}
}
@Test
fun `saveSearch puts RecentSearchEntity into box`() {
newRecentSearchDao.saveSearch("title", "id", "https://kiwix.app/mainPage")
verify {
box.put(
recentSearchEntity(
searchTerm = "title",
zimId = "id",
url = "https://kiwix.app/mainPage"
)
)
}
}
@Test
fun `deleteSearchString removes query results for the term`() {
val searchTerm = "searchTerm"
val queryBuilder: QueryBuilder<RecentSearchEntity> = mockk()
every { box.query() } returns queryBuilder
every {
queryBuilder.equal(
RecentSearchEntity_.searchTerm,
searchTerm,
QueryBuilder.StringOrder.CASE_INSENSITIVE
)
} returns queryBuilder
val query: Query<RecentSearchEntity> = mockk(relaxed = true)
every { queryBuilder.build() } returns query
newRecentSearchDao.deleteSearchString(searchTerm)
verify { query.remove() }
}
@Test
fun `deleteSearchHistory deletes everything`() {
newRecentSearchDao.deleteSearchHistory()
verify { box.removeAll() }
}
}

View File

@ -28,7 +28,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Test
import org.kiwix.kiwixmobile.core.base.SideEffect
import org.kiwix.kiwixmobile.core.dao.NewBookmarksDao
import org.kiwix.kiwixmobile.core.dao.LibkiwixBookmarks
import org.kiwix.kiwixmobile.core.main.CoreMainActivity
import org.kiwix.kiwixmobile.core.page.bookmarkState
import org.kiwix.kiwixmobile.core.page.libkiwixBookmarkItem
@ -41,7 +41,7 @@ import java.util.UUID
internal class ShowDeleteBookmarksDialogTest {
val effects = mockk<MutableSharedFlow<SideEffect<*>>>(relaxed = true)
private val newBookmarksDao = mockk<NewBookmarksDao>()
private val libkiwixBookmark = mockk<LibkiwixBookmarks>()
val activity = mockk<CoreMainActivity>()
private val dialogShower = mockk<DialogShower>(relaxed = true)
private val viewModelScope = CoroutineScope(Dispatchers.IO)
@ -52,7 +52,7 @@ internal class ShowDeleteBookmarksDialogTest {
ShowDeleteBookmarksDialog(
effects,
bookmarkState(),
newBookmarksDao,
libkiwixBookmark,
viewModelScope,
dialogShower
)
@ -61,7 +61,7 @@ internal class ShowDeleteBookmarksDialogTest {
showDeleteBookmarksDialog.invokeWith(activity)
verify { dialogShower.show(any(), capture(lambdaSlot)) }
lambdaSlot.captured.invoke()
verify { effects.tryEmit(DeletePageItems(bookmarkState(), newBookmarksDao, viewModelScope)) }
verify { effects.tryEmit(DeletePageItems(bookmarkState(), libkiwixBookmark, viewModelScope)) }
}
private fun mockkActivityInjection(showDeleteBookmarksDialog: ShowDeleteBookmarksDialog) {
@ -88,7 +88,7 @@ internal class ShowDeleteBookmarksDialogTest {
)
)
),
newBookmarksDao,
libkiwixBookmark,
viewModelScope,
dialogShower
)
@ -113,7 +113,7 @@ internal class ShowDeleteBookmarksDialogTest {
)
)
),
newBookmarksDao,
libkiwixBookmark,
viewModelScope,
dialogShower
)

View File

@ -10,7 +10,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Test
import org.kiwix.kiwixmobile.core.base.SideEffect
import org.kiwix.kiwixmobile.core.dao.HistoryDao
import org.kiwix.kiwixmobile.core.dao.HistoryRoomDao
import org.kiwix.kiwixmobile.core.main.CoreMainActivity
import org.kiwix.kiwixmobile.core.page.historyItem
import org.kiwix.kiwixmobile.core.page.historyState
@ -21,7 +21,7 @@ import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog.DeleteSelectedHistory
internal class ShowDeleteHistoryDialogTest {
val effects = mockk<MutableSharedFlow<SideEffect<*>>>(relaxed = true)
private val historyDao = mockk<HistoryDao>()
private val historyRoomDao = mockk<HistoryRoomDao>()
val activity = mockk<CoreMainActivity>()
private val dialogShower = mockk<DialogShower>(relaxed = true)
private val viewModelScope = CoroutineScope(Dispatchers.IO)
@ -33,7 +33,7 @@ internal class ShowDeleteHistoryDialogTest {
ShowDeleteHistoryDialog(
effects,
historyState(),
historyDao,
historyRoomDao,
viewModelScope,
dialogShower
)
@ -42,7 +42,7 @@ internal class ShowDeleteHistoryDialogTest {
showDeleteHistoryDialog.invokeWith(activity)
verify { dialogShower.show(any(), capture(lambdaSlot)) }
lambdaSlot.captured.invoke()
verify { effects.tryEmit(DeletePageItems(historyState(), historyDao, viewModelScope)) }
verify { effects.tryEmit(DeletePageItems(historyState(), historyRoomDao, viewModelScope)) }
}
@Test
@ -52,7 +52,7 @@ internal class ShowDeleteHistoryDialogTest {
ShowDeleteHistoryDialog(
effects,
historyState(listOf(historyItem(isSelected = true, zimReaderSource = mockk()))),
historyDao,
historyRoomDao,
viewModelScope,
dialogShower
)
@ -68,7 +68,7 @@ internal class ShowDeleteHistoryDialogTest {
ShowDeleteHistoryDialog(
effects,
historyState(),
historyDao,
historyRoomDao,
viewModelScope,
dialogShower
)