Fixed: Move canReadFile() method to IO thread.

* Moved the file readability check to the IO thread to prevent ANR.
* Refactored the code to accommodate this change.
This commit is contained in:
MohitMaliFtechiz 2024-10-16 16:30:05 +05:30 committed by MohitMaliFtechiz
parent 5ceb3ceccb
commit 33bd3397e3
18 changed files with 170 additions and 100 deletions

View File

@ -31,7 +31,11 @@ import android.view.View.VISIBLE
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.drawerlayout.widget.DrawerLayout import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.lifecycleScope
import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomnavigation.BottomNavigationView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.kiwix.kiwixmobile.R import org.kiwix.kiwixmobile.R
import org.kiwix.kiwixmobile.cachedComponent import org.kiwix.kiwixmobile.cachedComponent
import org.kiwix.kiwixmobile.core.R.anim import org.kiwix.kiwixmobile.core.R.anim
@ -219,23 +223,27 @@ class KiwixReaderFragment : CoreReaderFragment() {
) { ) {
val settings = requireActivity().getSharedPreferences(SharedPreferenceUtil.PREF_KIWIX_MOBILE, 0) val settings = requireActivity().getSharedPreferences(SharedPreferenceUtil.PREF_KIWIX_MOBILE, 0)
val zimReaderSource = fromDatabaseValue(settings.getString(TAG_CURRENT_FILE, null)) val zimReaderSource = fromDatabaseValue(settings.getString(TAG_CURRENT_FILE, null))
lifecycleScope.launch {
if (zimReaderSource != null && zimReaderSource.canOpenInLibkiwix()) { val canOpenInLibkiwix = withContext(Dispatchers.IO) {
if (zimReaderContainer?.zimReaderSource == null) { zimReaderSource?.canOpenInLibkiwix()
openZimFile(zimReaderSource)
Log.d(
TAG_KIWIX,
"Kiwix normal start, Opened last used zimFile: -> ${zimReaderSource.toDatabase()}"
)
} else {
zimReaderContainer?.zimFileReader?.let(::setUpBookmarks)
} }
} else { if (zimReaderSource != null && canOpenInLibkiwix == true) {
getCurrentWebView()?.snack(string.zim_not_opened) if (zimReaderContainer?.zimReaderSource == null) {
exitBook() // hide the options for zim file to avoid unexpected UI behavior openZimFile(zimReaderSource)
return // book not found so don't need to restore the tabs for this file Log.d(
TAG_KIWIX,
"Kiwix normal start, Opened last used zimFile: -> ${zimReaderSource.toDatabase()}"
)
} else {
zimReaderContainer?.zimFileReader?.let(::setUpBookmarks)
}
} else {
getCurrentWebView()?.snack(string.zim_not_opened)
exitBook() // hide the options for zim file to avoid unexpected UI behavior
return@launch // book not found so don't need to restore the tabs for this file
}
restoreTabs(zimArticles, zimPositions, currentTab)
} }
restoreTabs(zimArticles, zimPositions, currentTab)
} }
@Throws(IllegalArgumentException::class) @Throws(IllegalArgumentException::class)

View File

@ -19,11 +19,16 @@
package org.kiwix.kiwixmobile.zimManager.fileselectView.effects package org.kiwix.kiwixmobile.zimManager.fileselectView.effects
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.base.SideEffect import org.kiwix.kiwixmobile.core.base.SideEffect
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.navigate import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.navigate
import org.kiwix.kiwixmobile.core.extensions.toast import org.kiwix.kiwixmobile.core.extensions.toast
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.adapter.BooksOnDiskListItem import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.adapter.BooksOnDiskListItem
import org.kiwix.kiwixmobile.main.KiwixMainActivity
import org.kiwix.kiwixmobile.nav.destination.library.LocalLibraryFragmentDirections.actionNavigationLibraryToNavigationReader import org.kiwix.kiwixmobile.nav.destination.library.LocalLibraryFragmentDirections.actionNavigationLibraryToNavigationReader
data class OpenFileWithNavigation(private val bookOnDisk: BooksOnDiskListItem.BookOnDisk) : data class OpenFileWithNavigation(private val bookOnDisk: BooksOnDiskListItem.BookOnDisk) :
@ -31,14 +36,19 @@ data class OpenFileWithNavigation(private val bookOnDisk: BooksOnDiskListItem.Bo
override fun invokeWith(activity: AppCompatActivity) { override fun invokeWith(activity: AppCompatActivity) {
val zimReaderSource = bookOnDisk.zimReaderSource val zimReaderSource = bookOnDisk.zimReaderSource
if (!zimReaderSource.canOpenInLibkiwix()) { (activity as KiwixMainActivity).lifecycleScope.launch {
activity.toast(R.string.error_file_not_found) val canOpenInLibkiwix = withContext(Dispatchers.IO) {
} else { zimReaderSource.canOpenInLibkiwix()
activity.navigate( }
actionNavigationLibraryToNavigationReader().apply { if (!canOpenInLibkiwix) {
zimFileUri = zimReaderSource.toDatabase() activity.toast(R.string.error_file_not_found)
} } else {
) activity.navigate(
actionNavigationLibraryToNavigationReader().apply {
zimFileUri = zimReaderSource.toDatabase()
}
)
}
} }
} }
} }

View File

@ -22,6 +22,9 @@ object Libs {
"org.jetbrains.kotlinx:kotlinx-coroutines-android:" + "org.jetbrains.kotlinx:kotlinx-coroutines-android:" +
Versions.org_jetbrains_kotlinx_kotlinx_coroutines Versions.org_jetbrains_kotlinx_kotlinx_coroutines
const val kotlinx_coroutines_rx3: String =
"org.jetbrains.kotlinx:kotlinx-coroutines-rx3:" + Versions.kotlinx_coroutines_rx3
/** /**
* https://github.com/Kotlin/kotlinx.coroutines * https://github.com/Kotlin/kotlinx.coroutines
*/ */

View File

@ -16,6 +16,8 @@ object Versions {
const val org_jetbrains_kotlinx_kotlinx_coroutines: String = "1.8.1" const val org_jetbrains_kotlinx_kotlinx_coroutines: String = "1.8.1"
const val kotlinx_coroutines_rx3: String = "1.3.9"
const val androidx_test_espresso: String = "3.5.1" const val androidx_test_espresso: String = "3.5.1"
const val tracing: String = "1.1.0" const val tracing: String = "1.1.0"

View File

@ -61,5 +61,6 @@ dependencies {
implementation(Libs.webkit) implementation(Libs.webkit)
testImplementation(Libs.kotlinx_coroutines_test) testImplementation(Libs.kotlinx_coroutines_test)
implementation(Libs.kotlinx_coroutines_android) implementation(Libs.kotlinx_coroutines_android)
implementation(Libs.kotlinx_coroutines_rx3)
implementation(Libs.zxing) implementation(Libs.zxing)
} }

View File

@ -12,7 +12,7 @@
<ID>LongParameterList:MainMenu.kt$MainMenu$( private val activity: Activity, zimFileReader: ZimFileReader?, menu: Menu, webViews: MutableList&lt;KiwixWebView>, urlIsValid: Boolean, disableReadAloud: Boolean = false, disableTabs: Boolean = false, private val menuClickListener: MenuClickListener )</ID> <ID>LongParameterList:MainMenu.kt$MainMenu$( private val activity: Activity, zimFileReader: ZimFileReader?, menu: Menu, webViews: MutableList&lt;KiwixWebView>, urlIsValid: Boolean, disableReadAloud: Boolean = false, disableTabs: Boolean = false, private val menuClickListener: MenuClickListener )</ID>
<ID>LongParameterList:MainMenu.kt$MainMenu.Factory$( menu: Menu, webViews: MutableList&lt;KiwixWebView>, urlIsValid: Boolean, menuClickListener: MenuClickListener, disableReadAloud: Boolean, disableTabs: Boolean )</ID> <ID>LongParameterList:MainMenu.kt$MainMenu.Factory$( menu: Menu, webViews: MutableList&lt;KiwixWebView>, urlIsValid: Boolean, menuClickListener: MenuClickListener, disableReadAloud: Boolean, disableTabs: Boolean )</ID>
<ID>LongParameterList:PageTestHelpers.kt$( bookmarkTitle: String = "bookmarkTitle", isSelected: Boolean = false, id: Long = 2, zimId: String = "zimId", zimName: String = "zimName", zimFilePath: String = "zimFilePath", bookmarkUrl: String = "bookmarkUrl", favicon: String = "favicon" )</ID> <ID>LongParameterList:PageTestHelpers.kt$( bookmarkTitle: String = "bookmarkTitle", isSelected: Boolean = false, id: Long = 2, zimId: String = "zimId", zimName: String = "zimName", zimFilePath: String = "zimFilePath", bookmarkUrl: String = "bookmarkUrl", favicon: String = "favicon" )</ID>
<ID>LongParameterList:Repository.kt$Repository$( @param:IO private val io: Scheduler, @param:MainThread private val mainThread: Scheduler, private val bookDao: NewBookDao, private val bookmarksDao: NewBookmarksDao, private val historyDao: HistoryDao, private val languageDao: NewLanguagesDao, private val recentSearchDao: NewRecentSearchDao, private val zimReaderContainer: ZimReaderContainer )</ID> <ID>LongParameterList:Repository.kt$Repository$( @param:IO private val ioThread: Scheduler, @param:MainThread private val mainThread: Scheduler, private val bookDao: NewBookDao, private val libkiwixBookmarks: LibkiwixBookmarks, private val historyRoomDao: HistoryRoomDao, private val notesRoomDao: NotesRoomDao, private val languageDao: NewLanguagesDao, private val recentSearchRoomDao: RecentSearchRoomDao, private val zimReaderContainer: ZimReaderContainer )</ID>
<ID>LongParameterList:ToolbarScrollingKiwixWebView.kt$ToolbarScrollingKiwixWebView$( context: Context, callback: WebViewCallback, attrs: AttributeSet, nonVideoView: ViewGroup, videoView: ViewGroup, webViewClient: CoreWebViewClient, private val toolbarView: View, private val bottomBarView: View, sharedPreferenceUtil: SharedPreferenceUtil, private val parentNavigationBar: View? = null )</ID> <ID>LongParameterList:ToolbarScrollingKiwixWebView.kt$ToolbarScrollingKiwixWebView$( context: Context, callback: WebViewCallback, attrs: AttributeSet, nonVideoView: ViewGroup, videoView: ViewGroup, webViewClient: CoreWebViewClient, private val toolbarView: View, private val bottomBarView: View, sharedPreferenceUtil: SharedPreferenceUtil, private val parentNavigationBar: View? = null )</ID>
<ID>MagicNumber:ArticleCount.kt$ArticleCount$3</ID> <ID>MagicNumber:ArticleCount.kt$ArticleCount$3</ID>
<ID>MagicNumber:CompatFindActionModeCallback.kt$CompatFindActionModeCallback$100</ID> <ID>MagicNumber:CompatFindActionModeCallback.kt$CompatFindActionModeCallback$100</ID>

View File

@ -29,10 +29,12 @@ import io.reactivex.subjects.BehaviorSubject
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.rx3.rxSingle
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
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.isCustomApp import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.isCustomApp
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.extensions.toast import org.kiwix.kiwixmobile.core.extensions.toast
import org.kiwix.kiwixmobile.core.page.adapter.Page import org.kiwix.kiwixmobile.core.page.adapter.Page
@ -68,8 +70,13 @@ 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")
private val bookmarkListBehaviour: BehaviorSubject<List<LibkiwixBookmarkItem>>? by lazy { private val bookmarkListBehaviour: BehaviorSubject<List<LibkiwixBookmarkItem>>? by lazy {
BehaviorSubject.createDefault(getBookmarksList()) BehaviorSubject.create<List<LibkiwixBookmarkItem>>().also { subject ->
rxSingle { getBookmarksList() }
.subscribeOn(io.reactivex.rxjava3.schedulers.Schedulers.io())
.subscribe(subject::onNext, subject::onError)
}
} }
private val bookmarksFolderPath: String by lazy { private val bookmarksFolderPath: String by lazy {
@ -113,7 +120,7 @@ class LibkiwixBookmarks @Inject constructor(
override fun deletePages(pagesToDelete: List<Page>) = override fun deletePages(pagesToDelete: List<Page>) =
deleteBookmarks(pagesToDelete as List<LibkiwixBookmarkItem>) deleteBookmarks(pagesToDelete as List<LibkiwixBookmarkItem>)
fun getCurrentZimBookmarksUrl(zimFileReader: ZimFileReader?): List<String> { suspend fun getCurrentZimBookmarksUrl(zimFileReader: ZimFileReader?): List<String> {
return zimFileReader?.let { reader -> return zimFileReader?.let { reader ->
getBookmarksList() getBookmarksList()
.filter { it.zimId == reader.id } .filter { it.zimId == reader.id }
@ -134,7 +141,7 @@ class LibkiwixBookmarks @Inject constructor(
* during data migration, where data is written to the file only once after all bookmarks * during data migration, where data is written to the file only once after all bookmarks
* have been added to libkiwix to optimize the process. * have been added to libkiwix to optimize the process.
*/ */
fun saveBookmark( suspend fun saveBookmark(
libkiwixBookmarkItem: LibkiwixBookmarkItem, libkiwixBookmarkItem: LibkiwixBookmarkItem,
shouldWriteBookmarkToFile: Boolean = true shouldWriteBookmarkToFile: Boolean = true
) { ) {
@ -241,7 +248,7 @@ class LibkiwixBookmarks @Inject constructor(
} }
@Suppress("ReturnCount") @Suppress("ReturnCount")
private fun getBookmarksList(): List<LibkiwixBookmarkItem> { private suspend fun getBookmarksList(): List<LibkiwixBookmarkItem> {
if (!bookmarksChanged && bookmarkList.isNotEmpty()) { if (!bookmarksChanged && bookmarkList.isNotEmpty()) {
// No changes, return the cached data // No changes, return the cached data
return bookmarkList.distinctBy(LibkiwixBookmarkItem::bookmarkUrl) return bookmarkList.distinctBy(LibkiwixBookmarkItem::bookmarkUrl)
@ -290,7 +297,7 @@ class LibkiwixBookmarks @Inject constructor(
} }
@Suppress("NestedBlockDepth") @Suppress("NestedBlockDepth")
private fun deleteDuplicateBookmarks() { private suspend fun deleteDuplicateBookmarks() {
bookmarkList.groupBy { it.bookmarkUrl to it.zimReaderSource } bookmarkList.groupBy { it.bookmarkUrl to it.zimReaderSource }
.filter { it.value.size > 1 } .filter { it.value.size > 1 }
.forEach { (_, value) -> .forEach { (_, value) ->
@ -319,7 +326,7 @@ class LibkiwixBookmarks @Inject constructor(
} }
} }
private fun getZimFileReaderFromBookmark( private suspend fun getZimFileReaderFromBookmark(
bookmarkItem: LibkiwixBookmarkItem, bookmarkItem: LibkiwixBookmarkItem,
coreApp: CoreApp coreApp: CoreApp
): ZimFileReader? { ): ZimFileReader? {
@ -342,7 +349,7 @@ class LibkiwixBookmarks @Inject constructor(
} }
} }
private fun isBookMarkExist(libkiwixBookmarkItem: LibkiwixBookmarkItem): Boolean = private suspend fun isBookMarkExist(libkiwixBookmarkItem: LibkiwixBookmarkItem): Boolean =
getBookmarksList() getBookmarksList()
.any { .any {
it.url == libkiwixBookmarkItem.bookmarkUrl && it.url == libkiwixBookmarkItem.bookmarkUrl &&
@ -368,7 +375,11 @@ class LibkiwixBookmarks @Inject constructor(
} }
private fun updateFlowableBookmarkList() { private fun updateFlowableBookmarkList() {
bookmarkListBehaviour?.onNext(getBookmarksList()) bookmarkListBehaviour?.let { subject ->
rxSingle { getBookmarksList() }
.subscribeOn(io.reactivex.rxjava3.schedulers.Schedulers.io())
.subscribe(subject::onNext, subject::onError)
}
} }
// 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.
@ -408,7 +419,7 @@ class LibkiwixBookmarks @Inject constructor(
}.first { !it.isFileExist() } }.first { !it.isFileExist() }
} }
fun importBookmarks(bookmarkFile: File) { suspend fun importBookmarks(bookmarkFile: File) {
// Create a temporary library manager to import the bookmarks. // Create a temporary library manager to import the bookmarks.
val tempLibrary = Library() val tempLibrary = Library()
Manager(tempLibrary).apply { Manager(tempLibrary).apply {
@ -426,7 +437,7 @@ class LibkiwixBookmarks @Inject constructor(
sharedPreferenceUtil.context.toast(R.string.bookmark_imported_message) sharedPreferenceUtil.context.toast(R.string.bookmark_imported_message)
if (bookmarkFile.exists()) { if (bookmarkFile.exists()) {
bookmarkFile.delete() bookmarkFile.deleteFile()
} }
} }

View File

@ -21,6 +21,7 @@ import io.objectbox.Box
import io.objectbox.kotlin.inValues import io.objectbox.kotlin.inValues
import io.objectbox.kotlin.query import io.objectbox.kotlin.query
import io.objectbox.query.QueryBuilder import io.objectbox.query.QueryBuilder
import kotlinx.coroutines.rx3.rxSingle
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.Book import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book
@ -31,23 +32,29 @@ import javax.inject.Inject
class NewBookDao @Inject constructor(private val box: Box<BookOnDiskEntity>) { class NewBookDao @Inject constructor(private val box: Box<BookOnDiskEntity>) {
fun books() = box.asFlowable() fun books() = box.asFlowable()
.map { books -> .flatMap { books ->
books.map { bookOnDiskEntity -> io.reactivex.rxjava3.core.Flowable.fromIterable(books)
bookOnDiskEntity.file.let { file -> .flatMapSingle { bookOnDiskEntity ->
// set zimReaderSource for previously saved books val file = bookOnDiskEntity.file
val zimReaderSource = ZimReaderSource(file) val zimReaderSource = ZimReaderSource(file)
if (zimReaderSource.canOpenInLibkiwix()) {
bookOnDiskEntity.zimReaderSource = zimReaderSource rxSingle { zimReaderSource.canOpenInLibkiwix() }
} .map { canOpen ->
if (canOpen) {
bookOnDiskEntity.zimReaderSource = zimReaderSource
}
bookOnDiskEntity
}
.onErrorReturn { bookOnDiskEntity }
} }
bookOnDiskEntity .toList()
} .toFlowable()
} }
.doOnNext { removeBooksThatDoNotExist(it.toMutableList()) } .doOnNext { removeBooksThatDoNotExist(it.toMutableList()) }
.map { books -> books.filter { it.zimReaderSource.exists() } } .map { books -> books.filter { it.zimReaderSource.exists() } }
.map { it.map(::BookOnDisk) } .map { it.map(::BookOnDisk) }
fun getBooks() = box.all.map { bookOnDiskEntity -> suspend fun getBooks() = box.all.map { bookOnDiskEntity ->
bookOnDiskEntity.file.let { file -> bookOnDiskEntity.file.let { file ->
// set zimReaderSource for previously saved books // set zimReaderSource for previously saved books
val zimReaderSource = ZimReaderSource(file) val zimReaderSource = ZimReaderSource(file)

View File

@ -41,9 +41,11 @@ interface DataSource {
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(): Flowable<List<LibkiwixBookmarkItem>>
fun getCurrentZimBookmarksUrl(): Single<List<String>> fun getCurrentZimBookmarksUrl(): io.reactivex.rxjava3.core.Single<List<String>>
fun saveBookmark(libkiwixBookmarkItem: LibkiwixBookmarkItem):
io.reactivex.rxjava3.core.Completable
fun saveBookmark(libkiwixBookmarkItem: LibkiwixBookmarkItem): Completable
fun deleteBookmarks(bookmarks: List<LibkiwixBookmarkItem>): Completable fun deleteBookmarks(bookmarks: List<LibkiwixBookmarkItem>): Completable
fun deleteBookmark(bookId: String, bookmarkUrl: String): Completable? fun deleteBookmark(bookId: String, bookmarkUrl: String): Completable?
fun booksOnDiskAsListItems(): Flowable<List<BooksOnDiskListItem>> fun booksOnDiskAsListItems(): Flowable<List<BooksOnDiskListItem>>

View File

@ -21,7 +21,8 @@ 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 io.reactivex.Single 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
@ -47,10 +48,9 @@ import javax.inject.Singleton
* A central repository of data which should provide the presenters with the required data. * A central repository of data which should provide the presenters with the required data.
*/ */
@Suppress("LongParameterList")
@Singleton @Singleton
class Repository @Inject internal constructor( class Repository @Inject internal constructor(
@param:IO private val io: Scheduler, @param:IO private val ioThread: Scheduler,
@param:MainThread private val mainThread: Scheduler, @param:MainThread private val mainThread: Scheduler,
private val bookDao: NewBookDao, private val bookDao: NewBookDao,
private val libkiwixBookmarks: LibkiwixBookmarks, private val libkiwixBookmarks: LibkiwixBookmarks,
@ -64,7 +64,7 @@ class Repository @Inject internal constructor(
override fun getLanguageCategorizedBooks() = override fun getLanguageCategorizedBooks() =
booksOnDiskAsListItems() booksOnDiskAsListItems()
.first(emptyList()) .first(emptyList())
.subscribeOn(io) .subscribeOn(ioThread)
.observeOn(mainThread) .observeOn(mainThread)
override fun booksOnDiskAsListItems(): Flowable<List<BooksOnDiskListItem>> = bookDao.books() override fun booksOnDiskAsListItems(): Flowable<List<BooksOnDiskListItem>> = bookDao.books()
@ -91,60 +91,60 @@ class Repository @Inject internal constructor(
override fun saveBooks(books: List<BookOnDisk>) = override fun saveBooks(books: List<BookOnDisk>) =
Completable.fromAction { bookDao.insert(books) } Completable.fromAction { bookDao.insert(books) }
.subscribeOn(io) .subscribeOn(ioThread)
override fun saveBook(book: BookOnDisk) = override fun saveBook(book: BookOnDisk) =
Completable.fromAction { bookDao.insert(listOf(book)) } Completable.fromAction { bookDao.insert(listOf(book)) }
.subscribeOn(io) .subscribeOn(ioThread)
override fun saveLanguages(languages: List<Language>) = override fun saveLanguages(languages: List<Language>) =
Completable.fromAction { languageDao.insert(languages) } Completable.fromAction { languageDao.insert(languages) }
.subscribeOn(io) .subscribeOn(ioThread)
override fun saveHistory(history: HistoryItem) = override fun saveHistory(history: HistoryItem) =
Completable.fromAction { historyRoomDao.saveHistory(history) } Completable.fromAction { historyRoomDao.saveHistory(history) }
.subscribeOn(io) .subscribeOn(ioThread)
override fun deleteHistory(historyList: List<HistoryListItem>) = override fun deleteHistory(historyList: List<HistoryListItem>) =
Completable.fromAction { Completable.fromAction {
historyRoomDao.deleteHistory(historyList.filterIsInstance(HistoryItem::class.java)) historyRoomDao.deleteHistory(historyList.filterIsInstance(HistoryItem::class.java))
} }
.subscribeOn(io) .subscribeOn(ioThread)
override fun clearHistory() = Completable.fromAction { override fun clearHistory() = Completable.fromAction {
historyRoomDao.deleteAllHistory() historyRoomDao.deleteAllHistory()
recentSearchRoomDao.deleteSearchHistory() recentSearchRoomDao.deleteSearchHistory()
}.subscribeOn(io) }.subscribeOn(ioThread)
override fun getBookmarks() = override fun getBookmarks() =
libkiwixBookmarks.bookmarks() as Flowable<List<LibkiwixBookmarkItem>> libkiwixBookmarks.bookmarks() as Flowable<List<LibkiwixBookmarkItem>>
override fun getCurrentZimBookmarksUrl() = override fun getCurrentZimBookmarksUrl() =
Single.just(libkiwixBookmarks.getCurrentZimBookmarksUrl(zimReaderContainer.zimFileReader)) rxSingle {
.subscribeOn(io) libkiwixBookmarks.getCurrentZimBookmarksUrl(zimReaderContainer.zimFileReader)
.observeOn(mainThread) }.subscribeOn(io.reactivex.rxjava3.schedulers.Schedulers.io())
override fun saveBookmark(libkiwixBookmarkItem: LibkiwixBookmarkItem) = override fun saveBookmark(libkiwixBookmarkItem: LibkiwixBookmarkItem) =
Completable.fromAction { libkiwixBookmarks.saveBookmark(libkiwixBookmarkItem) } rxCompletable { libkiwixBookmarks.saveBookmark(libkiwixBookmarkItem) }
.subscribeOn(io) .subscribeOn(io.reactivex.rxjava3.schedulers.Schedulers.io())
override fun deleteBookmarks(bookmarks: List<LibkiwixBookmarkItem>) = override fun deleteBookmarks(bookmarks: List<LibkiwixBookmarkItem>) =
Completable.fromAction { libkiwixBookmarks.deleteBookmarks(bookmarks) } Completable.fromAction { libkiwixBookmarks.deleteBookmarks(bookmarks) }
.subscribeOn(io) .subscribeOn(ioThread)
override fun deleteBookmark(bookId: String, bookmarkUrl: String): Completable? = override fun deleteBookmark(bookId: String, bookmarkUrl: String): Completable? =
Completable.fromAction { libkiwixBookmarks.deleteBookmark(bookId, bookmarkUrl) } Completable.fromAction { libkiwixBookmarks.deleteBookmark(bookId, bookmarkUrl) }
.subscribeOn(io) .subscribeOn(ioThread)
override fun saveNote(noteListItem: NoteListItem): Completable = override fun saveNote(noteListItem: NoteListItem): Completable =
Completable.fromAction { notesRoomDao.saveNote(noteListItem) } Completable.fromAction { notesRoomDao.saveNote(noteListItem) }
.subscribeOn(io) .subscribeOn(ioThread)
override fun deleteNotes(noteList: List<NoteListItem>) = override fun deleteNotes(noteList: List<NoteListItem>) =
Completable.fromAction { notesRoomDao.deleteNotes(noteList) } Completable.fromAction { notesRoomDao.deleteNotes(noteList) }
.subscribeOn(io) .subscribeOn(ioThread)
override fun deleteNote(noteTitle: String): Completable = override fun deleteNote(noteTitle: String): Completable =
Completable.fromAction { notesRoomDao.deleteNote(noteTitle) } Completable.fromAction { notesRoomDao.deleteNote(noteTitle) }
.subscribeOn(io) .subscribeOn(ioThread)
} }

View File

@ -25,6 +25,8 @@ import android.os.Process
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import org.kiwix.kiwixmobile.core.CoreApp.Companion.coreComponent import org.kiwix.kiwixmobile.core.CoreApp.Companion.coreComponent
import org.kiwix.kiwixmobile.core.base.BaseActivity import org.kiwix.kiwixmobile.core.base.BaseActivity
import org.kiwix.kiwixmobile.core.compat.CompatHelper.Companion.getPackageInformation import org.kiwix.kiwixmobile.core.compat.CompatHelper.Companion.getPackageInformation
@ -87,7 +89,9 @@ open class ErrorActivity : BaseActivity() {
private fun setupReportButton() { private fun setupReportButton() {
activityKiwixErrorBinding?.reportButton?.setOnClickListener { activityKiwixErrorBinding?.reportButton?.setOnClickListener {
sendEmailLauncher.launch(Intent.createChooser(emailIntent(), "Send email...")) lifecycleScope.launch {
sendEmailLauncher.launch(Intent.createChooser(emailIntent(), "Send email..."))
}
} }
} }
@ -96,7 +100,7 @@ open class ErrorActivity : BaseActivity() {
restartApp() restartApp()
} }
private fun emailIntent(): Intent { private suspend fun emailIntent(): Intent {
val emailBody = buildBody() val emailBody = buildBody()
return Intent(Intent.ACTION_SEND).apply { return Intent(Intent.ACTION_SEND).apply {
type = "text/plain" type = "text/plain"
@ -122,7 +126,7 @@ open class ErrorActivity : BaseActivity() {
} }
} }
private fun buildBody(): String = """ private suspend fun buildBody(): String = """
$initialBody $initialBody
${if (activityKiwixErrorBinding?.allowCrash?.isChecked == true && exception != null) exceptionDetails() else ""} ${if (activityKiwixErrorBinding?.allowCrash?.isChecked == true && exception != null) exceptionDetails() else ""}
@ -139,7 +143,7 @@ open class ErrorActivity : BaseActivity() {
${exception?.let(::toStackTraceString)} ${exception?.let(::toStackTraceString)}
""".trimIndent() """.trimIndent()
private fun zimFiles(): String { private suspend fun zimFiles(): String {
val allZimFiles = bookDao.getBooks().joinToString { val allZimFiles = bookDao.getBooks().joinToString {
""" """
${it.book.title}: ${it.book.title}:

View File

@ -41,10 +41,6 @@ fun File.totalSpace(): Long = runBlocking {
} }
} }
fun File.canReadFile(): Boolean = runBlocking { suspend fun File.canReadFile(): Boolean = withContext(Dispatchers.IO) { canRead() }
withContext(Dispatchers.IO) {
canRead()
}
}
suspend fun File.deleteFile(): Boolean = withContext(Dispatchers.IO) { delete() } suspend fun File.deleteFile(): Boolean = withContext(Dispatchers.IO) { delete() }

View File

@ -80,6 +80,7 @@ import androidx.core.widget.ContentLoadingProgressBar
import androidx.drawerlayout.widget.DrawerLayout import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -93,6 +94,9 @@ import io.reactivex.Flowable
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
import io.reactivex.processors.BehaviorProcessor import io.reactivex.processors.BehaviorProcessor
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONException import org.json.JSONException
import org.kiwix.kiwixmobile.core.BuildConfig import org.kiwix.kiwixmobile.core.BuildConfig
@ -1641,16 +1645,21 @@ abstract class CoreReaderFragment :
fun openZimFile(zimReaderSource: ZimReaderSource, isCustomApp: Boolean = false) { fun openZimFile(zimReaderSource: ZimReaderSource, isCustomApp: Boolean = false) {
if (hasPermission(Manifest.permission.READ_EXTERNAL_STORAGE) || isCustomApp) { if (hasPermission(Manifest.permission.READ_EXTERNAL_STORAGE) || isCustomApp) {
if (zimReaderSource.canOpenInLibkiwix()) { lifecycleScope.launch {
// Show content if there is `Open Library` button showing val canOpenInLibkiwix = withContext(Dispatchers.IO) {
// and we are opening the ZIM file zimReaderSource.canOpenInLibkiwix()
reopenBook() }
openAndSetInContainer(zimReaderSource) if (canOpenInLibkiwix) {
updateTitle() // Show content if there is `Open Library` button showing
} else { // and we are opening the ZIM file
exitBook() reopenBook()
Log.w(TAG_KIWIX, "ZIM file doesn't exist at " + zimReaderSource.toDatabase()) openAndSetInContainer(zimReaderSource)
requireActivity().toast(R.string.error_file_not_found, Toast.LENGTH_LONG) updateTitle()
} else {
exitBook()
Log.w(TAG_KIWIX, "ZIM file doesn't exist at " + zimReaderSource.toDatabase())
requireActivity().toast(R.string.error_file_not_found, Toast.LENGTH_LONG)
}
} }
} else { } else {
this.zimReaderSource = zimReaderSource this.zimReaderSource = zimReaderSource

View File

@ -17,13 +17,13 @@
*/ */
package org.kiwix.kiwixmobile.core.main package org.kiwix.kiwixmobile.core.main
import org.kiwix.kiwixmobile.core.utils.files.Log
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
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
import org.kiwix.kiwixmobile.core.page.bookmark.adapter.LibkiwixBookmarkItem import org.kiwix.kiwixmobile.core.page.bookmark.adapter.LibkiwixBookmarkItem
import org.kiwix.kiwixmobile.core.page.history.adapter.HistoryListItem.HistoryItem import org.kiwix.kiwixmobile.core.page.history.adapter.HistoryListItem.HistoryItem
import org.kiwix.kiwixmobile.core.page.notes.adapter.NoteListItem import org.kiwix.kiwixmobile.core.page.notes.adapter.NoteListItem
import org.kiwix.kiwixmobile.core.utils.files.Log
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.adapter.BooksOnDiskListItem.BookOnDisk import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.adapter.BooksOnDiskListItem.BookOnDisk
import javax.inject.Inject import javax.inject.Inject
@ -32,7 +32,7 @@ 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: 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

View File

@ -62,7 +62,7 @@ class ZimReaderSource(
} }
} }
fun canOpenInLibkiwix(): Boolean { suspend fun canOpenInLibkiwix(): Boolean {
return when { return when {
file?.canReadFile() == true -> true file?.canReadFile() == true -> true
assetFileDescriptorList?.get(0)?.parcelFileDescriptor?.fd assetFileDescriptorList?.get(0)?.parcelFileDescriptor?.fd
@ -72,7 +72,7 @@ class ZimReaderSource(
} }
} }
fun createArchive(): Archive? { suspend fun createArchive(): Archive? {
if (canOpenInLibkiwix()) { if (canOpenInLibkiwix()) {
return when { return when {
file != null -> Archive(file.canonicalPath) file != null -> Archive(file.canonicalPath)

View File

@ -40,6 +40,9 @@ import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import eu.mhutti1.utils.storage.StorageDevice import eu.mhutti1.utils.storage.StorageDevice
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.kiwix.kiwixmobile.core.CoreApp.Companion.coreComponent import org.kiwix.kiwixmobile.core.CoreApp.Companion.coreComponent
import org.kiwix.kiwixmobile.core.CoreApp.Companion.instance import org.kiwix.kiwixmobile.core.CoreApp.Companion.instance
import org.kiwix.kiwixmobile.core.DarkModeConfig import org.kiwix.kiwixmobile.core.DarkModeConfig
@ -403,7 +406,9 @@ abstract class CorePrefsFragment :
createTempFile(contentResolver.openInputStream(uri)).apply { createTempFile(contentResolver.openInputStream(uri)).apply {
if (isValidXmlFile(this)) { if (isValidXmlFile(this)) {
libkiwixBookmarks?.importBookmarks(this) CoroutineScope(Dispatchers.IO).launch {
libkiwixBookmarks?.importBookmarks(this@apply)
}
} else { } else {
activity.toast( activity.toast(
resources.getString(R.string.error_invalid_bookmark_file), resources.getString(R.string.error_invalid_bookmark_file),

View File

@ -19,8 +19,11 @@
package org.kiwix.kiwixmobile.core.utils package org.kiwix.kiwixmobile.core.utils
import android.app.Activity import android.app.Activity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import org.kiwix.kiwixmobile.core.dao.NewBookDao import org.kiwix.kiwixmobile.core.dao.NewBookDao
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.isCustomApp import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.isCustomApp
import org.kiwix.kiwixmobile.core.main.CoreMainActivity
import javax.inject.Inject import javax.inject.Inject
const val THREE_DAYS_IN_MILLISECONDS = 3 * 24 * 60 * 60 * 1000L const val THREE_DAYS_IN_MILLISECONDS = 3 * 24 * 60 * 60 * 1000L
@ -42,15 +45,19 @@ class DonationDialogHandler @Inject constructor(
val currentMilliSeconds = System.currentTimeMillis() val currentMilliSeconds = System.currentTimeMillis()
val lastPopupMillis = sharedPreferenceUtil.lastDonationPopupShownInMilliSeconds val lastPopupMillis = sharedPreferenceUtil.lastDonationPopupShownInMilliSeconds
val timeDifference = currentMilliSeconds - lastPopupMillis val timeDifference = currentMilliSeconds - lastPopupMillis
if (shouldShowInitialPopup(lastPopupMillis) || timeDifference >= THREE_MONTHS_IN_MILLISECONDS) { (activity as CoreMainActivity).lifecycleScope.launch {
if (isZimFilesAvailableInLibrary() && isTimeToShowDonation(currentMilliSeconds)) { if (shouldShowInitialPopup(lastPopupMillis) ||
showDonationDialogCallback?.showDonationDialog() timeDifference >= THREE_MONTHS_IN_MILLISECONDS
resetDonateLater() ) {
if (isZimFilesAvailableInLibrary() && isTimeToShowDonation(currentMilliSeconds)) {
showDonationDialogCallback?.showDonationDialog()
resetDonateLater()
}
} }
} }
} }
private fun shouldShowInitialPopup(lastPopupMillis: Long): Boolean = private suspend fun shouldShowInitialPopup(lastPopupMillis: Long): Boolean =
lastPopupMillis == 0L && isZimFilesAvailableInLibrary() lastPopupMillis == 0L && isZimFilesAvailableInLibrary()
private fun isTimeToShowDonation(currentMillis: Long): Boolean = private fun isTimeToShowDonation(currentMillis: Long): Boolean =
@ -63,7 +70,7 @@ class DonationDialogHandler @Inject constructor(
return timeDifference >= THREE_DAYS_IN_MILLISECONDS return timeDifference >= THREE_DAYS_IN_MILLISECONDS
} }
fun isZimFilesAvailableInLibrary(): Boolean = suspend fun isZimFilesAvailableInLibrary(): Boolean =
if (activity.isCustomApp()) true else newBookDao.getBooks().isNotEmpty() if (activity.isCustomApp()) true else newBookDao.getBooks().isNotEmpty()
fun updateLastDonationPopupShownTime() { fun updateLastDonationPopupShownTime() {

View File

@ -22,11 +22,14 @@ import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import org.kiwix.kiwixmobile.core.BuildConfig import org.kiwix.kiwixmobile.core.BuildConfig
import org.kiwix.kiwixmobile.core.compat.CompatHelper.Companion.getPackageInformation import org.kiwix.kiwixmobile.core.compat.CompatHelper.Companion.getPackageInformation
import org.kiwix.kiwixmobile.core.dao.NewBookDao import org.kiwix.kiwixmobile.core.dao.NewBookDao
import org.kiwix.kiwixmobile.core.di.ActivityScope import org.kiwix.kiwixmobile.core.di.ActivityScope
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.isCustomApp import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.isCustomApp
import org.kiwix.kiwixmobile.core.main.CoreMainActivity
import org.kiwix.kiwixmobile.core.utils.NetworkUtils import org.kiwix.kiwixmobile.core.utils.NetworkUtils
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
import javax.inject.Inject import javax.inject.Inject
@ -67,18 +70,20 @@ class RateDialogHandler @Inject constructor(
tempVisitCount = visitCounterPref?.count ?: 0 tempVisitCount = visitCounterPref?.count ?: 0
++tempVisitCount ++tempVisitCount
visitCounterPref?.count = tempVisitCount visitCounterPref?.count = tempVisitCount
if (shouldShowRateDialog() && NetworkUtils.isNetworkAvailable(activity)) { (activity as CoreMainActivity).lifecycleScope.launch {
showRateDialog(iconResId) if (shouldShowRateDialog() && NetworkUtils.isNetworkAvailable(activity)) {
showRateDialog(iconResId)
}
} }
} }
private fun shouldShowRateDialog(): Boolean { private suspend fun shouldShowRateDialog(): Boolean {
return tempVisitCount >= VISITS_REQUIRED_TO_SHOW_RATE_DIALOG && return tempVisitCount >= VISITS_REQUIRED_TO_SHOW_RATE_DIALOG &&
visitCounterPref?.noThanksState == false && isTwoWeekPassed() && visitCounterPref?.noThanksState == false && isTwoWeekPassed() &&
isZimFilesAvailableInLibrary() && !BuildConfig.DEBUG isZimFilesAvailableInLibrary() && !BuildConfig.DEBUG
} }
private fun isZimFilesAvailableInLibrary(): Boolean { private suspend fun isZimFilesAvailableInLibrary(): Boolean {
// If it is a custom app, return true since custom apps always have the ZIM file. // If it is a custom app, return true since custom apps always have the ZIM file.
if (activity.isCustomApp()) return true if (activity.isCustomApp()) return true
// For Kiwix app, check if there are ZIM files available in the library. // For Kiwix app, check if there are ZIM files available in the library.