Merge pull request #4333 from kiwix/Fixes#4298

Introduced support of OPDS catalog.
This commit is contained in:
Kelson 2025-06-07 13:29:33 +02:00 committed by GitHub
commit 18e3b3d9b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
48 changed files with 504 additions and 349 deletions

View File

@ -55,13 +55,6 @@
<init>(...);
}
## keep everything in LibraryNetworkEntity.kt
-keep class org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity { *; }
-keep class org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity$* { *; }
-keepclassmembers class org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity$* {
<init>(...);
}
-keep class javax.xml.stream.** { *; }
-dontwarn javax.xml.stream.Location
-dontwarn javax.xml.stream.XMLEventReader

View File

@ -127,7 +127,7 @@ class DownloadRobot : BaseRobot() {
testFlakyView({
composeTestRule.apply {
waitUntilTimeout()
onAllNodesWithTag(ONLINE_BOOK_ITEM_TESTING_TAG)[0].performClick()
onAllNodesWithTag(ONLINE_BOOK_ITEM_TESTING_TAG)[1].performClick()
}
})
}

View File

@ -48,7 +48,7 @@ import org.kiwix.kiwixmobile.core.di.modules.CALL_TIMEOUT
import org.kiwix.kiwixmobile.core.di.modules.CONNECTION_TIMEOUT
import org.kiwix.kiwixmobile.core.di.modules.READ_TIMEOUT
import org.kiwix.kiwixmobile.core.di.modules.USER_AGENT
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity
import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
import org.kiwix.kiwixmobile.core.ui.components.SWIPE_REFRESH_TESTING_TAG
import org.kiwix.kiwixmobile.core.utils.files.Log
import java.io.File
@ -150,7 +150,7 @@ object TestUtils {
@JvmStatic fun withContent(content: String): Matcher<Any?> {
return object : BoundedMatcher<Any?, Any?>(Any::class.java) {
public override fun matchesSafely(myObj: Any?): Boolean {
if (myObj !is LibraryNetworkEntity.Book) {
if (myObj !is LibkiwixBook) {
return false
}
return if (myObj.url != null) {

View File

@ -35,7 +35,7 @@ import org.junit.Rule
import org.junit.Test
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Assertions.assertEquals
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity
import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
import org.kiwix.kiwixmobile.core.utils.TestingUtils.RETRY_RULE_ORDER
import org.kiwix.kiwixmobile.core.utils.files.DocumentResolverWrapper
import org.kiwix.kiwixmobile.core.utils.files.FileUtils
@ -109,7 +109,7 @@ class FileUtilsInstrumentationTest {
}
char1++
}
val book = LibraryNetworkEntity.Book()
val book = LibkiwixBook()
book.file = File(fileName + "bg")
val files = getAllZimParts(book)

View File

@ -44,7 +44,6 @@ import androidx.compose.ui.semantics.testTag
import com.tonyodev.fetch2.Status
import org.kiwix.kiwixmobile.R
import org.kiwix.kiwixmobile.core.R.string
import org.kiwix.kiwixmobile.core.downloader.model.toPainter
import org.kiwix.kiwixmobile.core.ui.components.ContentLoadingProgressBar
import org.kiwix.kiwixmobile.core.ui.components.ProgressBarStyle
import org.kiwix.kiwixmobile.core.ui.models.IconItem
@ -104,7 +103,7 @@ private fun DownloadBookContent(
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
BookIcon(item.favIcon.toPainter())
BookIcon(item.favIconUrl, isOnlineLibrary = true)
Column(
modifier = Modifier
.weight(1f)

View File

@ -48,8 +48,6 @@ import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.zIndex
import org.kiwix.kiwixmobile.R
import org.kiwix.kiwixmobile.core.downloader.model.Base64String
import org.kiwix.kiwixmobile.core.downloader.model.toPainter
import org.kiwix.kiwixmobile.core.extensions.toast
import org.kiwix.kiwixmobile.core.ui.theme.KiwixTheme
import org.kiwix.kiwixmobile.core.ui.theme.PureGrey
@ -58,7 +56,7 @@ import org.kiwix.kiwixmobile.core.utils.ComposeDimens.FIVE_DP
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.ONLINE_BOOK_DISABLED_COLOR_ALPHA
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SIXTEEN_DP
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.TWO_DP
import org.kiwix.kiwixmobile.core.zim_manager.KiloByte
import org.kiwix.kiwixmobile.core.zim_manager.Byte
import org.kiwix.kiwixmobile.ui.BookDate
import org.kiwix.kiwixmobile.ui.BookDescription
import org.kiwix.kiwixmobile.ui.BookIcon
@ -167,7 +165,7 @@ private fun OnlineBookContent(item: BookItem, bookUtils: BookUtils) {
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
BookIcon(Base64String(item.book.favicon).toPainter())
BookIcon(item.book.favicon, isOnlineLibrary = true)
Column(
modifier = Modifier
.weight(1f)
@ -207,7 +205,7 @@ private fun BookSizeAndDateRow(item: BookItem) {
verticalAlignment = Alignment.CenterVertically
) {
BookSize(
KiloByte(item.book.size).humanReadable,
Byte(item.book.size).humanReadable,
modifier = Modifier.weight(1f).testTag(ONLINE_BOOK_SIZE_TEXT_TESTING_TAG)
)
BookDate(item.book.date)

View File

@ -19,6 +19,7 @@
package org.kiwix.kiwixmobile.ui
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@ -31,18 +32,17 @@ import androidx.compose.foundation.layout.width
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import coil3.compose.AsyncImage
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.downloader.model.Base64String
import org.kiwix.kiwixmobile.core.downloader.model.toPainter
@ -53,7 +53,7 @@ import org.kiwix.kiwixmobile.core.utils.ComposeDimens.FIVE_DP
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.FOUR_DP
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.SIXTEEN_DP
import org.kiwix.kiwixmobile.core.utils.ComposeDimens.TWO_DP
import org.kiwix.kiwixmobile.core.zim_manager.KiloByte
import org.kiwix.kiwixmobile.core.zim_manager.Byte
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.ArticleCount
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem.BookOnDisk
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.SelectionMode
@ -116,7 +116,7 @@ private fun BookContent(
if (selectionMode == SelectionMode.MULTI) {
BookCheckbox(bookOnDisk, selectionMode, onMultiSelect, onClick, index)
}
BookIcon(Base64String(bookOnDisk.book.favicon).toPainter())
BookIcon(bookOnDisk.book.favicon, isOnlineLibrary = false)
BookDetails(Modifier.weight(1f), bookOnDisk)
}
}
@ -142,14 +142,23 @@ private fun BookCheckbox(
}
@Composable
fun BookIcon(painter: Painter) {
Icon(
painter = painter,
contentDescription = stringResource(R.string.fav_icon),
modifier = Modifier
.size(BOOK_ICON_SIZE),
tint = Color.Unspecified
)
fun BookIcon(iconSource: String, isOnlineLibrary: Boolean) {
val modifier = Modifier.size(BOOK_ICON_SIZE)
if (isOnlineLibrary) {
AsyncImage(
model = iconSource,
contentDescription = stringResource(R.string.fav_icon),
modifier = modifier,
placeholder = painterResource(R.drawable.default_zim_file_icon),
error = painterResource(R.drawable.default_zim_file_icon),
)
} else {
Image(
painter = Base64String(iconSource).toPainter(),
contentDescription = stringResource(R.string.fav_icon),
modifier = modifier
)
}
}
@Composable
@ -164,7 +173,7 @@ private fun BookDetails(modifier: Modifier, bookOnDisk: BookOnDisk) {
) {
BookDate(bookOnDisk.book.date)
Spacer(modifier = Modifier.width(EIGHT_DP))
BookSize(KiloByte(bookOnDisk.book.size).humanReadable)
BookSize(Byte(bookOnDisk.book.size).humanReadable)
Spacer(modifier = Modifier.width(EIGHT_DP))
BookArticleCount(
ArticleCount(bookOnDisk.book.articleCount.orEmpty())

View File

@ -22,18 +22,18 @@ import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.data.remote.OnlineLibraryProgressListener
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DEFAULT_INT_VALUE
import org.kiwix.kiwixmobile.core.downloader.downloadManager.HUNDERED
import org.kiwix.kiwixmobile.core.downloader.downloadManager.NINE
import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO
class AppProgressListenerProvider(
private val zimManageViewModel: ZimManageViewModel
) : OnlineLibraryProgressListener {
@Suppress("MagicNumber")
override fun onProgress(bytesRead: Long, contentLength: Long) {
val progress =
if (contentLength == DEFAULT_INT_VALUE.toLong()) {
ZERO
} else {
(bytesRead * 3 * HUNDERED / contentLength).coerceAtMost(HUNDERED.toLong())
(bytesRead * NINE * HUNDERED / contentLength).coerceAtMost(HUNDERED.toLong())
}
zimManageViewModel.downloadProgress.postValue(
zimManageViewModel.context.getString(

View File

@ -70,20 +70,19 @@ import org.kiwix.kiwixmobile.core.dao.NewBookDao
import org.kiwix.kiwixmobile.core.dao.NewLanguagesDao
import org.kiwix.kiwixmobile.core.data.DataSource
import org.kiwix.kiwixmobile.core.data.remote.KiwixService
import org.kiwix.kiwixmobile.core.data.remote.KiwixService.Companion.LIBRARY_NETWORK_PATH
import org.kiwix.kiwixmobile.core.data.remote.KiwixService.Companion.OPDS_LIBRARY_NETWORK_PATH
import org.kiwix.kiwixmobile.core.data.remote.ProgressResponseBody
import org.kiwix.kiwixmobile.core.data.remote.UserAgentInterceptor
import org.kiwix.kiwixmobile.core.di.modules.CALL_TIMEOUT
import org.kiwix.kiwixmobile.core.di.modules.CONNECTION_TIMEOUT
import org.kiwix.kiwixmobile.core.di.modules.KIWIX_DOWNLOAD_URL
import org.kiwix.kiwixmobile.core.di.modules.KIWIX_OPDS_LIBRARY_URL
import org.kiwix.kiwixmobile.core.di.modules.READ_TIMEOUT
import org.kiwix.kiwixmobile.core.di.modules.USER_AGENT
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DEFAULT_INT_VALUE
import org.kiwix.kiwixmobile.core.downloader.downloadManager.FIVE
import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO
import org.kiwix.kiwixmobile.core.downloader.model.DownloadModel
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book
import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
import org.kiwix.kiwixmobile.core.extensions.calculateSearchMatches
import org.kiwix.kiwixmobile.core.extensions.registerReceiver
import org.kiwix.kiwixmobile.core.ui.components.ONE
@ -97,6 +96,7 @@ import org.kiwix.kiwixmobile.core.zim_manager.ConnectivityBroadcastReceiver
import org.kiwix.kiwixmobile.core.zim_manager.Language
import org.kiwix.kiwixmobile.core.zim_manager.NetworkState
import org.kiwix.kiwixmobile.core.zim_manager.NetworkState.CONNECTED
import org.kiwix.kiwixmobile.core.zim_manager.OnlineLibraryManager
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem.BookOnDisk
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.SelectionMode.MULTI
@ -121,7 +121,8 @@ import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem
import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem.BookItem
import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem.DividerItem
import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem.LibraryDownloadItem
import java.util.LinkedList
import org.kiwix.libkiwix.Library
import org.kiwix.libkiwix.Manager
import java.util.Locale
import java.util.concurrent.TimeUnit.SECONDS
import javax.inject.Inject
@ -132,6 +133,7 @@ const val MAX_PROGRESS = 100
const val THREE = 3
const val FOUR = 4
@Suppress("LongParameterList")
class ZimManageViewModel @Inject constructor(
private val downloadDao: DownloadRoomDao,
private val bookDao: NewBookDao,
@ -145,7 +147,7 @@ class ZimManageViewModel @Inject constructor(
private val defaultLanguageProvider: DefaultLanguageProvider,
private val dataSource: DataSource,
private val connectivityManager: ConnectivityManager,
private val sharedPreferenceUtil: SharedPreferenceUtil
private val sharedPreferenceUtil: SharedPreferenceUtil,
) : ViewModel() {
sealed class FileSelectActions {
data class RequestNavigateTo(val bookOnDisk: BookOnDisk) : FileSelectActions()
@ -158,6 +160,12 @@ class ZimManageViewModel @Inject constructor(
object UserClickedDownloadBooksButton : FileSelectActions()
}
private val library by lazy { Library() }
private val manager by lazy { Manager(library) }
private val onlineLibraryManager by lazy {
OnlineLibraryManager(library, manager)
}
private var isUnitTestCase: Boolean = false
val sideEffects: MutableSharedFlow<SideEffect<*>> = MutableSharedFlow()
private val _libraryItems = MutableStateFlow<List<LibraryListItem>>(emptyList())
@ -168,7 +176,7 @@ class ZimManageViewModel @Inject constructor(
val onlineLibraryDownloading = MutableStateFlow(false)
val shouldShowWifiOnlyDialog = MutableLiveData<Boolean>()
val networkStates = MutableLiveData<NetworkState>()
val networkLibrary = MutableSharedFlow<LibraryNetworkEntity>(replay = 0)
val networkLibrary = MutableSharedFlow<List<LibkiwixBook>>(replay = 0)
val requestFileSystemCheck = MutableSharedFlow<Unit>(replay = 0)
val fileSelectActions = MutableSharedFlow<FileSelectActions>()
private val requestDownloadLibrary = MutableSharedFlow<Unit>(
@ -238,7 +246,10 @@ class ZimManageViewModel @Inject constructor(
} ?: originalResponse
}
.build()
return KiwixService.ServiceCreator.newHackListService(customOkHttpClient, KIWIX_DOWNLOAD_URL)
return KiwixService.ServiceCreator.newHackListService(
customOkHttpClient,
KIWIX_OPDS_LIBRARY_URL
)
.also {
kiwixService = it
}
@ -249,7 +260,7 @@ class ZimManageViewModel @Inject constructor(
private fun getContentLengthOfLibraryXmlFile(): Long {
val headRequest =
Request.Builder()
.url("$KIWIX_DOWNLOAD_URL$LIBRARY_NETWORK_PATH")
.url("$KIWIX_OPDS_LIBRARY_URL$OPDS_LIBRARY_NETWORK_PATH")
.head()
.header("Accept-Encoding", "identity")
.build()
@ -388,7 +399,7 @@ class ZimManageViewModel @Inject constructor(
@OptIn(ExperimentalCoroutinesApi::class)
private fun requestsAndConnectivityChangesToLibraryRequests(
library: MutableSharedFlow<LibraryNetworkEntity>,
library: MutableSharedFlow<List<LibkiwixBook>>,
dispatcher: CoroutineDispatcher = Dispatchers.IO
) = requestDownloadLibrary.flatMapConcat {
connectivityBroadcastReceiver.networkStates
@ -408,6 +419,7 @@ class ZimManageViewModel @Inject constructor(
it.printStackTrace().also {
isOnlineLibraryDownloading = false
onlineLibraryDownloading.tryEmit(false)
library.emit(emptyList())
}
}
.onEach {
@ -439,16 +451,27 @@ class ZimManageViewModel @Inject constructor(
private fun downloadLibraryFlow(
kiwixService: KiwixService
): Flow<LibraryNetworkEntity?> = flow {
): Flow<List<LibkiwixBook>> = flow {
downloadProgress.postValue(context.getString(R.string.starting_downloading_remote_library))
val response = kiwixService.getLibrary()
val resolvedUrl = response.raw().networkResponse?.request?.url
?: response.raw().request.url
val baseHostUrl = "${resolvedUrl.scheme}://${resolvedUrl.host}"
downloadProgress.postValue(context.getString(R.string.parsing_remote_library))
emit(response)
val libraryXml = response.body()
val isLibraryParsed = onlineLibraryManager.parseOPDSStream(libraryXml, baseHostUrl)
emit(
if (isLibraryParsed) {
onlineLibraryManager.getOnlineBooks()
} else {
emptyList()
}
)
}
.retry(5)
.catch { e ->
e.printStackTrace()
emit(LibraryNetworkEntity().apply { book = LinkedList() })
emit(emptyList())
}
private fun updateNetworkStates() = connectivityBroadcastReceiver.networkStates
@ -460,7 +483,7 @@ class ZimManageViewModel @Inject constructor(
private fun updateLibraryItems(
booksFromDao: Flow<List<BookOnDisk>>,
downloads: Flow<List<DownloadModel>>,
library: MutableSharedFlow<LibraryNetworkEntity>,
library: MutableSharedFlow<List<LibkiwixBook>>,
languages: Flow<List<Language>>,
dispatcher: CoroutineDispatcher = Dispatchers.IO
) = viewModelScope.launch(dispatcher) {
@ -483,14 +506,14 @@ class ZimManageViewModel @Inject constructor(
val books = args[ZERO] as List<BookOnDisk>
val activeDownloads = args[ONE] as List<DownloadModel>
val languageList = args[TWO] as List<Language>
val libraryNetworkEntity = args[THREE] as LibraryNetworkEntity
val libraryNetworkEntity = args[THREE] as List<LibkiwixBook>
val filter = args[FOUR] as String
val fileSystemState = args[FIVE] as FileSystemState
combineLibrarySources(
booksOnFileSystem = books,
activeDownloads = activeDownloads,
allLanguages = languageList,
libraryNetworkEntity = libraryNetworkEntity,
onlineBooks = libraryNetworkEntity,
filter = filter,
fileSystemState = fileSystemState
)
@ -505,12 +528,12 @@ class ZimManageViewModel @Inject constructor(
}
private fun updateLanguagesInDao(
library: MutableSharedFlow<LibraryNetworkEntity>,
library: MutableSharedFlow<List<LibkiwixBook>>,
languages: Flow<List<Language>>,
dispatcher: CoroutineDispatcher = Dispatchers.IO
) =
combine(
library.map { it.book }.filterNotNull(),
library,
languages
) { books, existingLanguages ->
combineToLanguageList(books, existingLanguages)
@ -523,7 +546,7 @@ class ZimManageViewModel @Inject constructor(
.launchIn(viewModelScope)
private fun combineToLanguageList(
booksFromNetwork: List<Book>,
booksFromNetwork: List<LibkiwixBook>,
allLanguages: List<Language>
) = when {
booksFromNetwork.isEmpty() -> {
@ -547,8 +570,8 @@ class ZimManageViewModel @Inject constructor(
)
}
private fun networkLanguageCounts(booksFromNetwork: List<Book>) =
booksFromNetwork.mapNotNull(Book::language)
private fun networkLanguageCounts(booksFromNetwork: List<LibkiwixBook>) =
booksFromNetwork.map { it.language }
.fold(
mutableMapOf<String, Int>()
) { acc, language -> acc.increment(language) }
@ -585,14 +608,14 @@ class ZimManageViewModel @Inject constructor(
booksOnFileSystem: List<BookOnDisk>,
activeDownloads: List<DownloadModel>,
allLanguages: List<Language>,
libraryNetworkEntity: LibraryNetworkEntity,
onlineBooks: List<LibkiwixBook>,
filter: String,
fileSystemState: FileSystemState
): List<LibraryListItem> {
val activeLanguageCodes =
allLanguages.filter(Language::active)
.map(Language::languageCode)
val allBooks = libraryNetworkEntity.book!! - booksOnFileSystem.map(BookOnDisk::book).toSet()
val allBooks = onlineBooks - booksOnFileSystem.map(BookOnDisk::book).toSet()
val downloadingBooks =
activeDownloads.mapNotNull { download ->
allBooks.firstOrNull { it.id == download.book.id }
@ -630,7 +653,7 @@ class ZimManageViewModel @Inject constructor(
}
private fun createLibrarySection(
books: List<Book>,
books: List<LibkiwixBook>,
activeDownloads: List<DownloadModel>,
fileSystemState: FileSystemState,
sectionStringId: Int,
@ -644,7 +667,7 @@ class ZimManageViewModel @Inject constructor(
}
private fun applySearchFilter(
unDownloadedBooks: List<Book>,
unDownloadedBooks: List<LibkiwixBook>,
filter: String
) = if (filter.isEmpty()) {
unDownloadedBooks
@ -653,7 +676,7 @@ class ZimManageViewModel @Inject constructor(
unDownloadedBooks.filter { it.searchMatches > 0 }
}
private fun List<Book>.asLibraryItems(
private fun List<LibkiwixBook>.asLibraryItems(
activeDownloads: List<DownloadModel>,
fileSystemState: FileSystemState
) = map { book ->

View File

@ -19,12 +19,11 @@
package org.kiwix.kiwixmobile.zimManager.libraryView
import eu.mhutti1.utils.storage.Bytes
import eu.mhutti1.utils.storage.KB
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import org.kiwix.kiwixmobile.core.dao.DownloadRoomDao
import org.kiwix.kiwixmobile.core.downloader.model.DownloadModel
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book
import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
import org.kiwix.kiwixmobile.core.settings.StorageCalculator
import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem
import javax.inject.Inject
@ -42,13 +41,13 @@ class AvailableSpaceCalculator @Inject constructor(
.map { downloads -> downloads.sumOf(DownloadModel::bytesRemaining) }
.map { bytesToBeDownloaded -> storageCalculator.availableBytes() - bytesToBeDownloaded }
.first()
if (bookItem.book.size.toLong() * KB < trueAvailableBytes) {
if (bookItem.book.size.toLong() < trueAvailableBytes) {
successAction.invoke(bookItem)
} else {
failureAction.invoke(Bytes(trueAvailableBytes).humanReadable)
}
}
suspend fun hasAvailableSpaceForBook(book: Book) =
book.size.toLong() * KB < storageCalculator.availableBytes()
suspend fun hasAvailableSpaceForBook(book: LibkiwixBook) =
book.size.toLong() < storageCalculator.availableBytes()
}

View File

@ -20,18 +20,17 @@ package org.kiwix.kiwixmobile.zimManager.libraryView.adapter
import androidx.annotation.StringRes
import com.tonyodev.fetch2.Status
import org.kiwix.kiwixmobile.core.downloader.model.Base64String
import org.kiwix.kiwixmobile.core.downloader.model.DownloadModel
import org.kiwix.kiwixmobile.core.downloader.model.DownloadState
import org.kiwix.kiwixmobile.core.downloader.model.Seconds
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book
import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
import org.kiwix.kiwixmobile.core.zim_manager.KiwixTag
import org.kiwix.kiwixmobile.zimManager.Fat32Checker
import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState
import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState.CanWrite4GbFile
import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState.CannotWrite4GbFile
import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState.NotEnoughSpaceFor4GbFile
import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState.DetectingFileSystem
import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState.NotEnoughSpaceFor4GbFile
sealed class LibraryListItem {
abstract val id: Long
@ -42,7 +41,7 @@ sealed class LibraryListItem {
) : LibraryListItem()
data class BookItem constructor(
val book: Book,
val book: LibkiwixBook,
val fileSystemState: FileSystemState,
val tags: List<KiwixTag> = KiwixTag.from(book.tags),
override val id: Long = book.id.hashCode().toLong()
@ -54,14 +53,14 @@ sealed class LibraryListItem {
}
companion object {
private fun Book.isLessThan4GB() =
private fun LibkiwixBook.isLessThan4GB() =
size.toLongOrNull() ?: 0L < Fat32Checker.FOUR_GIGABYTES_IN_KILOBYTES
}
}
data class LibraryDownloadItem(
val downloadId: Long,
val favIcon: Base64String,
val favIconUrl: String,
val title: String,
val description: String?,
val bytesDownloaded: Long,
@ -76,7 +75,7 @@ sealed class LibraryListItem {
constructor(downloadModel: DownloadModel) : this(
downloadModel.downloadId,
Base64String(downloadModel.book.favicon),
downloadModel.book.favicon,
downloadModel.book.title,
downloadModel.book.description,
downloadModel.bytesDownloaded,

View File

@ -28,7 +28,6 @@ import app.cash.turbine.TurbineTestContext
import app.cash.turbine.test
import com.jraska.livedata.test
import io.mockk.clearAllMocks
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
@ -60,7 +59,7 @@ import org.kiwix.kiwixmobile.core.dao.NewLanguagesDao
import org.kiwix.kiwixmobile.core.data.DataSource
import org.kiwix.kiwixmobile.core.data.remote.KiwixService
import org.kiwix.kiwixmobile.core.downloader.model.DownloadModel
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book
import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
import org.kiwix.kiwixmobile.core.utils.BookUtils
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
import org.kiwix.kiwixmobile.core.utils.dialog.AlertDialogShower
@ -90,11 +89,10 @@ import org.kiwix.kiwixmobile.zimManager.fileselectView.effects.ShareFiles
import org.kiwix.kiwixmobile.zimManager.fileselectView.effects.StartMultiSelection
import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem
import org.kiwix.sharedFunctions.InstantExecutorExtension
import org.kiwix.sharedFunctions.book
import org.kiwix.sharedFunctions.bookOnDisk
import org.kiwix.sharedFunctions.downloadModel
import org.kiwix.sharedFunctions.language
import org.kiwix.sharedFunctions.libraryNetworkEntity
import org.kiwix.sharedFunctions.libkiwixBook
import java.util.Locale
@OptIn(ExperimentalCoroutinesApi::class)
@ -191,7 +189,7 @@ class ZimManageViewModelTest {
setAlertDialogShower(alertDialogShower)
}
viewModel.fileSelectListStates.value = FileSelectListState(emptyList())
runBlocking { viewModel.networkLibrary.emit(libraryNetworkEntity()) }
runBlocking { viewModel.networkLibrary.emit(emptyList()) }
}
@Nested
@ -238,8 +236,8 @@ class ZimManageViewModelTest {
@Test
fun `books found on filesystem are filtered by books already in db`() = runTest {
every { application.getString(any()) } returns ""
val expectedBook = bookOnDisk(1L, book("1"))
val bookToRemove = bookOnDisk(1L, book("2"))
val expectedBook = bookOnDisk(1L, libkiwixBook("1"))
val bookToRemove = bookOnDisk(1L, libkiwixBook("2"))
advanceUntilIdle()
viewModel.requestFileSystemCheck.emit(Unit)
advanceUntilIdle()
@ -314,9 +312,9 @@ class ZimManageViewModelTest {
)
expectNetworkDbAndDefault(
listOf(
book(language = "eng"),
book(language = "eng"),
book(language = "fra")
libkiwixBook(language = "eng"),
libkiwixBook(language = "eng"),
libkiwixBook(language = "fra")
),
listOf(),
defaultLanguage
@ -352,9 +350,9 @@ class ZimManageViewModelTest {
)
expectNetworkDbAndDefault(
listOf(
book(language = "eng"),
book(language = "eng"),
book(language = "fra")
libkiwixBook(language = "eng"),
libkiwixBook(language = "eng"),
libkiwixBook(language = "fra")
),
listOf(dbLanguage),
language(isActive = true, occurencesOfLanguage = 1)
@ -378,15 +376,14 @@ class ZimManageViewModelTest {
}
private suspend fun TestScope.expectNetworkDbAndDefault(
networkBooks: List<Book>,
networkBooks: List<LibkiwixBook>,
dbBooks: List<Language>,
defaultLanguage: Language
) {
every { application.getString(any()) } returns ""
every { application.getString(any(), any()) } returns ""
coEvery { kiwixService.getLibrary() } returns libraryNetworkEntity(networkBooks)
every { defaultLanguageProvider.provide() } returns defaultLanguage
viewModel.networkLibrary.emit(libraryNetworkEntity(networkBooks))
viewModel.networkLibrary.emit(networkBooks)
runCurrent()
languages.value = dbBooks
runCurrent()
@ -405,10 +402,10 @@ class ZimManageViewModelTest {
@Test
fun `library update removes from sources and maps to list items`() = runTest {
val bookAlreadyOnDisk = book(id = "0", url = "", language = Locale.ENGLISH.language)
val bookDownloading = book(id = "1", url = "")
val bookWithActiveLanguage = book(id = "3", language = "activeLanguage", url = "")
val bookWithInactiveLanguage = book(id = "4", language = "inactiveLanguage", url = "")
val bookAlreadyOnDisk = libkiwixBook(id = "0", url = "", language = Locale.ENGLISH.language)
val bookDownloading = libkiwixBook(id = "1", url = "")
val bookWithActiveLanguage = libkiwixBook(id = "3", language = "activeLanguage", url = "")
val bookWithInactiveLanguage = libkiwixBook(id = "4", language = "inactiveLanguage", url = "")
testFlow(
flow = viewModel.libraryItems,
triggerAction = {
@ -429,13 +426,11 @@ class ZimManageViewModelTest {
fileSystemStates.tryEmit(CanWrite4GbFile)
advanceUntilIdle()
viewModel.networkLibrary.emit(
libraryNetworkEntity(
listOf(
bookAlreadyOnDisk,
bookDownloading,
bookWithActiveLanguage,
bookWithInactiveLanguage
)
listOf(
bookAlreadyOnDisk,
bookDownloading,
bookWithActiveLanguage,
bookWithInactiveLanguage
)
)
},
@ -458,7 +453,7 @@ class ZimManageViewModelTest {
@Test
fun `library marks files over 4GB as can't download if file system state says to`() = runTest {
val bookOver4Gb =
book(
libkiwixBook(
id = "0",
url = "",
size = "${Fat32Checker.FOUR_GIGABYTES_IN_KILOBYTES + 1}"
@ -477,7 +472,7 @@ class ZimManageViewModelTest {
)
)
fileSystemStates.tryEmit(CannotWrite4GbFile)
viewModel.networkLibrary.emit(libraryNetworkEntity(listOf(bookOver4Gb)))
viewModel.networkLibrary.emit(listOf(bookOver4Gb))
},
assert = {
skipItems(1)

View File

@ -24,17 +24,17 @@ import io.mockk.mockk
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book
import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
import org.kiwix.kiwixmobile.zimManager.Fat32Checker
import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState
import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState.CanWrite4GbFile
import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState.CannotWrite4GbFile
import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState.NotEnoughSpaceFor4GbFile
import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState.DetectingFileSystem
import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState.NotEnoughSpaceFor4GbFile
import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem.BookItem
internal class LibraryListItemTest {
private val book = mockk<Book>()
private val book = mockk<LibkiwixBook>()
@BeforeEach
fun init() {
@ -76,6 +76,6 @@ internal class LibraryListItemTest {
assertThat(canBeDownloaded(book, NotEnoughSpaceFor4GbFile)).isTrue
}
private fun canBeDownloaded(book: Book, fileSystemState: FileSystemState) =
private fun canBeDownloaded(book: LibkiwixBook, fileSystemState: FileSystemState) =
BookItem(book, fileSystemState).canBeDownloaded
}

View File

@ -63,6 +63,12 @@ object Libs {
const val tracing: String = "androidx.tracing:tracing:" + Versions.tracing
/**
* https://github.com/square/retrofit
*/
const val converter_scalars: String = "com.squareup.retrofit2:converter-scalars:" +
Versions.com_squareup_retrofit2
/**
* https://github.com/square/retrofit
*/
@ -363,4 +369,7 @@ object Libs {
const val COMPOSE_LIVE_DATA =
"androidx.compose.runtime:runtime-livedata:${Versions.COMPOSE_VERSION}"
const val COIL3_COMPOSE = "io.coil-kt.coil3:coil-compose:${Versions.COIL_COMPOSE}"
const val COIL3_OKHTTP_COMPOSE = "io.coil-kt.coil3:coil-network-okhttp:${Versions.COIL_COMPOSE}"
}

View File

@ -115,6 +115,8 @@ object Versions {
const val COMPOSE_MATERIAL3 = "1.3.1"
const val TURBINE_FLOW_TEST = "1.2.0"
const val COIL_COMPOSE = "3.2.0"
}
/**

View File

@ -243,6 +243,8 @@ class AllProjectConfigurer {
implementation(Libs.ANDROIDX_ACTIVITY_COMPOSE)
implementation(Libs.COMPOSE_TOOLING_PREVIEW)
implementation(Libs.COMPOSE_LIVE_DATA)
implementation(Libs.COIL3_COMPOSE)
implementation(Libs.COIL3_OKHTTP_COMPOSE)
// Compose UI test implementation
androidTestImplementation(Libs.COMPOSE_UI_TEST_JUNIT)

View File

@ -47,6 +47,11 @@ dependencies {
implementation(Libs.select_folder_document_file)
// Square
implementation(Libs.converter_scalars) {
exclude(group = "xpp3", module = "xpp3")
exclude(group = "stax", module = "stax-api")
exclude(group = "stax", module = "stax")
}
implementation(Libs.converter_simplexml) {
exclude(group = "xpp3", module = "xpp3")
exclude(group = "stax", module = "stax-api")

View File

@ -21,7 +21,7 @@
<ID>MagicNumber:DownloaderModule.kt$DownloaderModule$5</ID>
<ID>MagicNumber:FileUtils.kt$FileUtils$3</ID>
<ID>MagicNumber:JNIInitialiser.kt$JNIInitialiser$1024</ID>
<ID>MagicNumber:KiloByte.kt$KiloByte$1024.0</ID>
<ID>MagicNumber:Byte.kt$Byte$1024.0</ID>
<ID>MagicNumber:MainMenu.kt$MainMenu$99</ID>
<ID>MagicNumber:OnSwipeTouchListener.kt$OnSwipeTouchListener.GestureListener$100</ID>
<ID>MagicNumber:SearchResultGenerator.kt$ZimSearchResultGenerator$200</ID>
@ -42,7 +42,7 @@
<ID>NestedBlockDepth:StorageDeviceUtils.kt$StorageDeviceUtils$// Amazingly file.canWrite() does not always return the correct value private fun canWrite(file: File): Boolean</ID>
<ID>PackageNaming:ArticleCount.kt$package org.kiwix.kiwixmobile.core.zim_manager.fileselect_view</ID>
<ID>PackageNaming:BooksOnDiskListItem.kt$package org.kiwix.kiwixmobile.core.zim_manager.fileselect_view</ID>
<ID>PackageNaming:KiloByte.kt$package org.kiwix.kiwixmobile.core.zim_manager</ID>
<ID>PackageNaming:Byte.kt$package org.kiwix.kiwixmobile.core.zim_manager</ID>
<ID>PackageNaming:KiwixTag.kt$package org.kiwix.kiwixmobile.core.zim_manager</ID>
<ID>PackageNaming:Language.kt$package org.kiwix.kiwixmobile.core.zim_manager</ID>
<ID>PackageNaming:MountPointProducer.kt$package org.kiwix.kiwixmobile.core.zim_manager</ID>
@ -50,6 +50,7 @@
<ID>PackageNaming:TagsView.kt$package org.kiwix.kiwixmobile.core.zim_manager</ID>
<ID>PackageNaming:ConnectivityBroadcastReceiver.kt$package org.kiwix.kiwixmobile.core.zim_manager</ID>
<ID>PackageNaming:NetworkState.kt$package org.kiwix.kiwixmobile.core.zim_manager</ID>
<ID>PackageNaming:OnlineLibraryManager.kt$package org.kiwix.kiwixmobile.core.zim_manager</ID>
<ID>ReturnCount:FileUtils.kt$FileUtils$@JvmStatic fun getAllZimParts(book: Book): List&lt;File></ID>
<ID>ReturnCount:FileUtils.kt$FileUtils$@JvmStatic suspend fun getLocalFilePathByUri( context: Context, uri: Uri ): String?</ID>
<ID>ReturnCount:FileUtils.kt$FileUtils$@JvmStatic suspend fun hasPart(file: File): Boolean</ID>

View File

@ -18,6 +18,7 @@
package org.kiwix.kiwixmobile.core.dao
import android.util.Base64
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
@ -25,16 +26,20 @@ import androidx.room.Query
import androidx.room.Update
import com.tonyodev.fetch2.Download
import com.tonyodev.fetch2.Status.COMPLETED
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.withContext
import org.kiwix.kiwixmobile.core.dao.entities.DownloadRoomEntity
import org.kiwix.kiwixmobile.core.downloader.DownloadRequester
import org.kiwix.kiwixmobile.core.downloader.model.DownloadModel
import org.kiwix.kiwixmobile.core.downloader.model.DownloadRequest
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity
import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
import org.kiwix.kiwixmobile.core.reader.ILLUSTRATION_SIZE
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem
import org.kiwix.libzim.Archive
import javax.inject.Inject
@Dao
@ -53,15 +58,37 @@ abstract class DownloadRoomDao {
fun allDownloads() = getAllDownloads().map { it.map(::DownloadModel) }
private fun moveCompletedToBooksOnDiskDao(downloadEntities: List<DownloadRoomEntity>) {
@Suppress("InjectDispatcher")
private suspend fun moveCompletedToBooksOnDiskDao(downloadEntities: List<DownloadRoomEntity>) {
downloadEntities.filter { it.status == COMPLETED }
.takeIf(List<DownloadRoomEntity>::isNotEmpty)
?.let {
deleteDownloadsList(it)
newBookDao.insert(it.map(BooksOnDiskListItem::BookOnDisk))
?.let { completedDownloads ->
deleteDownloadsList(completedDownloads)
// We now use the OPDS stream instead of the custom library.xml handling.
// In the OPDS stream, the favicon is a URL instead of a Base64 string.
// So when a download is completed, we extract the illustration directly from the archive.
val booksOnDisk = completedDownloads.map { download ->
val archive = withContext(Dispatchers.IO) {
Archive(download.file)
}
val favicon = getOnlineBookFaviconForOfflineUsages(archive).orEmpty()
val updatedEntity = download.copy(favIcon = favicon)
BooksOnDiskListItem.BookOnDisk(updatedEntity)
}
newBookDao.insert(booksOnDisk)
}
}
private fun getOnlineBookFaviconForOfflineUsages(archive: Archive): String? =
if (archive.hasIllustration(ILLUSTRATION_SIZE)) {
Base64.encodeToString(
archive.getIllustrationItem(ILLUSTRATION_SIZE).data.data,
Base64.DEFAULT
)
} else {
null
}
fun update(download: Download) {
getEntityForDownloadId(download.id.toLong())?.let { downloadRoomEntity ->
downloadRoomEntity.updateWith(download)
@ -100,7 +127,7 @@ abstract class DownloadRoomDao {
fun addIfDoesNotExist(
url: String,
book: LibraryNetworkEntity.Book,
book: LibkiwixBook,
downloadRequester: DownloadRequester
) {
if (doesNotAlreadyExist(book)) {
@ -113,6 +140,6 @@ abstract class DownloadRoomDao {
}
}
private fun doesNotAlreadyExist(book: LibraryNetworkEntity.Book) =
private fun doesNotAlreadyExist(book: LibkiwixBook) =
count(book.id) == 0
}

View File

@ -20,7 +20,6 @@ package org.kiwix.kiwixmobile.core.dao
import android.os.Build
import android.os.Environment
import android.util.Base64
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -36,11 +35,11 @@ import org.kiwix.kiwixmobile.core.DarkModeConfig
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.isCustomApp
import org.kiwix.kiwixmobile.core.extensions.deleteFile
import org.kiwix.kiwixmobile.core.extensions.getFavicon
import org.kiwix.kiwixmobile.core.extensions.isFileExist
import org.kiwix.kiwixmobile.core.extensions.toast
import org.kiwix.kiwixmobile.core.page.adapter.Page
import org.kiwix.kiwixmobile.core.page.bookmark.adapter.LibkiwixBookmarkItem
import org.kiwix.kiwixmobile.core.reader.ILLUSTRATION_SIZE
import org.kiwix.kiwixmobile.core.reader.ZimFileReader
import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer
import org.kiwix.kiwixmobile.core.reader.ZimReaderSource
@ -205,7 +204,7 @@ class LibkiwixBookmarks @Inject constructor(
library.addBook(libKiwixBook).also {
// now library has changed so update our library list.
libraryBooksList = library.booksIds.toList()
Log.d(
Log.e(
TAG,
"Added Book to Library:\n" +
"ZIM File Path: ${book.path}\n" +
@ -234,6 +233,7 @@ class LibkiwixBookmarks @Inject constructor(
CoroutineScope(dispatcher).launch {
writeBookMarksAndSaveLibraryToFile()
updateFlowBookmarkList()
removeBookFromLibraryIfNoRelatedBookmarksAreStored(dispatcher, bookmarks)
}
}
}
@ -242,6 +242,34 @@ class LibkiwixBookmarks @Inject constructor(
deleteBookmarks(listOf(LibkiwixBookmarkItem(zimId = bookId, bookmarkUrl = bookmarkUrl)))
}
/**
* Removes books from the library that no longer have any associated bookmarks.
*
* This function checks if any of the books associated with the given deleted bookmarks
* are still referenced by other existing bookmarks. If not, those books are removed from the library.
*
* @param dispatcher The coroutine dispatcher to run the operation on (typically Dispatchers.IO).
* @param deletedBookmarks The list of bookmarks that were just deleted.
*/
private suspend fun removeBookFromLibraryIfNoRelatedBookmarksAreStored(
dispatcher: CoroutineDispatcher,
deletedBookmarks: List<LibkiwixBookmarkItem>
) {
withContext(dispatcher) {
val currentBookmarks = getBookmarksList()
val deletedZimIds = deletedBookmarks.map { it.zimId }.distinct()
deletedZimIds.forEach { zimId ->
val stillExists = currentBookmarks.any { it.zimId == zimId }
if (!stillExists) {
library.removeBookById(zimId)
Log.d(TAG, "Removed book from library since no bookmarks exist for: $zimId")
}
}
}
writeBookMarksAndSaveLibraryToFile()
}
/**
* Asynchronously writes the library and bookmarks data to their respective files in a background thread
* to prevent potential data loss and ensures that the library holds the updated ZIM file paths and favicons.
@ -285,10 +313,7 @@ class LibkiwixBookmarks @Inject constructor(
}
// Check if the book has an illustration of the specified size and encode it to Base64.
val favicon =
book?.getIllustration(ILLUSTRATION_SIZE)?.data?.let {
Base64.encodeToString(it, Base64.DEFAULT)
}
val favicon = book?.getFavicon()
val zimReaderSource = book?.path?.let { ZimReaderSource(File(it)) }
// Return the LibkiwixBookmarkItem, filtering out null results.

View File

@ -29,7 +29,7 @@ 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.LibraryNetworkEntity.Book
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
@ -124,7 +124,7 @@ class NewBookDao @Inject constructor(private val box: Box<BookOnDiskEntity>) {
}
@Suppress("UnsafeCallOnNullableType")
fun migrationInsert(books: List<Book>) {
fun migrationInsert(books: List<LibkiwixBook>) {
insert(books.map { BookOnDisk(book = it, zimReaderSource = ZimReaderSource(it.file!!)) })
}

View File

@ -22,7 +22,7 @@ import io.objectbox.annotation.Convert
import io.objectbox.annotation.Entity
import io.objectbox.annotation.Id
import io.objectbox.converter.PropertyConverter
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book
import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
import org.kiwix.kiwixmobile.core.reader.ZimReaderSource
import org.kiwix.kiwixmobile.core.reader.ZimReaderSource.Companion.fromDatabaseValue
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem.BookOnDisk
@ -70,7 +70,7 @@ data class BookOnDiskEntity(
bookOnDisk.book.tags
)
fun toBook() = Book().apply {
fun toBook() = LibkiwixBook().apply {
id = bookId
title = this@BookOnDiskEntity.title
description = this@BookOnDiskEntity.description

View File

@ -20,12 +20,12 @@ package org.kiwix.kiwixmobile.core.dao.entities
import androidx.room.Entity
import androidx.room.PrimaryKey
import io.objectbox.annotation.Convert
import io.objectbox.converter.PropertyConverter
import com.tonyodev.fetch2.Download
import com.tonyodev.fetch2.Error
import com.tonyodev.fetch2.Status
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book
import io.objectbox.annotation.Convert
import io.objectbox.converter.PropertyConverter
import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
@Entity
data class DownloadRoomEntity(
@ -56,7 +56,7 @@ data class DownloadRoomEntity(
val favIcon: String,
val tags: String? = null
) {
constructor(downloadId: Long, book: Book) : this(
constructor(downloadId: Long, book: LibkiwixBook) : this(
downloadId = downloadId,
bookId = book.id,
title = book.title,
@ -75,7 +75,7 @@ data class DownloadRoomEntity(
)
fun toBook() =
Book().apply {
LibkiwixBook().apply {
id = bookId
title = this@DownloadRoomEntity.title
description = this@DownloadRoomEntity.description

View File

@ -71,7 +71,7 @@ class Repository @Inject internal constructor(
// Split languages if there are multiple, otherwise return the single book. Bug fix #3892
if (bookOnDisk.book.language.contains(',')) {
bookOnDisk.book.language.split(',').map { lang ->
bookOnDisk.copy(book = bookOnDisk.book.copy(language = lang.trim()))
bookOnDisk.copy(book = bookOnDisk.book.copy(_language = lang.trim()))
}
} else {
listOf(bookOnDisk)

View File

@ -20,16 +20,17 @@
package org.kiwix.kiwixmobile.core.data.remote
import okhttp3.OkHttpClient
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity
import org.kiwix.kiwixmobile.core.entity.MetaLinkNetworkEntity
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.scalars.ScalarsConverterFactory
import retrofit2.converter.simplexml.SimpleXmlConverterFactory
import retrofit2.http.GET
import retrofit2.http.Url
interface KiwixService {
@GET(LIBRARY_NETWORK_PATH)
suspend fun getLibrary(): LibraryNetworkEntity?
@GET(OPDS_LIBRARY_NETWORK_PATH)
suspend fun getLibrary(): Response<String>
@GET
suspend fun getMetaLinks(
@ -43,6 +44,7 @@ interface KiwixService {
val retrofit = Retrofit.Builder()
.baseUrl(baseUrl)
.client(okHttpClient)
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(SimpleXmlConverterFactory.create())
.build()
return retrofit.create(KiwixService::class.java)
@ -50,6 +52,8 @@ interface KiwixService {
}
companion object {
const val LIBRARY_NETWORK_PATH = "/library/library_zim.xml"
// To fetch the full OPDS catalog.
// TODO we will change this to pagination later once we migrate to OPDS properly.
const val OPDS_LIBRARY_NETWORK_PATH = "v2/entries?count=-1"
}
}

View File

@ -38,7 +38,7 @@ const val CONNECTION_TIMEOUT = 10L
const val READ_TIMEOUT = 300L
const val CALL_TIMEOUT = 300L
const val USER_AGENT = "kiwix-android-version:${BuildConfig.VERSION_CODE}"
const val KIWIX_DOWNLOAD_URL = "https://mirror.download.kiwix.org/"
const val KIWIX_OPDS_LIBRARY_URL = "https://opds.library.kiwix.org/"
@Module
class NetworkModule {
@ -59,5 +59,5 @@ class NetworkModule {
}
@Provides @Singleton fun provideKiwixService(okHttpClient: OkHttpClient): KiwixService =
ServiceCreator.newHackListService(okHttpClient, KIWIX_DOWNLOAD_URL)
ServiceCreator.newHackListService(okHttpClient, KIWIX_OPDS_LIBRARY_URL)
}

View File

@ -17,10 +17,10 @@
*/
package org.kiwix.kiwixmobile.core.downloader
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity
import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
interface Downloader {
fun download(book: LibraryNetworkEntity.Book)
fun download(book: LibkiwixBook)
fun cancelDownload(downloadId: Long)
fun retryDownload(downloadId: Long)
fun pauseResumeDownload(downloadId: Long, isPause: Boolean)

View File

@ -23,8 +23,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.kiwix.kiwixmobile.core.dao.DownloadRoomDao
import org.kiwix.kiwixmobile.core.data.remote.KiwixService
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book
import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
import javax.inject.Inject
class DownloaderImpl @Inject constructor(
@ -33,7 +32,7 @@ class DownloaderImpl @Inject constructor(
private val kiwixService: KiwixService
) : Downloader {
@Suppress("InjectDispatcher")
override fun download(book: LibraryNetworkEntity.Book) {
override fun download(book: LibkiwixBook) {
CoroutineScope(Dispatchers.IO).launch {
runCatching {
urlProvider(book)?.let {
@ -46,7 +45,7 @@ class DownloaderImpl @Inject constructor(
}
@Suppress("UnsafeCallOnNullableType")
private suspend fun urlProvider(book: Book): String? =
private suspend fun urlProvider(book: LibkiwixBook): String? =
if (book.url?.endsWith("meta4") == true) {
kiwixService.getMetaLinks(book.url!!)?.relevantUrl?.value
} else {

View File

@ -37,6 +37,7 @@ import javax.inject.Inject
const val ZERO = 0
const val FIVE = 5
const val SIX = 6
const val NINE = 9
const val HUNDERED = 100
const val DEFAULT_INT_VALUE = -1

View File

@ -38,7 +38,7 @@ value class Base64String(private val encodedString: String?) {
BitmapFactory.decodeByteArray(it, 0, it.size)
}
}
} catch (illegalArgumentException: IllegalArgumentException) {
} catch (_: IllegalArgumentException) {
null
}
}

View File

@ -34,7 +34,7 @@ import org.kiwix.kiwixmobile.core.R
data class DownloadItem(
val downloadId: Long,
val favIcon: Base64String,
val favIconUrl: String,
val title: String,
val description: String?,
val bytesDownloaded: Long,
@ -47,7 +47,7 @@ data class DownloadItem(
constructor(downloadModel: DownloadModel) : this(
downloadModel.downloadId,
Base64String(downloadModel.book.favicon),
downloadModel.book.favicon,
downloadModel.book.title,
downloadModel.book.description,
downloadModel.bytesDownloaded,

View File

@ -17,10 +17,10 @@
*/
package org.kiwix.kiwixmobile.core.downloader.model
import com.tonyodev.fetch2.Status
import com.tonyodev.fetch2.Error
import com.tonyodev.fetch2.Status
import org.kiwix.kiwixmobile.core.dao.entities.DownloadRoomEntity
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book
import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
import org.kiwix.kiwixmobile.core.utils.StorageUtils
data class DownloadModel(
@ -33,7 +33,7 @@ data class DownloadModel(
val state: Status,
val error: Error,
val progress: Int,
val book: Book
val book: LibkiwixBook
) {
val bytesRemaining: Long by lazy { totalSizeOfDownload - bytesDownloaded }
val fileNameFromUrl: String by lazy { StorageUtils.getFileNameFromUrl(book.url) }

View File

@ -0,0 +1,145 @@
/*
* Kiwix Android
* Copyright (c) 2025 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.entity
import org.kiwix.kiwixmobile.core.extensions.getFavicon
import org.kiwix.libkiwix.Book
import java.io.File
/**
* Wrapper around libkiwix's [Book] that allows setting custom values (e.g. from DB or UI),
* while still falling back to the original [nativeBook]'s properties when not provided.
*/
@Suppress("ConstructorParameterNaming")
data class LibkiwixBook(
private val nativeBook: Book? = null,
private var _id: String = "",
private var _title: String = "",
private var _description: String? = null,
private var _language: String = "",
private var _creator: String = "",
private var _publisher: String = "",
private var _date: String = "",
private var _url: String? = null,
private var _articleCount: String? = null,
private var _mediaCount: String? = null,
private var _size: String = "",
private var _bookName: String? = null,
private var _favicon: String = "",
private var _tags: String? = null,
var searchMatches: Int = 0,
var file: File? = null
) {
var id: String
get() = _id.ifEmpty { nativeBook?.id.orEmpty() }
set(id) {
_id = id
}
var title: String
get() = _title.ifEmpty { nativeBook?.title.orEmpty() }
set(title) {
_title = title
}
var description: String?
get() = _description ?: nativeBook?.description
set(description) {
_description = description
}
var language: String
get() = _language.ifEmpty { nativeBook?.language.orEmpty() }
set(language) {
_language = language
}
var creator: String
get() = _creator.ifEmpty { nativeBook?.creator.orEmpty() }
set(creator) {
_creator = creator
}
var publisher: String
get() = _publisher.ifEmpty { nativeBook?.publisher.orEmpty() }
set(publisher) {
_publisher = publisher
}
var date: String
get() = _date.ifEmpty { nativeBook?.date.orEmpty() }
set(date) {
_date = date
}
var url: String?
get() = _url ?: nativeBook?.url
set(url) {
_url = url
}
var articleCount: String?
get() = _articleCount ?: nativeBook?.articleCount?.toString()
set(articleCount) {
_articleCount = articleCount
}
var mediaCount: String?
get() = _mediaCount ?: nativeBook?.mediaCount?.toString()
set(mediaCount) {
_mediaCount = mediaCount
}
var size: String
get() = _size.ifEmpty { nativeBook?.size?.toString().orEmpty() }
set(size) {
_size = size
}
var bookName: String?
get() = _bookName ?: nativeBook?.name
set(bookName) {
_bookName = bookName
}
var favicon: String
get() = _favicon.ifEmpty { nativeBook?.getFavicon().orEmpty() }
set(favicon) {
_favicon = favicon
}
var tags: String?
get() = _tags ?: nativeBook?.tags
set(tags) {
_tags = tags
}
// Two books are equal if their ids match
override fun equals(other: Any?): Boolean {
if (other is LibkiwixBook) {
if (other.id == id) {
return true
}
}
return false
}
// Only use the book's id to generate a hash code
override fun hashCode(): Int = id.hashCode()
}

View File

@ -1,134 +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.entity
import org.simpleframework.xml.Attribute
import org.simpleframework.xml.ElementList
import org.simpleframework.xml.Root
import java.io.File
import java.io.Serializable
import java.util.LinkedList
@Root(name = "library")
class LibraryNetworkEntity {
@field:ElementList(name = "book", inline = true, required = false)
var book: LinkedList<Book>? = null
@field:Attribute(name = "version", required = false)
var version: String? = null
@Root(name = "book", strict = false)
class Book : Serializable {
@field:Attribute(name = "id", required = false)
var id: String = ""
@field:Attribute(name = "title", required = false)
var title: String = ""
@field:Attribute(name = "description", required = false)
var description: String? = null
@field:Attribute(name = "language", required = false)
var language: String = ""
@field:Attribute(name = "creator", required = false)
var creator: String = ""
@field:Attribute(name = "publisher", required = false)
var publisher: String = ""
@field:Attribute(name = "favicon", required = false)
var favicon: String = ""
@field:Attribute(name = "faviconMimeType", required = false)
var faviconMimeType: String? = null
@field:Attribute(name = "date", required = false)
var date: String = ""
@field:Attribute(name = "url", required = false)
var url: String? = null
@field:Attribute(name = "articleCount", required = false)
var articleCount: String? = null
@field:Attribute(name = "mediaCount", required = false)
var mediaCount: String? = null
@field:Attribute(name = "size", required = false)
var size: String = ""
@field:Attribute(name = "name", required = false)
var bookName: String? = null
@field:Attribute(name = "tags", required = false)
var tags: String? = null
var searchMatches = 0
var file: File? = null
// Two books are equal if their ids match
override fun equals(other: Any?): Boolean {
if (other is Book) {
if (other.id == id) {
return true
}
}
return false
}
// Only use the book's id to generate a hash code
override fun hashCode(): Int = id.hashCode()
@Suppress("LongParameterList")
fun copy(
language: String = this.language,
title: String = this.title,
description: String? = this.description,
creator: String = this.creator,
publisher: String = this.publisher,
favicon: String = this.favicon,
faviconMimeType: String? = this.faviconMimeType,
date: String = this.date,
url: String? = this.url,
articleCount: String? = this.articleCount,
mediaCount: String? = this.mediaCount,
size: String = this.size,
bookName: String? = this.bookName,
tags: String? = this.tags
): Book {
return Book().apply {
this.id = this@Book.id
this.title = title
this.description = description
this.language = language
this.creator = creator
this.publisher = publisher
this.favicon = favicon
this.faviconMimeType = faviconMimeType
this.date = date
this.url = url
this.articleCount = articleCount
this.mediaCount = mediaCount
this.size = size
this.bookName = bookName
this.tags = tags
}
}
}
}

View File

@ -18,30 +18,33 @@
package org.kiwix.kiwixmobile.core.extensions
import android.util.Base64
import android.util.Log
import org.kiwix.kiwixmobile.core.CoreApp
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book
import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
import org.kiwix.kiwixmobile.core.reader.ILLUSTRATION_SIZE
import org.kiwix.kiwixmobile.core.utils.BookUtils
import org.kiwix.kiwixmobile.core.utils.NetworkUtils
import org.kiwix.libkiwix.Book
fun Book.calculateSearchMatches(
fun LibkiwixBook.calculateSearchMatches(
filter: String,
bookUtils: BookUtils
) {
val searchableText = buildSearchableText(bookUtils)
searchMatches = filter.split("\\s+")
.foldRight(
0,
{ filterWord, acc ->
if (searchableText.contains(filterWord, true)) {
acc + 1
} else {
acc
}
0
) { filterWord, acc ->
if (searchableText.contains(filterWord, true)) {
acc + 1
} else {
acc
}
)
}
}
fun Book.buildSearchableText(bookUtils: BookUtils): String =
fun LibkiwixBook.buildSearchableText(bookUtils: BookUtils): String =
StringBuilder().apply {
append(title)
append("|")
@ -54,3 +57,21 @@ fun Book.buildSearchableText(bookUtils: BookUtils): String =
append("|")
}
}.toString()
fun Book?.getFavicon(): String? =
runCatching {
val illustration = this?.getIllustration(ILLUSTRATION_SIZE)
illustration?.url()?.ifBlank {
illustration.data?.let {
Base64.encodeToString(it, Base64.DEFAULT)
}
}
}.getOrElse {
it.printStackTrace().also {
this?.illustrations?.forEach { illustration ->
Log.e("BOOK", "getFavicon: ${illustration.data} and ${illustration.url()}")
}
Log.e("BOOK", "getFavicon: ${this?.title}")
}
""
}

View File

@ -24,20 +24,6 @@ import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat
import androidx.core.widget.ImageViewCompat
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.downloader.model.Base64String
fun ImageView.setBitmap(base64String: Base64String) {
base64String.toBitmap()
?.let(::setImageBitmap)
?: kotlin.run { setImageDrawableCompat(R.drawable.default_zim_file_icon) }
}
// methods that accept inline classes as parameters are not allowed to be called from java
// hence this facade
fun ImageView.setBitmapFromString(string: String?) {
setBitmap(Base64String(string))
}
fun ImageView.setImageDrawableCompat(
@DrawableRes id: Int

View File

@ -340,6 +340,7 @@ abstract class CoreReaderFragment :
private val navigationHistoryList: MutableList<NavigationHistoryListItem> = ArrayList()
private var isReadSelection = false
private var isReadAloudServiceRunning = false
private var libkiwixBook: Book? = null
private var readerLifeCycleScope: CoroutineScope? = null
val coreReaderLifeCycleScope: CoroutineScope?
@ -2038,9 +2039,7 @@ abstract class CoreReaderFragment :
lifecycleScope.launch {
getCurrentWebView()?.url?.let { articleUrl ->
zimReaderContainer?.zimFileReader?.let { zimFileReader ->
val libKiwixBook = Book().apply {
update(zimFileReader.jniKiwixReader)
}
val libKiwixBook = getLibkiwixBook(zimFileReader)
if (isBookmarked) {
repositoryActions?.deleteBookmark(libKiwixBook.id, articleUrl)
snackBarRoot?.snack(R.string.bookmark_removed)
@ -2071,6 +2070,19 @@ abstract class CoreReaderFragment :
}
}
/**
* Returns the libkiwix book evertime when user saves or remove the bookmark.
* the object will be created once to avoid creating it multiple times.
*/
private fun getLibkiwixBook(zimFileReader: ZimFileReader): Book {
libkiwixBook?.let { return it }
val book = Book().apply {
update(zimFileReader.jniKiwixReader)
}
libkiwixBook = book
return book
}
override fun onResume() {
super.onResume()
updateBottomToolbarVisibility()

View File

@ -28,7 +28,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.kiwix.kiwixmobile.core.CoreApp
import org.kiwix.kiwixmobile.core.DarkModeConfig
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book
import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
import org.kiwix.kiwixmobile.core.main.UNINITIALISER_ADDRESS
import org.kiwix.kiwixmobile.core.main.UNINITIALISE_HTML
import org.kiwix.kiwixmobile.core.reader.ZimFileReader.Companion.CONTENT_PREFIX
@ -377,7 +377,7 @@ class ZimFileReader constructor(
@Suppress("ExplicitThis") // this@ZimFileReader.name is required
fun toBook() =
Book().apply {
LibkiwixBook().apply {
title = this@ZimFileReader.title
id = this@ZimFileReader.id
size = "$fileSize"

View File

@ -42,7 +42,7 @@ import kotlinx.coroutines.sync.withLock
import org.kiwix.kiwixmobile.core.CoreApp
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.downloader.ChunkUtils
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book
import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
import org.kiwix.kiwixmobile.core.extensions.deleteFile
import org.kiwix.kiwixmobile.core.extensions.isFileExist
import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer
@ -494,7 +494,7 @@ object FileUtils {
@Suppress("NestedBlockDepth")
@JvmStatic
suspend fun getAllZimParts(book: Book): List<File> {
suspend fun getAllZimParts(book: LibkiwixBook): List<File> {
val files = ArrayList<File>()
book.file?.let {
if (it.path.endsWith(".zim") || it.path.endsWith(".zim.part")) {

View File

@ -23,10 +23,10 @@ import kotlin.math.log10
import kotlin.math.pow
@JvmInline
value class KiloByte(private val kilobyteString: String?) {
value class Byte(private val byteString: String?) {
val humanReadable
get() = kilobyteString?.toLongOrNull()?.let {
val units = arrayOf("KB", "MB", "GB", "TB")
get() = byteString?.toLongOrNull()?.let {
val units = arrayOf("B", "KB", "MB", "GB", "TB")
val conversion = (log10(it.toDouble()) / log10(1024.0)).toInt()
DecimalFormat("#,##0.#")
.format(it / 1024.0.pow(conversion.toDouble())) +

View File

@ -0,0 +1,43 @@
/*
* Kiwix Android
* Copyright (c) 2025 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.zim_manager
import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
import org.kiwix.libkiwix.Library
import org.kiwix.libkiwix.Manager
class OnlineLibraryManager(val library: Library, val manager: Manager) {
suspend fun parseOPDSStream(content: String?, urlHost: String): Boolean =
runCatching {
manager.readOpds(content, urlHost)
}.onFailure {
it.printStackTrace()
}.isSuccess
suspend fun getOnlineBooks(): List<LibkiwixBook> {
val onlineBooksList = arrayListOf<LibkiwixBook>()
runCatching {
library.booksIds.forEach { bookId ->
val book = library.getBookById(bookId)
onlineBooksList.add(LibkiwixBook(book))
}
}.onFailure { it.printStackTrace() }
return onlineBooksList
}
}

View File

@ -21,7 +21,7 @@ package org.kiwix.kiwixmobile.core.zim_manager.fileselect_view
import org.kiwix.kiwixmobile.core.compat.CompatHelper.Companion.convertToLocal
import org.kiwix.kiwixmobile.core.dao.entities.BookOnDiskEntity
import org.kiwix.kiwixmobile.core.dao.entities.DownloadRoomEntity
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity
import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
import org.kiwix.kiwixmobile.core.reader.ZimFileReader
import org.kiwix.kiwixmobile.core.reader.ZimReaderSource
import org.kiwix.kiwixmobile.core.zim_manager.KiwixTag
@ -44,7 +44,7 @@ sealed class BooksOnDiskListItem {
data class BookOnDisk constructor(
val databaseId: Long = 0L,
val book: LibraryNetworkEntity.Book,
val book: LibkiwixBook,
val file: File = File(""),
val zimReaderSource: ZimReaderSource,
val tags: List<KiwixTag> = KiwixTag.Companion.from(book.tags),

View File

@ -22,14 +22,12 @@ import com.tonyodev.fetch2.Status
import com.tonyodev.fetch2.Status.NONE
import org.kiwix.kiwixmobile.core.dao.entities.BookOnDiskEntity
import org.kiwix.kiwixmobile.core.dao.entities.RecentSearchEntity
import org.kiwix.kiwixmobile.core.downloader.model.Base64String
import org.kiwix.kiwixmobile.core.downloader.model.DownloadItem
import org.kiwix.kiwixmobile.core.downloader.model.DownloadModel
import org.kiwix.kiwixmobile.core.downloader.model.DownloadState
import org.kiwix.kiwixmobile.core.downloader.model.DownloadState.Pending
import org.kiwix.kiwixmobile.core.downloader.model.Seconds
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book
import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
import org.kiwix.kiwixmobile.core.entity.MetaLinkNetworkEntity
import org.kiwix.kiwixmobile.core.entity.MetaLinkNetworkEntity.FileElement
import org.kiwix.kiwixmobile.core.entity.MetaLinkNetworkEntity.Pieces
@ -38,11 +36,10 @@ import org.kiwix.kiwixmobile.core.reader.ZimReaderSource
import org.kiwix.kiwixmobile.core.zim_manager.Language
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem.BookOnDisk
import java.io.File
import java.util.LinkedList
fun bookOnDisk(
databaseId: Long = 0L,
book: Book = book(),
book: LibkiwixBook = libkiwixBook(),
zimReaderSource: ZimReaderSource = ZimReaderSource(File(""))
) = BookOnDisk(databaseId, book, File(""), zimReaderSource)
@ -56,7 +53,7 @@ fun downloadModel(
status: Status = NONE,
error: Error = Error.NONE,
progress: Int = 1,
book: Book = book()
book: LibkiwixBook = libkiwixBook()
) = DownloadModel(
databaseId, downloadId, file, etaInMilliSeconds, bytesDownloaded, totalSizeOfDownload,
status, error, progress, book
@ -64,7 +61,7 @@ fun downloadModel(
fun downloadItem(
downloadId: Long = 1L,
favIcon: Base64String = Base64String("favIcon"),
favIcon: String = "favIcon",
title: String = "title",
description: String = "description",
bytesDownloaded: Long = 1L,
@ -135,7 +132,7 @@ fun url(
this.value = value
}
fun book(
fun libkiwixBook(
id: String = "id",
title: String = "title",
description: String = "description",
@ -150,7 +147,7 @@ fun book(
name: String = "name",
favIcon: String = "favIcon",
file: File = File("")
) = Book().apply {
) = LibkiwixBook().apply {
this.id = id
this.title = title
this.description = description
@ -167,11 +164,6 @@ fun book(
favicon = favIcon
}
fun libraryNetworkEntity(books: List<Book> = emptyList()) =
LibraryNetworkEntity().apply {
book = LinkedList(books)
}
fun recentSearchEntity(
id: Long = 0L,
searchTerm: String = "",

View File

@ -40,7 +40,7 @@ import org.kiwix.kiwixmobile.core.utils.files.FileSearch
import org.kiwix.kiwixmobile.core.utils.files.ScanningProgressListener
import org.kiwix.kiwixmobile.core.utils.files.testFlow
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem.BookOnDisk
import org.kiwix.sharedFunctions.book
import org.kiwix.sharedFunctions.libkiwixBook
import org.kiwix.sharedFunctions.bookOnDisk
import java.io.File
@ -85,7 +85,7 @@ class StorageObserverTest {
@Test
fun `zim files are read by the file reader`() = runTest {
val expectedBook =
book(
libkiwixBook(
"id", "title", "1", "favicon", "creator", "publisher", "date",
"description", "language"
)

View File

@ -40,13 +40,13 @@ 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.LibraryNetworkEntity
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.book
import org.kiwix.sharedFunctions.bookOnDisk
import org.kiwix.sharedFunctions.bookOnDiskEntity
import org.kiwix.sharedFunctions.libkiwixBook
import java.io.File
import java.util.concurrent.Callable
@ -131,9 +131,9 @@ internal class NewBookDaoTest {
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 = book(id = "same"))
val distinctBook: BookOnDisk = bookOnDisk(databaseId = 0, book = libkiwixBook(id = "same"))
newBookDao.insert(
listOf(distinctBook, bookOnDisk(databaseId = 1, book = book(id = "same")))
listOf(distinctBook, bookOnDisk(databaseId = 1, book = libkiwixBook(id = "same")))
)
val queryBuilder: QueryBuilder<BookOnDiskEntity> = mockk(relaxed = true)
every { box.query() } returns queryBuilder
@ -216,7 +216,7 @@ internal class NewBookDaoTest {
@Test
fun migrationInsert() {
val book: LibraryNetworkEntity.Book = book()
val book: LibkiwixBook = libkiwixBook()
val slot: CapturingSlot<Callable<Unit>> = slot()
every { box.store.callInTx(capture(slot)) } returns Unit
newBookDao.migrationInsert(listOf(book))

View File

@ -22,16 +22,16 @@ import io.mockk.clearMocks
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.kiwix.kiwixmobile.core.CoreApp
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book
import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
import java.io.File
class FileUtilsTest {
private val mockFile: File = mockk()
private val testBook = Book().apply { file = mockFile }
private val testBook = LibkiwixBook().apply { file = mockFile }
private val testId = "8ce5775a-10a9-bbf3-178a-9df69f23263c"
private val fileName = "/data/user/0/org.kiwix.kiwixmobile/files${File.separator}$testId"

View File

@ -21,7 +21,7 @@ package org.kiwix.kiwixmobile.custom.download.effects
import androidx.appcompat.app.AppCompatActivity
import org.kiwix.kiwixmobile.core.base.SideEffect
import org.kiwix.kiwixmobile.core.downloader.Downloader
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book
import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
import org.kiwix.kiwixmobile.custom.BuildConfig
import javax.inject.Inject
@ -45,7 +45,7 @@ data class DownloadCustom @Inject constructor(val downloader: Downloader) : Side
name: String = "",
favIcon: String = ""
) =
Book().apply {
LibkiwixBook().apply {
this.id = id
this.title = title
this.description = description

View File

@ -22,7 +22,7 @@ import io.mockk.coVerify
import io.mockk.mockk
import org.junit.jupiter.api.Test
import org.kiwix.kiwixmobile.core.downloader.Downloader
import org.kiwix.sharedFunctions.book
import org.kiwix.sharedFunctions.libkiwixBook
internal class DownloadCustomTest {
@Test
@ -35,7 +35,7 @@ internal class DownloadCustomTest {
}
private fun expectedBook() =
book(
libkiwixBook(
"custom",
"",
"",