mirror of
https://github.com/kiwix/kiwix-android.git
synced 2025-09-23 04:33:54 -04:00
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:
parent
b827cfe542
commit
683c35d5ac
@ -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()
|
||||||
|
|
||||||
|
@ -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
|
||||||
) {
|
) {
|
||||||
|
runCatching {
|
||||||
val settings = requireActivity().getSharedPreferences(
|
val settings = requireActivity().getSharedPreferences(
|
||||||
SharedPreferenceUtil.PREF_KIWIX_MOBILE,
|
SharedPreferenceUtil.PREF_KIWIX_MOBILE,
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
val currentTab = safelyGetCurrentTab(settings)
|
val currentTab = safelyGetCurrentTab(settings)
|
||||||
try {
|
|
||||||
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.
|
||||||
|
@ -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(),
|
||||||
|
@ -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,8 +181,10 @@ 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 {
|
||||||
|
coreReaderLifeCycleScope?.launch {
|
||||||
openObbOrZim(true)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user