From 6c1b82da3d5dc58039e43116acfb65a319831a0d Mon Sep 17 00:00:00 2001 From: MohitMaliFtechiz Date: Wed, 15 Jan 2025 20:46:23 +0530 Subject: [PATCH 1/2] Fixed: The ZIM file was displayed on the library screen but failed to open (the ZIM file was located in the SD card's trash folder). * Improved the scanning process for ZIM files to reduce scan time by excluding hidden folders, which are unnecessary and caused delays in scanning. * Updated MediaStore to exclude ZIM files located in the SD card's trash folder, addressing the issue. * Refactored the NewBookDao to remove any ZIM files from the library screen if they are located in the trash folder. --- .../utils/storage/StorageDeviceUtils.kt | 5 ++++- .../kiwix/kiwixmobile/core/dao/NewBookDao.kt | 18 ++++++++++++++++-- .../kiwixmobile/core/utils/files/FileSearch.kt | 16 +++++++++++----- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/eu/mhutti1/utils/storage/StorageDeviceUtils.kt b/core/src/main/java/eu/mhutti1/utils/storage/StorageDeviceUtils.kt index 0ed5fbd545..ee0eec2a33 100644 --- a/core/src/main/java/eu/mhutti1/utils/storage/StorageDeviceUtils.kt +++ b/core/src/main/java/eu/mhutti1/utils/storage/StorageDeviceUtils.kt @@ -20,6 +20,7 @@ package eu.mhutti1.utils.storage import android.content.Context import android.content.ContextWrapper +import android.os.Build import android.os.Environment import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -52,7 +53,9 @@ object StorageDeviceUtils { // In the Play Store variant, we can only access app-specific directories, // so scanning other directories is unnecessary, wastes resources, // and increases the scanning time. - if (sharedPreferenceUtil?.isNotPlayStoreBuildWithAndroid11OrAbove() == true) { + if (sharedPreferenceUtil?.isPlayStoreBuild == false || + Build.VERSION.SDK_INT < Build.VERSION_CODES.R + ) { addAll(externalMountPointDevices()) addAll(externalFilesDirsDevices(context, false)) } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/NewBookDao.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/NewBookDao.kt index 7fc1ad4b8b..b05d691aee 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/NewBookDao.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/NewBookDao.kt @@ -55,7 +55,10 @@ class NewBookDao @Inject constructor(private val box: Box) { .toList() .toFlowable() .flatMap { booksList -> - completableFromCoroutine { removeBooksThatDoNotExist(booksList.toMutableList()) } + completableFromCoroutine { + removeBooksThatAreInTrashFolder(booksList) + removeBooksThatDoNotExist(booksList.toMutableList()) + } .andThen(io.reactivex.rxjava3.core.Flowable.just(booksList)) } } @@ -68,7 +71,9 @@ class NewBookDao @Inject constructor(private val box: Box) { bookOnDiskEntity to exists } } - .filter(Pair::second) + .filter { (bookOnDiskEntity, exists) -> + exists && !isInTrashFolder(bookOnDiskEntity.zimReaderSource.toDatabase()) + } .map(Pair::first) .toList() .toFlowable() @@ -150,6 +155,15 @@ class NewBookDao @Inject constructor(private val box: Box) { delete(books.filterNot { it.zimReaderSource.exists() }) } + // Remove the existing books from database which are showing on the library screen. + private fun removeBooksThatAreInTrashFolder(books: List) { + delete(books.filter { isInTrashFolder(it.zimReaderSource.toDatabase()) }) + } + + // Check if any existing ZIM file showing on the library screen which is inside the trash folder. + private fun isInTrashFolder(filePath: String) = + Regex("/\\.Trash/").containsMatchIn(filePath) + private fun delete(books: List) { box.remove(books) } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/files/FileSearch.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/files/FileSearch.kt index 3c18836849..b9e90ff244 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/files/FileSearch.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/files/FileSearch.kt @@ -48,11 +48,16 @@ class FileSearch @Inject constructor(private val context: Context) { private fun scanMediaStore() = mutableListOf().apply { queryMediaStore() ?.forEachRow { cursor -> - File(cursor.get(MediaColumns.DATA)).takeIf(File::canRead) - ?.also { add(it) } + File(cursor.get(MediaColumns.DATA)) + .takeIf { it.canRead() && isNotInTrashFolder(it) } + ?.also(::add) } } + // Exclude any file in trash folder. + private fun isNotInTrashFolder(it: File) = + !Regex("/\\.Trash/").containsMatchIn(it.path) + private fun queryMediaStore() = context.contentResolver .query( Files.getContentUri("external"), @@ -86,8 +91,8 @@ class FileSearch @Inject constructor(private val context: Context) { private fun scanDirectory(directory: String): List { return File(directory).walk() .onEnter { dir -> - // Excluding the "data," "obb," and "Trash" folders from scanning is justified for - // several reasons. The "Trash" folder contains deleted files, + // Excluding the "data," "obb," "hidden folders," and "Trash" folders from scanning is + // justified for several reasons. The "Trash" folder contains deleted files, // making it unnecessary for scanning. Additionally, // the "data" and "obb" folders are specifically designed for the // app's private directory, and users usually do not store ZIM files there. @@ -97,7 +102,8 @@ class FileSearch @Inject constructor(private val context: Context) { // which are irrelevant to our application. !dir.name.equals(".Trash", ignoreCase = true) && !dir.name.equals("data", ignoreCase = true) && - !dir.name.equals("obb", ignoreCase = true) + !dir.name.equals("obb", ignoreCase = true) && + !dir.name.startsWith(".", ignoreCase = true) }.filter { it.extension.isAny(*zimFileExtensions) }.toList() From 8ded3d350ac8b8b0bc0b855f055710afcb842547 Mon Sep 17 00:00:00 2001 From: MohitMaliFtechiz Date: Thu, 16 Jan 2025 11:41:06 +0530 Subject: [PATCH 2/2] Fixed: Failing unit test case. * Added a new unit test to verify that books located in the trash folder do not appear on the library screen. --- .../kiwix/kiwixmobile/core/dao/NewBookDaoTest.kt | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/core/src/test/java/org/kiwix/kiwixmobile/core/dao/NewBookDaoTest.kt b/core/src/test/java/org/kiwix/kiwixmobile/core/dao/NewBookDaoTest.kt index 91209c4d84..3fd8b7c65b 100644 --- a/core/src/test/java/org/kiwix/kiwixmobile/core/dao/NewBookDaoTest.kt +++ b/core/src/test/java/org/kiwix/kiwixmobile/core/dao/NewBookDaoTest.kt @@ -81,7 +81,17 @@ internal class NewBookDaoTest { verify { box.remove(listOf(deletedEntity)) } } - private fun expectEmissionOfExistingAndNotExistingBook(): + @Test + fun `books removes entities whose files are in the trash folder`() { + runBlocking { + val (_, _) = expectEmissionOfExistingAndNotExistingBook(true) + val books = newBookDao.books().test() + delay(1000) + books.assertValues(emptyList()) + } + } + + private fun expectEmissionOfExistingAndNotExistingBook(isInTrashFolder: Boolean = false): Pair { val query: Query = mockk() every { box.query().build() } returns query @@ -89,6 +99,10 @@ internal class NewBookDaoTest { val zimReaderSourceThatDoesNotExist = mockk() coEvery { zimReaderSourceThatExists.exists() } returns true coEvery { zimReaderSourceThatDoesNotExist.exists() } returns false + every { + zimReaderSourceThatExists.toDatabase() + } returns if (isInTrashFolder) "/.Trash/test.zim" else "" + every { zimReaderSourceThatDoesNotExist.toDatabase() } returns "" val entityThatExists = bookOnDiskEntity(zimReaderSource = zimReaderSourceThatExists) val entityThatDoesNotExist = bookOnDiskEntity(zimReaderSource = zimReaderSourceThatDoesNotExist)