Merge pull request #3937 from CalebKL/task/anr-fix-zimfile-reader

Task/anr fix zimfile reader
This commit is contained in:
Kelson 2025-01-27 14:17:51 +01:00 committed by GitHub
commit 8bd23938bc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 76 additions and 20 deletions

View File

@ -159,12 +159,25 @@ class KiwixReaderFragment : CoreReaderFragment() {
override fun openHomeScreen() { override fun openHomeScreen() {
Handler(Looper.getMainLooper()).postDelayed({ Handler(Looper.getMainLooper()).postDelayed({
if (webViewList.size == 0) { if (webViewList.size == 0) {
hideTabSwitcher() hideTabSwitcher(false)
} }
}, HIDE_TAB_SWITCHER_DELAY) }, HIDE_TAB_SWITCHER_DELAY)
} }
override fun hideTabSwitcher() { /**
* Hides the tab switcher and optionally closes the ZIM book based on the `shouldCloseZimBook` parameter.
*
* @param shouldCloseZimBook If `true`, the ZIM book will be closed, and the `ZimFileReader` will be set to `null`.
* If `false`, it skips setting the `ZimFileReader` to `null`. This is particularly useful when restoring tabs,
* as setting the `ZimFileReader` to `null` would require re-creating it, which is a resource-intensive operation,
* especially for large ZIM files.
*
* Refer to the following methods for more details:
* @See exitBook
* @see closeTab
* @see closeAllTabs
*/
override fun hideTabSwitcher(shouldCloseZimBook: Boolean) {
actionBar?.let { actionBar -> actionBar?.let { actionBar ->
actionBar.setDisplayShowTitleEnabled(true) actionBar.setDisplayShowTitleEnabled(true)
toolbar?.let { activity?.setupDrawerToggle(it, true) } toolbar?.let { activity?.setupDrawerToggle(it, true) }
@ -181,7 +194,7 @@ class KiwixReaderFragment : CoreReaderFragment() {
} }
mainMenu?.showWebViewOptions(true) mainMenu?.showWebViewOptions(true)
if (webViewList.isEmpty()) { if (webViewList.isEmpty()) {
exitBook() exitBook(shouldCloseZimBook)
} else { } else {
// Reset the top margin of web views to 0 to remove any previously set margin // Reset the top margin of web views to 0 to remove any previously set margin
// This ensures that the web views are displayed without any additional // This ensures that the web views are displayed without any additional

View File

@ -19,9 +19,12 @@
package org.kiwix.kiwixmobile.core package org.kiwix.kiwixmobile.core
import io.reactivex.Flowable import io.reactivex.Flowable
import io.reactivex.Single
import io.reactivex.functions.BiFunction import io.reactivex.functions.BiFunction
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.kiwix.kiwixmobile.core.dao.DownloadRoomDao import org.kiwix.kiwixmobile.core.dao.DownloadRoomDao
import org.kiwix.kiwixmobile.core.dao.LibkiwixBookmarks import org.kiwix.kiwixmobile.core.dao.LibkiwixBookmarks
import org.kiwix.kiwixmobile.core.downloader.model.DownloadModel import org.kiwix.kiwixmobile.core.downloader.model.DownloadModel
@ -45,7 +48,17 @@ class StorageObserver @Inject constructor(
): Flowable<List<BookOnDisk>> { ): Flowable<List<BookOnDisk>> {
return scanFiles(scanningProgressListener) return scanFiles(scanningProgressListener)
.withLatestFrom(downloadRoomDao.downloads(), BiFunction(::toFilesThatAreNotDownloading)) .withLatestFrom(downloadRoomDao.downloads(), BiFunction(::toFilesThatAreNotDownloading))
.map { it.mapNotNull(::convertToBookOnDisk) } .flatMapSingle { files ->
Single.create { emitter ->
CoroutineScope(Dispatchers.IO).launch {
try {
emitter.onSuccess(files.mapNotNull { convertToBookOnDisk(it) })
} catch (ignore: Exception) {
emitter.onError(ignore)
}
}
}
}
} }
private fun scanFiles(scanningProgressListener: ScanningProgressListener) = private fun scanFiles(scanningProgressListener: ScanningProgressListener) =
@ -57,7 +70,7 @@ class StorageObserver @Inject constructor(
private fun fileHasNoMatchingDownload(downloads: List<DownloadModel>, file: File) = private fun fileHasNoMatchingDownload(downloads: List<DownloadModel>, file: File) =
downloads.firstOrNull { file.absolutePath.endsWith(it.fileNameFromUrl) } == null downloads.firstOrNull { file.absolutePath.endsWith(it.fileNameFromUrl) } == null
private fun convertToBookOnDisk(file: File) = runBlocking { private suspend fun convertToBookOnDisk(file: File) =
zimReaderFactory.create(ZimReaderSource(file)) zimReaderFactory.create(ZimReaderSource(file))
?.let { zimFileReader -> ?.let { zimFileReader ->
BookOnDisk(zimFileReader).also { BookOnDisk(zimFileReader).also {
@ -67,4 +80,3 @@ class StorageObserver @Inject constructor(
} }
} }
} }
}

View File

@ -129,10 +129,10 @@ class LibkiwixBookmarks @Inject constructor(
} ?: emptyList() } ?: emptyList()
} }
fun bookmarkUrlsForCurrentBook(zimFileReader: ZimFileReader): Flowable<List<String>> = fun bookmarkUrlsForCurrentBook(zimId: String): Flowable<List<String>> =
flowableBookmarkList() flowableBookmarkList()
.map { bookmarksList -> .map { bookmarksList ->
bookmarksList.filter { it.zimId == zimFileReader.id } bookmarksList.filter { it.zimId == zimId }
.map(LibkiwixBookmarkItem::bookmarkUrl) .map(LibkiwixBookmarkItem::bookmarkUrl)
} }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())

View File

@ -860,7 +860,13 @@ abstract class CoreReaderFragment :
view?.startAnimation(AnimationUtils.loadAnimation(view.context, anim)) view?.startAnimation(AnimationUtils.loadAnimation(view.context, anim))
} }
protected open fun hideTabSwitcher() { /**
* @param shouldCloseZimBook A flag to indicate whether the ZIM book should be closed.
* - Default is `true`, which ensures normal behavior for most scenarios.
* - If `false`, the ZIM book is not closed. This is useful in cases where the user restores tabs,
* as closing the ZIM book would require reloading the ZIM file, which can be a resource-intensive operation.
*/
protected open fun hideTabSwitcher(shouldCloseZimBook: Boolean = true) {
actionBar?.apply { actionBar?.apply {
setDisplayShowTitleEnabled(true) setDisplayShowTitleEnabled(true)
} }
@ -1388,7 +1394,17 @@ abstract class CoreReaderFragment :
.setAction(R.string.undo) { undoButton -> .setAction(R.string.undo) { undoButton ->
undoButton.isEnabled = false undoButton.isEnabled = false
restoreDeletedTab(index) restoreDeletedTab(index)
}.show() }.addCallback(object : Snackbar.Callback() {
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
super.onDismissed(transientBottomBar, event)
// If the undo button is not clicked and no tabs are left, exit the book and
// clean up resources.
if (event != DISMISS_EVENT_ACTION && webViewList.isEmpty()) {
closeZimBook()
}
}
})
.show()
} }
openHomeScreen() openHomeScreen()
} }
@ -1399,19 +1415,23 @@ abstract class CoreReaderFragment :
mainMenu?.showBookSpecificMenuItems() mainMenu?.showBookSpecificMenuItems()
} }
protected fun exitBook() { protected fun exitBook(shouldCloseZimBook: Boolean = true) {
showNoBookOpenViews() showNoBookOpenViews()
bottomToolbar?.visibility = View.GONE bottomToolbar?.visibility = View.GONE
actionBar?.title = getString(R.string.reader) actionBar?.title = getString(R.string.reader)
contentFrame?.visibility = View.GONE contentFrame?.visibility = View.GONE
hideProgressBar() hideProgressBar()
mainMenu?.hideBookSpecificMenuItems() mainMenu?.hideBookSpecificMenuItems()
if (shouldCloseZimBook) {
closeZimBook() closeZimBook()
} }
}
fun closeZimBook() { fun closeZimBook() {
lifecycleScope.launch {
zimReaderContainer?.setZimReaderSource(null) zimReaderContainer?.setZimReaderSource(null)
} }
}
protected fun showProgressBarWithProgress(progress: Int) { protected fun showProgressBarWithProgress(progress: Int) {
progressBar?.apply { progressBar?.apply {
@ -1443,7 +1463,6 @@ abstract class CoreReaderFragment :
LinearLayout.LayoutParams.MATCH_PARENT LinearLayout.LayoutParams.MATCH_PARENT
) )
} }
zimReaderContainer?.setZimReaderSource(tempZimSourceForUndo)
webViewList.add(index, it) webViewList.add(index, it)
tabsAdapter?.notifyDataSetChanged() tabsAdapter?.notifyDataSetChanged()
snackBarRoot?.let { root -> snackBarRoot?.let { root ->
@ -1800,7 +1819,7 @@ abstract class CoreReaderFragment :
protected fun setUpBookmarks(zimFileReader: ZimFileReader) { protected fun setUpBookmarks(zimFileReader: ZimFileReader) {
safeDispose() safeDispose()
bookmarkingDisposable = Flowable.combineLatest( bookmarkingDisposable = Flowable.combineLatest(
libkiwixBookmarks?.bookmarkUrlsForCurrentBook(zimFileReader), libkiwixBookmarks?.bookmarkUrlsForCurrentBook(zimFileReader.id),
webUrlsProcessor, webUrlsProcessor,
List<String?>::contains List<String?>::contains
) )
@ -1876,7 +1895,16 @@ abstract class CoreReaderFragment :
setIsCloseAllTabButtonClickable(true) setIsCloseAllTabButtonClickable(true)
restoreDeletedTabs() restoreDeletedTabs()
} }
}.show() }.addCallback(object : Snackbar.Callback() {
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
super.onDismissed(transientBottomBar, event)
// If the undo button is not clicked and no tabs are left, exit the book and
// clean up resources.
if (event != DISMISS_EVENT_ACTION && webViewList.isEmpty()) {
closeZimBook()
}
}
}).show()
} }
} }
@ -1886,7 +1914,6 @@ abstract class CoreReaderFragment :
private fun restoreDeletedTabs() { private fun restoreDeletedTabs() {
if (tempWebViewListForUndo.isNotEmpty()) { if (tempWebViewListForUndo.isNotEmpty()) {
zimReaderContainer?.setZimReaderSource(tempZimSourceForUndo)
webViewList.addAll(tempWebViewListForUndo) webViewList.addAll(tempWebViewListForUndo)
tabsAdapter?.notifyDataSetChanged() tabsAdapter?.notifyDataSetChanged()
snackBarRoot?.let { root -> snackBarRoot?.let { root ->

View File

@ -18,7 +18,9 @@
package org.kiwix.kiwixmobile.core.reader package org.kiwix.kiwixmobile.core.reader
import android.webkit.WebResourceResponse import android.webkit.WebResourceResponse
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.kiwix.kiwixmobile.core.reader.ZimFileReader.Factory import org.kiwix.kiwixmobile.core.reader.ZimFileReader.Factory
import java.net.HttpURLConnection import java.net.HttpURLConnection
import javax.inject.Inject import javax.inject.Inject
@ -32,11 +34,11 @@ class ZimReaderContainer @Inject constructor(private val zimFileReaderFactory: F
field = value field = value
} }
fun setZimReaderSource(zimReaderSource: ZimReaderSource?) { suspend fun setZimReaderSource(zimReaderSource: ZimReaderSource?) {
if (zimReaderSource == zimFileReader?.zimReaderSource) { if (zimReaderSource == zimFileReader?.zimReaderSource) {
return return
} }
zimFileReader = runBlocking { zimFileReader = withContext(Dispatchers.IO) {
if (zimReaderSource?.exists() == true && zimReaderSource.canOpenInLibkiwix()) if (zimReaderSource?.exists() == true && zimReaderSource.canOpenInLibkiwix())
zimFileReaderFactory.create(zimReaderSource) zimFileReaderFactory.create(zimReaderSource)
else null else null

View File

@ -42,6 +42,7 @@ import org.kiwix.sharedFunctions.bookOnDisk
import org.kiwix.sharedFunctions.resetSchedulers import org.kiwix.sharedFunctions.resetSchedulers
import org.kiwix.sharedFunctions.setScheduler import org.kiwix.sharedFunctions.setScheduler
import java.io.File import java.io.File
import java.util.concurrent.TimeUnit
class StorageObserverTest { class StorageObserverTest {
@ -106,6 +107,7 @@ class StorageObserverTest {
.also { .also {
downloads.offer(listOf(downloadModel)) downloads.offer(listOf(downloadModel))
files.offer(listOf(file)) files.offer(listOf(file))
it.awaitDone(2, TimeUnit.SECONDS)
} }
private fun withFiltering() { private fun withFiltering() {