From e5d7b2a17e47715d9c9efbb915ef60eb8620dfe2 Mon Sep 17 00:00:00 2001 From: MohitMaliFtechiz Date: Tue, 3 Sep 2024 18:02:14 +0530 Subject: [PATCH] Added unit and instrumentation test cases for CopyMoveFileHandler to properly test the all scenario with edge cases. --- .../localLibrary/CopyMoveFileHandlerRobot.kt | 95 ++++++ .../localLibrary/CopyMoveFileHandlerTest.kt | 278 ++++++++++++++++ .../CopyMoveFileHandler.kt | 51 +-- .../library/LocalLibraryFragment.kt | 11 +- .../localLibrary/CopyMoveFileHandlerTest.kt | 298 ++++++++++++++++++ .../reader/CopyMoveFileHandlerTest.kt | 253 --------------- 6 files changed, 708 insertions(+), 278 deletions(-) create mode 100644 app/src/androidTest/java/org/kiwix/kiwixmobile/localLibrary/CopyMoveFileHandlerRobot.kt create mode 100644 app/src/androidTest/java/org/kiwix/kiwixmobile/localLibrary/CopyMoveFileHandlerTest.kt rename app/src/main/java/org/kiwix/kiwixmobile/nav/destination/{reader => library}/CopyMoveFileHandler.kt (91%) create mode 100644 app/src/test/java/org/kiwix/kiwixmobile/localLibrary/CopyMoveFileHandlerTest.kt delete mode 100644 app/src/test/java/org/kiwix/kiwixmobile/reader/CopyMoveFileHandlerTest.kt diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/localLibrary/CopyMoveFileHandlerRobot.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/localLibrary/CopyMoveFileHandlerRobot.kt new file mode 100644 index 0000000000..0d027ee56a --- /dev/null +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/localLibrary/CopyMoveFileHandlerRobot.kt @@ -0,0 +1,95 @@ +/* + * 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.localLibrary + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.espresso.web.sugar.Web +import androidx.test.espresso.web.webdriver.DriverAtoms +import androidx.test.espresso.web.webdriver.Locator +import applyWithViewHierarchyPrinting +import com.adevinta.android.barista.interaction.BaristaSleepInteractions +import junit.framework.AssertionFailedError +import org.kiwix.kiwixmobile.BaseRobot +import org.kiwix.kiwixmobile.Findable +import org.kiwix.kiwixmobile.Findable.StringId.TextId +import org.kiwix.kiwixmobile.R.id +import org.kiwix.kiwixmobile.core.R +import org.kiwix.kiwixmobile.testutils.TestUtils +import org.kiwix.kiwixmobile.testutils.TestUtils.testFlakyView + +fun copyMoveFileHandler(func: CopyMoveFileHandlerRobot.() -> Unit) = + CopyMoveFileHandlerRobot().applyWithViewHierarchyPrinting(func) + +class CopyMoveFileHandlerRobot : BaseRobot() { + + fun assertCopyMovePermissionDialogDisplayed() { + isVisible(TextId(R.string.move_files_permission_dialog_title)) + } + + fun assertCopyMoveDialogDisplayed() { + isVisible(TextId(R.string.copy_move_files_dialog_description)) + } + + fun clickOnCopy() { + testFlakyView({ + onView(withText(R.string.copy)).perform(click()) + }) + } + + fun clickOnMove() { + testFlakyView({ + onView(withText(R.string.move)).perform(click()) + }) + } + + fun assertZimFileCopiedAndShowingIntoTheReader() { + pauseForBetterTestPerformance() + isVisible(Findable.ViewId(id.readerFragment)) + testFlakyView({ + Web.onWebView() + .withElement( + DriverAtoms.findElement( + Locator.XPATH, + "//*[contains(text(), 'Android_(operating_system)')]" + ) + ) + }) + } + + fun assertZimFileAddedInTheLocalLibrary() { + try { + onView(ViewMatchers.withId(id.file_management_no_files)).check( + ViewAssertions.matches( + ViewMatchers.isDisplayed() + ) + ) + throw RuntimeException("ZimFile not added in the local library") + } catch (e: AssertionFailedError) { + // do nothing zim file is added in the local library + } + } + + fun pauseForBetterTestPerformance() { + BaristaSleepInteractions.sleep(TestUtils.TEST_PAUSE_MS_FOR_SEARCH_TEST.toLong()) + } +} diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/localLibrary/CopyMoveFileHandlerTest.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/localLibrary/CopyMoveFileHandlerTest.kt new file mode 100644 index 0000000000..a7f7b14a79 --- /dev/null +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/localLibrary/CopyMoveFileHandlerTest.kt @@ -0,0 +1,278 @@ +/* + * 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.localLibrary + +import android.net.Uri +import android.os.Build +import androidx.core.content.ContextCompat +import androidx.core.content.edit +import androidx.lifecycle.Lifecycle +import androidx.navigation.fragment.NavHostFragment +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.Assert +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.kiwix.kiwixmobile.BaseActivityTest +import org.kiwix.kiwixmobile.R +import org.kiwix.kiwixmobile.core.extensions.deleteFile +import org.kiwix.kiwixmobile.core.extensions.isFileExist +import org.kiwix.kiwixmobile.core.settings.StorageCalculator +import org.kiwix.kiwixmobile.core.utils.LanguageUtils +import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil +import org.kiwix.kiwixmobile.core.utils.dialog.AlertDialogShower +import org.kiwix.kiwixmobile.main.KiwixMainActivity +import org.kiwix.kiwixmobile.nav.destination.library.CopyMoveFileHandler +import org.kiwix.kiwixmobile.nav.destination.library.LocalLibraryFragment +import org.kiwix.kiwixmobile.testutils.RetryRule +import org.kiwix.kiwixmobile.testutils.TestUtils +import org.kiwix.kiwixmobile.zimManager.Fat32Checker +import org.kiwix.kiwixmobile.zimManager.FileWritingFileSystemChecker +import java.io.File +import java.io.FileOutputStream +import java.io.OutputStream + +class CopyMoveFileHandlerTest : BaseActivityTest() { + @Rule + @JvmField + var retryRule = RetryRule() + + private lateinit var sharedPreferenceUtil: SharedPreferenceUtil + private lateinit var kiwixMainActivity: KiwixMainActivity + private lateinit var selectedFile: File + private lateinit var destinationFile: File + private lateinit var parentFile: File + + @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_IS_TEST, true) + putBoolean(SharedPreferenceUtil.PREF_PLAY_STORE_RESTRICTION, false) + putString(SharedPreferenceUtil.PREF_LANG, "en") + } + activityScenario = ActivityScenario.launch(KiwixMainActivity::class.java).apply { + moveToState(Lifecycle.State.RESUMED) + sharedPreferenceUtil = SharedPreferenceUtil(context) + onActivity { + LanguageUtils.handleLocaleChange( + it, + "en", + sharedPreferenceUtil + ) + parentFile = File(sharedPreferenceUtil.prefStorage) + } + } + } + + @Test + fun testCopyingZimFileIntoPublicStorage() { + deleteAllFilesInDirectory(parentFile) + // Test the scenario in playStore build on Android 11 and above. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + selectedFile = getSelectedFile() + activityScenario.onActivity { + kiwixMainActivity = it + kiwixMainActivity.navigate(R.id.libraryFragment) + } + copyMoveFileHandler(CopyMoveFileHandlerRobot::pauseForBetterTestPerformance) + // test with first launch + sharedPreferenceUtil.copyMoveZimFilePermissionDialog = false + showMoveFileToPublicDirectoryDialog() + // should show the permission dialog. + copyMoveFileHandler { + assertCopyMovePermissionDialogDisplayed() + clickOnCopy() + assertZimFileCopiedAndShowingIntoTheReader() + } + assertZimFileAddedInTheLocalLibrary() + + // Test with second launch, this time permission dialog should not show. + // delete the parent directory so that all the previous file will be deleted. + deleteAllFilesInDirectory(parentFile) + showMoveFileToPublicDirectoryDialog() + // should show the copyMove dialog. + copyMoveFileHandler { + assertCopyMoveDialogDisplayed() + clickOnCopy() + assertZimFileCopiedAndShowingIntoTheReader() + } + assertZimFileAddedInTheLocalLibrary() + deleteAllFilesInDirectory(parentFile) + } + } + + @Test + fun testMovingZimFileIntoPublicDirectory() { + deleteAllFilesInDirectory(parentFile) + // Test the scenario in playStore build on Android 11 and above. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + selectedFile = getSelectedFile() + activityScenario.onActivity { + kiwixMainActivity = it + kiwixMainActivity.navigate(R.id.libraryFragment) + } + copyMoveFileHandler(CopyMoveFileHandlerRobot::pauseForBetterTestPerformance) + // test with first launch + sharedPreferenceUtil.copyMoveZimFilePermissionDialog = false + showMoveFileToPublicDirectoryDialog() + // should show the permission dialog. + copyMoveFileHandler { + assertCopyMovePermissionDialogDisplayed() + clickOnMove() + assertZimFileCopiedAndShowingIntoTheReader() + } + assertZimFileAddedInTheLocalLibrary() + // Test with second launch, this time permission dialog should not show. + // delete the parent directory so that all the previous file will be deleted. + deleteAllFilesInDirectory(parentFile) + selectedFile = getSelectedFile() + showMoveFileToPublicDirectoryDialog() + // should show the copyMove dialog. + copyMoveFileHandler { + assertCopyMoveDialogDisplayed() + clickOnMove() + assertZimFileCopiedAndShowingIntoTheReader() + } + assertZimFileAddedInTheLocalLibrary() + assertSelectedZimFileIsDeletedFromTheStorage(selectedFile) + deleteAllFilesInDirectory(parentFile) + } + } + + private fun assertSelectedZimFileIsDeletedFromTheStorage(selectedZimFile: File) { + if (selectedZimFile.isFileExist()) { + throw RuntimeException("Selected zim file is not deleted from the storage") + } + } + + private fun assertZimFileAddedInTheLocalLibrary() { + UiThreadStatement.runOnUiThread { + kiwixMainActivity.navigate(R.id.libraryFragment) + } + copyMoveFileHandler(CopyMoveFileHandlerRobot::assertZimFileAddedInTheLocalLibrary) + } + + private fun showMoveFileToPublicDirectoryDialog() { + UiThreadStatement.runOnUiThread { + val navHostFragment: NavHostFragment = + kiwixMainActivity.supportFragmentManager + .findFragmentById(R.id.nav_host_fragment) as NavHostFragment + val localLibraryFragment = + navHostFragment.childFragmentManager.fragments[0] as LocalLibraryFragment + localLibraryFragment.copyMoveFileHandler?.showMoveFileToPublicDirectoryDialog( + Uri.fromFile(selectedFile), + selectedFile + ) + } + } + + private fun getSelectedFile(): File { + val loadFileStream = + CopyMoveFileHandlerTest::class.java.classLoader.getResourceAsStream("testzim.zim") + val zimFile = File( + ContextCompat.getExternalFilesDirs(context, null)[0], + "testzim.zim" + ) + if (zimFile.exists()) zimFile.delete() + zimFile.createNewFile() + loadFileStream.use { inputStream -> + val outputStream: OutputStream = FileOutputStream(zimFile) + outputStream.use { it -> + val buffer = ByteArray(inputStream.available()) + var length: Int + while (inputStream.read(buffer).also { length = it } > 0) { + it.write(buffer, 0, length) + } + } + } + return zimFile + } + + @Test + fun testGetDestinationFile() { + activityScenario.onActivity { + kiwixMainActivity = it + kiwixMainActivity.navigate(R.id.libraryFragment) + } + val selectedFileName = "testCopyMove.zim" + deleteAllFilesInDirectory(parentFile) + val copyMoveFileHandler = CopyMoveFileHandler( + kiwixMainActivity, + sharedPreferenceUtil, + AlertDialogShower(kiwixMainActivity), + 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, selectedFile) + destinationFile = copyMoveFileHandler.getDestinationFile() + Assert.assertNotEquals( + destinationFile.name, + selectedFile.name + ) + Assert.assertEquals( + destinationFile.name, + "testCopyMove_1.zim" + ) + deleteBothPreviousFiles() + + // test when there is no zim file available in the storage it should return the same fileName + selectedFile = File(parentFile, selectedFileName) + copyMoveFileHandler.setSelectedFileAndUri(null, selectedFile) + destinationFile = copyMoveFileHandler.getDestinationFile() + Assert.assertEquals( + destinationFile.name, + selectedFile.name + ) + deleteBothPreviousFiles() + } + + private fun deleteBothPreviousFiles() { + selectedFile.deleteFile() + destinationFile.deleteFile() + } + + private fun deleteAllFilesInDirectory(directory: File) { + if (directory.isDirectory) { + directory.listFiles()?.forEach { file -> + if (file.isDirectory) { + // Recursively delete files in subdirectories + deleteAllFilesInDirectory(file) + } + file.delete() + } + } + } +} diff --git a/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/reader/CopyMoveFileHandler.kt b/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/CopyMoveFileHandler.kt similarity index 91% rename from app/src/main/java/org/kiwix/kiwixmobile/nav/destination/reader/CopyMoveFileHandler.kt rename to app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/CopyMoveFileHandler.kt index 8f5a73bbe9..96450f574d 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/reader/CopyMoveFileHandler.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/CopyMoveFileHandler.kt @@ -16,7 +16,7 @@ * */ -package org.kiwix.kiwixmobile.nav.destination.reader +package org.kiwix.kiwixmobile.nav.destination.library import android.annotation.SuppressLint import android.app.Activity @@ -61,16 +61,16 @@ class CopyMoveFileHandler @Inject constructor( private val storageCalculator: StorageCalculator, private val fat32Checker: Fat32Checker ) { - var fileCopyMoveCallback: FileCopyMoveCallback? = null + private var fileCopyMoveCallback: FileCopyMoveCallback? = null private var selectedFileUri: Uri? = null private var selectedFile: File? = null private var copyMovePreparingDialog: Dialog? = null - var progressBarDialog: AlertDialog? = null - var lifecycleScope: CoroutineScope? = null + private var progressBarDialog: AlertDialog? = null + private var lifecycleScope: CoroutineScope? = null private var progressBar: ProgressBar? = null private var progressBarTextView: TextView? = null private var isMoveOperation = false - var fileSystemDisposable: Disposable? = null + private var fileSystemDisposable: Disposable? = null private val copyMoveTitle: String by lazy { if (isMoveOperation) { @@ -103,6 +103,14 @@ class CopyMoveFileHandler @Inject constructor( selectedFile = file } + fun setFileCopyMoveCallback(fileCopyMoveCallback: FileCopyMoveCallback?) { + this.fileCopyMoveCallback = fileCopyMoveCallback + } + + fun setLifeCycleScope(coroutineScope: CoroutineScope?) { + lifecycleScope = coroutineScope + } + private fun showMoveToPublicDirectoryPermissionDialog() { alertDialogShower.show( KiwixDialog.MoveFileToPublicDirectoryPermissionDialog, @@ -121,7 +129,7 @@ class CopyMoveFileHandler @Inject constructor( ) } - private fun isBookLessThan4GB(): Boolean = + fun isBookLessThan4GB(): Boolean = (selectedFile?.length() ?: 0L) < FOUR_GIGABYTES_IN_KILOBYTES fun validateZimFileCanCopyOrMove(file: File = File(sharedPreferenceUtil.prefStorage)): Boolean { @@ -146,7 +154,7 @@ class CopyMoveFileHandler @Inject constructor( } } - private fun handleDetectingFileSystemState() { + fun handleDetectingFileSystemState() { if (isBookLessThan4GB()) { showCopyMoveDialog() } else { @@ -155,7 +163,7 @@ class CopyMoveFileHandler @Inject constructor( } } - private fun handleCannotWrite4GbFileState() { + fun handleCannotWrite4GbFileState() { if (isBookLessThan4GB()) { showCopyMoveDialog() } else { @@ -164,7 +172,7 @@ class CopyMoveFileHandler @Inject constructor( } } - private fun observeFileSystemState() { + fun observeFileSystemState() { if (fileSystemDisposable?.isDisposed == false) return fileSystemDisposable = fat32Checker.fileSystemStates .observeOn(AndroidSchedulers.mainThread()) @@ -176,7 +184,7 @@ class CopyMoveFileHandler @Inject constructor( } } - private fun showCopyMoveDialog() { + fun showCopyMoveDialog() { alertDialogShower.show( KiwixDialog.CopyMoveFileToPublicDirectoryDialog, ::performCopyOperation, @@ -184,17 +192,17 @@ class CopyMoveFileHandler @Inject constructor( ) } - private fun performCopyOperation() { + fun performCopyOperation() { isMoveOperation = false copyZimFileToPublicAppDirectory() } - private fun performMoveOperation() { + fun performMoveOperation() { isMoveOperation = true moveZimFileToPublicAppDirectory() } - fun copyZimFileToPublicAppDirectory() { + private fun copyZimFileToPublicAppDirectory() { lifecycleScope?.launch { val destinationFile = getDestinationFile() try { @@ -320,11 +328,10 @@ class CopyMoveFileHandler @Inject constructor( } @Suppress("MagicNumber") - suspend fun copyFile(sourceUri: Uri, destinationFile: File) = + private suspend fun copyFile(sourceUri: Uri, destinationFile: File) = withContext(Dispatchers.IO) { val contentResolver = activity.contentResolver - // Open the ParcelFileDescriptor from the Uri val parcelFileDescriptor = contentResolver.openFileDescriptor(sourceUri, "r") val fileSize = parcelFileDescriptor?.fileDescriptor?.let { FileInputStream(it).channel.size() } ?: 0L @@ -354,7 +361,8 @@ class CopyMoveFileHandler @Inject constructor( } ?: throw FileNotFoundException("The selected file could not be opened") } - fun getDestinationFile(root: File = File(sharedPreferenceUtil.prefStorage)): File { + fun getDestinationFile(): File { + val root = File(sharedPreferenceUtil.prefStorage) val fileName = selectedFile?.name ?: "" val destinationFile = sequence { @@ -373,8 +381,7 @@ class CopyMoveFileHandler @Inject constructor( private fun hasNotSufficientStorageSpace(availableSpace: Long): Boolean = availableSpace < (selectedFile?.length() ?: 0L) - @SuppressLint("InflateParams") - private fun showPreparingCopyMoveDialog() { + @SuppressLint("InflateParams") fun showPreparingCopyMoveDialog() { if (copyMovePreparingDialog == null) { val dialogView: View = activity.layoutInflater.inflate(R.layout.item_custom_spinner, null) @@ -389,7 +396,7 @@ class CopyMoveFileHandler @Inject constructor( } @SuppressLint("InflateParams") - fun showProgressDialog() { + private fun showProgressDialog() { val dialogView = activity.layoutInflater.inflate(layout.copy_move_progress_bar, null) progressBar = @@ -414,6 +421,12 @@ class CopyMoveFileHandler @Inject constructor( } } + fun dispose() { + fileSystemDisposable?.dispose() + setFileCopyMoveCallback(null) + setLifeCycleScope(null) + } + interface FileCopyMoveCallback { fun onFileCopied(file: File) fun onFileMoved(file: File) 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 1af021240f..56cb3bf6ef 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 @@ -98,7 +98,6 @@ import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.adapter.BooksOnDis import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.adapter.BooksOnDiskListItem import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.adapter.BooksOnDiskListItem.BookOnDisk import org.kiwix.kiwixmobile.databinding.FragmentDestinationLibraryBinding -import org.kiwix.kiwixmobile.nav.destination.reader.CopyMoveFileHandler import org.kiwix.kiwixmobile.zimManager.MAX_PROGRESS import org.kiwix.kiwixmobile.zimManager.ZimManageViewModel import org.kiwix.kiwixmobile.zimManager.ZimManageViewModel.FileSelectActions @@ -244,8 +243,10 @@ class LocalLibraryFragment : BaseFragment(), CopyMoveFileHandler.FileCopyMoveCal override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setUpSwipeRefreshLayout() - copyMoveFileHandler?.fileCopyMoveCallback = this - copyMoveFileHandler?.lifecycleScope = lifecycleScope + copyMoveFileHandler?.apply { + setFileCopyMoveCallback(this@LocalLibraryFragment) + setLifeCycleScope(lifecycleScope) + } fragmentDestinationLibraryBinding?.zimfilelist?.run { adapter = booksOnDiskAdapter layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false) @@ -486,9 +487,7 @@ class LocalLibraryFragment : BaseFragment(), CopyMoveFileHandler.FileCopyMoveCal disposable.clear() storagePermissionLauncher?.unregister() storagePermissionLauncher = null - copyMoveFileHandler?.fileCopyMoveCallback = null - copyMoveFileHandler?.lifecycleScope = null - copyMoveFileHandler?.fileSystemDisposable?.dispose() + copyMoveFileHandler?.dispose() copyMoveFileHandler = null } diff --git a/app/src/test/java/org/kiwix/kiwixmobile/localLibrary/CopyMoveFileHandlerTest.kt b/app/src/test/java/org/kiwix/kiwixmobile/localLibrary/CopyMoveFileHandlerTest.kt new file mode 100644 index 0000000000..057c122060 --- /dev/null +++ b/app/src/test/java/org/kiwix/kiwixmobile/localLibrary/CopyMoveFileHandlerTest.kt @@ -0,0 +1,298 @@ +/* + * 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.localLibrary + +import android.app.Activity +import io.mockk.Runs +import io.mockk.clearAllMocks +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.slot +import io.mockk.spyk +import io.mockk.verify +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.kiwix.kiwixmobile.core.settings.StorageCalculator +import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil +import org.kiwix.kiwixmobile.core.utils.dialog.AlertDialogShower +import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog +import org.kiwix.kiwixmobile.nav.destination.library.CopyMoveFileHandler +import org.kiwix.kiwixmobile.nav.destination.library.CopyMoveFileHandler.FileCopyMoveCallback +import org.kiwix.kiwixmobile.zimManager.Fat32Checker +import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState.CanWrite4GbFile +import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState.CannotWrite4GbFile +import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState.DetectingFileSystem +import java.io.File + +class CopyMoveFileHandlerTest { + private lateinit var fileHandler: CopyMoveFileHandler + + private val activity: Activity = mockk(relaxed = true) + private val sharedPreferenceUtil: SharedPreferenceUtil = mockk(relaxed = true) + private val alertDialogShower: AlertDialogShower = mockk(relaxed = true) + private val storageCalculator: StorageCalculator = mockk(relaxed = true) + private val fat32Checker: Fat32Checker = mockk(relaxed = true) + private val fileCopyMoveCallback: FileCopyMoveCallback = mockk(relaxed = true) + private val testDispatcher = StandardTestDispatcher() + private val testScope = TestScope(testDispatcher) + private val storageFile: File = mockk(relaxed = true) + private val selectedFile: File = mockk(relaxed = true) + private val storagePath = "storage/0/emulated/Android/media/org.kiwix.kiwixmobile" + + @BeforeEach + fun setup() { + clearAllMocks() + fileHandler = CopyMoveFileHandler( + activity, + sharedPreferenceUtil, + alertDialogShower, + storageCalculator, + fat32Checker + ).apply { + setSelectedFileAndUri(null, selectedFile) + setLifeCycleScope(testScope) + setFileCopyMoveCallback(this@CopyMoveFileHandlerTest.fileCopyMoveCallback) + } + } + + @Test + fun validateZimFileCanCopyOrMoveShouldReturnTrueWhenSufficientSpaceAndValidFileSystem() { + prepareFileSystemAndFileForMockk() + + val result = fileHandler.validateZimFileCanCopyOrMove(storageFile) + + 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) + + assertFalse(result) + verify { fileCopyMoveCallback.insufficientSpaceInStorage(any()) } + } + + @Test + fun validateZimFileCanCopyOrMoveShouldReturnFalseWhenDetectingAndCanNotWrite4GBFiles() { + prepareFileSystemAndFileForMockk(fileSystemState = DetectingFileSystem) + // check when detecting the fileSystem + assertFalse(fileHandler.validateZimFileCanCopyOrMove(storageFile)) + + prepareFileSystemAndFileForMockk(fileSystemState = CannotWrite4GbFile) + + // check when Can not write 4GB files on the fileSystem + assertFalse(fileHandler.validateZimFileCanCopyOrMove()) + } + + @Test + fun validateZimFileCanCopyOrMoveShouldReturnFalseWhenDetectingFileSystem() { + every { fileHandler.isBookLessThan4GB() } returns true + every { fileHandler.showCopyMoveDialog() } just Runs + prepareFileSystemAndFileForMockk(fileSystemState = DetectingFileSystem) + + val result = fileHandler.validateZimFileCanCopyOrMove(storageFile) + + assertFalse(result) + verify { fileHandler.handleDetectingFileSystemState() } + } + + @Test + fun validateZimFileCanCopyOrMoveShouldReturnFalseWhenCannotWrite4GbFile() { + every { fileHandler.isBookLessThan4GB() } returns true + every { fileHandler.showCopyMoveDialog() } just Runs + every { + fileCopyMoveCallback.filesystemDoesNotSupportedCopyMoveFilesOver4GB() + } just Runs + prepareFileSystemAndFileForMockk(fileSystemState = CannotWrite4GbFile) + + val result = fileHandler.validateZimFileCanCopyOrMove(storageFile) + + assertFalse(result) + verify { fileHandler.handleCannotWrite4GbFileState() } + } + + @Test + fun handleDetectingFileSystemStateShouldShowCopyMoveDialogIfBookLessThan4GB() { + fileHandler = spyk(fileHandler) + prepareFileSystemAndFileForMockk() + every { fileHandler.isBookLessThan4GB() } returns true + every { fileHandler.showCopyMoveDialog() } just Runs + + fileHandler.handleDetectingFileSystemState() + + verify { fileHandler.showCopyMoveDialog() } + } + + @Test + fun handleDetectingFileSystemStateShouldObserveFileSystemStateIfBookGreaterThan4GB() { + fileHandler = spyk(fileHandler) + prepareFileSystemAndFileForMockk(fileSystemState = DetectingFileSystem) + every { fileHandler.isBookLessThan4GB() } returns false + every { fileHandler.observeFileSystemState() } just Runs + + fileHandler.handleDetectingFileSystemState() + verify { fileHandler.observeFileSystemState() } + } + + @Test + fun handleCannotWrite4GbFileStateShouldShowCopyMoveDialogIfBookLessThan4GB() { + fileHandler = spyk(fileHandler) + prepareFileSystemAndFileForMockk() + every { fileHandler.isBookLessThan4GB() } returns true + every { fileHandler.showCopyMoveDialog() } just Runs + + fileHandler.handleCannotWrite4GbFileState() + + verify { fileHandler.showCopyMoveDialog() } + } + + @Test + fun handleCannotWrite4GbFileStateShouldCallCallbackIfBookGreaterThan4GB() { + fileHandler = spyk(fileHandler) + prepareFileSystemAndFileForMockk() + every { fileHandler.isBookLessThan4GB() } returns false + every { + fileCopyMoveCallback.filesystemDoesNotSupportedCopyMoveFilesOver4GB() + } just Runs + + fileHandler.handleCannotWrite4GbFileState() + + verify { + fileCopyMoveCallback.filesystemDoesNotSupportedCopyMoveFilesOver4GB() + } + } + + @Test + fun showMoveToPublicDirectoryPermissionDialogShouldShowPermissionDialogAtFirstLaunch() { + every { sharedPreferenceUtil.copyMoveZimFilePermissionDialog } returns false + every { alertDialogShower.show(any(), any(), any()) } just Runs + fileHandler.showMoveFileToPublicDirectoryDialog() + + verify { + alertDialogShower.show( + KiwixDialog.MoveFileToPublicDirectoryPermissionDialog, + any(), + any() + ) + } + } + + @Test + fun copyMoveFunctionsShouldCallWhenClickingOnButtonsInPermissionDialog() { + val positiveButtonClickSlot = slot<() -> Unit>() + val negativeButtonClickSlot = slot<() -> Unit>() + fileHandler = spyk(fileHandler) + every { sharedPreferenceUtil.copyMoveZimFilePermissionDialog } returns false + every { + alertDialogShower.show( + KiwixDialog.MoveFileToPublicDirectoryPermissionDialog, + capture(positiveButtonClickSlot), + capture(negativeButtonClickSlot) + ) + } just Runs + + fileHandler.showMoveFileToPublicDirectoryDialog() + every { fileHandler.validateZimFileCanCopyOrMove() } returns true + every { fileHandler.performCopyOperation() } just Runs + + positiveButtonClickSlot.captured.invoke() + verify { fileHandler.performCopyOperation() } + every { sharedPreferenceUtil.copyMoveZimFilePermissionDialog } returns false + every { fileHandler.performMoveOperation() } just Runs + negativeButtonClickSlot.captured.invoke() + + verify { fileHandler.performMoveOperation() } + + verify { sharedPreferenceUtil.copyMoveZimFilePermissionDialog = true } + } + + @Test + fun showCopyMoveDialog() { + every { sharedPreferenceUtil.copyMoveZimFilePermissionDialog } returns true + prepareFileSystemAndFileForMockk() + every { alertDialogShower.show(any(), any(), any()) } just Runs + fileHandler.showMoveFileToPublicDirectoryDialog() + + verify { + alertDialogShower.show( + KiwixDialog.CopyMoveFileToPublicDirectoryDialog, + any(), + any() + ) + } + } + + @Test + fun copyMoveFunctionsShouldCallWhenClickingOnButtonsInCopyMoveDialog() { + val positiveButtonClickSlot = slot<() -> Unit>() + val negativeButtonClickSlot = slot<() -> Unit>() + fileHandler = spyk(fileHandler) + every { sharedPreferenceUtil.copyMoveZimFilePermissionDialog } returns true + every { + alertDialogShower.show( + KiwixDialog.CopyMoveFileToPublicDirectoryDialog, + capture(positiveButtonClickSlot), + capture(negativeButtonClickSlot) + ) + } just Runs + + every { fileHandler.validateZimFileCanCopyOrMove() } returns true + fileHandler.showMoveFileToPublicDirectoryDialog() + every { fileHandler.performCopyOperation() } just Runs + + positiveButtonClickSlot.captured.invoke() + verify { fileHandler.performCopyOperation() } + every { fileHandler.performMoveOperation() } just Runs + negativeButtonClickSlot.captured.invoke() + + verify { fileHandler.performMoveOperation() } + } + + private fun prepareFileSystemAndFileForMockk( + storageFileExist: Boolean = true, + freeSpaceInStorage: Long = 1000L, + selectedFileLength: Long = 100L, + availableStorageSize: Long = 1000L, + fileSystemState: Fat32Checker.FileSystemState = CanWrite4GbFile + ) { + every { storageFile.exists() } returns storageFileExist + every { storageFile.freeSpace } returns freeSpaceInStorage + every { storageFile.path } returns storagePath + every { selectedFile.length() } returns selectedFileLength + every { storageCalculator.availableBytes(storageFile) } returns availableStorageSize + every { fat32Checker.fileSystemStates.value } returns fileSystemState + } + + @AfterEach + fun dispose() { + fileHandler.dispose() + } +} diff --git a/app/src/test/java/org/kiwix/kiwixmobile/reader/CopyMoveFileHandlerTest.kt b/app/src/test/java/org/kiwix/kiwixmobile/reader/CopyMoveFileHandlerTest.kt deleted file mode 100644 index d24af3dc98..0000000000 --- a/app/src/test/java/org/kiwix/kiwixmobile/reader/CopyMoveFileHandlerTest.kt +++ /dev/null @@ -1,253 +0,0 @@ -/* - * 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 android.app.Activity -import android.app.AlertDialog -import android.content.ContentResolver -import android.net.Uri -import android.os.ParcelFileDescriptor -import android.view.View -import android.widget.ProgressBar -import android.widget.TextView -import io.mockk.Runs -import io.mockk.coEvery -import io.mockk.every -import io.mockk.just -import io.mockk.mockk -import io.mockk.spyk -import io.mockk.verify -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.advanceUntilIdle -import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertFalse -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.kiwix.kiwixmobile.R -import org.kiwix.kiwixmobile.core.settings.StorageCalculator -import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil -import org.kiwix.kiwixmobile.core.utils.dialog.AlertDialogShower -import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog -import org.kiwix.kiwixmobile.nav.destination.reader.CopyMoveFileHandler -import org.kiwix.kiwixmobile.nav.destination.reader.CopyMoveFileHandler.FileCopyMoveCallback -import org.kiwix.kiwixmobile.zimManager.Fat32Checker -import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState.CanWrite4GbFile -import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState.CannotWrite4GbFile -import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState.DetectingFileSystem -import java.io.File -import java.io.FileDescriptor -import java.io.FileNotFoundException - -class CopyMoveFileHandlerTest { - private lateinit var fileHandler: CopyMoveFileHandler - - private val activity: Activity = mockk(relaxed = true) - private val sharedPreferenceUtil: SharedPreferenceUtil = mockk(relaxed = true) - private val alertDialogShower: AlertDialogShower = mockk(relaxed = true) - private val storageCalculator: StorageCalculator = mockk(relaxed = true) - private val fat32Checker: Fat32Checker = mockk(relaxed = true) - private val fileCopyMoveCallback: FileCopyMoveCallback = mockk(relaxed = true) - private val testDispatcher = StandardTestDispatcher() - private val testScope = TestScope(testDispatcher) - private val progressBarDialog: AlertDialog = mockk(relaxed = true) - private val destinationFile: File = mockk(relaxed = true) - private val parcelFileDescriptor: ParcelFileDescriptor = mockk(relaxed = true) - private val storageFile: File = mockk(relaxed = true) - private val selectedFile: File = mockk(relaxed = true) - private val storagePath = "storage/0/emulated/Android/media/org.kiwix.kiwixmobile" - - @BeforeEach - fun setup() { - fileHandler = CopyMoveFileHandler( - activity, - sharedPreferenceUtil, - alertDialogShower, - storageCalculator, - fat32Checker - ).apply { - setSelectedFileAndUri(null, selectedFile) - lifecycleScope = testScope - this.fileCopyMoveCallback = this@CopyMoveFileHandlerTest.fileCopyMoveCallback - } - } - - @Test - fun validateZimFileCanCopyOrMoveShouldReturnTrueWhenSufficientSpaceAndValidFileSystem() { - every { storageFile.exists() } returns true - every { storageFile.freeSpace } returns 1000L - every { storageFile.path } returns storagePath - every { selectedFile.length() } returns 100L - every { storageCalculator.availableBytes(storageFile) } returns 1000L - every { fat32Checker.fileSystemStates.value } returns CanWrite4GbFile - - val result = fileHandler.validateZimFileCanCopyOrMove(storageFile) - - assertTrue(result) - // check insufficientSpaceInStorage callback should not call. - verify(exactly = 0) { fileCopyMoveCallback.insufficientSpaceInStorage(any()) } - } - - @Test - fun validateZimFileCanCopyOrMoveShouldReturnFalseAndCallCallbackWhenInsufficientSpace() { - every { selectedFile.length() } returns 2000L - every { storageFile.exists() } returns true - every { storageFile.freeSpace } returns 1000L - every { storageFile.path } returns storagePath - every { storageCalculator.availableBytes(storageFile) } returns 1000L - every { fat32Checker.fileSystemStates.value } returns CanWrite4GbFile - - val result = fileHandler.validateZimFileCanCopyOrMove() - - assertFalse(result) - verify { fileCopyMoveCallback.insufficientSpaceInStorage(any()) } - } - - @Test - fun validateZimFileCanCopyOrMoveShouldReturnFalseWhenDetectingAndCanNotWrite4GBFiles() { - every { selectedFile.length() } returns 1000L - every { storageFile.exists() } returns true - every { storageFile.freeSpace } returns 2000L - every { storageFile.path } returns storagePath - every { storageCalculator.availableBytes(storageFile) } returns 2000L - every { fat32Checker.fileSystemStates.value } returns DetectingFileSystem - - // check when detecting the fileSystem - assertFalse(fileHandler.validateZimFileCanCopyOrMove()) - - every { fat32Checker.fileSystemStates.value } returns CannotWrite4GbFile - - // check when Can not write 4GB files on the fileSystem - assertFalse(fileHandler.validateZimFileCanCopyOrMove()) - } - - @Test - fun showMoveToPublicDirectoryPermissionDialogShouldShowPermissionDialogAtFirstLaunch() { - every { sharedPreferenceUtil.copyMoveZimFilePermissionDialog } returns false - every { alertDialogShower.show(any(), any(), any()) } just Runs - fileHandler.showMoveFileToPublicDirectoryDialog() - - verify { - alertDialogShower.show( - KiwixDialog.MoveFileToPublicDirectoryPermissionDialog, - any(), - any() - ) - } - } - - @Test - fun showProgressDialogShouldDisplayProgressDialog() { - val progressBar: ProgressBar = mockk(relaxed = true) - val progressTextView: TextView = mockk(relaxed = true) - val inflatedView: View = mockk() - val alertDialogBuilder: AlertDialog.Builder = mockk(relaxed = true) - - every { - activity.layoutInflater.inflate( - R.layout.copy_move_progress_bar, - null - ) - } returns inflatedView - every { inflatedView.findViewById(R.id.progressBar) } returns progressBar - every { inflatedView.findViewById(R.id.progressTextView) } returns progressTextView - - every { AlertDialog.Builder(activity) } returns alertDialogBuilder - every { alertDialogBuilder.setTitle(any()) } returns alertDialogBuilder - every { alertDialogBuilder.setView(inflatedView) } returns alertDialogBuilder - every { alertDialogBuilder.setCancelable(any()) } returns alertDialogBuilder - every { alertDialogBuilder.create() } returns progressBarDialog - every { progressBarDialog.show() } just Runs - - fileHandler.showProgressDialog() - - assertTrue(fileHandler.progressBarDialog?.isShowing == true) - } - - @OptIn(ExperimentalCoroutinesApi::class) - @Test - fun copyZimFileToPublicAppDirectory() = testScope.runTest { - val sourceUri: Uri = mockk() - val mockFileDescriptor = mockk(relaxed = true) - val contentResolver: ContentResolver = mockk() - every { activity.contentResolver } returns contentResolver - every { - contentResolver.openFileDescriptor( - sourceUri, - "r" - ) - } returns parcelFileDescriptor - every { parcelFileDescriptor.fileDescriptor } returns mockFileDescriptor - every { destinationFile.createNewFile() } returns true - every { destinationFile.name } returns "demo.zim" - every { sharedPreferenceUtil.prefStorage } returns storagePath - fileHandler = spyk(fileHandler) - every { fileHandler.getDestinationFile() } returns destinationFile - - // test when selected file is not found - fileHandler.setSelectedFileAndUri(null, null) - fileHandler.copyZimFileToPublicAppDirectory() - verify { fileCopyMoveCallback.onError(any()) } - verify { destinationFile.delete() } - - // test when selected file found - fileHandler.setSelectedFileAndUri(sourceUri, selectedFile) - fileHandler.copyZimFileToPublicAppDirectory() - verify { fileCopyMoveCallback.onFileCopied(destinationFile) } - - // test when there is an error in copying file - coEvery { - fileHandler.copyFile( - sourceUri, - destinationFile - ) - } throws FileNotFoundException("Test Exception") - - fileHandler.copyZimFileToPublicAppDirectory() - - advanceUntilIdle() - - verify(exactly = 0) { fileCopyMoveCallback.onFileCopied(destinationFile) } - verify { fileCopyMoveCallback.onError(any()) } - } - - @Test - fun getDestinationFile() { - val fileName = "test.txt" - val rootFile: File = mockk(relaxed = true) - val newFile: File = mockk(relaxed = true) - every { newFile.name } returns fileName - every { rootFile.path } returns storagePath - - every { selectedFile.name } returns fileName - every { File(rootFile, fileName).exists() } returns false - every { File(rootFile, fileName).createNewFile() } returns true - fileHandler = spyk(fileHandler) - every { fileHandler.getDestinationFile(rootFile) } returns newFile - - // Run the test - val resultFile = fileHandler.getDestinationFile(rootFile) - - assertEquals(newFile, resultFile) - verify { File(rootFile, fileName).createNewFile() } - } -}