Fixed: Bookmarks were not showing for newly added books from the OPDS stream.

* Removed books from `library.xml` if they no longer have any associated bookmarks, as keeping them is unnecessary and wastes resources and memory, while also increasing the time needed to load the library from storage.
* Fixed: The downloading online library progress was showing incorrect.
* Refactored the all unit test cases according to the new changes.
This commit is contained in:
MohitMaliFtechiz 2025-06-07 00:02:09 +05:30
parent ebbe1b9889
commit 40edd03396
21 changed files with 97 additions and 228 deletions

View File

@ -5,7 +5,7 @@
<ID>EmptyFunctionBlock:None.kt$None${ }</ID> <ID>EmptyFunctionBlock:None.kt$None${ }</ID>
<ID>EmptyFunctionBlock:SimplePageChangeListener.kt$SimplePageChangeListener${ }</ID> <ID>EmptyFunctionBlock:SimplePageChangeListener.kt$SimplePageChangeListener${ }</ID>
<ID>LongParameterList:ZimManageViewModel.kt$ZimManageViewModel$( booksOnFileSystem: List&lt;BookOnDisk>, activeDownloads: List&lt;DownloadModel>, allLanguages: List&lt;Language>, libraryNetworkEntity: LibraryNetworkEntity, filter: String, fileSystemState: FileSystemState )</ID> <ID>LongParameterList:ZimManageViewModel.kt$ZimManageViewModel$( booksOnFileSystem: List&lt;BookOnDisk>, activeDownloads: List&lt;DownloadModel>, allLanguages: List&lt;Language>, libraryNetworkEntity: LibraryNetworkEntity, filter: String, fileSystemState: FileSystemState )</ID>
<ID>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 )</ID> <ID>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 )</ID>
<ID>MagicNumber:LibraryListItem.kt$LibraryListItem.LibraryDownloadItem$1000L</ID> <ID>MagicNumber:LibraryListItem.kt$LibraryListItem.LibraryDownloadItem$1000L</ID>
<ID>MagicNumber:PeerGroupHandshake.kt$PeerGroupHandshake$15000</ID> <ID>MagicNumber:PeerGroupHandshake.kt$PeerGroupHandshake$15000</ID>
<ID>MagicNumber:ShareFiles.kt$ShareFiles$24</ID> <ID>MagicNumber:ShareFiles.kt$ShareFiles$24</ID>

View File

@ -55,13 +55,6 @@
<init>(...); <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.** { *; } -keep class javax.xml.stream.** { *; }
-dontwarn javax.xml.stream.Location -dontwarn javax.xml.stream.Location
-dontwarn javax.xml.stream.XMLEventReader -dontwarn javax.xml.stream.XMLEventReader

View File

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

View File

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

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

View File

@ -121,6 +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.BookItem
import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem.DividerItem import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem.DividerItem
import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem.LibraryDownloadItem import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryListItem.LibraryDownloadItem
import org.kiwix.libkiwix.Library
import org.kiwix.libkiwix.Manager
import java.util.Locale import java.util.Locale
import java.util.concurrent.TimeUnit.SECONDS import java.util.concurrent.TimeUnit.SECONDS
import javax.inject.Inject import javax.inject.Inject
@ -146,7 +148,6 @@ class ZimManageViewModel @Inject constructor(
private val dataSource: DataSource, private val dataSource: DataSource,
private val connectivityManager: ConnectivityManager, private val connectivityManager: ConnectivityManager,
private val sharedPreferenceUtil: SharedPreferenceUtil, private val sharedPreferenceUtil: SharedPreferenceUtil,
private val onlineLibraryManager: OnlineLibraryManager
) : ViewModel() { ) : ViewModel() {
sealed class FileSelectActions { sealed class FileSelectActions {
data class RequestNavigateTo(val bookOnDisk: BookOnDisk) : FileSelectActions() data class RequestNavigateTo(val bookOnDisk: BookOnDisk) : FileSelectActions()
@ -159,6 +160,12 @@ class ZimManageViewModel @Inject constructor(
object UserClickedDownloadBooksButton : FileSelectActions() 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 private var isUnitTestCase: Boolean = false
val sideEffects: MutableSharedFlow<SideEffect<*>> = MutableSharedFlow() val sideEffects: MutableSharedFlow<SideEffect<*>> = MutableSharedFlow()
private val _libraryItems = MutableStateFlow<List<LibraryListItem>>(emptyList()) private val _libraryItems = MutableStateFlow<List<LibraryListItem>>(emptyList())
@ -412,6 +419,7 @@ class ZimManageViewModel @Inject constructor(
it.printStackTrace().also { it.printStackTrace().also {
isOnlineLibraryDownloading = false isOnlineLibraryDownloading = false
onlineLibraryDownloading.tryEmit(false) onlineLibraryDownloading.tryEmit(false)
library.emit(emptyList())
} }
} }
.onEach { .onEach {

View File

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

View File

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

View File

@ -204,7 +204,7 @@ class LibkiwixBookmarks @Inject constructor(
library.addBook(libKiwixBook).also { library.addBook(libKiwixBook).also {
// now library has changed so update our library list. // now library has changed so update our library list.
libraryBooksList = library.booksIds.toList() libraryBooksList = library.booksIds.toList()
Log.d( Log.e(
TAG, TAG,
"Added Book to Library:\n" + "Added Book to Library:\n" +
"ZIM File Path: ${book.path}\n" + "ZIM File Path: ${book.path}\n" +
@ -233,6 +233,7 @@ class LibkiwixBookmarks @Inject constructor(
CoroutineScope(dispatcher).launch { CoroutineScope(dispatcher).launch {
writeBookMarksAndSaveLibraryToFile() writeBookMarksAndSaveLibraryToFile()
updateFlowBookmarkList() updateFlowBookmarkList()
removeBookFromLibraryIfNoRelatedBookmarksAreStored(dispatcher, bookmarks)
} }
} }
} }
@ -241,6 +242,34 @@ class LibkiwixBookmarks @Inject constructor(
deleteBookmarks(listOf(LibkiwixBookmarkItem(zimId = bookId, bookmarkUrl = bookmarkUrl))) 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 * 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. * to prevent potential data loss and ensures that the library holds the updated ZIM file paths and favicons.
@ -285,7 +314,6 @@ class LibkiwixBookmarks @Inject constructor(
// Check if the book has an illustration of the specified size and encode it to Base64. // Check if the book has an illustration of the specified size and encode it to Base64.
val favicon = book?.getFavicon() val favicon = book?.getFavicon()
Log.e(TAG, "getBookmarksList: $favicon")
val zimReaderSource = book?.path?.let { ZimReaderSource(File(it)) } val zimReaderSource = book?.path?.let { ZimReaderSource(File(it)) }
// Return the LibkiwixBookmarkItem, filtering out null results. // Return the LibkiwixBookmarkItem, filtering out null results.

View File

@ -58,7 +58,6 @@ import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer
import org.kiwix.kiwixmobile.core.search.viewmodel.SearchResultGenerator import org.kiwix.kiwixmobile.core.search.viewmodel.SearchResultGenerator
import org.kiwix.kiwixmobile.core.utils.BookUtils import org.kiwix.kiwixmobile.core.utils.BookUtils
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
import org.kiwix.kiwixmobile.core.zim_manager.OnlineLibraryManager
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
@ -100,7 +99,6 @@ interface CoreComponent {
fun connectivityManager(): ConnectivityManager fun connectivityManager(): ConnectivityManager
fun objectBoxToLibkiwixMigrator(): ObjectBoxToLibkiwixMigrator fun objectBoxToLibkiwixMigrator(): ObjectBoxToLibkiwixMigrator
fun libkiwixBookmarks(): LibkiwixBookmarks fun libkiwixBookmarks(): LibkiwixBookmarks
fun onlineLibraryManager(): OnlineLibraryManager
fun recentSearchRoomDao(): RecentSearchRoomDao fun recentSearchRoomDao(): RecentSearchRoomDao
fun historyRoomDao(): HistoryRoomDao fun historyRoomDao(): HistoryRoomDao
fun webViewHistoryRoomDao(): WebViewHistoryRoomDao fun webViewHistoryRoomDao(): WebViewHistoryRoomDao

View File

@ -24,7 +24,6 @@ import org.kiwix.kiwixmobile.core.dao.LibkiwixBookmarks
import org.kiwix.kiwixmobile.core.dao.NewBookDao import org.kiwix.kiwixmobile.core.dao.NewBookDao
import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
import org.kiwix.kiwixmobile.core.zim_manager.OnlineLibraryManager
import org.kiwix.libkiwix.JNIKiwix import org.kiwix.libkiwix.JNIKiwix
import org.kiwix.libkiwix.Library import org.kiwix.libkiwix.Library
import org.kiwix.libkiwix.Manager import org.kiwix.libkiwix.Manager
@ -53,11 +52,4 @@ class JNIModule {
zimReaderContainer: ZimReaderContainer zimReaderContainer: ZimReaderContainer
): LibkiwixBookmarks = ): LibkiwixBookmarks =
LibkiwixBookmarks(library, manager, sharedPreferenceUtil, bookDao, zimReaderContainer) LibkiwixBookmarks(library, manager, sharedPreferenceUtil, bookDao, zimReaderContainer)
@Provides
@Singleton
fun provideOnlineLibraryParser(
library: Library,
manager: Manager
): OnlineLibraryManager = OnlineLibraryManager(library, manager)
} }

View File

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

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

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

View File

@ -21,12 +21,8 @@ package org.kiwix.kiwixmobile.core.zim_manager
import org.kiwix.kiwixmobile.core.entity.LibkiwixBook import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
import org.kiwix.libkiwix.Library import org.kiwix.libkiwix.Library
import org.kiwix.libkiwix.Manager import org.kiwix.libkiwix.Manager
import javax.inject.Inject
class OnlineLibraryManager @Inject constructor( class OnlineLibraryManager(val library: Library, val manager: Manager) {
val library: Library,
val manager: Manager
) {
suspend fun parseOPDSStream(content: String?, urlHost: String): Boolean = suspend fun parseOPDSStream(content: String?, urlHost: String): Boolean =
runCatching { runCatching {
manager.readOpds(content, urlHost) manager.readOpds(content, urlHost)

View File

@ -22,14 +22,12 @@ import com.tonyodev.fetch2.Status
import com.tonyodev.fetch2.Status.NONE import com.tonyodev.fetch2.Status.NONE
import org.kiwix.kiwixmobile.core.dao.entities.BookOnDiskEntity import org.kiwix.kiwixmobile.core.dao.entities.BookOnDiskEntity
import org.kiwix.kiwixmobile.core.dao.entities.RecentSearchEntity 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.DownloadItem
import org.kiwix.kiwixmobile.core.downloader.model.DownloadModel import org.kiwix.kiwixmobile.core.downloader.model.DownloadModel
import org.kiwix.kiwixmobile.core.downloader.model.DownloadState import org.kiwix.kiwixmobile.core.downloader.model.DownloadState
import org.kiwix.kiwixmobile.core.downloader.model.DownloadState.Pending import org.kiwix.kiwixmobile.core.downloader.model.DownloadState.Pending
import org.kiwix.kiwixmobile.core.downloader.model.Seconds import org.kiwix.kiwixmobile.core.downloader.model.Seconds
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity import org.kiwix.kiwixmobile.core.entity.LibkiwixBook
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book
import org.kiwix.kiwixmobile.core.entity.MetaLinkNetworkEntity import org.kiwix.kiwixmobile.core.entity.MetaLinkNetworkEntity
import org.kiwix.kiwixmobile.core.entity.MetaLinkNetworkEntity.FileElement import org.kiwix.kiwixmobile.core.entity.MetaLinkNetworkEntity.FileElement
import org.kiwix.kiwixmobile.core.entity.MetaLinkNetworkEntity.Pieces 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.Language
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem.BookOnDisk import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem.BookOnDisk
import java.io.File import java.io.File
import java.util.LinkedList
fun bookOnDisk( fun bookOnDisk(
databaseId: Long = 0L, databaseId: Long = 0L,
book: Book = book(), book: LibkiwixBook = libkiwixBook(),
zimReaderSource: ZimReaderSource = ZimReaderSource(File("")) zimReaderSource: ZimReaderSource = ZimReaderSource(File(""))
) = BookOnDisk(databaseId, book, File(""), zimReaderSource) ) = BookOnDisk(databaseId, book, File(""), zimReaderSource)
@ -56,7 +53,7 @@ fun downloadModel(
status: Status = NONE, status: Status = NONE,
error: Error = Error.NONE, error: Error = Error.NONE,
progress: Int = 1, progress: Int = 1,
book: Book = book() book: LibkiwixBook = libkiwixBook()
) = DownloadModel( ) = DownloadModel(
databaseId, downloadId, file, etaInMilliSeconds, bytesDownloaded, totalSizeOfDownload, databaseId, downloadId, file, etaInMilliSeconds, bytesDownloaded, totalSizeOfDownload,
status, error, progress, book status, error, progress, book
@ -64,7 +61,7 @@ fun downloadModel(
fun downloadItem( fun downloadItem(
downloadId: Long = 1L, downloadId: Long = 1L,
favIcon: Base64String = Base64String("favIcon"), favIcon: String = "favIcon",
title: String = "title", title: String = "title",
description: String = "description", description: String = "description",
bytesDownloaded: Long = 1L, bytesDownloaded: Long = 1L,
@ -135,7 +132,7 @@ fun url(
this.value = value this.value = value
} }
fun book( fun libkiwixBook(
id: String = "id", id: String = "id",
title: String = "title", title: String = "title",
description: String = "description", description: String = "description",
@ -150,7 +147,7 @@ fun book(
name: String = "name", name: String = "name",
favIcon: String = "favIcon", favIcon: String = "favIcon",
file: File = File("") file: File = File("")
) = Book().apply { ) = LibkiwixBook().apply {
this.id = id this.id = id
this.title = title this.title = title
this.description = description this.description = description
@ -167,11 +164,6 @@ fun book(
favicon = favIcon favicon = favIcon
} }
fun libraryNetworkEntity(books: List<Book> = emptyList()) =
LibraryNetworkEntity().apply {
book = LinkedList(books)
}
fun recentSearchEntity( fun recentSearchEntity(
id: Long = 0L, id: Long = 0L,
searchTerm: String = "", 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.ScanningProgressListener
import org.kiwix.kiwixmobile.core.utils.files.testFlow import org.kiwix.kiwixmobile.core.utils.files.testFlow
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem.BookOnDisk 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 org.kiwix.sharedFunctions.bookOnDisk
import java.io.File import java.io.File
@ -85,7 +85,7 @@ class StorageObserverTest {
@Test @Test
fun `zim files are read by the file reader`() = runTest { fun `zim files are read by the file reader`() = runTest {
val expectedBook = val expectedBook =
book( libkiwixBook(
"id", "title", "1", "favicon", "creator", "publisher", "date", "id", "title", "1", "favicon", "creator", "publisher", "date",
"description", "language" "description", "language"
) )

View File

@ -40,13 +40,13 @@ import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test 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.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.reader.ZimReaderSource
import org.kiwix.kiwixmobile.core.utils.files.testFlow import org.kiwix.kiwixmobile.core.utils.files.testFlow
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem.BookOnDisk 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.bookOnDisk
import org.kiwix.sharedFunctions.bookOnDiskEntity import org.kiwix.sharedFunctions.bookOnDiskEntity
import org.kiwix.sharedFunctions.libkiwixBook
import java.io.File import java.io.File
import java.util.concurrent.Callable import java.util.concurrent.Callable
@ -131,9 +131,9 @@ internal class NewBookDaoTest {
fun `insert transaction adds books to the box that have distinct ids`() { fun `insert transaction adds books to the box that have distinct ids`() {
val slot: CapturingSlot<Callable<Unit>> = slot() val slot: CapturingSlot<Callable<Unit>> = slot()
every { box.store.callInTx(capture(slot)) } returns Unit 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( 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) val queryBuilder: QueryBuilder<BookOnDiskEntity> = mockk(relaxed = true)
every { box.query() } returns queryBuilder every { box.query() } returns queryBuilder
@ -216,7 +216,7 @@ internal class NewBookDaoTest {
@Test @Test
fun migrationInsert() { fun migrationInsert() {
val book: LibraryNetworkEntity.Book = book() val book: LibkiwixBook = libkiwixBook()
val slot: CapturingSlot<Callable<Unit>> = slot() val slot: CapturingSlot<Callable<Unit>> = slot()
every { box.store.callInTx(capture(slot)) } returns Unit every { box.store.callInTx(capture(slot)) } returns Unit
newBookDao.migrationInsert(listOf(book)) newBookDao.migrationInsert(listOf(book))

View File

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

View File

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