diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/localLibrary/CopyMoveFileHandlerTest.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/localLibrary/CopyMoveFileHandlerTest.kt index f192f6ab15..e681288906 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/localLibrary/CopyMoveFileHandlerTest.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/localLibrary/CopyMoveFileHandlerTest.kt @@ -174,13 +174,15 @@ class CopyMoveFileHandlerTest : BaseActivityTest() { clickOnMove() assertZimFileCopiedAndShowingIntoTheReader() } - assertZimFileAddedInTheLocalLibrary() - assertSelectedZimFileIsDeletedFromTheStorage(selectedFile) - deleteAllFilesInDirectory(parentFile) + kiwixMainActivity.lifecycleScope.launch { + assertZimFileAddedInTheLocalLibrary() + assertSelectedZimFileIsDeletedFromTheStorage(selectedFile) + deleteAllFilesInDirectory(parentFile) + } } } - private fun assertSelectedZimFileIsDeletedFromTheStorage(selectedZimFile: File) { + private suspend fun assertSelectedZimFileIsDeletedFromTheStorage(selectedZimFile: File) { if (selectedZimFile.isFileExist()) { throw RuntimeException("Selected zim file is not deleted from the storage") } @@ -194,7 +196,7 @@ class CopyMoveFileHandlerTest : BaseActivityTest() { } private fun showMoveFileToPublicDirectoryDialog() { - UiThreadStatement.runOnUiThread { + kiwixMainActivity.lifecycleScope.launch { val navHostFragment: NavHostFragment = kiwixMainActivity.supportFragmentManager .findFragmentById(R.id.nav_host_fragment) as NavHostFragment @@ -268,22 +270,22 @@ class CopyMoveFileHandlerTest : BaseActivityTest() { StorageCalculator(sharedPreferenceUtil), Fat32Checker(sharedPreferenceUtil, listOf(FileWritingFileSystemChecker())) ) - // test fileName when there is already a file available with same name. - // it should return different name - selectedFile = File(parentFile, selectedFileName).apply { - if (!isFileExist()) createNewFile() - } - copyMoveFileHandler.setSelectedFileAndUri(null, DocumentFile.fromFile(selectedFile)) - destinationFile = copyMoveFileHandler.getDestinationFile() - Assert.assertNotEquals( - destinationFile.name, - selectedFile.name - ) - Assert.assertEquals( - destinationFile.name, - "testCopyMove_1.zim" - ) kiwixMainActivity.lifecycleScope.launch { + // test fileName when there is already a file available with same name. + // it should return different name + selectedFile = File(parentFile, selectedFileName).apply { + if (!isFileExist()) createNewFile() + } + copyMoveFileHandler.setSelectedFileAndUri(null, DocumentFile.fromFile(selectedFile)) + destinationFile = copyMoveFileHandler.getDestinationFile() + Assert.assertNotEquals( + destinationFile.name, + selectedFile.name + ) + Assert.assertEquals( + destinationFile.name, + "testCopyMove_1.zim" + ) withContext(Dispatchers.IO) { deleteBothPreviousFiles() } diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/utils/UnsupportedMimeTypeHandlerTest.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/utils/UnsupportedMimeTypeHandlerTest.kt index 939481c386..8996271116 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/utils/UnsupportedMimeTypeHandlerTest.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/utils/UnsupportedMimeTypeHandlerTest.kt @@ -27,6 +27,9 @@ import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.slot import io.mockk.verify +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking import org.junit.Before import org.junit.jupiter.api.Test import org.kiwix.kiwixmobile.core.R @@ -34,8 +37,8 @@ import org.kiwix.kiwixmobile.core.extensions.toast import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil import org.kiwix.kiwixmobile.core.utils.dialog.AlertDialogShower -import org.kiwix.kiwixmobile.core.utils.dialog.UnsupportedMimeTypeHandler import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog +import org.kiwix.kiwixmobile.core.utils.dialog.UnsupportedMimeTypeHandler import java.io.File import java.io.InputStream @@ -48,6 +51,7 @@ class UnsupportedMimeTypeHandlerTest { private val savedFile: File = mockk(relaxed = true) private val activity: Activity = mockk() private val webResourceResponse: WebResourceResponse = mockk() + private val coroutineScope = CoroutineScope(Dispatchers.Main) private val inputStream: InputStream = mockk() private val unsupportedMimeTypeHandler = UnsupportedMimeTypeHandler( activity, @@ -85,13 +89,17 @@ class UnsupportedMimeTypeHandlerTest { } @Test - fun testOpeningFileInExternalReaderApplication() { + fun testOpeningFileInExternalReaderApplication() = runBlocking { every { unsupportedMimeTypeHandler.intent.resolveActivity(activity.packageManager) } returns mockk() every { activity.startActivity(unsupportedMimeTypeHandler.intent) } returns mockk() val lambdaSlot = slot<() -> Unit>() - unsupportedMimeTypeHandler.showSaveOrOpenUnsupportedFilesDialog(demoUrl, "application/pdf") + unsupportedMimeTypeHandler.showSaveOrOpenUnsupportedFilesDialog( + demoUrl, + "application/pdf", + coroutineScope + ) verify { alertDialogShower.show( KiwixDialog.SaveOrOpenUnsupportedFiles, @@ -116,7 +124,11 @@ class UnsupportedMimeTypeHandlerTest { Toast.makeText(activity, R.string.no_reader_application_installed, Toast.LENGTH_LONG).show() } val lambdaSlot = slot<() -> Unit>() - unsupportedMimeTypeHandler.showSaveOrOpenUnsupportedFilesDialog(demoUrl, "application/pdf") + unsupportedMimeTypeHandler.showSaveOrOpenUnsupportedFilesDialog( + demoUrl, + "application/pdf", + coroutineScope + ) verify { alertDialogShower.show( KiwixDialog.SaveOrOpenUnsupportedFiles, @@ -143,7 +155,8 @@ class UnsupportedMimeTypeHandlerTest { val lambdaSlot = slot<() -> Unit>() unsupportedMimeTypeHandler.showSaveOrOpenUnsupportedFilesDialog( demoUrl, - "application/pdf" + "application/pdf", + coroutineScope ) verify { alertDialogShower.show( @@ -160,7 +173,11 @@ class UnsupportedMimeTypeHandlerTest { @Test fun testUserClicksOnNoThanksButton() { val lambdaSlot = slot<() -> Unit>() - unsupportedMimeTypeHandler.showSaveOrOpenUnsupportedFilesDialog(demoUrl, "application/pdf") + unsupportedMimeTypeHandler.showSaveOrOpenUnsupportedFilesDialog( + demoUrl, + "application/pdf", + coroutineScope + ) verify { alertDialogShower.show( KiwixDialog.SaveOrOpenUnsupportedFiles, @@ -186,7 +203,11 @@ class UnsupportedMimeTypeHandlerTest { Toast.makeText(activity, R.string.save_media_error, Toast.LENGTH_LONG).show() } val lambdaSlot = slot<() -> Unit>() - downloadOrOpenEpubAndPdfHandler.showSaveOrOpenUnsupportedFilesDialog(null, "application/pdf") + downloadOrOpenEpubAndPdfHandler.showSaveOrOpenUnsupportedFilesDialog( + null, + "application/pdf", + coroutineScope + ) verify { alertDialogShower.show( KiwixDialog.SaveOrOpenUnsupportedFiles, diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/utils/files/FileUtilsInstrumentationTest.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/utils/files/FileUtilsInstrumentationTest.kt index 9c0a42d5c2..801828e2aa 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/utils/files/FileUtilsInstrumentationTest.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/utils/files/FileUtilsInstrumentationTest.kt @@ -24,6 +24,10 @@ import android.os.Environment import androidx.test.platform.app.InstrumentationRegistry import io.mockk.every import io.mockk.mockk +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import org.junit.After import org.junit.Assert import org.junit.Before @@ -71,7 +75,7 @@ class FileUtilsInstrumentationTest { @Test @Throws(IOException::class) - fun testGetAllZimParts() { + fun testGetAllZimParts() = runBlocking { // Filename ends with .zimXX and the files up till "FileName.zimer" exist // i.e. 26 * 4 + 18 = 122 files exist @@ -140,7 +144,7 @@ class FileUtilsInstrumentationTest { @Test @Throws(IOException::class) - fun testHasPart() { + fun testHasPart() = runBlocking { val testId = "3yd5474g-55d1-aqw0-108z-1xp69x25260d" val baseName = testDir?.path + "/" + testId + "testFile" @@ -418,12 +422,14 @@ class FileUtilsInstrumentationTest { ) ) context?.let { context -> - dummyUriData.forEach { dummyUrlData -> - dummyUrlData.uri?.let { uri -> - Assertions.assertEquals( - FileUtils.getLocalFilePathByUri(context, uri), - dummyUrlData.expectedFileName - ) + CoroutineScope(Dispatchers.Main).launch { + dummyUriData.forEach { dummyUrlData -> + dummyUrlData.uri?.let { uri -> + Assertions.assertEquals( + FileUtils.getLocalFilePathByUri(context, uri), + dummyUrlData.expectedFileName + ) + } } } } @@ -489,7 +495,7 @@ class FileUtilsInstrumentationTest { } @Test - fun testDocumentProviderContentQuery() { + fun testDocumentProviderContentQuery() = runBlocking { // We are not running this test case on Android 13 and above. In this version, // numerous security updates have been included, preventing us from modifying the // default behavior of ContentResolver. @@ -544,7 +550,7 @@ class FileUtilsInstrumentationTest { } } - private fun testWithDownloadUri( + private suspend fun testWithDownloadUri( uri: Uri, expectedPath: String, documentsContractWrapper: DocumentResolverWrapper = DocumentResolverWrapper() diff --git a/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/CopyMoveFileHandler.kt b/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/CopyMoveFileHandler.kt index bcd35d292e..77d38518d2 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/CopyMoveFileHandler.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/CopyMoveFileHandler.kt @@ -102,7 +102,7 @@ class CopyMoveFileHandler @Inject constructor( } } - fun showMoveFileToPublicDirectoryDialog( + suspend fun showMoveFileToPublicDirectoryDialog( uri: Uri? = null, documentFile: DocumentFile? = null, shouldValidateZimFile: Boolean = false, @@ -155,16 +155,18 @@ class CopyMoveFileHandler @Inject constructor( ) fun copyMoveZIMFileInSelectedStorage(storageDevice: StorageDevice) { - sharedPreferenceUtil.apply { - shouldShowStorageSelectionDialog = false - putPrefStorage(sharedPreferenceUtil.getPublicDirectoryPath(storageDevice.name)) - putStoragePosition( - if (storageDevice.isInternal) INTERNAL_SELECT_POSITION - else EXTERNAL_SELECT_POSITION - ) - } - if (validateZimFileCanCopyOrMove()) { - performCopyMoveOperation() + lifecycleScope?.launch { + sharedPreferenceUtil.apply { + shouldShowStorageSelectionDialog = false + putPrefStorage(sharedPreferenceUtil.getPublicDirectoryPath(storageDevice.name)) + putStoragePosition( + if (storageDevice.isInternal) INTERNAL_SELECT_POSITION + else EXTERNAL_SELECT_POSITION + ) + } + if (validateZimFileCanCopyOrMove()) { + performCopyMoveOperation() + } } } @@ -182,7 +184,9 @@ class CopyMoveFileHandler @Inject constructor( private fun hasNotSufficientStorageSpace(availableSpace: Long): Boolean = availableSpace < (selectedFile?.length() ?: 0L) - fun validateZimFileCanCopyOrMove(file: File = File(sharedPreferenceUtil.prefStorage)): Boolean { + suspend fun validateZimFileCanCopyOrMove( + file: File = File(sharedPreferenceUtil.prefStorage) + ): Boolean { hidePreparingCopyMoveDialog() // hide the dialog if already showing val availableSpace = storageCalculator.availableBytes(file) if (hasNotSufficientStorageSpace(availableSpace)) { @@ -227,19 +231,23 @@ class CopyMoveFileHandler @Inject constructor( fileSystemDisposable = fat32Checker.fileSystemStates .observeOn(AndroidSchedulers.mainThread()) .subscribe { - hidePreparingCopyMoveDialog() - if (validateZimFileCanCopyOrMove()) { - performCopyMoveOperation() + lifecycleScope?.launch { + hidePreparingCopyMoveDialog() + if (validateZimFileCanCopyOrMove()) { + performCopyMoveOperation() + } } } } fun performCopyMoveOperationIfSufficientSpaceAvailable() { - val availableSpace = storageCalculator.availableBytes(File(sharedPreferenceUtil.prefStorage)) - if (hasNotSufficientStorageSpace(availableSpace)) { - fileCopyMoveCallback?.insufficientSpaceInStorage(availableSpace) - } else { - performCopyMoveOperation() + lifecycleScope?.launch { + val availableSpace = storageCalculator.availableBytes(File(sharedPreferenceUtil.prefStorage)) + if (hasNotSufficientStorageSpace(availableSpace)) { + fileCopyMoveCallback?.insufficientSpaceInStorage(availableSpace) + } else { + performCopyMoveOperation() + } } } @@ -475,7 +483,7 @@ class CopyMoveFileHandler @Inject constructor( } ?: throw FileNotFoundException("The selected file could not be opened") } - fun getDestinationFile(): File { + suspend fun getDestinationFile(): File { val root = File(sharedPreferenceUtil.prefStorage) val fileName = selectedFile?.name ?: "" diff --git a/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/LocalLibraryFragment.kt b/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/LocalLibraryFragment.kt index f201eca2b1..f2e8abbbc7 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/LocalLibraryFragment.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/LocalLibraryFragment.kt @@ -74,6 +74,7 @@ import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.isManageExternal import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.navigate import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.viewModel import org.kiwix.kiwixmobile.core.extensions.coreMainActivity +import org.kiwix.kiwixmobile.core.extensions.isFileExist import org.kiwix.kiwixmobile.core.extensions.setBottomMarginToFragmentContainerView import org.kiwix.kiwixmobile.core.extensions.snack import org.kiwix.kiwixmobile.core.extensions.toast @@ -412,40 +413,42 @@ class LocalLibraryFragment : BaseFragment(), CopyMoveFileHandler.FileCopyMoveCal } fun handleSelectedFileUri(uri: Uri) { - if (sharedPreferenceUtil.isPlayStoreBuildWithAndroid11OrAbove()) { - val documentFile = when (uri.scheme) { - "file" -> DocumentFile.fromFile(File("$uri")) - else -> { - DocumentFile.fromSingleUri(requireActivity(), uri) + lifecycleScope.launch { + if (sharedPreferenceUtil.isPlayStoreBuildWithAndroid11OrAbove()) { + val documentFile = when (uri.scheme) { + "file" -> DocumentFile.fromFile(File("$uri")) + else -> { + DocumentFile.fromSingleUri(requireActivity(), uri) + } } + // If the file is not valid, it shows an error message and stops further processing. + // If the file name is not found, then let them to copy the file + // and we will handle this later. + val fileName = documentFile?.name + if (fileName != null && !FileUtils.isValidZimFile(fileName)) { + activity.toast(string.error_file_invalid) + return@launch + } + copyMoveFileHandler?.showMoveFileToPublicDirectoryDialog( + uri, + documentFile, + // pass if fileName is null then we will validate it after copying/moving + fileName == null, + parentFragmentManager + ) + } else { + getZimFileFromUri(uri)?.let(::navigateToReaderFragment) } - // If the file is not valid, it shows an error message and stops further processing. - // If the file name is not found, then let them to copy the file - // and we will handle this later. - val fileName = documentFile?.name - if (fileName != null && !FileUtils.isValidZimFile(fileName)) { - activity.toast(string.error_file_invalid) - return - } - copyMoveFileHandler?.showMoveFileToPublicDirectoryDialog( - uri, - documentFile, - // pass if fileName is null then we will validate it after copying/moving - fileName == null, - parentFragmentManager - ) - } else { - getZimFileFromUri(uri)?.let(::navigateToReaderFragment) } } - private fun getZimFileFromUri( + private suspend fun getZimFileFromUri( uri: Uri ): File? { val filePath = FileUtils.getLocalFilePathByUri( requireActivity().applicationContext, uri ) - if (filePath == null || !File(filePath).exists()) { + if (filePath == null || !File(filePath).isFileExist()) { activity.toast(string.error_file_not_found) return null } diff --git a/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/OnlineLibraryFragment.kt b/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/OnlineLibraryFragment.kt index b0f8ecc398..9999f9ca47 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/OnlineLibraryFragment.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/OnlineLibraryFragment.kt @@ -47,12 +47,14 @@ import androidx.fragment.app.FragmentActivity import androidx.lifecycle.Lifecycle import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import eu.mhutti1.utils.storage.STORAGE_SELECT_STORAGE_TITLE_TEXTVIEW_SIZE import eu.mhutti1.utils.storage.StorageDevice import eu.mhutti1.utils.storage.StorageDeviceUtils import eu.mhutti1.utils.storage.StorageSelectDialog +import kotlinx.coroutines.launch import org.kiwix.kiwixmobile.R import org.kiwix.kiwixmobile.cachedComponent import org.kiwix.kiwixmobile.core.R.string @@ -89,8 +91,8 @@ import org.kiwix.kiwixmobile.core.utils.dialog.AlertDialogShower import org.kiwix.kiwixmobile.core.utils.dialog.DialogShower import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog.YesNoDialog.WifiOnly -import org.kiwix.kiwixmobile.databinding.FragmentDestinationDownloadBinding import org.kiwix.kiwixmobile.core.zim_manager.NetworkState +import org.kiwix.kiwixmobile.databinding.FragmentDestinationDownloadBinding import org.kiwix.kiwixmobile.zimManager.ZimManageViewModel import org.kiwix.kiwixmobile.zimManager.libraryView.AvailableSpaceCalculator import org.kiwix.kiwixmobile.zimManager.libraryView.adapter.LibraryAdapter @@ -123,7 +125,12 @@ class OnlineLibraryFragment : BaseFragment(), FragmentActivityExtensions { private val libraryAdapter: LibraryAdapter by lazy { LibraryAdapter( - LibraryDelegate.BookDelegate(bookUtils, ::onBookItemClick, availableSpaceCalculator), + LibraryDelegate.BookDelegate( + bookUtils, + ::onBookItemClick, + availableSpaceCalculator, + lifecycleScope + ), LibraryDelegate.DownloadDelegate( { if (it.currentDownloadState == Status.FAILED) { @@ -322,7 +329,6 @@ class OnlineLibraryFragment : BaseFragment(), FragmentActivityExtensions { override fun onDestroyView() { super.onDestroyView() - availableSpaceCalculator.dispose() fragmentDestinationDownloadBinding?.libraryList?.adapter = null fragmentDestinationDownloadBinding = null } @@ -520,58 +526,60 @@ class OnlineLibraryFragment : BaseFragment(), FragmentActivityExtensions { @Suppress("NestedBlockDepth") private fun onBookItemClick(item: LibraryListItem.BookItem) { - if (checkExternalStorageWritePermission()) { - downloadBookItem = item - if (requireActivity().hasNotificationPermission(sharedPreferenceUtil)) { - when { - isNotConnected -> { - noInternetSnackbar() - return - } + lifecycleScope.launch { + if (checkExternalStorageWritePermission()) { + downloadBookItem = item + if (requireActivity().hasNotificationPermission(sharedPreferenceUtil)) { + when { + isNotConnected -> { + noInternetSnackbar() + return@launch + } - noWifiWithWifiOnlyPreferenceSet -> { - dialogShower.show(WifiOnly, { - sharedPreferenceUtil.putPrefWifiOnly(false) - clickOnBookItem() - }) - return - } + noWifiWithWifiOnlyPreferenceSet -> { + dialogShower.show(WifiOnly, { + sharedPreferenceUtil.putPrefWifiOnly(false) + clickOnBookItem() + }) + return@launch + } - else -> if (sharedPreferenceUtil.showStorageOption) { - // Show the storage selection dialog for configuration if there is an SD card available. - if (storageDeviceList.size > 1) { - showStorageSelectDialog() + else -> if (sharedPreferenceUtil.showStorageOption) { + // Show the storage selection dialog for configuration if there is an SD card available. + if (storageDeviceList.size > 1) { + showStorageSelectDialog() + } else { + // If only internal storage is available, proceed with the ZIM file download directly. + // Displaying a configuration dialog is unnecessary in this case. + sharedPreferenceUtil.showStorageOption = false + onBookItemClick(item) + } + } else if (!requireActivity().isManageExternalStoragePermissionGranted( + sharedPreferenceUtil + ) + ) { + showManageExternalStoragePermissionDialog() } else { - // If only internal storage is available, proceed with the ZIM file download directly. - // Displaying a configuration dialog is unnecessary in this case. - sharedPreferenceUtil.showStorageOption = false - onBookItemClick(item) + availableSpaceCalculator.hasAvailableSpaceFor( + item, + { downloadFile() }, + { + fragmentDestinationDownloadBinding?.libraryList?.snack( + """ + ${getString(string.download_no_space)} + ${getString(string.space_available)} $it + """.trimIndent(), + requireActivity().findViewById(R.id.bottom_nav_view), + string.download_change_storage, + ::showStorageSelectDialog + ) + } + ) } - } else if (!requireActivity().isManageExternalStoragePermissionGranted( - sharedPreferenceUtil - ) - ) { - showManageExternalStoragePermissionDialog() - } else { - availableSpaceCalculator.hasAvailableSpaceFor( - item, - { downloadFile() }, - { - fragmentDestinationDownloadBinding?.libraryList?.snack( - """ - ${getString(string.download_no_space)} - ${getString(string.space_available)} $it - """.trimIndent(), - requireActivity().findViewById(R.id.bottom_nav_view), - string.download_change_storage, - ::showStorageSelectDialog - ) - } - ) } + } else { + requestNotificationPermission() } - } else { - requestNotificationPermission() } } } diff --git a/app/src/main/java/org/kiwix/kiwixmobile/settings/KiwixPrefsFragment.kt b/app/src/main/java/org/kiwix/kiwixmobile/settings/KiwixPrefsFragment.kt index 8e40ca4c7d..1383a2c17a 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/settings/KiwixPrefsFragment.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/settings/KiwixPrefsFragment.kt @@ -22,6 +22,7 @@ import android.os.Build import android.os.Bundle import android.os.Environment import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.lifecycleScope import androidx.preference.Preference import androidx.preference.PreferenceCategory import eu.mhutti1.utils.storage.StorageDevice @@ -30,6 +31,7 @@ import io.reactivex.Flowable import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.launch import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.extensions.getFreeSpace import org.kiwix.kiwixmobile.core.extensions.getUsedSpace @@ -74,23 +76,25 @@ class KiwixPrefsFragment : CorePrefsFragment() { } private fun setUpStoragePreference(sharedPreferenceUtil: SharedPreferenceUtil) { - storageDeviceList.forEachIndexed { index, storageDevice -> - val preferenceKey = if (index == 0) PREF_INTERNAL_STORAGE else PREF_EXTERNAL_STORAGE - val selectedStoragePosition = sharedPreferenceUtil.storagePosition - val isChecked = selectedStoragePosition == index - findPreference(preferenceKey)?.apply { - this.isChecked = isChecked - setOnPreferenceClickListener { - onStorageDeviceSelected(storageDevice) - true - } - storageCalculator?.let { - setPathAndTitleForStorage( - storageDevice.storagePathAndTitle(context, index, sharedPreferenceUtil, it) - ) - setFreeSpace(storageDevice.getFreeSpace(context, it)) - setUsedSpace(storageDevice.getUsedSpace(context, it)) - setProgress(storageDevice.usedPercentage(it)) + lifecycleScope.launch { + storageDeviceList.forEachIndexed { index, storageDevice -> + val preferenceKey = if (index == 0) PREF_INTERNAL_STORAGE else PREF_EXTERNAL_STORAGE + val selectedStoragePosition = sharedPreferenceUtil.storagePosition + val isChecked = selectedStoragePosition == index + findPreference(preferenceKey)?.apply { + this.isChecked = isChecked + setOnPreferenceClickListener { + onStorageDeviceSelected(storageDevice) + true + } + storageCalculator?.let { + setPathAndTitleForStorage( + storageDevice.storagePathAndTitle(context, index, sharedPreferenceUtil, it) + ) + setFreeSpace(storageDevice.getFreeSpace(context, it)) + setUsedSpace(storageDevice.getUsedSpace(context, it)) + setProgress(storageDevice.usedPercentage(it)) + } } } } diff --git a/app/src/main/java/org/kiwix/kiwixmobile/zimManager/libraryView/AvailableSpaceCalculator.kt b/app/src/main/java/org/kiwix/kiwixmobile/zimManager/libraryView/AvailableSpaceCalculator.kt index f48d022f73..befd97d584 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/zimManager/libraryView/AvailableSpaceCalculator.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/zimManager/libraryView/AvailableSpaceCalculator.kt @@ -20,9 +20,8 @@ package org.kiwix.kiwixmobile.zimManager.libraryView import eu.mhutti1.utils.storage.Bytes import eu.mhutti1.utils.storage.Kb -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.disposables.Disposable -import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map import org.kiwix.kiwixmobile.core.dao.DownloadRoomDao import org.kiwix.kiwixmobile.core.downloader.model.DownloadModel import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book @@ -34,30 +33,22 @@ class AvailableSpaceCalculator @Inject constructor( private val downloadRoomDao: DownloadRoomDao, private val storageCalculator: StorageCalculator ) { - private var availableSpaceCalculatorDisposable: Disposable? = null - fun hasAvailableSpaceFor( + suspend fun hasAvailableSpaceFor( bookItem: LibraryListItem.BookItem, successAction: (LibraryListItem.BookItem) -> Unit, failureAction: (String) -> Unit ) { - availableSpaceCalculatorDisposable = downloadRoomDao.allDownloads() - .map { it.map(DownloadModel::bytesRemaining).sum() } + val trueAvailableBytes = downloadRoomDao.allDownloads() + .map { downloads -> downloads.sumOf(DownloadModel::bytesRemaining) } .map { bytesToBeDownloaded -> storageCalculator.availableBytes() - bytesToBeDownloaded } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe { trueAvailableBytes -> - if (bookItem.book.size.toLong() * Kb < trueAvailableBytes) { - successAction.invoke(bookItem) - } else { - failureAction.invoke(Bytes(trueAvailableBytes).humanReadable) - } - } + .first() + if (bookItem.book.size.toLong() * Kb < trueAvailableBytes) { + successAction.invoke(bookItem) + } else { + failureAction.invoke(Bytes(trueAvailableBytes).humanReadable) + } } - fun hasAvailableSpaceForBook(book: Book) = + suspend fun hasAvailableSpaceForBook(book: Book) = book.size.toLong() * Kb < storageCalculator.availableBytes() - - fun dispose() { - availableSpaceCalculatorDisposable?.dispose() - } } diff --git a/app/src/main/java/org/kiwix/kiwixmobile/zimManager/libraryView/adapter/LibraryDelegate.kt b/app/src/main/java/org/kiwix/kiwixmobile/zimManager/libraryView/adapter/LibraryDelegate.kt index 42c8747b45..5285a138dc 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/zimManager/libraryView/adapter/LibraryDelegate.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/zimManager/libraryView/adapter/LibraryDelegate.kt @@ -18,6 +18,7 @@ package org.kiwix.kiwixmobile.zimManager.libraryView.adapter import android.view.ViewGroup +import androidx.lifecycle.LifecycleCoroutineScope import org.kiwix.kiwixmobile.core.base.adapter.AbsDelegateAdapter import org.kiwix.kiwixmobile.core.extensions.ViewGroupExtensions.viewBinding import org.kiwix.kiwixmobile.core.utils.BookUtils @@ -38,7 +39,8 @@ sealed class LibraryDelegate> class BookDelegate( private val bookUtils: BookUtils, private val clickAction: (BookItem) -> Unit, - private val availableSpaceCalculator: AvailableSpaceCalculator + private val availableSpaceCalculator: AvailableSpaceCalculator, + private val lifecycleCoroutineScope: LifecycleCoroutineScope ) : LibraryDelegate() { override val itemClass = BookItem::class.java @@ -47,7 +49,8 @@ sealed class LibraryDelegate> parent.viewBinding(ItemLibraryBinding::inflate, false), bookUtils, clickAction, - availableSpaceCalculator + availableSpaceCalculator, + lifecycleCoroutineScope ) } diff --git a/app/src/main/java/org/kiwix/kiwixmobile/zimManager/libraryView/adapter/LibraryViewHolder.kt b/app/src/main/java/org/kiwix/kiwixmobile/zimManager/libraryView/adapter/LibraryViewHolder.kt index ddb442b37e..89eae16a40 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/zimManager/libraryView/adapter/LibraryViewHolder.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/zimManager/libraryView/adapter/LibraryViewHolder.kt @@ -19,6 +19,8 @@ package org.kiwix.kiwixmobile.zimManager.libraryView.adapter import android.view.View +import androidx.lifecycle.LifecycleCoroutineScope +import kotlinx.coroutines.launch import org.kiwix.kiwixmobile.R import org.kiwix.kiwixmobile.core.R.string import org.kiwix.kiwixmobile.core.base.adapter.BaseViewHolder @@ -50,45 +52,49 @@ sealed class LibraryViewHolder(containerView: View) : private val itemLibraryBinding: ItemLibraryBinding, private val bookUtils: BookUtils, private val clickAction: (BookItem) -> Unit, - private val availableSpaceCalculator: AvailableSpaceCalculator + private val availableSpaceCalculator: AvailableSpaceCalculator, + private val lifecycleCoroutineScope: LifecycleCoroutineScope ) : LibraryViewHolder(itemLibraryBinding.root) { override fun bind(item: BookItem) { - itemLibraryBinding.libraryBookTitle.setTextAndVisibility(item.book.title) - itemLibraryBinding.libraryBookDescription.setTextAndVisibility(item.book.description) - itemLibraryBinding.libraryBookCreator.setTextAndVisibility(item.book.creator) - itemLibraryBinding.libraryBookDate.setTextAndVisibility(item.book.date) - itemLibraryBinding.libraryBookSize.setTextAndVisibility( - KiloByte(item.book.size).humanReadable - ) - itemLibraryBinding.libraryBookLanguage.text = bookUtils.getLanguage(item.book.language) - itemLibraryBinding.libraryBookFavicon.setBitmap(Base64String(item.book.favicon)) + lifecycleCoroutineScope.launch { + itemLibraryBinding.libraryBookTitle.setTextAndVisibility(item.book.title) + itemLibraryBinding.libraryBookDescription.setTextAndVisibility(item.book.description) + itemLibraryBinding.libraryBookCreator.setTextAndVisibility(item.book.creator) + itemLibraryBinding.libraryBookDate.setTextAndVisibility(item.book.date) + itemLibraryBinding.libraryBookSize.setTextAndVisibility( + KiloByte(item.book.size).humanReadable + ) + itemLibraryBinding.libraryBookLanguage.text = bookUtils.getLanguage(item.book.language) + itemLibraryBinding.libraryBookFavicon.setBitmap(Base64String(item.book.favicon)) - val hasAvailableSpaceInStorage = availableSpaceCalculator.hasAvailableSpaceForBook(item.book) - containerView.setOnClickListener { clickAction.invoke(item) } - containerView.isClickable = - item.canBeDownloaded && hasAvailableSpaceInStorage + val hasAvailableSpaceInStorage = + availableSpaceCalculator.hasAvailableSpaceForBook(item.book) + containerView.setOnClickListener { clickAction.invoke(item) } + containerView.isClickable = + item.canBeDownloaded && hasAvailableSpaceInStorage - itemLibraryBinding.tags.render(item.tags) + itemLibraryBinding.tags.render(item.tags) - itemLibraryBinding.unableToDownload.visibility = - if (item.canBeDownloaded && hasAvailableSpaceInStorage) - View.GONE - else - View.VISIBLE - itemLibraryBinding.unableToDownload.setOnLongClickListener { - val context = itemLibraryBinding.root.context - when (item.fileSystemState) { - CannotWrite4GbFile -> context.toast(R.string.file_system_does_not_support_4gb) - DetectingFileSystem -> context.toast(R.string.detecting_file_system) - else -> { - if (item.canBeDownloaded && !hasAvailableSpaceInStorage) { - clickAction.invoke(item) - } else { - throw RuntimeException("impossible invalid state: ${item.fileSystemState}") + itemLibraryBinding.unableToDownload.visibility = + if (item.canBeDownloaded && hasAvailableSpaceInStorage) + View.GONE + else + View.VISIBLE + itemLibraryBinding.unableToDownload.setOnLongClickListener { + val context = itemLibraryBinding.root.context + when (item.fileSystemState) { + CannotWrite4GbFile -> context.toast(R.string.file_system_does_not_support_4gb) + DetectingFileSystem -> context.toast(R.string.detecting_file_system) + else -> { + if (item.canBeDownloaded && !hasAvailableSpaceInStorage) { + clickAction.invoke(item) + } else { + throw RuntimeException("impossible invalid state: ${item.fileSystemState}") + } } } + true } - true } } } diff --git a/app/src/test/java/org/kiwix/kiwixmobile/localLibrary/CopyMoveFileHandlerTest.kt b/app/src/test/java/org/kiwix/kiwixmobile/localLibrary/CopyMoveFileHandlerTest.kt index a4558f6eb2..a8875e183c 100644 --- a/app/src/test/java/org/kiwix/kiwixmobile/localLibrary/CopyMoveFileHandlerTest.kt +++ b/app/src/test/java/org/kiwix/kiwixmobile/localLibrary/CopyMoveFileHandlerTest.kt @@ -32,6 +32,7 @@ import io.mockk.mockk import io.mockk.slot import io.mockk.spyk import io.mockk.verify +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest @@ -89,54 +90,57 @@ class CopyMoveFileHandlerTest { } @Test - fun validateZimFileCanCopyOrMoveShouldReturnTrueWhenSufficientSpaceAndValidFileSystem() { - prepareFileSystemAndFileForMockk() + fun validateZimFileCanCopyOrMoveShouldReturnTrueWhenSufficientSpaceAndValidFileSystem() = + runBlocking { + prepareFileSystemAndFileForMockk() - val result = fileHandler.validateZimFileCanCopyOrMove(storageFile) + val result = fileHandler.validateZimFileCanCopyOrMove(storageFile) - assertTrue(result) - // check insufficientSpaceInStorage callback should not call. - verify(exactly = 0) { fileCopyMoveCallback.insufficientSpaceInStorage(any()) } - } + assertTrue(result) + // check insufficientSpaceInStorage callback should not call. + verify(exactly = 0) { fileCopyMoveCallback.insufficientSpaceInStorage(any()) } + } @Test - fun validateZimFileCanCopyOrMoveShouldReturnFalseAndCallCallbackWhenInsufficientSpace() { - prepareFileSystemAndFileForMockk( - selectedFileLength = 2000L, - fileSystemState = CanWrite4GbFile - ) - val result = fileHandler.validateZimFileCanCopyOrMove(storageFile) + fun validateZimFileCanCopyOrMoveShouldReturnFalseAndCallCallbackWhenInsufficientSpace() = + runBlocking { + prepareFileSystemAndFileForMockk( + selectedFileLength = 2000L, + fileSystemState = CanWrite4GbFile + ) + val result = fileHandler.validateZimFileCanCopyOrMove(storageFile) - assertFalse(result) - verify { fileCopyMoveCallback.insufficientSpaceInStorage(any()) } - } + assertFalse(result) + verify { fileCopyMoveCallback.insufficientSpaceInStorage(any()) } + } @Test - fun validateZimFileCanCopyOrMoveShouldReturnFalseWhenDetectingAndCanNotWrite4GBFiles() { - prepareFileSystemAndFileForMockk(fileSystemState = DetectingFileSystem) - // check when detecting the fileSystem - assertFalse(fileHandler.validateZimFileCanCopyOrMove(storageFile)) + fun validateZimFileCanCopyOrMoveShouldReturnFalseWhenDetectingAndCanNotWrite4GBFiles() = + runBlocking { + prepareFileSystemAndFileForMockk(fileSystemState = DetectingFileSystem) + // check when detecting the fileSystem + assertFalse(fileHandler.validateZimFileCanCopyOrMove(storageFile)) - prepareFileSystemAndFileForMockk(fileSystemState = CannotWrite4GbFile) + prepareFileSystemAndFileForMockk(fileSystemState = CannotWrite4GbFile) - // check when Can not write 4GB files on the fileSystem - assertFalse(fileHandler.validateZimFileCanCopyOrMove()) - } + // check when Can not write 4GB files on the fileSystem + assertFalse(fileHandler.validateZimFileCanCopyOrMove()) + } @Test - fun validateZimFileCanCopyOrMoveShouldReturnFalseWhenDetectingFileSystem() { - every { fileHandler.isBookLessThan4GB() } returns true - every { fileHandler.performCopyMoveOperationIfSufficientSpaceAvailable() } just Runs - prepareFileSystemAndFileForMockk(fileSystemState = DetectingFileSystem) + fun validateZimFileCanCopyOrMoveShouldReturnFalseWhenDetectingFileSystem() = + runTest { + every { fileHandler.isBookLessThan4GB() } returns true + prepareFileSystemAndFileForMockk(fileSystemState = DetectingFileSystem) - val result = fileHandler.validateZimFileCanCopyOrMove(storageFile) + val result = fileHandler.validateZimFileCanCopyOrMove(storageFile) - assertFalse(result) - verify { fileHandler.handleDetectingFileSystemState() } - } + assertFalse(result) + verify { fileHandler.handleDetectingFileSystemState() } + } @Test - fun validateZimFileCanCopyOrMoveShouldReturnFalseWhenCannotWrite4GbFile() { + fun validateZimFileCanCopyOrMoveShouldReturnFalseWhenCannotWrite4GbFile() = runBlocking { every { fileHandler.isBookLessThan4GB() } returns true every { fileHandler.showCopyMoveDialog() } just Runs every { @@ -202,7 +206,7 @@ class CopyMoveFileHandlerTest { } @Test - fun showStorageConfigureDialogAtFirstLaunch() { + fun showStorageConfigureDialogAtFirstLaunch() = runBlocking { fileHandler = spyk(fileHandler) every { fileHandler.showStorageSelectDialog() } just Runs every { sharedPreferenceUtil.shouldShowStorageSelectionDialog } returns true @@ -216,13 +220,13 @@ class CopyMoveFileHandlerTest { ) } just Runs fileHandler.showMoveFileToPublicDirectoryDialog(fragmentManager = fragmentManager) - every { fileHandler.validateZimFileCanCopyOrMove() } returns true + coEvery { fileHandler.validateZimFileCanCopyOrMove() } returns true positiveButtonClickSlot.captured.invoke() verify { fileHandler.showStorageSelectDialog() } } @Test - fun shouldNotShowStorageConfigureDialogWhenThereIsOnlyInternalAvailable() { + fun shouldNotShowStorageConfigureDialogWhenThereIsOnlyInternalAvailable() = runBlocking { fileHandler = spyk(fileHandler) every { sharedPreferenceUtil.shouldShowStorageSelectionDialog } returns true every { fileHandler.storageDeviceList } returns listOf(mockk()) @@ -234,18 +238,18 @@ class CopyMoveFileHandlerTest { any() ) } just Runs - every { fileHandler.validateZimFileCanCopyOrMove() } returns true + coEvery { fileHandler.validateZimFileCanCopyOrMove() } returns true fileHandler.showMoveFileToPublicDirectoryDialog(fragmentManager = fragmentManager) positiveButtonClickSlot.captured.invoke() verify(exactly = 0) { fileHandler.showStorageSelectDialog() } } @Test - fun showDirectlyCopyMoveDialogAfterFirstLaunch() { + fun showDirectlyCopyMoveDialogAfterFirstLaunch() = runBlocking { fileHandler = spyk(fileHandler) every { sharedPreferenceUtil.shouldShowStorageSelectionDialog } returns false every { fileHandler.storageDeviceList } returns listOf(mockk(), mockk()) - every { fileHandler.validateZimFileCanCopyOrMove() } returns true + coEvery { fileHandler.validateZimFileCanCopyOrMove() } returns true prepareFileSystemAndFileForMockk() every { alertDialogShower.show(any(), any(), any()) } just Runs fileHandler.showMoveFileToPublicDirectoryDialog(fragmentManager = fragmentManager) @@ -260,7 +264,7 @@ class CopyMoveFileHandlerTest { } @Test - fun copyMoveFunctionsShouldCallWhenClickingOnButtonsInCopyMoveDialog() { + fun copyMoveFunctionsShouldCallWhenClickingOnButtonsInCopyMoveDialog() = runBlocking { val positiveButtonClickSlot = slot<() -> Unit>() val negativeButtonClickSlot = slot<() -> Unit>() fileHandler = spyk(fileHandler) @@ -274,7 +278,7 @@ class CopyMoveFileHandlerTest { ) } just Runs - every { fileHandler.validateZimFileCanCopyOrMove() } returns true + coEvery { fileHandler.validateZimFileCanCopyOrMove() } returns true fileHandler.showMoveFileToPublicDirectoryDialog(fragmentManager = fragmentManager) every { fileHandler.performCopyOperation() } just Runs @@ -297,7 +301,7 @@ class CopyMoveFileHandlerTest { every { storageFile.freeSpace } returns freeSpaceInStorage every { storageFile.path } returns storagePath every { selectedFile.length() } returns selectedFileLength - every { storageCalculator.availableBytes(storageFile) } returns availableStorageSize + coEvery { storageCalculator.availableBytes(storageFile) } returns availableStorageSize every { fat32Checker.fileSystemStates.value } returns fileSystemState } diff --git a/core/detekt_baseline.xml b/core/detekt_baseline.xml index a8770c6630..6415a52a83 100644 --- a/core/detekt_baseline.xml +++ b/core/detekt_baseline.xml @@ -35,7 +35,7 @@ MaxLineLength:MetaLinkNetworkEntityTest.kt$MetaLinkNetworkEntityTest$"http://www.mirrorservice.org/sites/download.kiwix.org/zim/wikipedia/wikipedia_af_all_nopic_2016-05.zim" MaxLineLength:NetworkUtilsTest.kt$NetworkUtilsTest$// Here the Method should return the substring between the first '?' character and the nearest '/' character preceeding it NestedBlockDepth:FileUtils.kt$FileUtils$@JvmStatic @Synchronized fun deleteZimFile(path: String) - NestedBlockDepth:FileUtils.kt$FileUtils$@JvmStatic fun getLocalFilePathByUri( context: Context, uri: Uri ): String? + NestedBlockDepth:FileUtils.kt$FileUtils$@JvmStatic suspend fun getLocalFilePathByUri( context: Context, uri: Uri ): String? NestedBlockDepth:ImageUtils.kt$ImageUtils$private fun getBitmapFromView(width: Int, height: Int, viewToDrawFrom: View): Bitmap? NestedBlockDepth:JNIInitialiser.kt$JNIInitialiser$private fun loadICUData(context: Context): String? NestedBlockDepth:OnSwipeTouchListener.kt$OnSwipeTouchListener.GestureListener$override fun onFling( e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float ): Boolean @@ -54,8 +54,8 @@ PackageNaming:ConnectivityBroadcastReceiver.kt$package org.kiwix.kiwixmobile.core.zim_manager PackageNaming:NetworkState.kt$package org.kiwix.kiwixmobile.core.zim_manager ReturnCount:FileUtils.kt$FileUtils$@JvmStatic fun getAllZimParts(book: Book): List<File> - ReturnCount:FileUtils.kt$FileUtils$@JvmStatic fun getLocalFilePathByUri( context: Context, uri: Uri ): String? - ReturnCount:FileUtils.kt$FileUtils$@JvmStatic fun hasPart(file: File): Boolean + ReturnCount:FileUtils.kt$FileUtils$@JvmStatic suspend fun getLocalFilePathByUri( context: Context, uri: Uri ): String? + ReturnCount:FileUtils.kt$FileUtils$@JvmStatic suspend fun hasPart(file: File): Boolean ReturnCount:FileUtils.kt$FileUtils$@Synchronized private fun deleteZimFileParts(path: String): Boolean ReturnCount:ImageUtils.kt$ImageUtils$private fun getBitmapFromView(width: Int, height: Int, viewToDrawFrom: View): Bitmap? ReturnCount:OnSwipeTouchListener.kt$OnSwipeTouchListener.GestureListener$override fun onFling( e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float ): Boolean diff --git a/core/src/main/java/eu/mhutti1/utils/storage/StorageSelectDialog.kt b/core/src/main/java/eu/mhutti1/utils/storage/StorageSelectDialog.kt index c37989c768..cc2664475c 100644 --- a/core/src/main/java/eu/mhutti1/utils/storage/StorageSelectDialog.kt +++ b/core/src/main/java/eu/mhutti1/utils/storage/StorageSelectDialog.kt @@ -25,6 +25,7 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.DialogFragment import androidx.fragment.app.FragmentManager +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import eu.mhutti1.utils.storage.adapter.StorageAdapter @@ -56,7 +57,8 @@ class StorageSelectDialog : DialogFragment() { StorageDelegate( storageCalculator, sharedPreferenceUtil, - shouldShowCheckboxSelected + shouldShowCheckboxSelected, + lifecycleScope ) { onSelectAction?.invoke(it) dismiss() diff --git a/core/src/main/java/eu/mhutti1/utils/storage/adapter/StorageDelegate.kt b/core/src/main/java/eu/mhutti1/utils/storage/adapter/StorageDelegate.kt index c809f84764..3097e812a8 100644 --- a/core/src/main/java/eu/mhutti1/utils/storage/adapter/StorageDelegate.kt +++ b/core/src/main/java/eu/mhutti1/utils/storage/adapter/StorageDelegate.kt @@ -19,6 +19,7 @@ package eu.mhutti1.utils.storage.adapter import android.view.ViewGroup +import androidx.lifecycle.LifecycleCoroutineScope import androidx.recyclerview.widget.RecyclerView.ViewHolder import eu.mhutti1.utils.storage.StorageDevice import org.kiwix.kiwixmobile.core.base.adapter.AdapterDelegate @@ -31,6 +32,7 @@ class StorageDelegate( private val storageCalculator: StorageCalculator, private val sharedPreferenceUtil: SharedPreferenceUtil, private val shouldShowCheckboxSelected: Boolean, + private val lifecycleCoroutineScope: LifecycleCoroutineScope, private val onClickAction: (StorageDevice) -> Unit ) : AdapterDelegate { override fun createViewHolder(parent: ViewGroup): ViewHolder { @@ -39,6 +41,7 @@ class StorageDelegate( storageCalculator, sharedPreferenceUtil, shouldShowCheckboxSelected, + lifecycleCoroutineScope, onClickAction ) } diff --git a/core/src/main/java/eu/mhutti1/utils/storage/adapter/StorageViewHolder.kt b/core/src/main/java/eu/mhutti1/utils/storage/adapter/StorageViewHolder.kt index 05f6492d84..958874be9d 100644 --- a/core/src/main/java/eu/mhutti1/utils/storage/adapter/StorageViewHolder.kt +++ b/core/src/main/java/eu/mhutti1/utils/storage/adapter/StorageViewHolder.kt @@ -18,12 +18,13 @@ package eu.mhutti1.utils.storage.adapter -import android.annotation.SuppressLint import android.text.Spannable import android.text.SpannableStringBuilder import android.text.style.AbsoluteSizeSpan import android.view.View.VISIBLE +import androidx.lifecycle.LifecycleCoroutineScope import eu.mhutti1.utils.storage.StorageDevice +import kotlinx.coroutines.launch import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.base.adapter.BaseViewHolder import org.kiwix.kiwixmobile.core.databinding.ItemStoragePreferenceBinding @@ -39,47 +40,50 @@ import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil const val FREE_SPACE_TEXTVIEW_SIZE = 12F const val STORAGE_TITLE_TEXTVIEW_SIZE = 15 -@SuppressLint("SetTextI18n") +@Suppress("LongParameterList") internal class StorageViewHolder( private val itemStoragePreferenceBinding: ItemStoragePreferenceBinding, private val storageCalculator: StorageCalculator, private val sharedPreferenceUtil: SharedPreferenceUtil, private val shouldShowCheckboxSelected: Boolean, + private val lifecycleCoroutineScope: LifecycleCoroutineScope, private val onClickAction: (StorageDevice) -> Unit ) : BaseViewHolder(itemStoragePreferenceBinding.root) { override fun bind(item: StorageDevice) { with(itemStoragePreferenceBinding) { - storagePathAndTitle.text = - resizeStoragePathAndTitle( - item.storagePathAndTitle( - root.context, - adapterPosition, - sharedPreferenceUtil, - storageCalculator + lifecycleCoroutineScope.launch { + storagePathAndTitle.text = + resizeStoragePathAndTitle( + item.storagePathAndTitle( + root.context, + adapterPosition, + sharedPreferenceUtil, + storageCalculator + ) ) - ) - radioButton.isChecked = shouldShowCheckboxSelected && - adapterPosition == sharedPreferenceUtil.storagePosition - freeSpace.apply { - text = item.getFreeSpace(root.context, storageCalculator) - textSize = FREE_SPACE_TEXTVIEW_SIZE - } - usedSpace.apply { - text = item.getUsedSpace(root.context, storageCalculator) - textSize = FREE_SPACE_TEXTVIEW_SIZE - } - storageProgressBar.progress = item.usedPercentage(storageCalculator) - clickOverlay.apply { - visibility = VISIBLE - setToolTipWithContentDescription( - root.context.getString( - R.string.storage_selection_dialog_accessibility_description + radioButton.isChecked = shouldShowCheckboxSelected && + adapterPosition == sharedPreferenceUtil.storagePosition + freeSpace.apply { + text = item.getFreeSpace(root.context, storageCalculator) + textSize = FREE_SPACE_TEXTVIEW_SIZE + } + usedSpace.apply { + text = item.getUsedSpace(root.context, storageCalculator) + textSize = FREE_SPACE_TEXTVIEW_SIZE + } + storageProgressBar.progress = item.usedPercentage(storageCalculator) + clickOverlay.apply { + visibility = VISIBLE + setToolTipWithContentDescription( + root.context.getString( + R.string.storage_selection_dialog_accessibility_description + ) ) - ) - setOnClickListener { - onClickAction.invoke(item) + setOnClickListener { + onClickAction.invoke(item) + } } } } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/DownloadRoomDao.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/DownloadRoomDao.kt index 21883f54d0..6f21298c13 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/DownloadRoomDao.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/DownloadRoomDao.kt @@ -24,9 +24,10 @@ import androidx.room.Insert import androidx.room.Query import androidx.room.Update import io.reactivex.Flowable -import io.reactivex.Single import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import org.kiwix.kiwixmobile.core.dao.entities.DownloadRoomEntity import org.kiwix.kiwixmobile.core.downloader.DownloadRequester @@ -50,7 +51,7 @@ abstract class DownloadRoomDao { abstract fun downloadRoomEntity(): Flowable> @Query("SELECT * FROM DownloadRoomEntity") - abstract fun getAllDownloads(): Single> + abstract fun getAllDownloads(): Flow> fun downloads(): Flowable> = downloadRoomEntity() diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/LibkiwixBookmarks.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/LibkiwixBookmarks.kt index 826c906c66..7d91f87cae 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/LibkiwixBookmarks.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/LibkiwixBookmarks.kt @@ -28,6 +28,7 @@ import io.reactivex.schedulers.Schedulers import io.reactivex.subjects.BehaviorSubject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import kotlinx.coroutines.rx3.rxSingle import org.kiwix.kiwixmobile.core.CoreApp @@ -59,7 +60,7 @@ class LibkiwixBookmarks @Inject constructor( manager: Manager, val sharedPreferenceUtil: SharedPreferenceUtil, private val bookDao: NewBookDao, - private val zimReaderContainer: ZimReaderContainer? + private val zimReaderContainer: ZimReaderContainer?, ) : PageDao { /** @@ -69,6 +70,7 @@ class LibkiwixBookmarks @Inject constructor( private var bookmarksChanged: Boolean = false private var bookmarkList: List = arrayListOf() private var libraryBooksList: List = arrayListOf() + private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) @Suppress("CheckResult") private val bookmarkListBehaviour: BehaviorSubject>? by lazy { @@ -99,16 +101,18 @@ class LibkiwixBookmarks @Inject constructor( } init { - // Check if bookmark folder exist if not then create the folder first. - if (!File(bookmarksFolderPath).isFileExist()) File(bookmarksFolderPath).mkdir() - // Check if library file exist if not then create the file to save the library with book information. - if (!libraryFile.isFileExist()) libraryFile.createNewFile() - // set up manager to read the library from this file - manager.readFile(libraryFile.canonicalPath) - // Check if bookmark file exist if not then create the file to save the bookmarks. - if (!bookmarkFile.isFileExist()) bookmarkFile.createNewFile() - // set up manager to read the bookmarks from this file - manager.readBookmarkFile(bookmarkFile.canonicalPath) + coroutineScope.launch { + // Check if bookmark folder exist if not then create the folder first. + if (!File(bookmarksFolderPath).isFileExist()) File(bookmarksFolderPath).mkdir() + // Check if library file exist if not then create the file to save the library with book information. + if (!libraryFile.isFileExist()) libraryFile.createNewFile() + // set up manager to read the library from this file + manager.readFile(libraryFile.canonicalPath) + // Check if bookmark file exist if not then create the file to save the bookmarks. + if (!bookmarkFile.isFileExist()) bookmarkFile.createNewFile() + // set up manager to read the bookmarks from this file + manager.readBookmarkFile(bookmarkFile.canonicalPath) + } } fun bookmarks(): Flowable> = @@ -376,7 +380,7 @@ class LibkiwixBookmarks @Inject constructor( } // Export the `bookmark.xml` file to the `Download/org.kiwix/` directory of internal storage. - fun exportBookmark() { + suspend fun exportBookmark() { try { val bookmarkDestinationFile = exportedFile("bookmark.xml") bookmarkFile.inputStream().use { inputStream -> @@ -394,7 +398,7 @@ class LibkiwixBookmarks @Inject constructor( } } - private fun exportedFile(fileName: String): File { + private suspend fun exportedFile(fileName: String): File { val rootFolder = File( "${Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)}" + "/org.kiwix" 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 410848931e..5c3fece876 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 @@ -21,6 +21,11 @@ import io.objectbox.Box import io.objectbox.kotlin.inValues import io.objectbox.kotlin.query import io.objectbox.query.QueryBuilder +import io.reactivex.rxjava3.core.Completable +import io.reactivex.rxjava3.core.Flowable +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import kotlinx.coroutines.rx3.rxSingle import org.kiwix.kiwixmobile.core.dao.entities.BookOnDiskEntity import org.kiwix.kiwixmobile.core.dao.entities.BookOnDiskEntity_ @@ -49,11 +54,42 @@ class NewBookDao @Inject constructor(private val box: Box) { } .toList() .toFlowable() + .flatMap { booksList -> + completableFromCoroutine { removeBooksThatDoNotExist(booksList.toMutableList()) } + .andThen(io.reactivex.rxjava3.core.Flowable.just(booksList)) + } + } + .flatMap { booksList -> + io.reactivex.rxjava3.core.Flowable.fromIterable(booksList) + .flatMapSingle { bookOnDiskEntity -> + // Check if the zimReaderSource exists as a suspend function + rxSingle { bookOnDiskEntity.zimReaderSource.exists() } + .flatMap { exists -> + if (exists) io.reactivex.rxjava3.core.Single.just(bookOnDiskEntity) + else io.reactivex.rxjava3.core.Single.never() + } + .onErrorResumeNext { _: Throwable -> io.reactivex.rxjava3.core.Single.never() } + } + .toList() + .toFlowable() } - .doOnNext { removeBooksThatDoNotExist(it.toMutableList()) } - .map { books -> books.filter { it.zimReaderSource.exists() } } .map { it.map(::BookOnDisk) } + private fun completableFromCoroutine(block: suspend () -> Unit): Completable { + return Completable.defer { + Completable.create { emitter -> + CoroutineScope(Dispatchers.IO).launch { + try { + block() + emitter.onComplete() + } catch (ignore: Exception) { + emitter.onError(ignore) + } + } + } + } + } + suspend fun getBooks() = box.all.map { bookOnDiskEntity -> bookOnDiskEntity.file.let { file -> // set zimReaderSource for previously saved books @@ -110,7 +146,7 @@ class NewBookDao @Inject constructor(private val box: Box) { insert(books.map { BookOnDisk(book = it, zimReaderSource = ZimReaderSource(it.file!!)) }) } - private fun removeBooksThatDoNotExist(books: MutableList) { + private suspend fun removeBooksThatDoNotExist(books: MutableList) { delete(books.filterNot { it.zimReaderSource.exists() }) } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/extensions/FileExtensions.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/extensions/FileExtensions.kt index 95c733ded9..1b309b1d35 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/extensions/FileExtensions.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/extensions/FileExtensions.kt @@ -23,11 +23,7 @@ import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import java.io.File -fun File.isFileExist(): Boolean = runBlocking { - withContext(Dispatchers.IO) { - exists() - } -} +suspend fun File.isFileExist(): Boolean = withContext(Dispatchers.IO) { exists() } fun File.freeSpace(): Long = runBlocking { withContext(Dispatchers.IO) { diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/extensions/StorageDeviceExtensions.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/extensions/StorageDeviceExtensions.kt index edc62ba8f1..9329ae89c5 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/extensions/StorageDeviceExtensions.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/extensions/StorageDeviceExtensions.kt @@ -24,25 +24,31 @@ import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.settings.StorageCalculator import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil -fun StorageDevice.getFreeSpace(context: Context, storageCalculator: StorageCalculator): String { +suspend fun StorageDevice.getFreeSpace( + context: Context, + storageCalculator: StorageCalculator +): String { val freeSpace = storageCalculator.calculateAvailableSpace(file) return context.getString(R.string.pref_free_storage, freeSpace) } -fun StorageDevice.getUsedSpace(context: Context, storageCalculator: StorageCalculator): String { +suspend fun StorageDevice.getUsedSpace( + context: Context, + storageCalculator: StorageCalculator +): String { val usedSpace = storageCalculator.calculateUsedSpace(file) return context.getString(R.string.pref_storage_used, usedSpace) } @Suppress("MagicNumber") -fun StorageDevice.usedPercentage(storageCalculator: StorageCalculator): Int { +suspend fun StorageDevice.usedPercentage(storageCalculator: StorageCalculator): Int { val totalSpace = storageCalculator.totalBytes(file) val availableSpace = storageCalculator.availableBytes(file) val usedSpace = totalSpace - availableSpace return (usedSpace.toDouble() / totalSpace * 100).toInt() } -fun StorageDevice.storagePathAndTitle( +suspend fun StorageDevice.storagePathAndTitle( context: Context, index: Int, sharedPreferenceUtil: SharedPreferenceUtil, diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/main/CoreReaderFragment.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/main/CoreReaderFragment.kt index 817923592f..a06f2cf458 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/main/CoreReaderFragment.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/main/CoreReaderFragment.kt @@ -1675,7 +1675,11 @@ abstract class CoreReaderFragment : } override fun showSaveOrOpenUnsupportedFilesDialog(url: String, documentType: String?) { - unsupportedMimeTypeHandler?.showSaveOrOpenUnsupportedFilesDialog(url, documentType) + unsupportedMimeTypeHandler?.showSaveOrOpenUnsupportedFilesDialog( + url, + documentType, + coreReaderLifeCycleScope + ) } suspend fun openZimFile(zimReaderSource: ZimReaderSource, isCustomApp: Boolean = false) { diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/main/KiwixWebView.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/main/KiwixWebView.kt index 5cdf54cd2f..5318b92fbf 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/main/KiwixWebView.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/main/KiwixWebView.kt @@ -28,6 +28,9 @@ import android.view.ContextMenu import android.view.ViewGroup import android.webkit.WebView import io.reactivex.disposables.CompositeDisposable +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import org.kiwix.kiwixmobile.core.BuildConfig import org.kiwix.kiwixmobile.core.CoreApp.Companion.coreComponent import org.kiwix.kiwixmobile.core.CoreApp.Companion.instance @@ -169,14 +172,16 @@ open class KiwixWebView @SuppressLint("SetJavaScriptEnabled") constructor( val url = msg.data.getString("url", null) val src = msg.data.getString("src", null) if (url != null || src != null) { - val savedFile = - FileUtils.downloadFileFromUrl(url, src, zimReaderContainer, sharedPreferenceUtil) - savedFile?.let { - instance.toast(instance.getString(R.string.save_media_saved, it.name)).also { - Log.e("savedFile", "handleMessage: ${savedFile.isFile} ${savedFile.path}") + CoroutineScope(Dispatchers.Main.immediate).launch { + val savedFile = + FileUtils.downloadFileFromUrl(url, src, zimReaderContainer, sharedPreferenceUtil) + savedFile?.let { + instance.toast(instance.getString(R.string.save_media_saved, it.name)).also { + Log.e("savedFile", "handleMessage: ${savedFile.isFile} ${savedFile.path}") + } + } ?: run { + instance.toast(R.string.save_media_error) } - } ?: run { - instance.toast(R.string.save_media_error) } } } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/reader/ZimReaderSource.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/reader/ZimReaderSource.kt index dc0ff40ad8..3b54e773fe 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/reader/ZimReaderSource.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/reader/ZimReaderSource.kt @@ -52,7 +52,7 @@ class ZimReaderSource( } } - fun exists(): Boolean { + suspend fun exists(): Boolean { return when { file != null -> file.isFileExist() assetFileDescriptorList?.isNotEmpty() == true -> diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/settings/CorePrefsFragment.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/settings/CorePrefsFragment.kt index 65638c765f..0e216a2e0f 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/settings/CorePrefsFragment.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/settings/CorePrefsFragment.kt @@ -34,6 +34,7 @@ import android.widget.Toast import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.ContextCompat +import androidx.lifecycle.lifecycleScope import androidx.preference.EditTextPreference import androidx.preference.ListPreference import androidx.preference.Preference @@ -364,7 +365,11 @@ abstract class CorePrefsFragment : private fun showExportBookmarkDialog() { alertDialogShower?.show( KiwixDialog.YesNoDialog.ExportBookmarks, - { libkiwixBookmarks?.exportBookmark() } + { + lifecycleScope.launch { + libkiwixBookmarks?.exportBookmark() + } + } ) } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/settings/StorageCalculator.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/settings/StorageCalculator.kt index 8fffecdbef..d57fb4db45 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/settings/StorageCalculator.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/settings/StorageCalculator.kt @@ -30,18 +30,18 @@ class StorageCalculator @Inject constructor( private val sharedPreferenceUtil: SharedPreferenceUtil ) { - fun calculateAvailableSpace(file: File = File(sharedPreferenceUtil.prefStorage)): String = + suspend fun calculateAvailableSpace(file: File = File(sharedPreferenceUtil.prefStorage)): String = Bytes(availableBytes(file)).humanReadable - fun calculateTotalSpace(file: File = File(sharedPreferenceUtil.prefStorage)): String = + suspend fun calculateTotalSpace(file: File = File(sharedPreferenceUtil.prefStorage)): String = Bytes(totalBytes(file)).humanReadable - fun calculateUsedSpace(file: File): String = + suspend fun calculateUsedSpace(file: File): String = Bytes(totalBytes(file) - availableBytes(file)).humanReadable - fun availableBytes(file: File = File(sharedPreferenceUtil.prefStorage)) = + suspend fun availableBytes(file: File = File(sharedPreferenceUtil.prefStorage)) = if (file.isFileExist()) file.freeSpace() else 0L - fun totalBytes(file: File) = if (file.isFileExist()) file.totalSpace() else 0L + suspend fun totalBytes(file: File) = if (file.isFileExist()) file.totalSpace() else 0L } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/SharedPreferenceUtil.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/SharedPreferenceUtil.kt index 10dabe2491..fc1e94a892 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/SharedPreferenceUtil.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/SharedPreferenceUtil.kt @@ -30,7 +30,6 @@ import io.reactivex.processors.PublishProcessor import org.kiwix.kiwixmobile.core.DarkModeConfig import org.kiwix.kiwixmobile.core.DarkModeConfig.Mode.Companion.from import org.kiwix.kiwixmobile.core.R -import org.kiwix.kiwixmobile.core.extensions.isFileExist import java.io.File import java.util.Locale import javax.inject.Inject @@ -115,7 +114,7 @@ class SharedPreferenceUtil @Inject constructor(val context: Context) { putStoragePosition(0) } - !File(storage).isFileExist() -> getPublicDirectoryPath(defaultPublicStorage()).also { + !File(storage).exists() -> getPublicDirectoryPath(defaultPublicStorage()).also { putStoragePosition(0) } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/dialog/UnsupportedMimeTypeHandler.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/dialog/UnsupportedMimeTypeHandler.kt index 830a94f582..22cd0bbf7b 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/dialog/UnsupportedMimeTypeHandler.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/dialog/UnsupportedMimeTypeHandler.kt @@ -22,6 +22,8 @@ import android.app.Activity import android.content.Intent import android.util.Log import androidx.core.content.FileProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.extensions.toast import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer @@ -38,31 +40,42 @@ class UnsupportedMimeTypeHandler @Inject constructor( ) { var intent: Intent = Intent(Intent.ACTION_VIEW) - fun showSaveOrOpenUnsupportedFilesDialog(url: String?, documentType: String?) { + fun showSaveOrOpenUnsupportedFilesDialog( + url: String?, + documentType: String?, + lifecycleScope: CoroutineScope? + ) { alertDialogShower.show( KiwixDialog.SaveOrOpenUnsupportedFiles, - { openOrSaveFile(url, documentType, true) }, - { openOrSaveFile(url, documentType, false) }, + { openOrSaveFile(url, documentType, true, lifecycleScope) }, + { openOrSaveFile(url, documentType, false, lifecycleScope) }, { } ) } - private fun openOrSaveFile(url: String?, documentType: String?, openFile: Boolean) { - downloadFileFromUrl( - url, - null, - zimReaderContainer, - sharedPreferenceUtil - )?.let { savedFile -> - if (openFile) { - openFile(savedFile, documentType) - } else { - activity.toast(activity.getString(R.string.save_media_saved, savedFile.name)).also { - Log.e("DownloadOrOpenEpubAndPdf", "File downloaded at = ${savedFile.path}") + private fun openOrSaveFile( + url: String?, + documentType: String?, + openFile: Boolean, + lifecycleScope: CoroutineScope? + ) { + lifecycleScope?.launch { + downloadFileFromUrl( + url, + null, + zimReaderContainer, + sharedPreferenceUtil + )?.let { savedFile -> + if (openFile) { + openFile(savedFile, documentType) + } else { + activity.toast(activity.getString(R.string.save_media_saved, savedFile.name)).also { + Log.e("DownloadOrOpenEpubAndPdf", "File downloaded at = ${savedFile.path}") + } } + } ?: run { + activity.toast(R.string.save_media_error) } - } ?: run { - activity.toast(R.string.save_media_error) } } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/files/FileUtils.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/files/FileUtils.kt index e621ccf93c..db36217978 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/files/FileUtils.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/files/FileUtils.kt @@ -117,7 +117,7 @@ object FileUtils { } @JvmStatic - fun getLocalFilePathByUri( + suspend fun getLocalFilePathByUri( context: Context, uri: Uri ): String? { @@ -169,7 +169,7 @@ object FileUtils { * 2. On devices below Android 11, when files are clicked directly in the file manager, the content * resolver may not be able to retrieve the path for certain URIs. */ - private fun getFilePathOfContentUri(context: Context, uri: Uri): String? { + private suspend fun getFilePathOfContentUri(context: Context, uri: Uri): String? { val filePath = contentQuery(context, uri) return if (!filePath.isNullOrEmpty()) { filePath @@ -179,7 +179,7 @@ object FileUtils { } } - private fun getFullFilePathFromFilePath( + private suspend fun getFullFilePathFromFilePath( context: Context, filePath: String? ): String? { @@ -252,7 +252,7 @@ object FileUtils { * 3. For other URIs, it attempts to resolve the full file path from the provided URI using a custom * method to retrieve the folder and file path. */ - private fun getActualFilePathOfContentUri(context: Context, uri: Uri): String? { + private suspend fun getActualFilePathOfContentUri(context: Context, uri: Uri): String? { return when { // For file managers that provide the full path in the URI (common on devices below Android 11). // This triggers when the user clicks directly on a ZIM file in the file manager, and the file @@ -295,7 +295,7 @@ object FileUtils { private fun isDownloadProviderUri(uri: Uri): Boolean = "$uri".contains("DownloadProvider") || "$uri".contains("/downloads") - fun documentProviderContentQuery( + suspend fun documentProviderContentQuery( context: Context, uri: Uri, documentsContractWrapper: DocumentResolverWrapper = DocumentResolverWrapper() @@ -417,7 +417,8 @@ object FileUtils { } @Suppress("NestedBlockDepth") - @JvmStatic fun getAllZimParts(book: Book): List { + @JvmStatic + suspend fun getAllZimParts(book: Book): List { val files = ArrayList() book.file?.let { if (it.path.endsWith(".zim") || it.path.endsWith(".zim.part")) { @@ -444,7 +445,7 @@ object FileUtils { } @JvmStatic - fun hasPart(file: File): Boolean { + suspend fun hasPart(file: File): Boolean { var file = file file = File(getFileName(file.path)) if (file.path.endsWith(".zim")) { @@ -469,7 +470,7 @@ object FileUtils { } @JvmStatic - fun getFileName(fileName: String) = + suspend fun getFileName(fileName: String) = when { File(fileName).isFileExist() -> fileName File("$fileName.part").isFileExist() -> "$fileName.part" @@ -543,7 +544,7 @@ object FileUtils { @Suppress("ReturnCount") @JvmStatic - fun downloadFileFromUrl( + suspend fun downloadFileFromUrl( url: String?, src: String?, zimReaderContainer: ZimReaderContainer, 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 5308977d4d..2e11f7a29f 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 @@ -21,6 +21,7 @@ package org.kiwix.kiwixmobile.core.dao import android.annotation.SuppressLint import io.mockk.CapturingSlot import io.mockk.clearAllMocks +import io.mockk.coEvery import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic @@ -84,8 +85,8 @@ internal class NewBookDaoTest { every { box.query().build() } returns query val zimReaderSourceThatExists = mockk() val zimReaderSourceThatDoesNotExist = mockk() - every { zimReaderSourceThatExists.exists() } returns true - every { zimReaderSourceThatDoesNotExist.exists() } returns false + coEvery { zimReaderSourceThatExists.exists() } returns true + coEvery { zimReaderSourceThatDoesNotExist.exists() } returns false val entityThatExists = bookOnDiskEntity(zimReaderSource = zimReaderSourceThatExists) val entityThatDoesNotExist = bookOnDiskEntity(zimReaderSource = zimReaderSourceThatDoesNotExist) @@ -93,7 +94,7 @@ internal class NewBookDaoTest { every { RxQuery.observable(query) } returns Observable.just( listOf(entityThatExists, entityThatDoesNotExist) ) - return Pair(entityThatExists, entityThatDoesNotExist) + return entityThatExists to entityThatDoesNotExist } } diff --git a/core/src/test/java/org/kiwix/kiwixmobile/core/settings/StorageCalculatorTest.kt b/core/src/test/java/org/kiwix/kiwixmobile/core/settings/StorageCalculatorTest.kt index dadced942f..287cb59c71 100644 --- a/core/src/test/java/org/kiwix/kiwixmobile/core/settings/StorageCalculatorTest.kt +++ b/core/src/test/java/org/kiwix/kiwixmobile/core/settings/StorageCalculatorTest.kt @@ -20,6 +20,7 @@ package org.kiwix.kiwixmobile.core.settings import io.mockk.every import io.mockk.mockk +import kotlinx.coroutines.test.runTest import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import java.io.File @@ -30,27 +31,27 @@ internal class StorageCalculatorTest { private val file: File = mockk() @Test - fun `calculate available space with existing file`() { + fun `calculate available space with existing file`() = runTest { every { file.freeSpace } returns 1 every { file.exists() } returns true assertThat(storageCalculator.calculateAvailableSpace(file)).isEqualTo("1 Bytes") } @Test - fun `calculate total space of existing file`() { + fun `calculate total space of existing file`() = runTest { every { file.totalSpace } returns 1 every { file.exists() } returns true assertThat(storageCalculator.calculateTotalSpace(file)).isEqualTo("1 Bytes") } @Test - fun `calculate total space of non existing file`() { + fun `calculate total space of non existing file`() = runTest { every { file.exists() } returns false assertThat(storageCalculator.calculateTotalSpace(file)).isEqualTo("0 Bytes") } @Test - fun `available bytes of non existing file`() { + fun `available bytes of non existing file`() = runTest { every { file.exists() } returns false assertThat(storageCalculator.availableBytes(file)).isEqualTo(0L) } diff --git a/core/src/test/java/org/kiwix/kiwixmobile/core/utils/files/FileUtilsTest.kt b/core/src/test/java/org/kiwix/kiwixmobile/core/utils/files/FileUtilsTest.kt index e271728a86..4aa7da02c7 100644 --- a/core/src/test/java/org/kiwix/kiwixmobile/core/utils/files/FileUtilsTest.kt +++ b/core/src/test/java/org/kiwix/kiwixmobile/core/utils/files/FileUtilsTest.kt @@ -21,6 +21,7 @@ package org.kiwix.kiwixmobile.core.utils.files import io.mockk.clearMocks import io.mockk.every import io.mockk.mockk +import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.Test import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeEach @@ -61,7 +62,7 @@ class FileUtilsTest { } @Test - fun fileNameEndsWithZimAndNoSuchFileExistsAtAnySuchLocation() { + fun fileNameEndsWithZimAndNoSuchFileExistsAtAnySuchLocation() = runBlocking { expect("zimab", false) assertEquals( FileUtils.getAllZimParts(testBook).size, @@ -70,7 +71,7 @@ class FileUtilsTest { ) } - private fun testWith(extension: String, fileExists: Boolean) { + private fun testWith(extension: String, fileExists: Boolean) = runBlocking { expect(extension, fileExists) val coreApp = mockk() CoreApp.instance = coreApp diff --git a/custom/src/main/java/org/kiwix/kiwixmobile/custom/download/effects/SetPreferredStorageWithMostSpace.kt b/custom/src/main/java/org/kiwix/kiwixmobile/custom/download/effects/SetPreferredStorageWithMostSpace.kt index 6e1d379c00..c7031a1498 100644 --- a/custom/src/main/java/org/kiwix/kiwixmobile/custom/download/effects/SetPreferredStorageWithMostSpace.kt +++ b/custom/src/main/java/org/kiwix/kiwixmobile/custom/download/effects/SetPreferredStorageWithMostSpace.kt @@ -19,6 +19,8 @@ package org.kiwix.kiwixmobile.custom.download.effects import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.launch import org.kiwix.kiwixmobile.core.base.SideEffect import org.kiwix.kiwixmobile.core.settings.StorageCalculator import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil @@ -29,9 +31,15 @@ class SetPreferredStorageWithMostSpace @Inject constructor( private val sharedPreferenceUtil: SharedPreferenceUtil ) : SideEffect { override fun invokeWith(activity: AppCompatActivity) { + activity.lifecycleScope.launch { + findAndSetPreferredStorage(activity) + } + } + + suspend fun findAndSetPreferredStorage(activity: AppCompatActivity) { activity.externalMediaDirs .filterNotNull() - .maxBy(storageCalculator::availableBytes) - ?.let { sharedPreferenceUtil.putPrefStorage(it.path) } + .maxBy { storageCalculator.availableBytes(it) } + .let { sharedPreferenceUtil.putPrefStorage(it.path) } } } diff --git a/custom/src/main/java/org/kiwix/kiwixmobile/custom/main/CustomFileValidator.kt b/custom/src/main/java/org/kiwix/kiwixmobile/custom/main/CustomFileValidator.kt index a72543ae3b..4fb5b1de8e 100644 --- a/custom/src/main/java/org/kiwix/kiwixmobile/custom/main/CustomFileValidator.kt +++ b/custom/src/main/java/org/kiwix/kiwixmobile/custom/main/CustomFileValidator.kt @@ -23,7 +23,6 @@ import android.content.pm.PackageManager import android.content.res.AssetFileDescriptor import android.content.res.AssetManager import androidx.core.content.ContextCompat -import org.kiwix.kiwixmobile.core.extensions.isFileExist import org.kiwix.kiwixmobile.core.utils.files.Log import org.kiwix.kiwixmobile.custom.main.ValidationState.HasBothFiles import org.kiwix.kiwixmobile.custom.main.ValidationState.HasFile @@ -97,7 +96,7 @@ class CustomFileValidator @Inject constructor(private val context: Context) { private fun obbFiles() = scanDirs( - ContextCompat.getObbDirs(context).filterNotNull().filter(File::isFileExist).toTypedArray(), + ContextCompat.getObbDirs(context).filterNotNull().filter(File::exists).toTypedArray(), "obb" ) @@ -107,7 +106,7 @@ class CustomFileValidator @Inject constructor(private val context: Context) { // Get the external files directories for the app ContextCompat.getExternalFilesDirs(context, null).filterNotNull() - .filter(File::isFileExist) + .filter(File::exists) .forEach { dir -> // Check if the directory's parent is not null dir.parent?.let { parentPath -> diff --git a/custom/src/main/java/org/kiwix/kiwixmobile/custom/main/CustomReaderFragment.kt b/custom/src/main/java/org/kiwix/kiwixmobile/custom/main/CustomReaderFragment.kt index 690cd65791..d6d353f491 100644 --- a/custom/src/main/java/org/kiwix/kiwixmobile/custom/main/CustomReaderFragment.kt +++ b/custom/src/main/java/org/kiwix/kiwixmobile/custom/main/CustomReaderFragment.kt @@ -226,7 +226,7 @@ class CustomReaderFragment : CoreReaderFragment() { ) } - private fun createDemoFile() = + private suspend fun createDemoFile() = File(getDemoFilePathForCustomApp(requireActivity())).also { if (!it.isFileExist()) it.createNewFile() } diff --git a/custom/src/test/java/org/kiwix/kiwixmobile/custom/download/effects/SetPreferredStorageWithMostSpaceTest.kt b/custom/src/test/java/org/kiwix/kiwixmobile/custom/download/effects/SetPreferredStorageWithMostSpaceTest.kt index f92de7c97e..ad46f65a32 100644 --- a/custom/src/test/java/org/kiwix/kiwixmobile/custom/download/effects/SetPreferredStorageWithMostSpaceTest.kt +++ b/custom/src/test/java/org/kiwix/kiwixmobile/custom/download/effects/SetPreferredStorageWithMostSpaceTest.kt @@ -19,9 +19,11 @@ package org.kiwix.kiwixmobile.custom.download.effects import androidx.appcompat.app.AppCompatActivity +import io.mockk.coEvery import io.mockk.every import io.mockk.mockk import io.mockk.verify +import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.Test import org.kiwix.kiwixmobile.core.settings.StorageCalculator import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil @@ -36,14 +38,17 @@ internal class SetPreferredStorageWithMostSpaceTest { val activity = mockk() val directoryWithMoreStorage = mockk() val directoryWithLessStorage = mockk() + val sut = SetPreferredStorageWithMostSpace(storageCalculator, sharedPreferenceUtil) every { activity.externalMediaDirs } returns arrayOf( directoryWithMoreStorage, null, directoryWithLessStorage ) - every { storageCalculator.availableBytes(directoryWithMoreStorage) } returns 1 - every { storageCalculator.availableBytes(directoryWithLessStorage) } returns 0 + coEvery { storageCalculator.availableBytes(directoryWithMoreStorage) } returns 1 + coEvery { storageCalculator.availableBytes(directoryWithLessStorage) } returns 0 val expectedStorage = "expectedStorage" every { directoryWithMoreStorage.path } returns expectedStorage - SetPreferredStorageWithMostSpace(storageCalculator, sharedPreferenceUtil).invokeWith(activity) + runBlocking { + sut.findAndSetPreferredStorage(activity) + } verify { sharedPreferenceUtil.putPrefStorage(expectedStorage) }