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) + } + } +} 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..18464a0f3 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,12 @@ class KiwixSettingsFragmentTest { clickClearHistoryPreference() assertHistoryDialogDisplayed() dismissDialog() + 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 1fd97f701..c3fb7c1a7 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,22 @@ 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 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) } 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 d84d61eea..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 @@ -19,8 +19,8 @@ package org.kiwix.kiwixmobile.core.dao import android.os.Build +import android.os.Environment 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,23 +29,28 @@ import io.reactivex.subjects.BehaviorSubject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import org.kiwix.kiwixmobile.core.R 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 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 { /** @@ -132,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) { @@ -145,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)) { @@ -209,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 -> @@ -241,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 = @@ -273,6 +320,65 @@ class LibkiwixBookmarks @Inject constructor( bookmarkListBehaviour?.onNext(getBookmarksList()) } + // Export the `bookmark.xml` file to the `Download/org.kiwix/` 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.name + ) + ) + } 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 = File( + "${Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)}" + + "/org.kiwix" + ) + if (!rootFolder.isFileExist()) rootFolder.mkdir() + return sequence { + yield(File(rootFolder, fileName)) + yieldAll( + generateSequence(1) { it + 1 }.map { + File( + rootFolder, fileName.replace(".", "_$it.") + ) + } + ) + }.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 81beeaa24..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 @@ -20,14 +20,18 @@ 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 +import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.ContextCompat import androidx.preference.EditTextPreference @@ -43,8 +47,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 @@ -56,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(), @@ -83,6 +92,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 +144,8 @@ abstract class CorePrefsFragment : override fun onDestroyView() { presenter?.dispose() + storagePermissionForNotesLauncher?.unregister() + storagePermissionForNotesLauncher = null super.onDestroyView() } @@ -289,9 +304,149 @@ abstract class CorePrefsFragment : if (preference.key.equals(SharedPreferenceUtil.PREF_STORAGE, ignoreCase = true)) { openFolderSelect() } + if (preference.key.equals(PREF_EXPORT_BOOKMARK, ignoreCase = true) && + requestExternalStorageWritePermissionForExportBookmark() + ) { + showExportBookmarkDialog() + } + if (preference.key.equals(PREF_IMPORT_BOOKMARK, ignoreCase = true)) { + showImportBookmarkDialog() + } 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 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 + } + + 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" + private fun openFolderSelect() { val dialogFragment = StorageSelectDialog() dialogFragment.onSelectAction = @@ -383,5 +538,7 @@ 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" + 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 6885196f5..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 @@ -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( @@ -282,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 7e6e6124e..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,11 @@ 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 All History Cleared Clear bookmarks Clear All History? @@ -226,6 +232,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 +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 081a15b34..1bbae55ac 100644 --- a/core/src/main/res/xml/preferences.xml +++ b/core/src/main/res/xml/preferences.xml @@ -95,6 +95,25 @@ android:title="@string/pref_clear_all_notes_title" app:iconSpaceReserved="false" /> + + + + + + + + > = PublishProcessor.create() @@ -71,8 +73,9 @@ 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) + storageObserver = StorageObserver(downloadDao, fileSearch, readerFactory, libkiwixBookmarks) } @Test