From be1369f805382a00a141663953e687ebba2aa2c4 Mon Sep 17 00:00:00 2001 From: MohitMaliFtechiz Date: Wed, 28 Feb 2024 20:49:20 +0530 Subject: [PATCH 1/7] Added the 'Export bookmarks' feature. * The application now exports 'bookmark.xml' file to the 'Android/media/org.kiwix...' folder. Users can copy this file to the 'Android/data/org.kiwix.../files/Bookmarks/' folder to view the exported bookmarks. * Introduced an export bookmark feature in the settings screen, allowing users to easily export all bookmarks with a simple click. A dialog prompt appears before exporting, indicating the location where the exported bookmarks will be saved and providing instructions on how to import them. * Added a test case for this change in our settings test. --- .../settings/KiwixSettingsFragmentTest.kt | 3 + .../kiwixmobile/settings/SettingsRobot.kt | 8 ++ .../kiwixmobile/core/dao/LibkiwixBookmarks.kt | 34 ++++++++- .../core/settings/CorePrefsFragment.kt | 73 +++++++++++++++++++ .../core/utils/dialog/KiwixDialog.kt | 5 ++ core/src/main/res/values/strings.xml | 8 ++ core/src/main/res/xml/preferences.xml | 13 ++++ 7 files changed, 143 insertions(+), 1 deletion(-) diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/settings/KiwixSettingsFragmentTest.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/settings/KiwixSettingsFragmentTest.kt index d64ceb4b5..97a4c2889 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/settings/KiwixSettingsFragmentTest.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/settings/KiwixSettingsFragmentTest.kt @@ -107,6 +107,9 @@ class KiwixSettingsFragmentTest { clickClearHistoryPreference() assertHistoryDialogDisplayed() dismissDialog() + clickExportBookmarkPreference() + assertExportBookmarkDialogDisplayed() + dismissDialog() clickNightModePreference() assertNightModeDialogDisplayed() dismissDialog() diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/settings/SettingsRobot.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/settings/SettingsRobot.kt index 1fd97f701..4fc9ee78f 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/settings/SettingsRobot.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/settings/SettingsRobot.kt @@ -116,6 +116,14 @@ class SettingsRobot : BaseRobot() { isVisible(TextId(R.string.clear_all_history_dialog_title)) } + fun clickExportBookmarkPreference() { + clickRecyclerViewItems(R.string.pref_export_bookmark_title) + } + + fun assertExportBookmarkDialogDisplayed() { + isVisible(TextId(R.string.export_all_bookmarks_dialog_title)) + } + fun clickNightModePreference() { clickRecyclerViewItems(R.string.pref_night_mode) } 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 d84d61eea..4da1539b8 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 @@ -20,7 +20,6 @@ package org.kiwix.kiwixmobile.core.dao import android.os.Build import android.util.Base64 -import org.kiwix.kiwixmobile.core.utils.files.Log import io.reactivex.BackpressureStrategy import io.reactivex.BackpressureStrategy.LATEST import io.reactivex.Flowable @@ -29,12 +28,17 @@ import io.reactivex.subjects.BehaviorSubject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import org.kiwix.kiwixmobile.core.CoreApp +import org.kiwix.kiwixmobile.core.R +import org.kiwix.kiwixmobile.core.extensions.deleteFile import org.kiwix.kiwixmobile.core.extensions.isFileExist +import org.kiwix.kiwixmobile.core.extensions.toast import org.kiwix.kiwixmobile.core.page.adapter.Page import org.kiwix.kiwixmobile.core.page.bookmark.adapter.LibkiwixBookmarkItem import org.kiwix.kiwixmobile.core.reader.ILLUSTRATION_SIZE import org.kiwix.kiwixmobile.core.reader.ZimFileReader import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil +import org.kiwix.kiwixmobile.core.utils.files.Log import org.kiwix.libkiwix.Book import org.kiwix.libkiwix.Bookmark import org.kiwix.libkiwix.Library @@ -273,6 +277,34 @@ class LibkiwixBookmarks @Inject constructor( bookmarkListBehaviour?.onNext(getBookmarksList()) } + // Export the `bookmark.xml` file to the `Android/media/` directory of internal storage. + fun exportBookmark() { + try { + val bookmarkDestinationFile = exportedFile("bookmark.xml") + bookmarkFile.inputStream().use { inputStream -> + bookmarkDestinationFile.outputStream().use(inputStream::copyTo) + } + sharedPreferenceUtil.context.toast( + sharedPreferenceUtil.context.getString( + R.string.export_bookmark_saved, + "${bookmarkDestinationFile.parent}/" + ) + ) + } catch (ignore: Exception) { + Log.e(TAG, "Error: bookmark couldn't export.\n Original exception = $ignore") + sharedPreferenceUtil.context.toast(R.string.export_bookmark_error) + } + } + + private fun exportedFile(fileName: String): File { + val rootFolder = CoreApp.instance.externalMediaDirs[0] + if (!rootFolder.isFileExist()) rootFolder.mkdir() + val file = File(rootFolder, fileName) + if (file.isFileExist()) file.deleteFile() + file.createNewFile() + return file + } + companion object { const val TAG = "LibkiwixBookmark" } 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 81beeaa24..dc399db69 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 @@ -28,6 +28,8 @@ import android.os.Build import android.os.Bundle import android.view.LayoutInflater import android.webkit.WebView +import android.widget.Toast +import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.ContextCompat import androidx.preference.EditTextPreference @@ -43,8 +45,11 @@ import org.kiwix.kiwixmobile.core.NightModeConfig import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.compat.CompatHelper.Companion.getPackageInformation import org.kiwix.kiwixmobile.core.compat.CompatHelper.Companion.getVersionCode +import org.kiwix.kiwixmobile.core.dao.LibkiwixBookmarks +import org.kiwix.kiwixmobile.core.extensions.toast import org.kiwix.kiwixmobile.core.main.AddNoteDialog import org.kiwix.kiwixmobile.core.main.CoreMainActivity +import org.kiwix.kiwixmobile.core.navigateToAppSettings import org.kiwix.kiwixmobile.core.utils.EXTERNAL_SELECT_POSITION import org.kiwix.kiwixmobile.core.utils.INTERNAL_SELECT_POSITION import org.kiwix.kiwixmobile.core.utils.LanguageUtils @@ -83,6 +88,10 @@ abstract class CorePrefsFragment : @JvmField @Inject protected var alertDialogShower: DialogShower? = null + + @JvmField + @Inject + internal var libkiwixBookmarks: LibkiwixBookmarks? = null override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { coreComponent .activityComponentBuilder() @@ -131,6 +140,8 @@ abstract class CorePrefsFragment : override fun onDestroyView() { presenter?.dispose() + storagePermissionForNotesLauncher?.unregister() + storagePermissionForNotesLauncher = null super.onDestroyView() } @@ -289,9 +300,70 @@ abstract class CorePrefsFragment : if (preference.key.equals(SharedPreferenceUtil.PREF_STORAGE, ignoreCase = true)) { openFolderSelect() } + if (preference.key.equals(PREF_EXPORT_BOOKMARK, ignoreCase = true) && + requestExternalStorageWritePermissionForExportBookmark() + ) { + showExportBookmarkDialog() + } return true } + @Suppress("NestedBlockDepth") + private fun requestExternalStorageWritePermissionForExportBookmark(): Boolean { + var isPermissionGranted = false + if (sharedPreferenceUtil?.isPlayStoreBuildWithAndroid11OrAbove() == false && + Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU + ) { + if (requireActivity().checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) + == PackageManager.PERMISSION_GRANTED + ) { + isPermissionGranted = true + } else { + storagePermissionForNotesLauncher?.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) + } + } else { + isPermissionGranted = true + } + return isPermissionGranted + } + + private var storagePermissionForNotesLauncher: ActivityResultLauncher? = + registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted -> + if (isGranted) { + // Successfully granted permission, so opening the export bookmark Dialog + showExportBookmarkDialog() + } else { + if (shouldShowRequestPermissionRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + /* shouldShowRequestPermissionRationale() returns false when: + * 1) User has previously checked on "Don't ask me again", and/or + * 2) Permission has been disabled on device + */ + requireActivity().toast( + R.string.ext_storage_permission_rationale_export_bookmark, + Toast.LENGTH_LONG + ) + } else { + requireActivity().toast( + R.string.ext_storage_write_permission_denied_export_bookmark, + Toast.LENGTH_LONG + ) + alertDialogShower?.show( + KiwixDialog.ReadPermissionRequired, + requireActivity()::navigateToAppSettings + ) + } + } + } + + private fun showExportBookmarkDialog() { + alertDialogShower?.show( + KiwixDialog.YesNoDialog.ExportBookmarks, + { libkiwixBookmarks?.exportBookmark() } + ) + } + private fun openFolderSelect() { val dialogFragment = StorageSelectDialog() dialogFragment.onSelectAction = @@ -383,5 +455,6 @@ abstract class CorePrefsFragment : private const val ZOOM_OFFSET = 2 private const val ZOOM_SCALE = 25 private const val INTERNAL_TEXT_ZOOM = "text_zoom" + private const val PREF_EXPORT_BOOKMARK = "pref_export_bookmark" } } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/dialog/KiwixDialog.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/dialog/KiwixDialog.kt index 6885196f5..17ad04568 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/dialog/KiwixDialog.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/dialog/KiwixDialog.kt @@ -272,6 +272,11 @@ sealed class KiwixDialog( object OpenInNewTab : YesNoDialog( null, R.string.open_in_new_tab ) + + object ExportBookmarks : YesNoDialog( + R.string.export_all_bookmarks_dialog_title, + message = R.string.export_all_bookmarks_dialog_message, + ) } object StorageConfigure : KiwixDialog( diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 7e6e6124e..35fe9c846 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -77,6 +77,9 @@ Clear recent searches and tabs history Notes Permissions + Export Bookmarks + Export all saved bookmarks + Bookmarks are exported in %s folder All History Cleared Clear bookmarks Clear All History? @@ -226,6 +229,9 @@ Save Note Wiki Article Title + Storage access is required for exporting Bookmarks + Bookmarks can’t export without access of storage + An error occurred when trying to export bookmark! Storage access is required for Notes Notes can\’t be used without access to storage Note save unsuccessful @@ -288,6 +294,8 @@ Delete Selected History? Delete All Bookmarks? Delete Selected Bookmarks? + Export All Bookmarks? + Exporting will generate bookmark.xml file in the Android/media/org.kiwix…/ folder. To import bookmarks, simply copy this file into the Android/data/org.kiwix…/files/Bookmarks/ folder. On Off Auto diff --git a/core/src/main/res/xml/preferences.xml b/core/src/main/res/xml/preferences.xml index 081a15b34..d7716d041 100644 --- a/core/src/main/res/xml/preferences.xml +++ b/core/src/main/res/xml/preferences.xml @@ -95,6 +95,19 @@ android:title="@string/pref_clear_all_notes_title" app:iconSpaceReserved="false" /> + + + + + + Date: Mon, 3 Jun 2024 17:39:16 +0530 Subject: [PATCH 2/7] Improved the exporting of bookmarks. * Improved the location of exporting the bookmarks. * Improved the messages that will clearly shows where and in which file bookmarks are exported. --- .../kiwixmobile/core/dao/LibkiwixBookmarks.kt | 24 ++++++++++++------- core/src/main/res/values/strings.xml | 4 ++-- 2 files changed, 18 insertions(+), 10 deletions(-) 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 4da1539b8..f9cbf48c2 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 @@ -19,6 +19,7 @@ package org.kiwix.kiwixmobile.core.dao import android.os.Build +import android.os.Environment import android.util.Base64 import io.reactivex.BackpressureStrategy import io.reactivex.BackpressureStrategy.LATEST @@ -28,9 +29,7 @@ import io.reactivex.subjects.BehaviorSubject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import org.kiwix.kiwixmobile.core.CoreApp import org.kiwix.kiwixmobile.core.R -import org.kiwix.kiwixmobile.core.extensions.deleteFile import org.kiwix.kiwixmobile.core.extensions.isFileExist import org.kiwix.kiwixmobile.core.extensions.toast import org.kiwix.kiwixmobile.core.page.adapter.Page @@ -287,7 +286,7 @@ class LibkiwixBookmarks @Inject constructor( sharedPreferenceUtil.context.toast( sharedPreferenceUtil.context.getString( R.string.export_bookmark_saved, - "${bookmarkDestinationFile.parent}/" + bookmarkDestinationFile.name ) ) } catch (ignore: Exception) { @@ -297,12 +296,21 @@ class LibkiwixBookmarks @Inject constructor( } private fun exportedFile(fileName: String): File { - val rootFolder = CoreApp.instance.externalMediaDirs[0] + val rootFolder = File( + "${Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)}" + + "/org.kiwix" + ) if (!rootFolder.isFileExist()) rootFolder.mkdir() - val file = File(rootFolder, fileName) - if (file.isFileExist()) file.deleteFile() - file.createNewFile() - return file + return sequence { + yield(File(rootFolder, fileName)) + yieldAll( + generateSequence(1) { it + 1 }.map { + File( + rootFolder, fileName.replace(".", "_$it.") + ) + } + ) + }.first { !it.isFileExist() } } companion object { diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 35fe9c846..b6ad95244 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -79,7 +79,7 @@ Permissions Export Bookmarks Export all saved bookmarks - Bookmarks are exported in %s folder + Bookmarks are exported in %s file All History Cleared Clear bookmarks Clear All History? @@ -295,7 +295,7 @@ Delete All Bookmarks? Delete Selected Bookmarks? Export All Bookmarks? - Exporting will generate bookmark.xml file in the Android/media/org.kiwix…/ folder. To import bookmarks, simply copy this file into the Android/data/org.kiwix…/files/Bookmarks/ folder. + Exporting will generate bookmark.xml file in the Download/org.kiwix…/ folder. On Off Auto From 42a4481707e14165f659a287d4f7bd7b8f9097ce Mon Sep 17 00:00:00 2001 From: MohitMaliFtechiz Date: Tue, 4 Jun 2024 17:05:39 +0530 Subject: [PATCH 3/7] Added the import bookmarks feature in setting screen. * Refactored the test cases for this new change. * Now user can easily import the exported bookmarks by just selecting the exported `bookmark.xml` file. --- .../kiwix/kiwixmobile/core/StorageObserver.kt | 12 ++- .../kiwixmobile/core/dao/LibkiwixBookmarks.kt | 76 +++++++++++++++++-- .../kiwixmobile/core/di/modules/JNIModule.kt | 6 +- .../core/settings/CorePrefsFragment.kt | 75 ++++++++++++++++++ .../core/utils/dialog/KiwixDialog.kt | 7 ++ core/src/main/res/values/strings.xml | 6 ++ core/src/main/res/xml/preferences.xml | 6 ++ .../kiwixmobile/core/StorageObserverTest.kt | 4 +- 8 files changed, 182 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/StorageObserver.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/StorageObserver.kt index 932a1f0bf..59ea755ea 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/StorageObserver.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/StorageObserver.kt @@ -23,6 +23,7 @@ import io.reactivex.functions.BiFunction import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.runBlocking import org.kiwix.kiwixmobile.core.dao.FetchDownloadDao +import org.kiwix.kiwixmobile.core.dao.LibkiwixBookmarks import org.kiwix.kiwixmobile.core.downloader.model.DownloadModel import org.kiwix.kiwixmobile.core.reader.ZimFileReader import org.kiwix.kiwixmobile.core.utils.files.FileSearch @@ -34,7 +35,8 @@ import javax.inject.Inject class StorageObserver @Inject constructor( private val downloadDao: FetchDownloadDao, private val fileSearch: FileSearch, - private val zimReaderFactory: ZimFileReader.Factory + private val zimReaderFactory: ZimFileReader.Factory, + private val libkiwixBookmarks: LibkiwixBookmarks ) { fun getBooksOnFileSystem( @@ -56,6 +58,12 @@ class StorageObserver @Inject constructor( private fun convertToBookOnDisk(file: File) = runBlocking { zimReaderFactory.create(file) - ?.let { zimFileReader -> BookOnDisk(file, zimFileReader).also { zimFileReader.dispose() } } + ?.let { zimFileReader -> + BookOnDisk(file, zimFileReader).also { + // add the book to libkiwix library to validate the imported bookmarks + libkiwixBookmarks.addBookToLibrary(archive = zimFileReader.jniKiwixReader) + zimFileReader.dispose() + } + } } } 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 f9cbf48c2..a1619de6e 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 @@ -42,13 +42,15 @@ import org.kiwix.libkiwix.Book import org.kiwix.libkiwix.Bookmark import org.kiwix.libkiwix.Library import org.kiwix.libkiwix.Manager +import org.kiwix.libzim.Archive import java.io.File import javax.inject.Inject class LibkiwixBookmarks @Inject constructor( val library: Library, manager: Manager, - val sharedPreferenceUtil: SharedPreferenceUtil + val sharedPreferenceUtil: SharedPreferenceUtil, + private val bookDao: NewBookDao ) : PageDao { /** @@ -135,7 +137,13 @@ class LibkiwixBookmarks @Inject constructor( bookId = libkiwixBookmarkItem.zimId title = libkiwixBookmarkItem.title url = libkiwixBookmarkItem.url - bookTitle = libkiwixBookmarkItem.libKiwixBook?.title ?: libkiwixBookmarkItem.zimId + bookTitle = when { + libkiwixBookmarkItem.libKiwixBook?.title != null -> + libkiwixBookmarkItem.libKiwixBook.title + + libkiwixBookmarkItem.zimName.isNotBlank() -> libkiwixBookmarkItem.zimName + else -> libkiwixBookmarkItem.zimId + } } library.addBookmark(bookmark).also { if (shouldWriteBookmarkToFile) { @@ -148,6 +156,26 @@ class LibkiwixBookmarks @Inject constructor( } } + fun addBookToLibrary(file: File? = null, archive: Archive? = null) { + try { + bookmarksChanged = true + val book = Book().apply { + archive?.let { + update(archive) + } ?: run { + update(Archive(file?.canonicalPath)) + } + } + addBookToLibraryIfNotExist(book) + updateFlowableBookmarkList() + } catch (ignore: Exception) { + Log.e( + TAG, + "Error: Couldn't add the book to library.\nOriginal exception = $ignore" + ) + } + } + private fun addBookToLibraryIfNotExist(libKiwixBook: Book?) { libKiwixBook?.let { book -> if (!isBookAlreadyExistInLibrary(book.id)) { @@ -212,7 +240,9 @@ class LibkiwixBookmarks @Inject constructor( return bookmarkList } // Retrieve the list of bookmarks from the library, or return an empty list if it's null. - val bookmarkArray = library.getBookmarks(false)?.toList() ?: return bookmarkList + val bookmarkArray = + library.getBookmarks(false)?.toList() + ?: return bookmarkList.distinctBy(LibkiwixBookmarkItem::bookmarkUrl) // Create a list to store LibkiwixBookmarkItem objects. bookmarkList = bookmarkArray.mapNotNull { bookmark -> @@ -244,7 +274,21 @@ class LibkiwixBookmarks @Inject constructor( bookmarksChanged = false } } - return bookmarkList + + // Delete duplicates bookmarks if any exist + deleteDuplicateBookmarks() + + return bookmarkList.distinctBy(LibkiwixBookmarkItem::bookmarkUrl) + } + + private fun deleteDuplicateBookmarks() { + bookmarkList.groupBy { it.bookmarkUrl to it.zimFilePath } + .filter { it.value.size > 1 } + .forEach { (_, value) -> + value.drop(1).forEach { bookmarkItem -> + deleteBookmark(bookmarkItem.zimId, bookmarkItem.bookmarkUrl) + } + } } private fun isBookMarkExist(libkiwixBookmarkItem: LibkiwixBookmarkItem): Boolean = @@ -276,7 +320,7 @@ class LibkiwixBookmarks @Inject constructor( bookmarkListBehaviour?.onNext(getBookmarksList()) } - // Export the `bookmark.xml` file to the `Android/media/` directory of internal storage. + // Export the `bookmark.xml` file to the `Download/org.kiwix/` directory of internal storage. fun exportBookmark() { try { val bookmarkDestinationFile = exportedFile("bookmark.xml") @@ -313,6 +357,28 @@ class LibkiwixBookmarks @Inject constructor( }.first { !it.isFileExist() } } + fun importBookmarks(bookmarkFile: File) { + // Create a temporary library manager to import the bookmarks. + val tempLibrary = Library() + Manager(tempLibrary).apply { + // Read the bookmark file. + readBookmarkFile(bookmarkFile.canonicalPath) + } + // Add the ZIM files to the library for validating the bookmarks. + bookDao.getBooks().forEach { + addBookToLibrary(file = it.file) + } + // Save the imported bookmarks to the current library. + tempLibrary.getBookmarks(false)?.toList()?.forEach { + saveBookmark(LibkiwixBookmarkItem(it, null, null)) + } + sharedPreferenceUtil.context.toast(R.string.bookmark_imported_message) + + if (bookmarkFile.exists()) { + bookmarkFile.delete() + } + } + companion object { const val TAG = "LibkiwixBookmark" } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/di/modules/JNIModule.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/di/modules/JNIModule.kt index f4b1a7ebe..16b2307ad 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/di/modules/JNIModule.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/di/modules/JNIModule.kt @@ -21,6 +21,7 @@ import android.content.Context import dagger.Module import dagger.Provides import org.kiwix.kiwixmobile.core.dao.LibkiwixBookmarks +import org.kiwix.kiwixmobile.core.dao.NewBookDao import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil import org.kiwix.libkiwix.JNIKiwix import org.kiwix.libkiwix.Library @@ -45,6 +46,7 @@ class JNIModule { fun providesLibkiwixBookmarks( library: Library, manager: Manager, - sharedPreferenceUtil: SharedPreferenceUtil - ): LibkiwixBookmarks = LibkiwixBookmarks(library, manager, sharedPreferenceUtil) + sharedPreferenceUtil: SharedPreferenceUtil, + bookDao: NewBookDao + ): LibkiwixBookmarks = LibkiwixBookmarks(library, manager, sharedPreferenceUtil, bookDao) } 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 dc399db69..b92876c90 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 @@ -20,12 +20,14 @@ package org.kiwix.kiwixmobile.core.settings import android.Manifest import android.annotation.SuppressLint import android.app.Activity.RESULT_OK +import android.content.ActivityNotFoundException import android.content.Intent import android.content.SharedPreferences import android.content.SharedPreferences.OnSharedPreferenceChangeListener import android.content.pm.PackageManager import android.os.Build import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.webkit.WebView import android.widget.Toast @@ -61,8 +63,10 @@ import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog.OpenCredits import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog.SelectFolder import org.kiwix.kiwixmobile.core.utils.files.FileUtils.getPathFromUri import java.io.File +import java.io.InputStream import java.util.Locale import javax.inject.Inject +import javax.xml.parsers.DocumentBuilderFactory abstract class CorePrefsFragment : PreferenceFragmentCompat(), @@ -305,6 +309,9 @@ abstract class CorePrefsFragment : ) { showExportBookmarkDialog() } + if (preference.key.equals(PREF_IMPORT_BOOKMARK, ignoreCase = true)) { + showImportBookmarkDialog() + } return true } @@ -364,6 +371,73 @@ abstract class CorePrefsFragment : ) } + private fun showImportBookmarkDialog() { + alertDialogShower?.show( + KiwixDialog.ImportBookmarks, + ::showFileChooser + ) + } + + private fun showFileChooser() { + val intent = Intent().apply { + action = Intent.ACTION_GET_CONTENT + type = "*/*" + addCategory(Intent.CATEGORY_OPENABLE) + } + try { + fileSelectLauncher.launch(Intent.createChooser(intent, "Select a bookmark file")) + } catch (ex: ActivityNotFoundException) { + activity.toast( + resources.getString(R.string.no_app_found_to_select_bookmark_file), + Toast.LENGTH_SHORT + ) + } + } + + private val fileSelectLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == RESULT_OK) { + result.data?.data?.let { uri -> + val contentResolver = requireActivity().contentResolver + if (!isValidBookmarkFile(contentResolver.getType(uri))) { + activity.toast( + resources.getString(R.string.error_invalid_bookmark_file), + Toast.LENGTH_SHORT + ) + return@registerForActivityResult + } + val inputStream: InputStream? = contentResolver.openInputStream(uri) + // create a temp file for importing the saved bookmarks + val tempFile = File(requireActivity().externalCacheDir, "bookmark.xml") + if (tempFile.exists()) { + tempFile.delete() + } + tempFile.createNewFile() + inputStream?.let { + tempFile.outputStream().use(inputStream::copyTo) + } + try { + // check if the xml file is valid or not + DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(tempFile) + // import the bookmarks + libkiwixBookmarks?.importBookmarks(tempFile) + } catch (ignore: Exception) { + Log.e( + "IMPORT_BOOKMARKS", + "Error in importing the bookmarks\nOrignal exception = $ignore" + ) + activity.toast( + resources.getString(R.string.error_invalid_bookmark_file), + Toast.LENGTH_SHORT + ) + } + } + } + } + + private fun isValidBookmarkFile(mimeType: String?) = + mimeType == "application/xml" || mimeType == "text/xml" + private fun openFolderSelect() { val dialogFragment = StorageSelectDialog() dialogFragment.onSelectAction = @@ -456,5 +530,6 @@ abstract class CorePrefsFragment : private const val ZOOM_SCALE = 25 private const val INTERNAL_TEXT_ZOOM = "text_zoom" private const val PREF_EXPORT_BOOKMARK = "pref_export_bookmark" + private const val PREF_IMPORT_BOOKMARK = "pref_import_bookmark" } } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/dialog/KiwixDialog.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/dialog/KiwixDialog.kt index 17ad04568..e0e1bbc7d 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/dialog/KiwixDialog.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/dialog/KiwixDialog.kt @@ -287,6 +287,13 @@ sealed class KiwixDialog( icon = R.drawable.ic_baseline_storage_24 ) + object ImportBookmarks : KiwixDialog( + R.string.import_bookmarks_dialog_title, + message = null, + positiveMessage = R.string.yes, + negativeMessage = R.string.no + ) + object DeleteSelectedHistory : KiwixDialog( R.string.delete_selected_history, null, diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index b6ad95244..84b9750bf 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -68,6 +68,7 @@ The language of this page is not supported. The article may not be properly read. Could not find an installed application for this type of file Please install an email service provider or email us at %1s + No app found to select a bookmark file No Content Headers Found To access offline content we need access to your storage To download zim files we need write access to your storage @@ -77,6 +78,8 @@ Clear recent searches and tabs history Notes Permissions + Import Bookmarks + Import the saved bookmarks Export Bookmarks Export all saved bookmarks Bookmarks are exported in %s file @@ -294,8 +297,11 @@ Delete Selected History? Delete All Bookmarks? Delete Selected Bookmarks? + Import Bookmarks? Export All Bookmarks? Exporting will generate bookmark.xml file in the Download/org.kiwix…/ folder. + All the bookmarks has been imported. + Error: The selected file is not a valid bookmark file. On Off Auto diff --git a/core/src/main/res/xml/preferences.xml b/core/src/main/res/xml/preferences.xml index d7716d041..1bbae55ac 100644 --- a/core/src/main/res/xml/preferences.xml +++ b/core/src/main/res/xml/preferences.xml @@ -102,6 +102,12 @@ android:title="@string/bookmarks" app:iconSpaceReserved="false"> + + > = PublishProcessor.create() @@ -72,7 +74,7 @@ class StorageObserverTest { every { fileSearch.scan(scanningProgressListener) } returns files every { downloadDao.downloads() } returns downloads every { runBlocking { readerFactory.create(file) } } returns zimFileReader - storageObserver = StorageObserver(downloadDao, fileSearch, readerFactory) + storageObserver = StorageObserver(downloadDao, fileSearch, readerFactory, libkiwixBookmarks) } @Test From e83a040f021725456a424df63c44f2150d9b624f Mon Sep 17 00:00:00 2001 From: MohitMaliFtechiz Date: Tue, 4 Jun 2024 17:32:01 +0530 Subject: [PATCH 4/7] Added test cases for testing the "Import Bookmark" feature. --- .../kiwixmobile/settings/KiwixSettingsFragmentTest.kt | 3 +++ .../java/org/kiwix/kiwixmobile/settings/SettingsRobot.kt | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/settings/KiwixSettingsFragmentTest.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/settings/KiwixSettingsFragmentTest.kt index 97a4c2889..18464a0f3 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/settings/KiwixSettingsFragmentTest.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/settings/KiwixSettingsFragmentTest.kt @@ -110,6 +110,9 @@ class KiwixSettingsFragmentTest { clickExportBookmarkPreference() assertExportBookmarkDialogDisplayed() dismissDialog() + clickOnImportBookmarkPreference() + assertImportBookmarkDialogDisplayed() + dismissDialog() clickNightModePreference() assertNightModeDialogDisplayed() dismissDialog() diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/settings/SettingsRobot.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/settings/SettingsRobot.kt index 4fc9ee78f..c3fb7c1a7 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/settings/SettingsRobot.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/settings/SettingsRobot.kt @@ -124,6 +124,14 @@ class SettingsRobot : BaseRobot() { isVisible(TextId(R.string.export_all_bookmarks_dialog_title)) } + fun clickOnImportBookmarkPreference() { + clickRecyclerViewItems(R.string.pref_import_bookmark_title) + } + + fun assertImportBookmarkDialogDisplayed() { + isVisible(TextId(R.string.import_bookmarks_dialog_title)) + } + fun clickNightModePreference() { clickRecyclerViewItems(R.string.pref_night_mode) } From a771f00cc2eb31e2ea103434abfe8246a1073d3f Mon Sep 17 00:00:00 2001 From: MohitMaliFtechiz Date: Tue, 4 Jun 2024 17:46:24 +0530 Subject: [PATCH 5/7] Improved the code of importing the bookmarks. --- .../core/settings/CorePrefsFragment.kt | 57 +++++++++++-------- 1 file changed, 33 insertions(+), 24 deletions(-) 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 b92876c90..0ee972a59 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 @@ -406,35 +406,44 @@ abstract class CorePrefsFragment : ) return@registerForActivityResult } - val inputStream: InputStream? = contentResolver.openInputStream(uri) - // create a temp file for importing the saved bookmarks - val tempFile = File(requireActivity().externalCacheDir, "bookmark.xml") - if (tempFile.exists()) { - tempFile.delete() - } - tempFile.createNewFile() - inputStream?.let { - tempFile.outputStream().use(inputStream::copyTo) - } - try { - // check if the xml file is valid or not - DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(tempFile) - // import the bookmarks - libkiwixBookmarks?.importBookmarks(tempFile) - } catch (ignore: Exception) { - Log.e( - "IMPORT_BOOKMARKS", - "Error in importing the bookmarks\nOrignal exception = $ignore" - ) - activity.toast( - resources.getString(R.string.error_invalid_bookmark_file), - Toast.LENGTH_SHORT - ) + + createTempFile(contentResolver.openInputStream(uri)).apply { + if (isValidXmlFile(this)) { + libkiwixBookmarks?.importBookmarks(this) + } else { + activity.toast( + resources.getString(R.string.error_invalid_bookmark_file), + Toast.LENGTH_SHORT + ) + } } } } } + private fun isValidXmlFile(file: File): Boolean { + return try { + DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(file) + true + } catch (ignore: Exception) { + Log.e("IMPORT_BOOKMARKS", "Invalid XML file", ignore) + false + } + } + + private fun createTempFile(inputStream: InputStream?): File { + // create a temp file for importing the saved bookmarks + val tempFile = File(requireActivity().externalCacheDir, "bookmark.xml") + if (tempFile.exists()) { + tempFile.delete() + } + tempFile.createNewFile() + inputStream?.let { + tempFile.outputStream().use(inputStream::copyTo) + } + return tempFile + } + private fun isValidBookmarkFile(mimeType: String?) = mimeType == "application/xml" || mimeType == "text/xml" From b2aeab5fdbcdd74df950b26dedecd8490374d187 Mon Sep 17 00:00:00 2001 From: MohitMaliFtechiz Date: Tue, 4 Jun 2024 18:25:27 +0530 Subject: [PATCH 6/7] Fixed `StorageObserverTest`. --- .../test/java/org/kiwix/kiwixmobile/core/StorageObserverTest.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/core/src/test/java/org/kiwix/kiwixmobile/core/StorageObserverTest.kt b/core/src/test/java/org/kiwix/kiwixmobile/core/StorageObserverTest.kt index 101d1e8db..d3c80eeb3 100644 --- a/core/src/test/java/org/kiwix/kiwixmobile/core/StorageObserverTest.kt +++ b/core/src/test/java/org/kiwix/kiwixmobile/core/StorageObserverTest.kt @@ -73,6 +73,7 @@ class StorageObserverTest { every { sharedPreferenceUtil.prefStorage } returns "a" every { fileSearch.scan(scanningProgressListener) } returns files every { downloadDao.downloads() } returns downloads + every { zimFileReader.jniKiwixReader } returns mockk() every { runBlocking { readerFactory.create(file) } } returns zimFileReader storageObserver = StorageObserver(downloadDao, fileSearch, readerFactory, libkiwixBookmarks) } From 9c6aeecae6416e5c996c01ae8c12fa3df983213d Mon Sep 17 00:00:00 2001 From: MohitMaliFtechiz Date: Tue, 4 Jun 2024 19:17:30 +0530 Subject: [PATCH 7/7] Added the test cases for "ImportBookmark" feature to properly test the importing of bookmarks. --- .../page/bookmarks/ImportBookmarkTest.kt | 189 ++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 app/src/androidTest/java/org/kiwix/kiwixmobile/page/bookmarks/ImportBookmarkTest.kt diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/page/bookmarks/ImportBookmarkTest.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/page/bookmarks/ImportBookmarkTest.kt new file mode 100644 index 000000000..f19564498 --- /dev/null +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/page/bookmarks/ImportBookmarkTest.kt @@ -0,0 +1,189 @@ +/* + * 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.page.bookmarks + +import androidx.core.content.edit +import androidx.lifecycle.Lifecycle +import androidx.preference.PreferenceManager +import androidx.test.core.app.ActivityScenario +import androidx.test.espresso.accessibility.AccessibilityChecks +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import io.objectbox.BoxStore +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.jupiter.api.Assertions.assertEquals +import org.kiwix.kiwixmobile.BaseActivityTest +import org.kiwix.kiwixmobile.core.dao.LibkiwixBookmarks +import org.kiwix.kiwixmobile.core.dao.NewBookDao +import org.kiwix.kiwixmobile.core.dao.entities.BookOnDiskEntity +import org.kiwix.kiwixmobile.core.di.modules.DatabaseModule +import org.kiwix.kiwixmobile.core.page.bookmark.adapter.LibkiwixBookmarkItem +import org.kiwix.kiwixmobile.core.utils.LanguageUtils +import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil +import org.kiwix.kiwixmobile.main.KiwixMainActivity +import org.kiwix.kiwixmobile.testutils.RetryRule +import org.kiwix.kiwixmobile.testutils.TestUtils +import org.kiwix.libkiwix.Library +import org.kiwix.libkiwix.Manager +import java.io.File + +class ImportBookmarkTest : BaseActivityTest() { + + @Rule + @JvmField + var retryRule = RetryRule() + + private val boxStore: BoxStore? = DatabaseModule.boxStore + private val library = Library() + private val manager = Manager(library) + private val newBookDao = NewBookDao(boxStore!!.boxFor(BookOnDiskEntity::class.java)) + lateinit var libkiwixBookmarks: LibkiwixBookmarks + + private val bookmarkXmlData = """ + + + + 1f88ab6f-c265-b3ff-8f49-b7f442950380 + Alpine Linux Wiki + alpinelinux_en_all + maxi + eng + 2023-01-18 + + Main Page + https://kiwix.app/A/Main_Page + + + + 1f88ab6f-c265-b3ff-8f49-b7f442950380 + Alpine Linux Wiki + alpinelinux_en_all + maxi + eng + 2023-01-18 + + Installation + https://kiwix.app/A/Installation + + + + 04bf4329-9bfb-3681-03e2-cfae7b047f24 + Ray Charles + wikipedia_en_ray_charles + maxi + eng + 2024-03-17 + + Wikipedia + https://kiwix.app/A/index + + + """.trimIndent() + + @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) + onActivity { + LanguageUtils.handleLocaleChange( + it, + "en", + SharedPreferenceUtil(context) + ) + } + } + libkiwixBookmarks = + LibkiwixBookmarks(library, manager, SharedPreferenceUtil(context), newBookDao) + } + + init { + AccessibilityChecks.enable().setRunChecksFromRootView(true) + } + + @Test + fun importBookmark() = runBlocking { + // clear the bookmarks to perform tes case properly. + clearBookmarks() + // test with empty data file + var tempBookmarkFile = getTemporaryBookmarkFile(true) + importBookmarks(tempBookmarkFile) + var actualDataAfterImporting = libkiwixBookmarks.bookmarks().blockingFirst() + assertEquals(0, actualDataAfterImporting.size) + + // import the bookmark + tempBookmarkFile = getTemporaryBookmarkFile() + importBookmarks(tempBookmarkFile) + actualDataAfterImporting = libkiwixBookmarks.bookmarks().blockingFirst() + assertEquals(3, actualDataAfterImporting.size) + assertEquals(actualDataAfterImporting[0].title, "Main Page") + assertEquals(actualDataAfterImporting[0].url, "https://kiwix.app/A/Main_Page") + assertEquals(actualDataAfterImporting[0].zimId, "1f88ab6f-c265-b3ff-8f49-b7f442950380") + + // import duplicate bookmarks + importBookmarks(tempBookmarkFile) + actualDataAfterImporting = libkiwixBookmarks.bookmarks().blockingFirst() + assertEquals(3, actualDataAfterImporting.size) + + // delete the temp file + if (tempBookmarkFile.exists()) tempBookmarkFile.delete() + } + + private fun importBookmarks(tempBookmarkFile: File) { + activityScenario.onActivity { + runBlocking { + libkiwixBookmarks.importBookmarks(tempBookmarkFile) + } + } + } + + private fun clearBookmarks() { + // delete bookmarks for testing other edge cases + libkiwixBookmarks.deleteBookmarks( + libkiwixBookmarks.bookmarks() + .blockingFirst() as List + ) + } + + private fun getTemporaryBookmarkFile(isWithEmptyData: Boolean = false): File = + File(context.externalCacheDir, "bookmark.xml").apply { + createNewFile() + if (exists()) delete() + + if (!isWithEmptyData) { + // Write the XML data to the temp file + writeText(bookmarkXmlData) + } + } +}