Fixed: Wikimedar application crash caused by Input Dispatching Timed Out.

* Moved the file-scanning logic to the IO thread, allowing smooth directory scanning or ZIM file preparation from the asset directory while keeping the main thread free.
* Refactored `CustomFileValidatorTest` to align with this change.
* Corrected the ZIM URL in `testZimFileRendering` test.
* Improved the `manageExternalLaunchAndRestoringViewState` for thread safety.
This commit is contained in:
MohitMaliFtechiz 2025-08-29 15:53:00 +05:30
parent b827cfe542
commit 683c35d5ac
5 changed files with 50 additions and 38 deletions

View File

@ -272,7 +272,7 @@ class KiwixReaderFragmentTest : BaseActivityTest() {
Request.Builder()
.url(
URI.create(
"https://download.kiwix.org/zim/wikipedia_fr_climate_change_mini.zim"
"https://download.kiwix.org/zim/wikipedia_fr_climate-change_mini.zim"
).toURL()
).build()

View File

@ -2487,17 +2487,16 @@ abstract class CoreReaderFragment :
)
}
@Suppress("TooGenericExceptionCaught")
protected suspend fun manageExternalLaunchAndRestoringViewState(
restoreOrigin: RestoreOrigin = FromExternalLaunch,
dispatchersToGetWebViewHistoryFromDatabase: CoroutineDispatcher = Dispatchers.IO
) {
runCatching {
val settings = requireActivity().getSharedPreferences(
SharedPreferenceUtil.PREF_KIWIX_MOBILE,
0
)
val currentTab = safelyGetCurrentTab(settings)
try {
val webViewHistoryList = withContext(dispatchersToGetWebViewHistoryFromDatabase) {
// perform database operation on IO thread.
repositoryActions?.loadWebViewPagesHistory().orEmpty()
@ -2524,10 +2523,10 @@ abstract class CoreReaderFragment :
findInPageTitle = null
handlePendingIntent()
}
} catch (e: Exception) {
}.onFailure {
Log.e(
TAG_KIWIX,
"Could not restore tabs. Original exception = ${e.printStackTrace()}"
"Could not restore tabs. Original exception = ${it.printStackTrace()}"
)
restoreViewStateOnInvalidWebViewHistory()
// handle the pending intent if any present.

View File

@ -23,6 +23,9 @@ import android.content.ContextWrapper
import android.content.pm.PackageManager
import android.content.res.AssetFileDescriptor
import android.content.res.AssetManager
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.kiwix.kiwixmobile.core.utils.files.Log
import org.kiwix.kiwixmobile.custom.main.ValidationState.HasBothFiles
import org.kiwix.kiwixmobile.custom.main.ValidationState.HasFile
@ -32,13 +35,18 @@ import java.io.IOException
import javax.inject.Inject
class CustomFileValidator @Inject constructor(private val context: Context) {
fun validate(onFilesFound: (ValidationState) -> Unit, onNoFilesFound: () -> Unit) =
suspend fun validate(
onFilesFound: suspend (ValidationState) -> Unit,
onNoFilesFound: suspend () -> Unit,
dispatcher: CoroutineDispatcher = Dispatchers.IO
) = withContext(dispatcher) {
when (val installationState = detectInstallationState()) {
is HasBothFiles,
is HasFile -> onFilesFound(installationState)
HasNothing -> onNoFilesFound()
}
}
private fun detectInstallationState(
obbFiles: List<File> = obbFiles(),

View File

@ -20,17 +20,17 @@ package org.kiwix.kiwixmobile.custom.main
import android.app.Dialog
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.Menu
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu
import androidx.compose.ui.graphics.Color
import androidx.core.net.toUri
import androidx.navigation.NavOptions
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.kiwix.kiwixmobile.core.base.BaseActivity
import org.kiwix.kiwixmobile.core.extensions.browserIntent
import org.kiwix.kiwixmobile.core.extensions.isFileExist
@ -80,10 +80,6 @@ class CustomReaderFragment : CoreReaderFragment() {
if (isAdded) {
enableLeftDrawer()
with(activity as AppCompatActivity) {
supportActionBar?.setDisplayHomeAsUpEnabled(true)
enableLeftDrawer()
}
loadPageFromNavigationArguments()
if (BuildConfig.DISABLE_EXTERNAL_LINK) {
// If "external links" are disabled in a custom app,
@ -185,8 +181,10 @@ class CustomReaderFragment : CoreReaderFragment() {
// See https://github.com/kiwix/kiwix-android/issues/3541
zimReaderContainer?.zimFileReader?.let(::setUpBookmarks)
} else {
coreReaderLifeCycleScope?.launch {
openObbOrZim(true)
}
}
requireArguments().clear()
}
@ -247,12 +245,13 @@ class CustomReaderFragment : CoreReaderFragment() {
* @param shouldManageExternalLaunch Indicates whether to manage external launch and
* restore the view state after opening the file. Default is false.
*/
private fun openObbOrZim(shouldManageExternalLaunch: Boolean = false) {
@Suppress("InjectDispatcher")
private suspend fun openObbOrZim(shouldManageExternalLaunch: Boolean = false) {
customFileValidator.validate(
onFilesFound = {
coreReaderLifeCycleScope?.launch {
when (it) {
is ValidationState.HasFile -> {
withContext(Dispatchers.Main) {
openZimFile(
ZimReaderSource(
file = it.file,
@ -276,31 +275,34 @@ class CustomReaderFragment : CoreReaderFragment() {
manageExternalLaunchAndRestoringViewState()
}
}
}
is ValidationState.HasBothFiles -> {
it.zimFile.delete()
withContext(Dispatchers.Main) {
openZimFile(ZimReaderSource(it.obbFile), true, shouldManageExternalLaunch)
if (shouldManageExternalLaunch) {
// Open the previous loaded pages after ZIM file loads.
manageExternalLaunchAndRestoringViewState()
}
}
}
else -> {}
}
}
},
onNoFilesFound = {
if (sharedPreferenceUtil?.prefIsTest == false) {
Handler(Looper.getMainLooper()).postDelayed({
delay(OPENING_DOWNLOAD_SCREEN_DELAY)
withContext(Dispatchers.Main) {
val navOptions = NavOptions.Builder()
.setPopUpTo(CustomDestination.Reader.route, true)
.build()
(requireActivity() as CoreMainActivity).navigate(
(activity as? CoreMainActivity)?.navigate(
CustomDestination.Downloads.route,
navOptions
)
}, OPENING_DOWNLOAD_SCREEN_DELAY)
}
}
}
)
@ -424,7 +426,9 @@ class CustomReaderFragment : CoreReaderFragment() {
super.onResume()
if (appSettingsLaunched) {
appSettingsLaunched = false
openObbOrZim()
coreReaderLifeCycleScope?.launch {
openObbOrZim(true)
}
}
}
}

View File

@ -26,6 +26,7 @@ import android.content.res.AssetManager
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkConstructor
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Assertions.fail
@ -48,7 +49,7 @@ class CustomFileValidatorTest {
}
@Test
fun `validate should call onFilesFound when both OBB and ZIM files are found`() {
fun `validate should call onFilesFound when both OBB and ZIM files are found`() = runTest {
val obbFile = mockk<File>()
val zimFile = mockk<File>()
mockZimFiles(arrayOf(obbFile), "obb")
@ -65,7 +66,7 @@ class CustomFileValidatorTest {
}
@Test
fun `validate should call onFilesFound when only OBB file is found`() {
fun `validate should call onFilesFound when only OBB file is found`() = runTest {
val obbFile = mockk<File>()
mockZimFiles(arrayOf(obbFile), "obb")
mockZimFiles(arrayOf(), "zim")
@ -80,7 +81,7 @@ class CustomFileValidatorTest {
}
@Test
fun `validate should call onFilesFound when only ZIM file is found`() {
fun `validate should call onFilesFound when only ZIM file is found`() = runTest {
val zimFile = mockk<File>()
mockZimFiles(arrayOf(), "obb")
mockZimFiles(arrayOf(zimFile), "zim")
@ -95,7 +96,7 @@ class CustomFileValidatorTest {
}
@Test
fun `validate should call onNoFilesFound when no OBB or ZIM files are found`() {
fun `validate should call onNoFilesFound when no OBB or ZIM files are found`() = runTest {
mockZimFiles(arrayOf(), extension = "zim")
mockZimFiles(arrayOf(), extension = "obb")
@ -106,7 +107,7 @@ class CustomFileValidatorTest {
}
@Test
fun `validate should call onNoFilesFound when directories are null`() {
fun `validate should call onNoFilesFound when directories are null`() = runTest {
mockZimFiles(null, "zim")
mockZimFiles(null, "obb")
@ -117,7 +118,7 @@ class CustomFileValidatorTest {
}
@Test
fun `validate should call onNoFilesFound when no matching files are found`() {
fun `validate should call onNoFilesFound when no matching files are found`() = runTest {
val textFile = mockk<File>()
mockZimFiles(arrayOf(textFile), "txt")
@ -128,7 +129,7 @@ class CustomFileValidatorTest {
}
@Test
fun `validate should call onFilesFound for case insensitive file extensions`() {
fun `validate should call onFilesFound for case insensitive file extensions`() = runTest {
val zimFile = mockk<File>()
mockZimFiles(arrayOf(zimFile), "ZIM")