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
+ }
+}