Refactored the code to use coroutines instead of rxJava for saving/retrieving the bookmarks from libkiwix.

* Refactored the all unit and UI test cases according to it.
This commit is contained in:
MohitMaliFtechiz 2025-05-13 12:34:29 +05:30
parent d6ef855795
commit 3e92cda80f
8 changed files with 127 additions and 134 deletions

View File

@ -28,6 +28,7 @@ import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiDevice
import io.objectbox.Box import io.objectbox.Box
import io.objectbox.BoxStore import io.objectbox.BoxStore
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
@ -130,7 +131,7 @@ class ObjectBoxToLibkiwixMigratorTest : BaseActivityTest() {
box = boxStore!!.boxFor(BookmarkEntity::class.java) box = boxStore!!.boxFor(BookmarkEntity::class.java)
// clear the data before running the test case // clear the data before running the test case
clearBookmarks() runBlocking { clearBookmarks() }
// add a file in fileSystem because we need to actual file path for making object of Archive. // add a file in fileSystem because we need to actual file path for making object of Archive.
val loadFileStream = val loadFileStream =
@ -162,7 +163,7 @@ class ObjectBoxToLibkiwixMigratorTest : BaseActivityTest() {
objectBoxToLibkiwixMigrator.migrateBookMarks(box) objectBoxToLibkiwixMigrator.migrateBookMarks(box)
// check if data successfully migrated to room // check if data successfully migrated to room
val actualDataAfterMigration = val actualDataAfterMigration =
objectBoxToLibkiwixMigrator.libkiwixBookmarks.bookmarks().blockingFirst() objectBoxToLibkiwixMigrator.libkiwixBookmarks.bookmarks().first()
assertEquals(1, actualDataAfterMigration.size) assertEquals(1, actualDataAfterMigration.size)
assertEquals(actualDataAfterMigration[0].zimReaderSource?.toDatabase(), expectedZimFilePath) assertEquals(actualDataAfterMigration[0].zimReaderSource?.toDatabase(), expectedZimFilePath)
assertEquals(actualDataAfterMigration[0].zimId, expectedZimId) assertEquals(actualDataAfterMigration[0].zimId, expectedZimId)
@ -178,7 +179,7 @@ class ObjectBoxToLibkiwixMigratorTest : BaseActivityTest() {
// Migrate data from empty ObjectBox database // Migrate data from empty ObjectBox database
objectBoxToLibkiwixMigrator.migrateBookMarks(box) objectBoxToLibkiwixMigrator.migrateBookMarks(box)
val actualDataAfterMigration = val actualDataAfterMigration =
objectBoxToLibkiwixMigrator.libkiwixBookmarks.bookmarks().blockingFirst() objectBoxToLibkiwixMigrator.libkiwixBookmarks.bookmarks().first()
assertTrue(actualDataAfterMigration.isEmpty()) assertTrue(actualDataAfterMigration.isEmpty())
// Clear the bookmarks list from device to not affect the other test cases. // Clear the bookmarks list from device to not affect the other test cases.
clearBookmarks() clearBookmarks()
@ -212,7 +213,7 @@ class ObjectBoxToLibkiwixMigratorTest : BaseActivityTest() {
// Migrate data into Room database // Migrate data into Room database
objectBoxToLibkiwixMigrator.migrateBookMarks(box) objectBoxToLibkiwixMigrator.migrateBookMarks(box)
val actualDataAfterMigration = val actualDataAfterMigration =
objectBoxToLibkiwixMigrator.libkiwixBookmarks.bookmarks().blockingFirst() objectBoxToLibkiwixMigrator.libkiwixBookmarks.bookmarks().first()
assertEquals(2, actualDataAfterMigration.size) assertEquals(2, actualDataAfterMigration.size)
val existingItem = val existingItem =
actualDataAfterMigration.find { actualDataAfterMigration.find {
@ -250,7 +251,7 @@ class ObjectBoxToLibkiwixMigratorTest : BaseActivityTest() {
objectBoxToLibkiwixMigrator.migrateBookMarks(box) objectBoxToLibkiwixMigrator.migrateBookMarks(box)
// Check if data successfully migrated to Room // Check if data successfully migrated to Room
val actualDataAfterMigration = val actualDataAfterMigration =
objectBoxToLibkiwixMigrator.libkiwixBookmarks.bookmarks().blockingFirst() objectBoxToLibkiwixMigrator.libkiwixBookmarks.bookmarks().first()
assertEquals(1000, actualDataAfterMigration.size) assertEquals(1000, actualDataAfterMigration.size)
// Clear the bookmarks list from device to not affect the other test cases. // Clear the bookmarks list from device to not affect the other test cases.
clearBookmarks() clearBookmarks()
@ -276,7 +277,7 @@ class ObjectBoxToLibkiwixMigratorTest : BaseActivityTest() {
objectBoxToLibkiwixMigrator.migrateBookMarks(box) objectBoxToLibkiwixMigrator.migrateBookMarks(box)
// check if data successfully migrated to room // check if data successfully migrated to room
val actualDataAfterMigration = val actualDataAfterMigration =
objectBoxToLibkiwixMigrator.libkiwixBookmarks.bookmarks().blockingFirst() objectBoxToLibkiwixMigrator.libkiwixBookmarks.bookmarks().first()
assertEquals(1, actualDataAfterMigration.size) assertEquals(1, actualDataAfterMigration.size)
assertEquals(actualDataAfterMigration[0].zimReaderSource?.toDatabase(), null) assertEquals(actualDataAfterMigration[0].zimReaderSource?.toDatabase(), null)
assertEquals(actualDataAfterMigration[0].zimId, expectedZimId) assertEquals(actualDataAfterMigration[0].zimId, expectedZimId)
@ -307,7 +308,7 @@ class ObjectBoxToLibkiwixMigratorTest : BaseActivityTest() {
objectBoxToLibkiwixMigrator.migrateBookMarks(box) objectBoxToLibkiwixMigrator.migrateBookMarks(box)
// check if data successfully migrated to room // check if data successfully migrated to room
val actualDataAfterMigration = val actualDataAfterMigration =
objectBoxToLibkiwixMigrator.libkiwixBookmarks.bookmarks().blockingFirst() objectBoxToLibkiwixMigrator.libkiwixBookmarks.bookmarks().first()
assertEquals(1, actualDataAfterMigration.size) assertEquals(1, actualDataAfterMigration.size)
assertEquals(actualDataAfterMigration[0].zimReaderSource?.toDatabase(), null) assertEquals(actualDataAfterMigration[0].zimReaderSource?.toDatabase(), null)
assertEquals(actualDataAfterMigration[0].zimId, expectedZimId) assertEquals(actualDataAfterMigration[0].zimId, expectedZimId)
@ -317,11 +318,11 @@ class ObjectBoxToLibkiwixMigratorTest : BaseActivityTest() {
clearBookmarks() clearBookmarks()
} }
private fun clearBookmarks() { private suspend fun clearBookmarks() {
// delete bookmarks for testing other edge cases // delete bookmarks for testing other edge cases
objectBoxToLibkiwixMigrator.libkiwixBookmarks.deleteBookmarks( objectBoxToLibkiwixMigrator.libkiwixBookmarks.deleteBookmarks(
objectBoxToLibkiwixMigrator.libkiwixBookmarks.bookmarks() objectBoxToLibkiwixMigrator.libkiwixBookmarks.bookmarks()
.blockingFirst() as List<LibkiwixBookmarkItem> .first() as List<LibkiwixBookmarkItem>
) )
box.removeAll() box.removeAll()
if (::zimFile.isInitialized) { if (::zimFile.isInitialized) {

View File

@ -31,6 +31,7 @@ import com.google.android.apps.common.testing.accessibility.framework.Accessibil
import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck
import com.google.android.apps.common.testing.accessibility.framework.checks.TouchTargetSizeCheck import com.google.android.apps.common.testing.accessibility.framework.checks.TouchTargetSizeCheck
import io.objectbox.BoxStore import io.objectbox.BoxStore
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.allOf
import org.hamcrest.Matchers.anyOf import org.hamcrest.Matchers.anyOf
@ -171,13 +172,13 @@ class ImportBookmarkTest : BaseActivityTest() {
// test with empty data file // test with empty data file
var tempBookmarkFile = getTemporaryBookmarkFile(true) var tempBookmarkFile = getTemporaryBookmarkFile(true)
importBookmarks(tempBookmarkFile) importBookmarks(tempBookmarkFile)
var actualDataAfterImporting = libkiwixBookmarks.bookmarks().blockingFirst() var actualDataAfterImporting = libkiwixBookmarks.bookmarks().first()
assertEquals(0, actualDataAfterImporting.size) assertEquals(0, actualDataAfterImporting.size)
// import the bookmark // import the bookmark
tempBookmarkFile = getTemporaryBookmarkFile() tempBookmarkFile = getTemporaryBookmarkFile()
importBookmarks(tempBookmarkFile) importBookmarks(tempBookmarkFile)
actualDataAfterImporting = libkiwixBookmarks.bookmarks().blockingFirst() actualDataAfterImporting = libkiwixBookmarks.bookmarks().first()
assertEquals(3, actualDataAfterImporting.size) assertEquals(3, actualDataAfterImporting.size)
assertEquals(actualDataAfterImporting[0].title, "Main Page") assertEquals(actualDataAfterImporting[0].title, "Main Page")
assertEquals(actualDataAfterImporting[0].url, "https://kiwix.app/A/Main_Page") assertEquals(actualDataAfterImporting[0].url, "https://kiwix.app/A/Main_Page")
@ -185,7 +186,7 @@ class ImportBookmarkTest : BaseActivityTest() {
// import duplicate bookmarks // import duplicate bookmarks
importBookmarks(tempBookmarkFile) importBookmarks(tempBookmarkFile)
actualDataAfterImporting = libkiwixBookmarks.bookmarks().blockingFirst() actualDataAfterImporting = libkiwixBookmarks.bookmarks().first()
assertEquals(3, actualDataAfterImporting.size) assertEquals(3, actualDataAfterImporting.size)
// delete the temp file // delete the temp file
@ -200,11 +201,11 @@ class ImportBookmarkTest : BaseActivityTest() {
} }
} }
private fun clearBookmarks() { private suspend fun clearBookmarks() {
// delete bookmarks for testing other edge cases // delete bookmarks for testing other edge cases
libkiwixBookmarks.deleteBookmarks( libkiwixBookmarks.deleteBookmarks(
libkiwixBookmarks.bookmarks() libkiwixBookmarks.bookmarks()
.blockingFirst() as List<LibkiwixBookmarkItem> .first() as List<LibkiwixBookmarkItem>
) )
} }

View File

@ -21,17 +21,18 @@ package org.kiwix.kiwixmobile.core.dao
import android.os.Build import android.os.Build
import android.os.Environment import android.os.Environment
import android.util.Base64 import android.util.Base64
import io.reactivex.BackpressureStrategy
import io.reactivex.BackpressureStrategy.LATEST
import io.reactivex.Flowable import io.reactivex.Flowable
import io.reactivex.schedulers.Schedulers
import io.reactivex.subjects.BehaviorSubject
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.reactive.asPublisher
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.rx3.rxSingle import kotlinx.coroutines.withContext
import org.kiwix.kiwixmobile.core.CoreApp import org.kiwix.kiwixmobile.core.CoreApp
import org.kiwix.kiwixmobile.core.DarkModeConfig import org.kiwix.kiwixmobile.core.DarkModeConfig
import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.R
@ -71,15 +72,19 @@ class LibkiwixBookmarks @Inject constructor(
private var bookmarkList: List<LibkiwixBookmarkItem> = arrayListOf() private var bookmarkList: List<LibkiwixBookmarkItem> = arrayListOf()
private var libraryBooksList: List<String> = arrayListOf() private var libraryBooksList: List<String> = arrayListOf()
@Suppress("CheckResult", "IgnoredReturnValue") @Suppress("InjectDispatcher", "TooGenericExceptionCaught")
private val bookmarkListBehaviour: BehaviorSubject<List<LibkiwixBookmarkItem>>? by lazy { private val bookmarkListFlow: MutableStateFlow<List<LibkiwixBookmarkItem>> by lazy {
BehaviorSubject.create<List<LibkiwixBookmarkItem>>().also { subject -> MutableStateFlow<List<LibkiwixBookmarkItem>>(emptyList()).also { flow ->
rxSingle { getBookmarksList() } CoroutineScope(Dispatchers.IO).launch {
.subscribeOn(io.reactivex.rxjava3.schedulers.Schedulers.io()) try {
.subscribe(subject::onNext, subject::onError) val bookmarks = getBookmarksList()
flow.emit(bookmarks)
} catch (e: Exception) {
e.printStackTrace()
}
}
} }
} }
private val bookmarksFolderPath: String by lazy { private val bookmarksFolderPath: String by lazy {
if (Build.DEVICE.contains("generic")) { if (Build.DEVICE.contains("generic")) {
// Workaround for emulators: Emulators have limited memory and // Workaround for emulators: Emulators have limited memory and
@ -112,30 +117,35 @@ class LibkiwixBookmarks @Inject constructor(
manager.readBookmarkFile(bookmarkFile.canonicalPath) manager.readBookmarkFile(bookmarkFile.canonicalPath)
} }
fun bookmarks(): Flowable<List<Page>> = fun bookmarks(): Flow<List<Page>> =
flowableBookmarkList() bookmarkListFlow
.map { it } .map { it }
override fun pages(): Flowable<List<Page>> = bookmarks() // Currently kept in RxJava Flowable because `PageViewModel` still expects RxJava streams.
// This can be refactored to use Kotlin Flow once `PageViewModel` is migrated to coroutines.
override fun pages(): Flowable<List<Page>> =
Flowable.fromPublisher(bookmarks().asPublisher())
override fun deletePages(pagesToDelete: List<Page>) = override fun deletePages(pagesToDelete: List<Page>) =
deleteBookmarks(pagesToDelete as List<LibkiwixBookmarkItem>) deleteBookmarks(pagesToDelete as List<LibkiwixBookmarkItem>)
suspend fun getCurrentZimBookmarksUrl(zimFileReader: ZimFileReader?): List<String> { @Suppress("InjectDispatcher")
return zimFileReader?.let { reader -> suspend fun getCurrentZimBookmarksUrl(zimFileReader: ZimFileReader?): List<String> =
getBookmarksList() withContext(Dispatchers.IO) {
.filter { it.zimId == reader.id } return@withContext zimFileReader?.let { reader ->
.map(LibkiwixBookmarkItem::bookmarkUrl) getBookmarksList()
}.orEmpty() .filter { it.zimId == reader.id }
} .map(LibkiwixBookmarkItem::bookmarkUrl)
}.orEmpty()
}
fun bookmarkUrlsForCurrentBook(zimId: String): Flowable<List<String>> = @Suppress("InjectDispatcher")
flowableBookmarkList() fun bookmarkUrlsForCurrentBook(zimId: String): Flow<List<String>> =
bookmarkListFlow
.map { bookmarksList -> .map { bookmarksList ->
bookmarksList.filter { it.zimId == zimId } bookmarksList.filter { it.zimId == zimId }
.map(LibkiwixBookmarkItem::bookmarkUrl) .map(LibkiwixBookmarkItem::bookmarkUrl)
} }.flowOn(Dispatchers.IO)
.subscribeOn(Schedulers.io())
/** /**
* Saves bookmarks in libkiwix. The use of `shouldWriteBookmarkToFile` is primarily * Saves bookmarks in libkiwix. The use of `shouldWriteBookmarkToFile` is primarily
@ -165,7 +175,7 @@ class LibkiwixBookmarks @Inject constructor(
library.addBookmark(bookmark).also { library.addBookmark(bookmark).also {
if (shouldWriteBookmarkToFile) { if (shouldWriteBookmarkToFile) {
writeBookMarksAndSaveLibraryToFile() writeBookMarksAndSaveLibraryToFile()
updateFlowableBookmarkList() updateFlowBookmarkList()
} }
// dispose the bookmark // dispose the bookmark
bookmark.dispose() bookmark.dispose()
@ -185,7 +195,7 @@ class LibkiwixBookmarks @Inject constructor(
} }
} }
addBookToLibraryIfNotExist(book) addBookToLibraryIfNotExist(book)
updateFlowableBookmarkList() updateFlowBookmarkList()
} catch (ignore: Exception) { } catch (ignore: Exception) {
Log.e( Log.e(
TAG, TAG,
@ -228,7 +238,7 @@ class LibkiwixBookmarks @Inject constructor(
.also { .also {
CoroutineScope(dispatcher).launch { CoroutineScope(dispatcher).launch {
writeBookMarksAndSaveLibraryToFile() writeBookMarksAndSaveLibraryToFile()
updateFlowableBookmarkList() updateFlowBookmarkList()
} }
} }
} }
@ -363,27 +373,8 @@ class LibkiwixBookmarks @Inject constructor(
it.zimReaderSource == libkiwixBookmarkItem.zimReaderSource it.zimReaderSource == libkiwixBookmarkItem.zimReaderSource
} }
private fun flowableBookmarkList( private suspend fun updateFlowBookmarkList() {
backpressureStrategy: BackpressureStrategy = LATEST bookmarkListFlow.emit(getBookmarksList())
): Flowable<List<LibkiwixBookmarkItem>> {
return Flowable.create({ emitter ->
val disposable =
bookmarkListBehaviour?.subscribe(
{ list ->
if (!emitter.isCancelled) {
emitter.onNext(list.toList())
}
},
emitter::onError,
emitter::onComplete
)
emitter.setDisposable(disposable)
}, backpressureStrategy)
}
private suspend fun updateFlowableBookmarkList() {
bookmarkListBehaviour?.onNext(getBookmarksList())
} }
// Export the `bookmark.xml` file to the `Download/org.kiwix/` directory of internal storage. // Export the `bookmark.xml` file to the `Download/org.kiwix/` directory of internal storage.

View File

@ -41,11 +41,11 @@ interface DataSource {
fun saveHistory(history: HistoryItem): Completable fun saveHistory(history: HistoryItem): Completable
fun deleteHistory(historyList: List<HistoryListItem>): Completable fun deleteHistory(historyList: List<HistoryListItem>): Completable
fun clearHistory(): Completable fun clearHistory(): Completable
fun getBookmarks(): Flowable<List<LibkiwixBookmarkItem>> fun getBookmarks(): Flow<List<LibkiwixBookmarkItem>>
fun getCurrentZimBookmarksUrl(): io.reactivex.rxjava3.core.Single<List<String>> suspend fun getCurrentZimBookmarksUrl(): List<String>
fun saveBookmark(libkiwixBookmarkItem: LibkiwixBookmarkItem): io.reactivex.rxjava3.core.Completable suspend fun saveBookmark(libkiwixBookmarkItem: LibkiwixBookmarkItem)
fun deleteBookmarks(bookmarks: List<LibkiwixBookmarkItem>): Completable suspend fun deleteBookmarks(bookmarks: List<LibkiwixBookmarkItem>)
fun deleteBookmark(bookId: String, bookmarkUrl: String): Completable? suspend fun deleteBookmark(bookId: String, bookmarkUrl: String)
fun booksOnDiskAsListItems(): Flowable<List<BooksOnDiskListItem>> fun booksOnDiskAsListItems(): Flowable<List<BooksOnDiskListItem>>
fun saveNote(noteListItem: NoteListItem): Completable fun saveNote(noteListItem: NoteListItem): Completable
fun deleteNote(noteTitle: String): Completable fun deleteNote(noteTitle: String): Completable

View File

@ -21,9 +21,7 @@ package org.kiwix.kiwixmobile.core.data
import io.reactivex.Completable import io.reactivex.Completable
import io.reactivex.Flowable import io.reactivex.Flowable
import io.reactivex.Scheduler import io.reactivex.Scheduler
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.rx3.rxCompletable
import kotlinx.coroutines.rx3.rxSingle
import org.kiwix.kiwixmobile.core.dao.HistoryRoomDao import org.kiwix.kiwixmobile.core.dao.HistoryRoomDao
import org.kiwix.kiwixmobile.core.dao.LibkiwixBookmarks import org.kiwix.kiwixmobile.core.dao.LibkiwixBookmarks
import org.kiwix.kiwixmobile.core.dao.NewBookDao import org.kiwix.kiwixmobile.core.dao.NewBookDao
@ -122,24 +120,19 @@ class Repository @Inject internal constructor(
}.subscribeOn(ioThread) }.subscribeOn(ioThread)
override fun getBookmarks() = override fun getBookmarks() =
libkiwixBookmarks.bookmarks() as Flowable<List<LibkiwixBookmarkItem>> libkiwixBookmarks.bookmarks() as Flow<List<LibkiwixBookmarkItem>>
override fun getCurrentZimBookmarksUrl() = override suspend fun getCurrentZimBookmarksUrl() =
rxSingle { libkiwixBookmarks.getCurrentZimBookmarksUrl(zimReaderContainer.zimFileReader)
libkiwixBookmarks.getCurrentZimBookmarksUrl(zimReaderContainer.zimFileReader)
}.subscribeOn(io.reactivex.rxjava3.schedulers.Schedulers.io())
override fun saveBookmark(libkiwixBookmarkItem: LibkiwixBookmarkItem) = override suspend fun saveBookmark(libkiwixBookmarkItem: LibkiwixBookmarkItem) =
rxCompletable { libkiwixBookmarks.saveBookmark(libkiwixBookmarkItem) } libkiwixBookmarks.saveBookmark(libkiwixBookmarkItem)
.subscribeOn(io.reactivex.rxjava3.schedulers.Schedulers.io())
override fun deleteBookmarks(bookmarks: List<LibkiwixBookmarkItem>) = override suspend fun deleteBookmarks(bookmarks: List<LibkiwixBookmarkItem>) =
Completable.fromAction { libkiwixBookmarks.deleteBookmarks(bookmarks) } libkiwixBookmarks.deleteBookmarks(bookmarks)
.subscribeOn(ioThread)
override fun deleteBookmark(bookId: String, bookmarkUrl: String): Completable? = override suspend fun deleteBookmark(bookId: String, bookmarkUrl: String) =
Completable.fromAction { libkiwixBookmarks.deleteBookmark(bookId, bookmarkUrl) } libkiwixBookmarks.deleteBookmark(bookId, bookmarkUrl)
.subscribeOn(ioThread)
override fun saveNote(noteListItem: NoteListItem): Completable = override fun saveNote(noteListItem: NoteListItem): Completable =
Completable.fromAction { notesRoomDao.saveNote(noteListItem) } Completable.fromAction { notesRoomDao.saveNote(noteListItem) }

View File

@ -94,15 +94,15 @@ import com.google.android.material.bottomappbar.BottomAppBar
import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.navigation.NavigationView import com.google.android.material.navigation.NavigationView
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import io.reactivex.Flowable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.processors.BehaviorProcessor
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
@ -202,7 +202,7 @@ abstract class CoreReaderFragment :
NavigationHistoryClickListener, NavigationHistoryClickListener,
ShowDonationDialogCallback { ShowDonationDialogCallback {
protected val webViewList: MutableList<KiwixWebView> = ArrayList() protected val webViewList: MutableList<KiwixWebView> = ArrayList()
private val webUrlsProcessor = BehaviorProcessor.create<String>() private val webUrlsFlow = MutableStateFlow("")
private var fragmentReaderBinding: FragmentReaderBinding? = null private var fragmentReaderBinding: FragmentReaderBinding? = null
var toolbar: Toolbar? = null var toolbar: Toolbar? = null
@ -333,7 +333,7 @@ abstract class CoreReaderFragment :
private var tableDrawerRight: RecyclerView? = null private var tableDrawerRight: RecyclerView? = null
private var tabCallback: ItemTouchHelper.Callback? = null private var tabCallback: ItemTouchHelper.Callback? = null
private var donationLayout: FrameLayout? = null private var donationLayout: FrameLayout? = null
private var bookmarkingDisposable: Disposable? = null private var bookmarkingJob: Job? = null
private var isBookmarked = false private var isBookmarked = false
private lateinit var serviceConnection: ServiceConnection private lateinit var serviceConnection: ServiceConnection
private var readAloudService: ReadAloudService? = null private var readAloudService: ReadAloudService? = null
@ -1269,7 +1269,7 @@ abstract class CoreReaderFragment :
(requireActivity() as? AppCompatActivity)?.setSupportActionBar(null) (requireActivity() as? AppCompatActivity)?.setSupportActionBar(null)
} }
repositoryActions?.dispose() repositoryActions?.dispose()
safeDispose() safelyCancelBookmarkJob()
unBindViewsAndBinding() unBindViewsAndBinding()
tabCallback = null tabCallback = null
hideBackToTopTimer?.cancel() hideBackToTopTimer?.cancel()
@ -1546,7 +1546,7 @@ abstract class CoreReaderFragment :
tabsAdapter?.selected = currentWebViewIndex tabsAdapter?.selected = currentWebViewIndex
updateBottomToolbarVisibility() updateBottomToolbarVisibility()
loadPrefs() loadPrefs()
updateUrlProcessor() updateUrlFlow()
updateTableOfContents() updateTableOfContents()
updateTitle() updateTitle()
} }
@ -1898,24 +1898,25 @@ abstract class CoreReaderFragment :
} }
protected fun setUpBookmarks(zimFileReader: ZimFileReader) { protected fun setUpBookmarks(zimFileReader: ZimFileReader) {
safeDispose() safelyCancelBookmarkJob()
bookmarkingDisposable = Flowable.combineLatest( bookmarkingJob = CoroutineScope(Dispatchers.Main).launch {
libkiwixBookmarks?.bookmarkUrlsForCurrentBook(zimFileReader.id), combine(
webUrlsProcessor, libkiwixBookmarks?.bookmarkUrlsForCurrentBook(zimFileReader.id) ?: emptyFlow(),
List<String?>::contains webUrlsFlow,
) List<String?>::contains
.observeOn(AndroidSchedulers.mainThread()) ).collect { isBookmarked ->
.subscribe({ isBookmarked: Boolean -> this@CoreReaderFragment.isBookmarked = isBookmarked
this.isBookmarked = isBookmarked
bottomToolbarBookmark?.setImageResource( bottomToolbarBookmark?.setImageResource(
if (isBookmarked) R.drawable.ic_bookmark_24dp else R.drawable.ic_bookmark_border_24dp if (isBookmarked) R.drawable.ic_bookmark_24dp else R.drawable.ic_bookmark_border_24dp
) )
}, Throwable::printStackTrace) }
updateUrlProcessor() }
updateUrlFlow()
} }
private fun safeDispose() { private fun safelyCancelBookmarkJob() {
bookmarkingDisposable?.dispose() bookmarkingJob?.cancel()
bookmarkingJob = null
} }
private fun isNotPreviouslyOpenZim(zimReaderSource: ZimReaderSource?): Boolean = private fun isNotPreviouslyOpenZim(zimReaderSource: ZimReaderSource?): Boolean =
@ -2464,8 +2465,8 @@ abstract class CoreReaderFragment :
protected fun urlIsValid(): Boolean = getCurrentWebView()?.url != null protected fun urlIsValid(): Boolean = getCurrentWebView()?.url != null
private fun updateUrlProcessor() { private fun updateUrlFlow() {
getCurrentWebView()?.url?.let(webUrlsProcessor::offer) getCurrentWebView()?.url?.let { webUrlsFlow.value = it }
} }
private fun updateNightMode() { private fun updateNightMode() {
@ -2673,7 +2674,7 @@ abstract class CoreReaderFragment :
// If a URL fails to load, update the bookmark toggle. // If a URL fails to load, update the bookmark toggle.
// This fixes the scenario where the previous page is bookmarked and the next // This fixes the scenario where the previous page is bookmarked and the next
// page fails to load, ensuring the bookmark toggle is unset correctly. // page fails to load, ensuring the bookmark toggle is unset correctly.
updateUrlProcessor() updateUrlFlow()
Log.d(TAG_KIWIX, String.format(getString(R.string.error_article_url_not_found), url)) Log.d(TAG_KIWIX, String.format(getString(R.string.error_article_url_not_found), url))
} }
} }
@ -2681,7 +2682,7 @@ abstract class CoreReaderFragment :
@Suppress("MagicNumber") @Suppress("MagicNumber")
override fun webViewProgressChanged(progress: Int, webView: WebView) { override fun webViewProgressChanged(progress: Int, webView: WebView) {
if (isAdded) { if (isAdded) {
updateUrlProcessor() updateUrlFlow()
showProgressBarWithProgress(progress) showProgressBarWithProgress(progress)
if (progress == 100) { if (progress == 100) {
hideProgressBar() hideProgressBar()

View File

@ -19,6 +19,8 @@ package org.kiwix.kiwixmobile.core.main
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.kiwix.kiwixmobile.core.dao.entities.WebViewHistoryEntity import org.kiwix.kiwixmobile.core.dao.entities.WebViewHistoryEntity
import org.kiwix.kiwixmobile.core.data.DataSource import org.kiwix.kiwixmobile.core.data.DataSource
import org.kiwix.kiwixmobile.core.di.ActivityScope import org.kiwix.kiwixmobile.core.di.ActivityScope
@ -35,7 +37,6 @@ private const val TAG = "MainPresenter"
@ActivityScope @ActivityScope
class MainRepositoryActions @Inject constructor(private val dataSource: DataSource) { class MainRepositoryActions @Inject constructor(private val dataSource: DataSource) {
private var saveHistoryDisposable: Disposable? = null private var saveHistoryDisposable: Disposable? = null
private var saveBookmarkDisposable: io.reactivex.rxjava3.disposables.Disposable? = null
private var saveNoteDisposable: Disposable? = null private var saveNoteDisposable: Disposable? = null
private var saveBookDisposable: Disposable? = null private var saveBookDisposable: Disposable? = null
private var deleteNoteDisposable: Disposable? = null private var deleteNoteDisposable: Disposable? = null
@ -48,16 +49,26 @@ class MainRepositoryActions @Inject constructor(private val dataSource: DataSour
.subscribe({}, { e -> Log.e(TAG, "Unable to save history", e) }) .subscribe({}, { e -> Log.e(TAG, "Unable to save history", e) })
} }
fun saveBookmark(bookmark: LibkiwixBookmarkItem) { @Suppress("InjectDispatcher", "TooGenericExceptionCaught")
saveBookmarkDisposable = suspend fun saveBookmark(bookmark: LibkiwixBookmarkItem) {
dataSource.saveBookmark(bookmark) withContext(Dispatchers.IO) {
.subscribe({}, { e -> Log.e(TAG, "Unable to save bookmark", e) }) try {
dataSource.saveBookmark(bookmark)
} catch (e: Exception) {
Log.e(TAG, "Unable to save bookmark", e)
}
}
} }
fun deleteBookmark(bookId: String, bookmarkUrl: String) { @Suppress("InjectDispatcher", "TooGenericExceptionCaught")
dataSource.deleteBookmark(bookId, bookmarkUrl) suspend fun deleteBookmark(bookId: String, bookmarkUrl: String) {
?.subscribe({}, { e -> Log.e(TAG, "Unable to delete bookmark", e) }) withContext(Dispatchers.IO) {
?: Log.e(TAG, "Unable to delete bookmark") try {
dataSource.deleteBookmark(bookId, bookmarkUrl)
} catch (e: Exception) {
Log.e(TAG, "Unable to delete bookmark", e)
}
}
} }
fun saveNote(note: NoteListItem) { fun saveNote(note: NoteListItem) {
@ -93,7 +104,6 @@ class MainRepositoryActions @Inject constructor(private val dataSource: DataSour
fun dispose() { fun dispose() {
saveHistoryDisposable?.dispose() saveHistoryDisposable?.dispose()
saveBookmarkDisposable?.dispose()
saveNoteDisposable?.dispose() saveNoteDisposable?.dispose()
deleteNoteDisposable?.dispose() deleteNoteDisposable?.dispose()
saveBookDisposable?.dispose() saveBookDisposable?.dispose()

View File

@ -21,11 +21,11 @@ package org.kiwix.kiwixmobile.core.page.bookmark.viewmodel
import io.mockk.clearAllMocks import io.mockk.clearAllMocks
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.reactivex.plugins.RxJavaPlugins import io.reactivex.Flowable
import io.reactivex.processors.PublishProcessor
import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.reactive.asPublisher
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
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
@ -45,7 +45,6 @@ import org.kiwix.kiwixmobile.core.reader.ZimReaderSource
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
import org.kiwix.sharedFunctions.InstantExecutorExtension import org.kiwix.sharedFunctions.InstantExecutorExtension
import org.kiwix.sharedFunctions.setScheduler
import java.util.UUID import java.util.UUID
@ExtendWith(InstantExecutorExtension::class) @ExtendWith(InstantExecutorExtension::class)
@ -58,13 +57,8 @@ internal class BookmarkViewModelTest {
private lateinit var viewModel: BookmarkViewModel private lateinit var viewModel: BookmarkViewModel
private val itemsFromDb: PublishProcessor<List<Page>> = private val itemsFromDb: MutableStateFlow<List<Page>> =
PublishProcessor.create() MutableStateFlow(emptyList())
init {
setScheduler(Schedulers.trampoline())
RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() }
}
@BeforeEach @BeforeEach
fun init() { fun init() {
@ -72,8 +66,10 @@ internal class BookmarkViewModelTest {
every { zimReaderContainer.id } returns "id" every { zimReaderContainer.id } returns "id"
every { zimReaderContainer.name } returns "zimName" every { zimReaderContainer.name } returns "zimName"
every { sharedPreferenceUtil.showBookmarksAllBooks } returns true every { sharedPreferenceUtil.showBookmarksAllBooks } returns true
every { libkiwixBookMarks.bookmarks() } returns itemsFromDb.distinctUntilChanged() every { libkiwixBookMarks.bookmarks() } returns itemsFromDb
every { libkiwixBookMarks.pages() } returns libkiwixBookMarks.bookmarks() every { libkiwixBookMarks.pages() } returns Flowable.fromPublisher(
libkiwixBookMarks.bookmarks().asPublisher()
)
viewModel = viewModel =
BookmarkViewModel(libkiwixBookMarks, zimReaderContainer, sharedPreferenceUtil).apply { BookmarkViewModel(libkiwixBookMarks, zimReaderContainer, sharedPreferenceUtil).apply {
alertDialogShower = dialogShower alertDialogShower = dialogShower