From 7a10528d1979bb3f3736a92d2a9fd8a99d903e6b Mon Sep 17 00:00:00 2001 From: MohitMaliFtechiz Date: Wed, 4 Jun 2025 22:16:09 +0530 Subject: [PATCH] Added the `com.squareup.retrofit2:converter-scalars` converter to `Retrofit` to parse `OPDS` network responses. * Created `LibkiwixBook` to wrap the `Book` class from `libkiwix`, allowing us to set custom values from the database and manage books received from the `OPDS` stream. This wrapper is necessary because the original Book class does not provide setters. We'll use `LibkiwixBook` throughout the codebase. * Refactored the codebase to replace `LibraryNetworkEntity.Book` with `LibkiwixBook`. --- .../zimManager/ZimManageViewModel.kt | 39 +++-- .../libraryView/AvailableSpaceCalculator.kt | 4 +- .../libraryView/adapter/LibraryListItem.kt | 8 +- buildSrc/src/main/kotlin/Libs.kt | 6 + core/build.gradle.kts | 5 + core/detekt_baseline.xml | 1 + .../kiwixmobile/core/dao/DownloadRoomDao.kt | 6 +- .../kiwixmobile/core/dao/LibkiwixBookmarks.kt | 8 +- .../kiwix/kiwixmobile/core/dao/NewBookDao.kt | 4 +- .../core/dao/entities/BookOnDiskEntity.kt | 4 +- .../core/dao/entities/DownloadRoomEntity.kt | 10 +- .../kiwix/kiwixmobile/core/data/Repository.kt | 2 +- .../core/data/remote/KiwixService.kt | 5 +- .../core/di/components/CoreComponent.kt | 2 + .../kiwixmobile/core/di/modules/JNIModule.kt | 23 +-- .../core/di/modules/NetworkModule.kt | 2 +- .../kiwixmobile/core/downloader/Downloader.kt | 4 +- .../core/downloader/DownloaderImpl.kt | 7 +- .../core/downloader/model/DownloadModel.kt | 6 +- .../kiwixmobile/core/entity/LibkiwixBook.kt | 145 ++++++++++++++++++ .../core/extensions/BookExtensions.kt | 28 ++-- .../kiwixmobile/core/reader/ZimFileReader.kt | 4 +- .../core/zim_manager/OnlineLibraryManager.kt | 11 +- .../fileselect_view/BooksOnDiskListItem.kt | 4 +- .../custom/download/effects/DownloadCustom.kt | 4 +- 25 files changed, 249 insertions(+), 93 deletions(-) create mode 100644 core/src/main/java/org/kiwix/kiwixmobile/core/entity/LibkiwixBook.kt diff --git a/app/src/main/java/org/kiwix/kiwixmobile/zimManager/ZimManageViewModel.kt b/app/src/main/java/org/kiwix/kiwixmobile/zimManager/ZimManageViewModel.kt index e6e6691a8..ce6df49de 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/zimManager/ZimManageViewModel.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/zimManager/ZimManageViewModel.kt @@ -82,7 +82,7 @@ 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.LibkiwixBook import org.kiwix.kiwixmobile.core.extensions.calculateSearchMatches import org.kiwix.kiwixmobile.core.extensions.registerReceiver import org.kiwix.kiwixmobile.core.ui.components.ONE @@ -131,6 +131,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, @@ -168,7 +169,7 @@ class ZimManageViewModel @Inject constructor( val onlineLibraryDownloading = MutableStateFlow(false) val shouldShowWifiOnlyDialog = MutableLiveData() val networkStates = MutableLiveData() - val networkLibrary = MutableSharedFlow>(replay = 0) + val networkLibrary = MutableSharedFlow>(replay = 0) val requestFileSystemCheck = MutableSharedFlow(replay = 0) val fileSelectActions = MutableSharedFlow() private val requestDownloadLibrary = MutableSharedFlow( @@ -391,7 +392,7 @@ class ZimManageViewModel @Inject constructor( @OptIn(ExperimentalCoroutinesApi::class) private fun requestsAndConnectivityChangesToLibraryRequests( - library: MutableSharedFlow>, + library: MutableSharedFlow>, dispatcher: CoroutineDispatcher = Dispatchers.IO ) = requestDownloadLibrary.flatMapConcat { connectivityBroadcastReceiver.networkStates @@ -442,12 +443,16 @@ class ZimManageViewModel @Inject constructor( private fun downloadLibraryFlow( kiwixService: KiwixService - ): Flow> = flow { + ): Flow> = flow { downloadProgress.postValue(context.getString(R.string.starting_downloading_remote_library)) val response = kiwixService.getLibrary() downloadProgress.postValue(context.getString(R.string.parsing_remote_library)) - onlineLibraryManager.parseOPDSStream(response, KIWIX_OPDS_LIBRARY_URL) - emit(onlineLibraryManager.getOnlineBooks()) + val isLibraryParsed = onlineLibraryManager.parseOPDSStream(response, KIWIX_OPDS_LIBRARY_URL) + if (isLibraryParsed) { + emit(onlineLibraryManager.getOnlineBooks()) + } else { + emit(emptyList()) + } } .retry(5) .catch { e -> @@ -464,7 +469,7 @@ class ZimManageViewModel @Inject constructor( private fun updateLibraryItems( booksFromDao: Flow>, downloads: Flow>, - library: MutableSharedFlow>, + library: MutableSharedFlow>, languages: Flow>, dispatcher: CoroutineDispatcher = Dispatchers.IO ) = viewModelScope.launch(dispatcher) { @@ -487,14 +492,14 @@ class ZimManageViewModel @Inject constructor( val books = args[ZERO] as List val activeDownloads = args[ONE] as List val languageList = args[TWO] as List - val libraryNetworkEntity = args[THREE] as List + val libraryNetworkEntity = args[THREE] as List 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 ) @@ -509,7 +514,7 @@ class ZimManageViewModel @Inject constructor( } private fun updateLanguagesInDao( - library: MutableSharedFlow>, + library: MutableSharedFlow>, languages: Flow>, dispatcher: CoroutineDispatcher = Dispatchers.IO ) = @@ -527,7 +532,7 @@ class ZimManageViewModel @Inject constructor( .launchIn(viewModelScope) private fun combineToLanguageList( - booksFromNetwork: List, + booksFromNetwork: List, allLanguages: List ) = when { booksFromNetwork.isEmpty() -> { @@ -551,8 +556,8 @@ class ZimManageViewModel @Inject constructor( ) } - private fun networkLanguageCounts(booksFromNetwork: List) = - booksFromNetwork.mapNotNull { it.language } + private fun networkLanguageCounts(booksFromNetwork: List) = + booksFromNetwork.map { it.language } .fold( mutableMapOf() ) { acc, language -> acc.increment(language) } @@ -589,7 +594,7 @@ class ZimManageViewModel @Inject constructor( booksOnFileSystem: List, activeDownloads: List, allLanguages: List, - onlineBooks: List, + onlineBooks: List, filter: String, fileSystemState: FileSystemState ): List { @@ -634,7 +639,7 @@ class ZimManageViewModel @Inject constructor( } private fun createLibrarySection( - books: List, + books: List, activeDownloads: List, fileSystemState: FileSystemState, sectionStringId: Int, @@ -648,7 +653,7 @@ class ZimManageViewModel @Inject constructor( } private fun applySearchFilter( - unDownloadedBooks: List, + unDownloadedBooks: List, filter: String ) = if (filter.isEmpty()) { unDownloadedBooks @@ -657,7 +662,7 @@ class ZimManageViewModel @Inject constructor( unDownloadedBooks.filter { it.searchMatches > 0 } } - private fun List.asLibraryItems( + private fun List.asLibraryItems( activeDownloads: List, fileSystemState: FileSystemState ) = map { book -> diff --git a/app/src/main/java/org/kiwix/kiwixmobile/zimManager/libraryView/AvailableSpaceCalculator.kt b/app/src/main/java/org/kiwix/kiwixmobile/zimManager/libraryView/AvailableSpaceCalculator.kt index 8bb12ced5..7b1193251 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/zimManager/libraryView/AvailableSpaceCalculator.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/zimManager/libraryView/AvailableSpaceCalculator.kt @@ -24,7 +24,7 @@ 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 @@ -49,6 +49,6 @@ class AvailableSpaceCalculator @Inject constructor( } } - suspend fun hasAvailableSpaceForBook(book: Book) = + suspend fun hasAvailableSpaceForBook(book: LibkiwixBook) = book.size.toLong() * KB < storageCalculator.availableBytes() } diff --git a/app/src/main/java/org/kiwix/kiwixmobile/zimManager/libraryView/adapter/LibraryListItem.kt b/app/src/main/java/org/kiwix/kiwixmobile/zimManager/libraryView/adapter/LibraryListItem.kt index f7cfb79c0..95b813789 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/zimManager/libraryView/adapter/LibraryListItem.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/zimManager/libraryView/adapter/LibraryListItem.kt @@ -24,14 +24,14 @@ 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 +42,7 @@ sealed class LibraryListItem { ) : LibraryListItem() data class BookItem constructor( - val book: Book, + val book: LibkiwixBook, val fileSystemState: FileSystemState, val tags: List = KiwixTag.from(book.tags), override val id: Long = book.id.hashCode().toLong() @@ -54,7 +54,7 @@ sealed class LibraryListItem { } companion object { - private fun Book.isLessThan4GB() = + private fun LibkiwixBook.isLessThan4GB() = size.toLongOrNull() ?: 0L < Fat32Checker.FOUR_GIGABYTES_IN_KILOBYTES } } diff --git a/buildSrc/src/main/kotlin/Libs.kt b/buildSrc/src/main/kotlin/Libs.kt index 49c9d1c43..ce38398e7 100644 --- a/buildSrc/src/main/kotlin/Libs.kt +++ b/buildSrc/src/main/kotlin/Libs.kt @@ -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 */ diff --git a/core/build.gradle.kts b/core/build.gradle.kts index b952e9d7c..0299c67dc 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -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") diff --git a/core/detekt_baseline.xml b/core/detekt_baseline.xml index 426a822c2..876ea7fd3 100644 --- a/core/detekt_baseline.xml +++ b/core/detekt_baseline.xml @@ -50,6 +50,7 @@ PackageNaming:TagsView.kt$package org.kiwix.kiwixmobile.core.zim_manager PackageNaming:ConnectivityBroadcastReceiver.kt$package org.kiwix.kiwixmobile.core.zim_manager PackageNaming:NetworkState.kt$package org.kiwix.kiwixmobile.core.zim_manager + PackageNaming:OnlineLibraryManager.kt$package org.kiwix.kiwixmobile.core.zim_manager ReturnCount:FileUtils.kt$FileUtils$@JvmStatic fun getAllZimParts(book: Book): List<File> ReturnCount:FileUtils.kt$FileUtils$@JvmStatic suspend fun getLocalFilePathByUri( context: Context, uri: Uri ): String? ReturnCount:FileUtils.kt$FileUtils$@JvmStatic suspend fun hasPart(file: File): Boolean diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/DownloadRoomDao.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/DownloadRoomDao.kt index ed3b857f2..e1d84a3e9 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/DownloadRoomDao.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/DownloadRoomDao.kt @@ -33,7 +33,7 @@ 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.zim_manager.fileselect_view.BooksOnDiskListItem import javax.inject.Inject @@ -100,7 +100,7 @@ abstract class DownloadRoomDao { fun addIfDoesNotExist( url: String, - book: LibraryNetworkEntity.Book, + book: LibkiwixBook, downloadRequester: DownloadRequester ) { if (doesNotAlreadyExist(book)) { @@ -113,6 +113,6 @@ abstract class DownloadRoomDao { } } - private fun doesNotAlreadyExist(book: LibraryNetworkEntity.Book) = + private fun doesNotAlreadyExist(book: LibkiwixBook) = count(book.id) == 0 } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/LibkiwixBookmarks.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/LibkiwixBookmarks.kt index e5e674e00..991c7ee7b 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/LibkiwixBookmarks.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/LibkiwixBookmarks.kt @@ -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 @@ -285,10 +284,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. diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/NewBookDao.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/NewBookDao.kt index 25262825e..102750ac0 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/NewBookDao.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/NewBookDao.kt @@ -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) { } @Suppress("UnsafeCallOnNullableType") - fun migrationInsert(books: List) { + fun migrationInsert(books: List) { insert(books.map { BookOnDisk(book = it, zimReaderSource = ZimReaderSource(it.file!!)) }) } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/entities/BookOnDiskEntity.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/entities/BookOnDiskEntity.kt index 95a1366f5..b2cd22680 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/entities/BookOnDiskEntity.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/entities/BookOnDiskEntity.kt @@ -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 diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/entities/DownloadRoomEntity.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/entities/DownloadRoomEntity.kt index b899ee6cb..8cd4fed5f 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/entities/DownloadRoomEntity.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/entities/DownloadRoomEntity.kt @@ -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 diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/data/Repository.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/data/Repository.kt index 5517a601a..e73d420f2 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/data/Repository.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/data/Repository.kt @@ -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) diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/data/remote/KiwixService.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/data/remote/KiwixService.kt index 325415469..5f549dceb 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/data/remote/KiwixService.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/data/remote/KiwixService.kt @@ -22,6 +22,7 @@ package org.kiwix.kiwixmobile.core.data.remote import okhttp3.OkHttpClient import org.kiwix.kiwixmobile.core.entity.MetaLinkNetworkEntity import retrofit2.Retrofit +import retrofit2.converter.scalars.ScalarsConverterFactory import retrofit2.http.GET import retrofit2.http.Url @@ -41,6 +42,8 @@ interface KiwixService { val retrofit = Retrofit.Builder() .baseUrl(baseUrl) .client(okHttpClient) + .addConverterFactory(ScalarsConverterFactory.create()) + // .addConverterFactory(SimpleXmlConverterFactory.create()) .build() return retrofit.create(KiwixService::class.java) } @@ -49,6 +52,6 @@ interface KiwixService { companion object { // 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 = "/entries?count=-1" + const val OPDS_LIBRARY_NETWORK_PATH = "entries?count=-1" } } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/di/components/CoreComponent.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/di/components/CoreComponent.kt index cb941d13e..160b7b7bc 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/di/components/CoreComponent.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/di/components/CoreComponent.kt @@ -58,6 +58,7 @@ import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer import org.kiwix.kiwixmobile.core.search.viewmodel.SearchResultGenerator import org.kiwix.kiwixmobile.core.utils.BookUtils import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil +import org.kiwix.kiwixmobile.core.zim_manager.OnlineLibraryManager import javax.inject.Singleton @Singleton @@ -99,6 +100,7 @@ interface CoreComponent { fun connectivityManager(): ConnectivityManager fun objectBoxToLibkiwixMigrator(): ObjectBoxToLibkiwixMigrator fun libkiwixBookmarks(): LibkiwixBookmarks + fun onlineLibraryManager(): OnlineLibraryManager fun recentSearchRoomDao(): RecentSearchRoomDao fun historyRoomDao(): HistoryRoomDao fun webViewHistoryRoomDao(): WebViewHistoryRoomDao diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/di/modules/JNIModule.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/di/modules/JNIModule.kt index 3d5dc34f5..bc8ff80e6 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/di/modules/JNIModule.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/di/modules/JNIModule.kt @@ -28,7 +28,6 @@ import org.kiwix.kiwixmobile.core.zim_manager.OnlineLibraryManager import org.kiwix.libkiwix.JNIKiwix import org.kiwix.libkiwix.Library import org.kiwix.libkiwix.Manager -import javax.inject.Named import javax.inject.Singleton @Module @@ -38,39 +37,27 @@ class JNIModule { @Provides @Singleton - @Named("bookmarks") fun provideLibrary(): Library = Library() @Provides @Singleton - @Named("bookmarks") fun providesManager(library: Library): Manager = Manager(library) @Provides @Singleton fun providesLibkiwixBookmarks( - @Named("bookmarks") library: Library, - @Named("bookmarks") manager: Manager, + library: Library, + manager: Manager, sharedPreferenceUtil: SharedPreferenceUtil, bookDao: NewBookDao, zimReaderContainer: ZimReaderContainer ): LibkiwixBookmarks = LibkiwixBookmarks(library, manager, sharedPreferenceUtil, bookDao, zimReaderContainer) - @Provides - @Singleton - @Named("onlineLibrary") - fun provideOnlineLibrary(): Library = Library() - - @Provides - @Singleton - @Named("onlineLibrary") - fun providesOnlineManager(library: Library): Manager = Manager(library) - @Provides @Singleton fun provideOnlineLibraryParser( - @Named("onlineLibrary") library: Library, - @Named("onlineLibrary") manager: Manager - ) = OnlineLibraryManager(library, manager) + library: Library, + manager: Manager + ): OnlineLibraryManager = OnlineLibraryManager(library, manager) } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/di/modules/NetworkModule.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/di/modules/NetworkModule.kt index e9e4b96c8..18e6d450a 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/di/modules/NetworkModule.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/di/modules/NetworkModule.kt @@ -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_OPDS_LIBRARY_URL = "https://opds.library.kiwix.org/v2" +const val KIWIX_OPDS_LIBRARY_URL = "https://opds.library.kiwix.org/v2/" @Module class NetworkModule { diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/Downloader.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/Downloader.kt index 06aa2eca9..24e0a1f62 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/Downloader.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/Downloader.kt @@ -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) diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/DownloaderImpl.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/DownloaderImpl.kt index 28cfc54ca..bf0a02e34 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/DownloaderImpl.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/DownloaderImpl.kt @@ -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 { diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/model/DownloadModel.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/model/DownloadModel.kt index 66d81e30b..9184a69e2 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/model/DownloadModel.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/model/DownloadModel.kt @@ -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) } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/entity/LibkiwixBook.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/entity/LibkiwixBook.kt new file mode 100644 index 000000000..bb77e6505 --- /dev/null +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/entity/LibkiwixBook.kt @@ -0,0 +1,145 @@ +/* + * Kiwix Android + * Copyright (c) 2025 Kiwix + * 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 . + * + */ + +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() +} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/extensions/BookExtensions.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/extensions/BookExtensions.kt index b023c1cb4..7b9b82e09 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/extensions/BookExtensions.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/extensions/BookExtensions.kt @@ -18,30 +18,32 @@ package org.kiwix.kiwixmobile.core.extensions +import android.util.Base64 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 +56,7 @@ fun Book.buildSearchableText(bookUtils: BookUtils): String = append("|") } }.toString() + +fun Book.getFavicon(): String? = getIllustration(ILLUSTRATION_SIZE)?.data?.let { + Base64.encodeToString(it, Base64.DEFAULT) +} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/reader/ZimFileReader.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/reader/ZimFileReader.kt index c29d1dded..35e462792 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/reader/ZimFileReader.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/reader/ZimFileReader.kt @@ -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" diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/zim_manager/OnlineLibraryManager.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/zim_manager/OnlineLibraryManager.kt index c464045a8..8bab1b8c0 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/zim_manager/OnlineLibraryManager.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/zim_manager/OnlineLibraryManager.kt @@ -18,11 +18,12 @@ package org.kiwix.kiwixmobile.core.zim_manager -import org.kiwix.libkiwix.Book +import org.kiwix.kiwixmobile.core.entity.LibkiwixBook import org.kiwix.libkiwix.Library import org.kiwix.libkiwix.Manager +import javax.inject.Inject -class OnlineLibraryManager( +class OnlineLibraryManager @Inject constructor( val library: Library, val manager: Manager ) { @@ -33,12 +34,12 @@ class OnlineLibraryManager( it.printStackTrace() }.isSuccess - suspend fun getOnlineBooks(): List { - val onlineBooksList = arrayListOf() + suspend fun getOnlineBooks(): List { + val onlineBooksList = arrayListOf() runCatching { library.booksIds.forEach { bookId -> val book = library.getBookById(bookId) - onlineBooksList.add(book) + onlineBooksList.add(LibkiwixBook(book)) } }.onFailure { it.printStackTrace() } return onlineBooksList diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/zim_manager/fileselect_view/BooksOnDiskListItem.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/zim_manager/fileselect_view/BooksOnDiskListItem.kt index 0ba5e8757..c5d9bab1c 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/zim_manager/fileselect_view/BooksOnDiskListItem.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/zim_manager/fileselect_view/BooksOnDiskListItem.kt @@ -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.Companion.from(book.tags), diff --git a/custom/src/main/java/org/kiwix/kiwixmobile/custom/download/effects/DownloadCustom.kt b/custom/src/main/java/org/kiwix/kiwixmobile/custom/download/effects/DownloadCustom.kt index 32f62966d..cdb3d54b6 100644 --- a/custom/src/main/java/org/kiwix/kiwixmobile/custom/download/effects/DownloadCustom.kt +++ b/custom/src/main/java/org/kiwix/kiwixmobile/custom/download/effects/DownloadCustom.kt @@ -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