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() Request.Builder()
.url( .url(
URI.create( 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() ).toURL()
).build() ).build()

View File

@ -2487,17 +2487,16 @@ abstract class CoreReaderFragment :
) )
} }
@Suppress("TooGenericExceptionCaught")
protected suspend fun manageExternalLaunchAndRestoringViewState( protected suspend fun manageExternalLaunchAndRestoringViewState(
restoreOrigin: RestoreOrigin = FromExternalLaunch, restoreOrigin: RestoreOrigin = FromExternalLaunch,
dispatchersToGetWebViewHistoryFromDatabase: CoroutineDispatcher = Dispatchers.IO dispatchersToGetWebViewHistoryFromDatabase: CoroutineDispatcher = Dispatchers.IO
) { ) {
val settings = requireActivity().getSharedPreferences( runCatching {
SharedPreferenceUtil.PREF_KIWIX_MOBILE, val settings = requireActivity().getSharedPreferences(
0 SharedPreferenceUtil.PREF_KIWIX_MOBILE,
) 0
val currentTab = safelyGetCurrentTab(settings) )
try { val currentTab = safelyGetCurrentTab(settings)
val webViewHistoryList = withContext(dispatchersToGetWebViewHistoryFromDatabase) { val webViewHistoryList = withContext(dispatchersToGetWebViewHistoryFromDatabase) {
// perform database operation on IO thread. // perform database operation on IO thread.
repositoryActions?.loadWebViewPagesHistory().orEmpty() repositoryActions?.loadWebViewPagesHistory().orEmpty()
@ -2524,10 +2523,10 @@ abstract class CoreReaderFragment :
findInPageTitle = null findInPageTitle = null
handlePendingIntent() handlePendingIntent()
} }
} catch (e: Exception) { }.onFailure {
Log.e( Log.e(
TAG_KIWIX, TAG_KIWIX,
"Could not restore tabs. Original exception = ${e.printStackTrace()}" "Could not restore tabs. Original exception = ${it.printStackTrace()}"
) )
restoreViewStateOnInvalidWebViewHistory() restoreViewStateOnInvalidWebViewHistory()
// handle the pending intent if any present. // 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.pm.PackageManager
import android.content.res.AssetFileDescriptor import android.content.res.AssetFileDescriptor
import android.content.res.AssetManager 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.core.utils.files.Log
import org.kiwix.kiwixmobile.custom.main.ValidationState.HasBothFiles import org.kiwix.kiwixmobile.custom.main.ValidationState.HasBothFiles
import org.kiwix.kiwixmobile.custom.main.ValidationState.HasFile import org.kiwix.kiwixmobile.custom.main.ValidationState.HasFile
@ -32,13 +35,18 @@ import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
class CustomFileValidator @Inject constructor(private val context: Context) { 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()) { when (val installationState = detectInstallationState()) {
is HasBothFiles, is HasBothFiles,
is HasFile -> onFilesFound(installationState) is HasFile -> onFilesFound(installationState)
HasNothing -> onNoFilesFound() HasNothing -> onNoFilesFound()
} }
}
private fun detectInstallationState( private fun detectInstallationState(
obbFiles: List<File> = obbFiles(), obbFiles: List<File> = obbFiles(),

View File

@ -20,17 +20,17 @@ package org.kiwix.kiwixmobile.custom.main
import android.app.Dialog import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.Menu import android.view.Menu
import android.view.View import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.Menu
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.navigation.NavOptions import androidx.navigation.NavOptions
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.kiwix.kiwixmobile.core.base.BaseActivity import org.kiwix.kiwixmobile.core.base.BaseActivity
import org.kiwix.kiwixmobile.core.extensions.browserIntent import org.kiwix.kiwixmobile.core.extensions.browserIntent
import org.kiwix.kiwixmobile.core.extensions.isFileExist import org.kiwix.kiwixmobile.core.extensions.isFileExist
@ -80,10 +80,6 @@ class CustomReaderFragment : CoreReaderFragment() {
if (isAdded) { if (isAdded) {
enableLeftDrawer() enableLeftDrawer()
with(activity as AppCompatActivity) {
supportActionBar?.setDisplayHomeAsUpEnabled(true)
enableLeftDrawer()
}
loadPageFromNavigationArguments() loadPageFromNavigationArguments()
if (BuildConfig.DISABLE_EXTERNAL_LINK) { if (BuildConfig.DISABLE_EXTERNAL_LINK) {
// If "external links" are disabled in a custom app, // If "external links" are disabled in a custom app,
@ -185,7 +181,9 @@ class CustomReaderFragment : CoreReaderFragment() {
// See https://github.com/kiwix/kiwix-android/issues/3541 // See https://github.com/kiwix/kiwix-android/issues/3541
zimReaderContainer?.zimFileReader?.let(::setUpBookmarks) zimReaderContainer?.zimFileReader?.let(::setUpBookmarks)
} else { } else {
openObbOrZim(true) coreReaderLifeCycleScope?.launch {
openObbOrZim(true)
}
} }
requireArguments().clear() requireArguments().clear()
} }
@ -247,12 +245,13 @@ class CustomReaderFragment : CoreReaderFragment() {
* @param shouldManageExternalLaunch Indicates whether to manage external launch and * @param shouldManageExternalLaunch Indicates whether to manage external launch and
* restore the view state after opening the file. Default is false. * 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( customFileValidator.validate(
onFilesFound = { onFilesFound = {
coreReaderLifeCycleScope?.launch { when (it) {
when (it) { is ValidationState.HasFile -> {
is ValidationState.HasFile -> { withContext(Dispatchers.Main) {
openZimFile( openZimFile(
ZimReaderSource( ZimReaderSource(
file = it.file, file = it.file,
@ -276,31 +275,34 @@ class CustomReaderFragment : CoreReaderFragment() {
manageExternalLaunchAndRestoringViewState() manageExternalLaunchAndRestoringViewState()
} }
} }
}
is ValidationState.HasBothFiles -> { is ValidationState.HasBothFiles -> {
it.zimFile.delete() it.zimFile.delete()
withContext(Dispatchers.Main) {
openZimFile(ZimReaderSource(it.obbFile), true, shouldManageExternalLaunch) openZimFile(ZimReaderSource(it.obbFile), true, shouldManageExternalLaunch)
if (shouldManageExternalLaunch) { if (shouldManageExternalLaunch) {
// Open the previous loaded pages after ZIM file loads. // Open the previous loaded pages after ZIM file loads.
manageExternalLaunchAndRestoringViewState() manageExternalLaunchAndRestoringViewState()
} }
} }
else -> {}
} }
else -> {}
} }
}, },
onNoFilesFound = { onNoFilesFound = {
if (sharedPreferenceUtil?.prefIsTest == false) { if (sharedPreferenceUtil?.prefIsTest == false) {
Handler(Looper.getMainLooper()).postDelayed({ delay(OPENING_DOWNLOAD_SCREEN_DELAY)
withContext(Dispatchers.Main) {
val navOptions = NavOptions.Builder() val navOptions = NavOptions.Builder()
.setPopUpTo(CustomDestination.Reader.route, true) .setPopUpTo(CustomDestination.Reader.route, true)
.build() .build()
(requireActivity() as CoreMainActivity).navigate( (activity as? CoreMainActivity)?.navigate(
CustomDestination.Downloads.route, CustomDestination.Downloads.route,
navOptions navOptions
) )
}, OPENING_DOWNLOAD_SCREEN_DELAY) }
} }
} }
) )
@ -424,7 +426,9 @@ class CustomReaderFragment : CoreReaderFragment() {
super.onResume() super.onResume()
if (appSettingsLaunched) { if (appSettingsLaunched) {
appSettingsLaunched = false 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.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.mockkConstructor import io.mockk.mockkConstructor
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Assertions.fail import org.junit.jupiter.api.Assertions.fail
@ -48,7 +49,7 @@ class CustomFileValidatorTest {
} }
@Test @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 obbFile = mockk<File>()
val zimFile = mockk<File>() val zimFile = mockk<File>()
mockZimFiles(arrayOf(obbFile), "obb") mockZimFiles(arrayOf(obbFile), "obb")
@ -65,7 +66,7 @@ class CustomFileValidatorTest {
} }
@Test @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>() val obbFile = mockk<File>()
mockZimFiles(arrayOf(obbFile), "obb") mockZimFiles(arrayOf(obbFile), "obb")
mockZimFiles(arrayOf(), "zim") mockZimFiles(arrayOf(), "zim")
@ -80,7 +81,7 @@ class CustomFileValidatorTest {
} }
@Test @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>() val zimFile = mockk<File>()
mockZimFiles(arrayOf(), "obb") mockZimFiles(arrayOf(), "obb")
mockZimFiles(arrayOf(zimFile), "zim") mockZimFiles(arrayOf(zimFile), "zim")
@ -95,7 +96,7 @@ class CustomFileValidatorTest {
} }
@Test @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 = "zim")
mockZimFiles(arrayOf(), extension = "obb") mockZimFiles(arrayOf(), extension = "obb")
@ -106,7 +107,7 @@ class CustomFileValidatorTest {
} }
@Test @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, "zim")
mockZimFiles(null, "obb") mockZimFiles(null, "obb")
@ -117,7 +118,7 @@ class CustomFileValidatorTest {
} }
@Test @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>() val textFile = mockk<File>()
mockZimFiles(arrayOf(textFile), "txt") mockZimFiles(arrayOf(textFile), "txt")
@ -128,7 +129,7 @@ class CustomFileValidatorTest {
} }
@Test @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>() val zimFile = mockk<File>()
mockZimFiles(arrayOf(zimFile), "ZIM") mockZimFiles(arrayOf(zimFile), "ZIM")