From b8971e1b64180d1bc7f4f3b503e01a398a30b816 Mon Sep 17 00:00:00 2001 From: MohitMaliFtechiz Date: Tue, 23 Apr 2024 16:31:10 +0530 Subject: [PATCH 1/6] Error on opening a split zim file on external storage. * Added test cases for testing that the ZimFileReader can open the splitted zim file or not. --- .../page/history/NavigationHistoryRobot.kt | 11 ++ .../ZimFileReaderWithSplittedZimFileTest.kt | 148 ++++++++++++++++++ 2 files changed, 159 insertions(+) create mode 100644 app/src/androidTest/java/org/kiwix/kiwixmobile/reader/ZimFileReaderWithSplittedZimFileTest.kt diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/page/history/NavigationHistoryRobot.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/page/history/NavigationHistoryRobot.kt index 3880a150e4..8ea8ac86b4 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/page/history/NavigationHistoryRobot.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/page/history/NavigationHistoryRobot.kt @@ -70,6 +70,17 @@ class NavigationHistoryRobot : BaseRobot() { .perform(webClick()) } + fun assertZimFileLoaded() { + pauseForBetterTestPerformance() + onWebView() + .withElement( + findElement( + Locator.XPATH, + "//*[contains(text(), 'Android_(operating_system)')]" + ) + ) + } + fun longClickOnBackwardButton() { pauseForBetterTestPerformance() longClickOn(ViewId(R.id.bottom_toolbar_arrow_back)) diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/reader/ZimFileReaderWithSplittedZimFileTest.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/reader/ZimFileReaderWithSplittedZimFileTest.kt new file mode 100644 index 0000000000..0b2ef463f6 --- /dev/null +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/reader/ZimFileReaderWithSplittedZimFileTest.kt @@ -0,0 +1,148 @@ +/* + * Kiwix Android + * Copyright (c) 2024 Kiwix + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package org.kiwix.kiwixmobile.reader + +import androidx.core.content.ContextCompat +import androidx.core.content.edit +import androidx.core.net.toUri +import androidx.lifecycle.Lifecycle +import androidx.preference.PreferenceManager +import androidx.test.core.app.ActivityScenario +import androidx.test.internal.runner.junit4.statement.UiThreadStatement +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.jupiter.api.fail +import org.kiwix.kiwixmobile.BaseActivityTest +import org.kiwix.kiwixmobile.R +import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil +import org.kiwix.kiwixmobile.main.KiwixMainActivity +import org.kiwix.kiwixmobile.nav.destination.library.LocalLibraryFragmentDirections +import org.kiwix.kiwixmobile.page.history.navigationHistory +import org.kiwix.kiwixmobile.testutils.RetryRule +import org.kiwix.kiwixmobile.testutils.TestUtils +import java.io.File +import java.io.FileOutputStream +import java.io.OutputStream + +class ZimFileReaderWithSplittedZimFileTest : BaseActivityTest() { + + @Rule + @JvmField + var retryRule = RetryRule() + + private lateinit var kiwixMainActivity: KiwixMainActivity + + @Before + override fun waitForIdle() { + UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).apply { + if (TestUtils.isSystemUINotRespondingDialogVisible(this)) { + TestUtils.closeSystemDialogs(context, this) + } + waitForIdle() + } + PreferenceManager.getDefaultSharedPreferences(context).edit { + putBoolean(SharedPreferenceUtil.PREF_SHOW_INTRO, false) + putBoolean(SharedPreferenceUtil.PREF_WIFI_ONLY, false) + putBoolean(SharedPreferenceUtil.PREF_PLAY_STORE_RESTRICTION, false) + } + activityScenario = ActivityScenario.launch(KiwixMainActivity::class.java).apply { + moveToState(Lifecycle.State.RESUMED) + } + } + + @Test + fun testZimFileReaderWithSplittedZimFile() { + activityScenario.onActivity { + kiwixMainActivity = it + kiwixMainActivity.navigate(R.id.libraryFragment) + } + createAndGetSplitedZimFile()?.let { + UiThreadStatement.runOnUiThread { + kiwixMainActivity.navigate( + LocalLibraryFragmentDirections.actionNavigationLibraryToNavigationReader() + .apply { zimFileUri = it.toUri().toString() } + ) + } + + navigationHistory { + checkZimFileLoadedSuccessful(R.id.readerFragment) + assertZimFileLoaded() // check if the zim file successfully loaded + clickOnAndroidArticle() + } + } ?: kotlin.run { + // error in creating the zim file chunk + fail("Couldn't create the zim file chunk") + } + } + + private fun createAndGetSplitedZimFile(): File? { + val loadFileStream = + EncodedUrlTest::class.java.classLoader.getResourceAsStream("testzim.zim") + val storageDir = ContextCompat.getExternalFilesDirs(context, null)[0] + + // Delete existing parts if they exist + (1..3) + .asSequence() + .map { File(storageDir, "testzim.zima${('a' + it - 1).toChar()}") } + .filter(File::exists) + .forEach(File::delete) + + // Calculate the size of each part + val totalFileSize = loadFileStream.available().toLong() + val partSize = totalFileSize / 3 // convert into 3 parts + var partNumber = 1 + + // Read the input stream in chunks and write to smaller files + loadFileStream.use { inputStream -> + var bytesRead: Long = 0 + var currentPartSize: Long = 0 + val buffer = ByteArray(1024) + var length: Int + var outputStream: OutputStream + + while (inputStream.read(buffer).also { length = it } > 0) { + // If current part size exceeds or equals partSize, move to the next part + if (currentPartSize >= partSize || bytesRead == 0L) { + outputStream = FileOutputStream( + File(storageDir, "testzim.zima${('a' + partNumber - 1).toChar()}") + ) + partNumber++ + currentPartSize = 0 + } else { + outputStream = FileOutputStream( + File(storageDir, "testzim.zima${('a' + partNumber - 2).toChar()}"), + true + ) + } + + outputStream.use { + it.write(buffer, 0, length) + } + + bytesRead += length + currentPartSize += length + } + } + val splittedZimFile = File(storageDir, "testzim.zimaa") + return if (splittedZimFile.exists()) splittedZimFile else null + } +} From 9cb881bf06dbc90bfa5239c1c1547e0d380dc984 Mon Sep 17 00:00:00 2001 From: MohitMaliFtechiz Date: Fri, 3 May 2024 12:38:36 +0530 Subject: [PATCH 2/6] Capturing the better exception when getting the `mediaCount` and `articleCount` because it was throwing `java.lang.Exception` with splitted zim file when the zim file is corrupted. If there is any zim file is corrupted and libzim throws this exception then our UI will not show the other zim file on `LocalLibraryScreen` due to the getting zim files process being crashed. --- .../org/kiwix/kiwixmobile/core/reader/ZimFileReader.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/reader/ZimFileReader.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/reader/ZimFileReader.kt index c4b6ae3c59..828fc1926e 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/reader/ZimFileReader.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/reader/ZimFileReader.kt @@ -160,15 +160,15 @@ class ZimFileReader constructor( private val mediaCount: Int? get() = try { jniKiwixReader.mediaCount - } catch (unsatisfiedLinkError: UnsatisfiedLinkError) { - Log.e(TAG, "Unable to find the media count $unsatisfiedLinkError") + } catch (ignore: Exception) { + Log.e(TAG, "Unable to find the media count $ignore") null } private val articleCount: Int? get() = try { jniKiwixReader.articleCount - } catch (unsatisfiedLinkError: UnsatisfiedLinkError) { - Log.e(TAG, "Unable to find the article count $unsatisfiedLinkError") + } catch (ignore: Exception) { + Log.e(TAG, "Unable to find the article count $ignore") null } From 326b3901c47fb630b4ae0bf127df9c068955de11 Mon Sep 17 00:00:00 2001 From: MohitMaliFtechiz Date: Fri, 3 May 2024 14:14:07 +0530 Subject: [PATCH 3/6] Added comments if libzim throw exception for articleCount and mediaCount this will necessary to catch the all exception thrown by libzim to prevent the aborting of the rendering process of other zim files. --- .../org/kiwix/kiwixmobile/core/reader/ZimFileReader.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/reader/ZimFileReader.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/reader/ZimFileReader.kt index 828fc1926e..429a266aa5 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/reader/ZimFileReader.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/reader/ZimFileReader.kt @@ -160,14 +160,18 @@ class ZimFileReader constructor( private val mediaCount: Int? get() = try { jniKiwixReader.mediaCount - } catch (ignore: Exception) { + } + // catch all exceptions to prevent the aborting of the rendering process of other zim files. + catch (ignore: Exception) { Log.e(TAG, "Unable to find the media count $ignore") null } private val articleCount: Int? get() = try { jniKiwixReader.articleCount - } catch (ignore: Exception) { + } + // catch all exceptions to prevent the aborting of the rendering process of other zim files. + catch (ignore: Exception) { Log.e(TAG, "Unable to find the article count $ignore") null } From c0912dee8957c3b9f677ececbf717fdbc483ceae Mon Sep 17 00:00:00 2001 From: MohitMaliFtechiz Date: Fri, 3 May 2024 19:57:52 +0530 Subject: [PATCH 4/6] Simplified the creation of splitted zim file. --- .../ZimFileReaderWithSplittedZimFileTest.kt | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/reader/ZimFileReaderWithSplittedZimFileTest.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/reader/ZimFileReaderWithSplittedZimFileTest.kt index 0b2ef463f6..e3f5c40986 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/reader/ZimFileReaderWithSplittedZimFileTest.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/reader/ZimFileReaderWithSplittedZimFileTest.kt @@ -117,30 +117,23 @@ class ZimFileReaderWithSplittedZimFileTest : BaseActivityTest() { var currentPartSize: Long = 0 val buffer = ByteArray(1024) var length: Int - var outputStream: OutputStream + var outputStream: OutputStream? = null while (inputStream.read(buffer).also { length = it } > 0) { - // If current part size exceeds or equals partSize, move to the next part if (currentPartSize >= partSize || bytesRead == 0L) { - outputStream = FileOutputStream( - File(storageDir, "testzim.zima${('a' + partNumber - 1).toChar()}") - ) + outputStream?.close() // Close the previous outputStream if any open. + val partFile = File(storageDir, "testzim.zima${('a' + partNumber - 1).toChar()}") + outputStream = FileOutputStream(partFile) partNumber++ currentPartSize = 0 - } else { - outputStream = FileOutputStream( - File(storageDir, "testzim.zima${('a' + partNumber - 2).toChar()}"), - true - ) } - outputStream.use { - it.write(buffer, 0, length) - } + outputStream?.write(buffer, 0, length) bytesRead += length currentPartSize += length } + outputStream?.close() } val splittedZimFile = File(storageDir, "testzim.zimaa") return if (splittedZimFile.exists()) splittedZimFile else null From 96debf9e53753b6abf3c09516fe8e17deac1f049 Mon Sep 17 00:00:00 2001 From: MohitMaliFtechiz Date: Mon, 6 May 2024 23:28:15 +0530 Subject: [PATCH 5/6] Added test case for testing splitted zim file with zero size file. --- .../ZimFileReaderWithSplittedZimFileTest.kt | 34 ++++++++++++++++++- .../kiwixmobile/core/reader/ZimFileReader.kt | 4 +-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/reader/ZimFileReaderWithSplittedZimFileTest.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/reader/ZimFileReaderWithSplittedZimFileTest.kt index e3f5c40986..e7dab4544a 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/reader/ZimFileReaderWithSplittedZimFileTest.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/reader/ZimFileReaderWithSplittedZimFileTest.kt @@ -27,18 +27,23 @@ import androidx.test.core.app.ActivityScenario import androidx.test.internal.runner.junit4.statement.UiThreadStatement import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice +import org.junit.Assert import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.jupiter.api.fail import org.kiwix.kiwixmobile.BaseActivityTest import org.kiwix.kiwixmobile.R +import org.kiwix.kiwixmobile.core.NightModeConfig +import org.kiwix.kiwixmobile.core.reader.ZimFileReader import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil import org.kiwix.kiwixmobile.main.KiwixMainActivity import org.kiwix.kiwixmobile.nav.destination.library.LocalLibraryFragmentDirections import org.kiwix.kiwixmobile.page.history.navigationHistory import org.kiwix.kiwixmobile.testutils.RetryRule import org.kiwix.kiwixmobile.testutils.TestUtils +import org.kiwix.libzim.Archive +import org.kiwix.libzim.SuggestionSearcher import java.io.File import java.io.FileOutputStream import java.io.OutputStream @@ -94,7 +99,28 @@ class ZimFileReaderWithSplittedZimFileTest : BaseActivityTest() { } } - private fun createAndGetSplitedZimFile(): File? { + @Test + fun testWithExtraZeroSizeFile() { + createAndGetSplitedZimFile(true)?.let { zimFile -> + // test the articleCount and mediaCount of this zim file. + val archive = Archive(zimFile.canonicalPath) + val zimFileReader = ZimFileReader( + zimFile, + null, + null, + archive, + NightModeConfig(SharedPreferenceUtil(context), context), + SuggestionSearcher(archive) + ) + Assert.assertEquals(zimFileReader.mediaCount, 16) + Assert.assertEquals(zimFileReader.articleCount, 4) + } ?: kotlin.run { + // error in creating the zim file chunk + fail("Couldn't create the zim file chunk") + } + } + + private fun createAndGetSplitedZimFile(shouldCreateExtraZeroSizeFile: Boolean = false): File? { val loadFileStream = EncodedUrlTest::class.java.classLoader.getResourceAsStream("testzim.zim") val storageDir = ContextCompat.getExternalFilesDirs(context, null)[0] @@ -135,6 +161,12 @@ class ZimFileReaderWithSplittedZimFileTest : BaseActivityTest() { } outputStream?.close() } + if (shouldCreateExtraZeroSizeFile) { + File(storageDir, "testzim.zimad").apply { + if (exists()) delete() // delete if already exist. + createNewFile() // create new zero size file. + } + } val splittedZimFile = File(storageDir, "testzim.zimaa") return if (splittedZimFile.exists()) splittedZimFile else null } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/reader/ZimFileReader.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/reader/ZimFileReader.kt index 429a266aa5..baee22b77b 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/reader/ZimFileReader.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/reader/ZimFileReader.kt @@ -157,7 +157,7 @@ class ZimFileReader constructor( val tags: String get() = getSafeMetaData("Tags", "") - private val mediaCount: Int? + val mediaCount: Int? get() = try { jniKiwixReader.mediaCount } @@ -166,7 +166,7 @@ class ZimFileReader constructor( Log.e(TAG, "Unable to find the media count $ignore") null } - private val articleCount: Int? + val articleCount: Int? get() = try { jniKiwixReader.articleCount } From ecf74ed803fdca18d8bd0160b67f11b07b0e0354 Mon Sep 17 00:00:00 2001 From: MohitMaliFtechiz Date: Wed, 8 May 2024 15:50:41 +0530 Subject: [PATCH 6/6] Corrected the comment for handling the exception. --- .../org/kiwix/kiwixmobile/core/reader/ZimFileReader.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/reader/ZimFileReader.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/reader/ZimFileReader.kt index baee22b77b..509e0607c5 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/reader/ZimFileReader.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/reader/ZimFileReader.kt @@ -161,7 +161,9 @@ class ZimFileReader constructor( get() = try { jniKiwixReader.mediaCount } - // catch all exceptions to prevent the aborting of the rendering process of other zim files. + // Catch all exceptions to prevent the rendering process of other zim files from aborting. + // If the zim file is split with zim-tool, + // refer to https://github.com/kiwix/kiwix-android/issues/3827. catch (ignore: Exception) { Log.e(TAG, "Unable to find the media count $ignore") null @@ -170,7 +172,9 @@ class ZimFileReader constructor( get() = try { jniKiwixReader.articleCount } - // catch all exceptions to prevent the aborting of the rendering process of other zim files. + // Catch all exceptions to prevent the rendering process of other zim files from aborting. + // If the zim file is split with zim-tool, + // refer to https://github.com/kiwix/kiwix-android/issues/3827. catch (ignore: Exception) { Log.e(TAG, "Unable to find the article count $ignore") null