From 20722fe15b74bfaaa66f878f5ef66a9bab1e7625 Mon Sep 17 00:00:00 2001 From: MohitMaliFtechiz Date: Tue, 3 Jun 2025 18:42:51 +0530 Subject: [PATCH] Introduced support of OPDS catalog. * Created the OnlineLibraryManager class to manage the OPDS stream with libkiwix. * Refactored the ZimManageViewModel, and KiwixService code according to OPDS catalog. --- app/detekt_baseline.xml | 2 +- .../zimManager/ZimManageViewModel.kt | 46 ++++++++++--------- .../core/data/remote/KiwixService.kt | 11 ++--- .../kiwixmobile/core/di/modules/JNIModule.kt | 25 +++++++++- .../core/di/modules/NetworkModule.kt | 4 +- .../core/zim_manager/OnlineLibraryManager.kt | 46 +++++++++++++++++++ 6 files changed, 102 insertions(+), 32 deletions(-) create mode 100644 core/src/main/java/org/kiwix/kiwixmobile/core/zim_manager/OnlineLibraryManager.kt diff --git a/app/detekt_baseline.xml b/app/detekt_baseline.xml index afafc1a17..ce19bdb98 100644 --- a/app/detekt_baseline.xml +++ b/app/detekt_baseline.xml @@ -5,7 +5,7 @@ EmptyFunctionBlock:None.kt$None${ } EmptyFunctionBlock:SimplePageChangeListener.kt$SimplePageChangeListener${ } LongParameterList:ZimManageViewModel.kt$ZimManageViewModel$( booksOnFileSystem: List<BookOnDisk>, activeDownloads: List<DownloadModel>, allLanguages: List<Language>, libraryNetworkEntity: LibraryNetworkEntity, filter: String, fileSystemState: FileSystemState ) - LongParameterList:ZimManageViewModel.kt$ZimManageViewModel$( private val downloadDao: DownloadRoomDao, private val bookDao: NewBookDao, private val languageDao: NewLanguagesDao, private val storageObserver: StorageObserver, private var kiwixService: KiwixService, val context: Application, private val connectivityBroadcastReceiver: ConnectivityBroadcastReceiver, private val bookUtils: BookUtils, private val fat32Checker: Fat32Checker, private val defaultLanguageProvider: DefaultLanguageProvider, private val dataSource: DataSource, private val connectivityManager: ConnectivityManager, private val sharedPreferenceUtil: SharedPreferenceUtil ) + LongParameterList:ZimManageViewModel.kt$ZimManageViewModel$( private val downloadDao: DownloadRoomDao, private val bookDao: NewBookDao, private val languageDao: NewLanguagesDao, private val storageObserver: StorageObserver, private var kiwixService: KiwixService, val context: Application, private val connectivityBroadcastReceiver: ConnectivityBroadcastReceiver, private val bookUtils: BookUtils, private val fat32Checker: Fat32Checker, private val defaultLanguageProvider: DefaultLanguageProvider, private val dataSource: DataSource, private val connectivityManager: ConnectivityManager, private val sharedPreferenceUtil: SharedPreferenceUtil, private val onlineLibraryParser: OnlineLibraryParser ) MagicNumber:LibraryListItem.kt$LibraryListItem.LibraryDownloadItem$1000L MagicNumber:PeerGroupHandshake.kt$PeerGroupHandshake$15000 MagicNumber:ShareFiles.kt$ShareFiles$24 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 1f47532d5..e6e6691a8 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/zimManager/ZimManageViewModel.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/zimManager/ZimManageViewModel.kt @@ -70,12 +70,12 @@ 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 @@ -83,7 +83,6 @@ 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.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,6 @@ 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 java.util.Locale import java.util.concurrent.TimeUnit.SECONDS import javax.inject.Inject @@ -145,7 +144,8 @@ class ZimManageViewModel @Inject constructor( private val defaultLanguageProvider: DefaultLanguageProvider, private val dataSource: DataSource, private val connectivityManager: ConnectivityManager, - private val sharedPreferenceUtil: SharedPreferenceUtil + private val sharedPreferenceUtil: SharedPreferenceUtil, + private val onlineLibraryManager: OnlineLibraryManager ) : ViewModel() { sealed class FileSelectActions { data class RequestNavigateTo(val bookOnDisk: BookOnDisk) : FileSelectActions() @@ -168,7 +168,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( @@ -238,7 +238,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 +252,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 +391,7 @@ class ZimManageViewModel @Inject constructor( @OptIn(ExperimentalCoroutinesApi::class) private fun requestsAndConnectivityChangesToLibraryRequests( - library: MutableSharedFlow, + library: MutableSharedFlow>, dispatcher: CoroutineDispatcher = Dispatchers.IO ) = requestDownloadLibrary.flatMapConcat { connectivityBroadcastReceiver.networkStates @@ -439,16 +442,17 @@ 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)) - emit(response) + onlineLibraryManager.parseOPDSStream(response, KIWIX_OPDS_LIBRARY_URL) + emit(onlineLibraryManager.getOnlineBooks()) } .retry(5) .catch { e -> e.printStackTrace() - emit(LibraryNetworkEntity().apply { book = LinkedList() }) + emit(emptyList()) } private fun updateNetworkStates() = connectivityBroadcastReceiver.networkStates @@ -460,7 +464,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) { @@ -483,7 +487,7 @@ 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 LibraryNetworkEntity + val libraryNetworkEntity = args[THREE] as List val filter = args[FOUR] as String val fileSystemState = args[FIVE] as FileSystemState combineLibrarySources( @@ -505,12 +509,12 @@ class ZimManageViewModel @Inject constructor( } private fun updateLanguagesInDao( - library: MutableSharedFlow, + library: MutableSharedFlow>, languages: Flow>, dispatcher: CoroutineDispatcher = Dispatchers.IO ) = combine( - library.map { it.book }.filterNotNull(), + library, languages ) { books, existingLanguages -> combineToLanguageList(books, existingLanguages) @@ -523,7 +527,7 @@ class ZimManageViewModel @Inject constructor( .launchIn(viewModelScope) private fun combineToLanguageList( - booksFromNetwork: List, + booksFromNetwork: List, allLanguages: List ) = when { booksFromNetwork.isEmpty() -> { @@ -547,8 +551,8 @@ class ZimManageViewModel @Inject constructor( ) } - private fun networkLanguageCounts(booksFromNetwork: List) = - booksFromNetwork.mapNotNull(Book::language) + private fun networkLanguageCounts(booksFromNetwork: List) = + booksFromNetwork.mapNotNull { it.language } .fold( mutableMapOf() ) { acc, language -> acc.increment(language) } @@ -585,14 +589,14 @@ class ZimManageViewModel @Inject constructor( booksOnFileSystem: List, activeDownloads: List, allLanguages: List, - libraryNetworkEntity: LibraryNetworkEntity, + onlineBooks: List, filter: String, fileSystemState: FileSystemState ): List { 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 } 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 ee6dcf180..325415469 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 @@ -20,16 +20,14 @@ 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.Retrofit -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(): String? @GET suspend fun getMetaLinks( @@ -43,13 +41,14 @@ interface KiwixService { val retrofit = Retrofit.Builder() .baseUrl(baseUrl) .client(okHttpClient) - .addConverterFactory(SimpleXmlConverterFactory.create()) .build() return retrofit.create(KiwixService::class.java) } } 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 = "/entries?count=-1" } } 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 dda7e0329..3d5dc34f5 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 @@ -24,9 +24,11 @@ import org.kiwix.kiwixmobile.core.dao.LibkiwixBookmarks import org.kiwix.kiwixmobile.core.dao.NewBookDao import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil +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 @@ -36,20 +38,39 @@ 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( - library: Library, - manager: Manager, + @Named("bookmarks") library: Library, + @Named("bookmarks") 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) } 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 909fd94ac..e9e4b96c8 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_DOWNLOAD_URL = "https://mirror.download.kiwix.org/" +const val KIWIX_OPDS_LIBRARY_URL = "https://opds.library.kiwix.org/v2" @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) } 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 new file mode 100644 index 000000000..c464045a8 --- /dev/null +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/zim_manager/OnlineLibraryManager.kt @@ -0,0 +1,46 @@ +/* + * 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.zim_manager + +import org.kiwix.libkiwix.Book +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 { + val onlineBooksList = arrayListOf() + runCatching { + library.booksIds.forEach { bookId -> + val book = library.getBookById(bookId) + onlineBooksList.add(book) + } + }.onFailure { it.printStackTrace() } + return onlineBooksList + } +}