Merge pull request #3724 from kiwix/Fix#822

Added the 'Export bookmarks' feature.
This commit is contained in:
Kelson 2024-06-07 06:40:40 +02:00 committed by GitHub
commit 1b255d56dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 542 additions and 10 deletions

View File

@ -0,0 +1,189 @@
/*
* Kiwix Android
* Copyright (c) 2024 Kiwix <android.kiwix.org>
* 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 <http://www.gnu.org/licenses/>.
*
*/
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 = """
<bookmarks>
<bookmark>
<book>
<id>1f88ab6f-c265-b3ff-8f49-b7f442950380</id>
<title>Alpine Linux Wiki</title>
<name>alpinelinux_en_all</name>
<flavour>maxi</flavour>
<language>eng</language>
<date>2023-01-18</date>
</book>
<title>Main Page</title>
<url>https://kiwix.app/A/Main_Page</url>
</bookmark>
<bookmark>
<book>
<id>1f88ab6f-c265-b3ff-8f49-b7f442950380</id>
<title>Alpine Linux Wiki</title>
<name>alpinelinux_en_all</name>
<flavour>maxi</flavour>
<language>eng</language>
<date>2023-01-18</date>
</book>
<title>Installation</title>
<url>https://kiwix.app/A/Installation</url>
</bookmark>
<bookmark>
<book>
<id>04bf4329-9bfb-3681-03e2-cfae7b047f24</id>
<title>Ray Charles</title>
<name>wikipedia_en_ray_charles</name>
<flavour>maxi</flavour>
<language>eng</language>
<date>2024-03-17</date>
</book>
<title>Wikipedia</title>
<url>https://kiwix.app/A/index</url>
</bookmark>
</bookmarks>
""".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<LibkiwixBookmarkItem>
)
}
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)
}
}
}

View File

@ -107,6 +107,12 @@ class KiwixSettingsFragmentTest {
clickClearHistoryPreference() clickClearHistoryPreference()
assertHistoryDialogDisplayed() assertHistoryDialogDisplayed()
dismissDialog() dismissDialog()
clickExportBookmarkPreference()
assertExportBookmarkDialogDisplayed()
dismissDialog()
clickOnImportBookmarkPreference()
assertImportBookmarkDialogDisplayed()
dismissDialog()
clickNightModePreference() clickNightModePreference()
assertNightModeDialogDisplayed() assertNightModeDialogDisplayed()
dismissDialog() dismissDialog()

View File

@ -116,6 +116,22 @@ class SettingsRobot : BaseRobot() {
isVisible(TextId(R.string.clear_all_history_dialog_title)) 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() { fun clickNightModePreference() {
clickRecyclerViewItems(R.string.pref_night_mode) clickRecyclerViewItems(R.string.pref_night_mode)
} }

View File

@ -23,6 +23,7 @@ import io.reactivex.functions.BiFunction
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.kiwix.kiwixmobile.core.dao.FetchDownloadDao 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.downloader.model.DownloadModel
import org.kiwix.kiwixmobile.core.reader.ZimFileReader import org.kiwix.kiwixmobile.core.reader.ZimFileReader
import org.kiwix.kiwixmobile.core.utils.files.FileSearch import org.kiwix.kiwixmobile.core.utils.files.FileSearch
@ -34,7 +35,8 @@ import javax.inject.Inject
class StorageObserver @Inject constructor( class StorageObserver @Inject constructor(
private val downloadDao: FetchDownloadDao, private val downloadDao: FetchDownloadDao,
private val fileSearch: FileSearch, private val fileSearch: FileSearch,
private val zimReaderFactory: ZimFileReader.Factory private val zimReaderFactory: ZimFileReader.Factory,
private val libkiwixBookmarks: LibkiwixBookmarks
) { ) {
fun getBooksOnFileSystem( fun getBooksOnFileSystem(
@ -56,6 +58,12 @@ class StorageObserver @Inject constructor(
private fun convertToBookOnDisk(file: File) = runBlocking { private fun convertToBookOnDisk(file: File) = runBlocking {
zimReaderFactory.create(file) 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()
}
}
} }
} }

View File

@ -19,8 +19,8 @@
package org.kiwix.kiwixmobile.core.dao package org.kiwix.kiwixmobile.core.dao
import android.os.Build import android.os.Build
import android.os.Environment
import android.util.Base64 import android.util.Base64
import org.kiwix.kiwixmobile.core.utils.files.Log
import io.reactivex.BackpressureStrategy import io.reactivex.BackpressureStrategy
import io.reactivex.BackpressureStrategy.LATEST import io.reactivex.BackpressureStrategy.LATEST
import io.reactivex.Flowable import io.reactivex.Flowable
@ -29,23 +29,28 @@ import io.reactivex.subjects.BehaviorSubject
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.extensions.isFileExist 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.adapter.Page
import org.kiwix.kiwixmobile.core.page.bookmark.adapter.LibkiwixBookmarkItem import org.kiwix.kiwixmobile.core.page.bookmark.adapter.LibkiwixBookmarkItem
import org.kiwix.kiwixmobile.core.reader.ILLUSTRATION_SIZE import org.kiwix.kiwixmobile.core.reader.ILLUSTRATION_SIZE
import org.kiwix.kiwixmobile.core.reader.ZimFileReader import org.kiwix.kiwixmobile.core.reader.ZimFileReader
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
import org.kiwix.kiwixmobile.core.utils.files.Log
import org.kiwix.libkiwix.Book import org.kiwix.libkiwix.Book
import org.kiwix.libkiwix.Bookmark import org.kiwix.libkiwix.Bookmark
import org.kiwix.libkiwix.Library import org.kiwix.libkiwix.Library
import org.kiwix.libkiwix.Manager import org.kiwix.libkiwix.Manager
import org.kiwix.libzim.Archive
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
class LibkiwixBookmarks @Inject constructor( class LibkiwixBookmarks @Inject constructor(
val library: Library, val library: Library,
manager: Manager, manager: Manager,
val sharedPreferenceUtil: SharedPreferenceUtil val sharedPreferenceUtil: SharedPreferenceUtil,
private val bookDao: NewBookDao
) : PageDao { ) : PageDao {
/** /**
@ -132,7 +137,13 @@ class LibkiwixBookmarks @Inject constructor(
bookId = libkiwixBookmarkItem.zimId bookId = libkiwixBookmarkItem.zimId
title = libkiwixBookmarkItem.title title = libkiwixBookmarkItem.title
url = libkiwixBookmarkItem.url 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 { library.addBookmark(bookmark).also {
if (shouldWriteBookmarkToFile) { 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?) { private fun addBookToLibraryIfNotExist(libKiwixBook: Book?) {
libKiwixBook?.let { book -> libKiwixBook?.let { book ->
if (!isBookAlreadyExistInLibrary(book.id)) { if (!isBookAlreadyExistInLibrary(book.id)) {
@ -209,7 +240,9 @@ class LibkiwixBookmarks @Inject constructor(
return bookmarkList return bookmarkList
} }
// Retrieve the list of bookmarks from the library, or return an empty list if it's null. // 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. // Create a list to store LibkiwixBookmarkItem objects.
bookmarkList = bookmarkArray.mapNotNull { bookmark -> bookmarkList = bookmarkArray.mapNotNull { bookmark ->
@ -241,7 +274,21 @@ class LibkiwixBookmarks @Inject constructor(
bookmarksChanged = false 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 = private fun isBookMarkExist(libkiwixBookmarkItem: LibkiwixBookmarkItem): Boolean =
@ -273,6 +320,65 @@ class LibkiwixBookmarks @Inject constructor(
bookmarkListBehaviour?.onNext(getBookmarksList()) 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 { companion object {
const val TAG = "LibkiwixBookmark" const val TAG = "LibkiwixBookmark"
} }

View File

@ -21,6 +21,7 @@ import android.content.Context
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import org.kiwix.kiwixmobile.core.dao.LibkiwixBookmarks import org.kiwix.kiwixmobile.core.dao.LibkiwixBookmarks
import org.kiwix.kiwixmobile.core.dao.NewBookDao
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
import org.kiwix.libkiwix.JNIKiwix import org.kiwix.libkiwix.JNIKiwix
import org.kiwix.libkiwix.Library import org.kiwix.libkiwix.Library
@ -45,6 +46,7 @@ class JNIModule {
fun providesLibkiwixBookmarks( fun providesLibkiwixBookmarks(
library: Library, library: Library,
manager: Manager, manager: Manager,
sharedPreferenceUtil: SharedPreferenceUtil sharedPreferenceUtil: SharedPreferenceUtil,
): LibkiwixBookmarks = LibkiwixBookmarks(library, manager, sharedPreferenceUtil) bookDao: NewBookDao
): LibkiwixBookmarks = LibkiwixBookmarks(library, manager, sharedPreferenceUtil, bookDao)
} }

View File

@ -20,14 +20,18 @@ package org.kiwix.kiwixmobile.core.settings
import android.Manifest import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity.RESULT_OK import android.app.Activity.RESULT_OK
import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.SharedPreferences.OnSharedPreferenceChangeListener import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.webkit.WebView import android.webkit.WebView
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.preference.EditTextPreference 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.R
import org.kiwix.kiwixmobile.core.compat.CompatHelper.Companion.getPackageInformation import org.kiwix.kiwixmobile.core.compat.CompatHelper.Companion.getPackageInformation
import org.kiwix.kiwixmobile.core.compat.CompatHelper.Companion.getVersionCode 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.AddNoteDialog
import org.kiwix.kiwixmobile.core.main.CoreMainActivity 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.EXTERNAL_SELECT_POSITION
import org.kiwix.kiwixmobile.core.utils.INTERNAL_SELECT_POSITION import org.kiwix.kiwixmobile.core.utils.INTERNAL_SELECT_POSITION
import org.kiwix.kiwixmobile.core.utils.LanguageUtils 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.dialog.KiwixDialog.SelectFolder
import org.kiwix.kiwixmobile.core.utils.files.FileUtils.getPathFromUri import org.kiwix.kiwixmobile.core.utils.files.FileUtils.getPathFromUri
import java.io.File import java.io.File
import java.io.InputStream
import java.util.Locale import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
import javax.xml.parsers.DocumentBuilderFactory
abstract class CorePrefsFragment : abstract class CorePrefsFragment :
PreferenceFragmentCompat(), PreferenceFragmentCompat(),
@ -83,6 +92,10 @@ abstract class CorePrefsFragment :
@JvmField @JvmField
@Inject @Inject
protected var alertDialogShower: DialogShower? = null protected var alertDialogShower: DialogShower? = null
@JvmField
@Inject
internal var libkiwixBookmarks: LibkiwixBookmarks? = null
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
coreComponent coreComponent
.activityComponentBuilder() .activityComponentBuilder()
@ -131,6 +144,8 @@ abstract class CorePrefsFragment :
override fun onDestroyView() { override fun onDestroyView() {
presenter?.dispose() presenter?.dispose()
storagePermissionForNotesLauncher?.unregister()
storagePermissionForNotesLauncher = null
super.onDestroyView() super.onDestroyView()
} }
@ -289,9 +304,149 @@ abstract class CorePrefsFragment :
if (preference.key.equals(SharedPreferenceUtil.PREF_STORAGE, ignoreCase = true)) { if (preference.key.equals(SharedPreferenceUtil.PREF_STORAGE, ignoreCase = true)) {
openFolderSelect() openFolderSelect()
} }
if (preference.key.equals(PREF_EXPORT_BOOKMARK, ignoreCase = true) &&
requestExternalStorageWritePermissionForExportBookmark()
) {
showExportBookmarkDialog()
}
if (preference.key.equals(PREF_IMPORT_BOOKMARK, ignoreCase = true)) {
showImportBookmarkDialog()
}
return true 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<String>? =
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() { private fun openFolderSelect() {
val dialogFragment = StorageSelectDialog() val dialogFragment = StorageSelectDialog()
dialogFragment.onSelectAction = dialogFragment.onSelectAction =
@ -383,5 +538,7 @@ abstract class CorePrefsFragment :
private const val ZOOM_OFFSET = 2 private const val ZOOM_OFFSET = 2
private const val ZOOM_SCALE = 25 private const val ZOOM_SCALE = 25
private const val INTERNAL_TEXT_ZOOM = "text_zoom" 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"
} }
} }

View File

@ -272,6 +272,11 @@ sealed class KiwixDialog(
object OpenInNewTab : YesNoDialog( object OpenInNewTab : YesNoDialog(
null, R.string.open_in_new_tab 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( object StorageConfigure : KiwixDialog(
@ -282,6 +287,13 @@ sealed class KiwixDialog(
icon = R.drawable.ic_baseline_storage_24 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( object DeleteSelectedHistory : KiwixDialog(
R.string.delete_selected_history, R.string.delete_selected_history,
null, null,

View File

@ -68,6 +68,7 @@
<string name="tts_lang_not_supported">The language of this page is not supported. The article may not be properly read.</string> <string name="tts_lang_not_supported">The language of this page is not supported. The article may not be properly read.</string>
<string name="no_reader_application_installed">Could not find an installed application for this type of file</string> <string name="no_reader_application_installed">Could not find an installed application for this type of file</string>
<string name="no_email_application_installed">Please install an email service provider or email us at %1s</string> <string name="no_email_application_installed">Please install an email service provider or email us at %1s</string>
<string name="no_app_found_to_select_bookmark_file">No app found to select a bookmark file</string>
<string name="no_section_info">No Content Headers Found</string> <string name="no_section_info">No Content Headers Found</string>
<string name="request_storage">To access offline content we need access to your storage</string> <string name="request_storage">To access offline content we need access to your storage</string>
<string name="request_write_storage">To download zim files we need write access to your storage</string> <string name="request_write_storage">To download zim files we need write access to your storage</string>
@ -77,6 +78,11 @@
<string name="pref_clear_all_history_summary">Clear recent searches and tabs history</string> <string name="pref_clear_all_history_summary">Clear recent searches and tabs history</string>
<string name="pref_notes">Notes</string> <string name="pref_notes">Notes</string>
<string name="pref_permission">Permissions</string> <string name="pref_permission">Permissions</string>
<string name="pref_import_bookmark_title">Import Bookmarks</string>
<string name="pref_import_bookmark_summary">Import the saved bookmarks</string>
<string name="pref_export_bookmark_title">Export Bookmarks</string>
<string name="pref_export_bookmark_summary">Export all saved bookmarks</string>
<string name="export_bookmark_saved">Bookmarks are exported in %s file</string>
<string name="all_history_cleared">All History Cleared</string> <string name="all_history_cleared">All History Cleared</string>
<string name="pref_clear_all_bookmarks_title">Clear bookmarks</string> <string name="pref_clear_all_bookmarks_title">Clear bookmarks</string>
<string name="clear_all_history_dialog_title">Clear All History?</string> <string name="clear_all_history_dialog_title">Clear All History?</string>
@ -226,6 +232,9 @@
<string name="save">Save</string> <string name="save">Save</string>
<string name="note">Note</string> <string name="note">Note</string>
<string name="wiki_article_title">Wiki Article Title</string> <string name="wiki_article_title">Wiki Article Title</string>
<string name="ext_storage_permission_rationale_export_bookmark">Storage access is required for exporting Bookmarks</string>
<string name="ext_storage_write_permission_denied_export_bookmark">Bookmarks cant export without access of storage</string>
<string name="export_bookmark_error">An error occurred when trying to export bookmark!</string>
<string name="ext_storage_permission_rationale_add_note">Storage access is required for Notes</string> <string name="ext_storage_permission_rationale_add_note">Storage access is required for Notes</string>
<string name="ext_storage_write_permission_denied_add_note">Notes can\t be used without access to storage</string> <string name="ext_storage_write_permission_denied_add_note">Notes can\t be used without access to storage</string>
<string name="note_save_unsuccessful">Note save unsuccessful</string> <string name="note_save_unsuccessful">Note save unsuccessful</string>
@ -288,6 +297,11 @@
<string name="delete_selected_history">Delete Selected History?</string> <string name="delete_selected_history">Delete Selected History?</string>
<string name="delete_bookmarks">Delete All Bookmarks?</string> <string name="delete_bookmarks">Delete All Bookmarks?</string>
<string name="delete_selected_bookmarks">Delete Selected Bookmarks?</string> <string name="delete_selected_bookmarks">Delete Selected Bookmarks?</string>
<string name="import_bookmarks_dialog_title">Import Bookmarks?</string>
<string name="export_all_bookmarks_dialog_title">Export All Bookmarks?</string>
<string name="export_all_bookmarks_dialog_message">Exporting will generate bookmark.xml file in the Download/org.kiwix…/ folder.</string>
<string name="bookmark_imported_message">All the bookmarks has been imported.</string>
<string name="error_invalid_bookmark_file">Error: The selected file is not a valid bookmark file.</string>
<string name="on">On</string> <string name="on">On</string>
<string name="off">Off</string> <string name="off">Off</string>
<string name="auto">Auto</string> <string name="auto">Auto</string>

View File

@ -95,6 +95,25 @@
android:title="@string/pref_clear_all_notes_title" android:title="@string/pref_clear_all_notes_title"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
</PreferenceCategory>
<PreferenceCategory
android:key="pref_notes"
android:title="@string/bookmarks"
app:iconSpaceReserved="false">
<Preference
android:key="pref_import_bookmark"
android:summary="@string/pref_import_bookmark_summary"
android:title="@string/pref_import_bookmark_title"
app:iconSpaceReserved="false" />
<Preference
android:key="pref_export_bookmark"
android:summary="@string/pref_export_bookmark_summary"
android:title="@string/pref_export_bookmark_title"
app:iconSpaceReserved="false" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory <PreferenceCategory
android:key="pref_permissions" android:key="pref_permissions"

View File

@ -29,6 +29,7 @@ import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.kiwix.kiwixmobile.core.dao.FetchDownloadDao 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.downloader.model.DownloadModel
import org.kiwix.kiwixmobile.core.reader.ZimFileReader import org.kiwix.kiwixmobile.core.reader.ZimFileReader
import org.kiwix.kiwixmobile.core.reader.ZimFileReader.Factory import org.kiwix.kiwixmobile.core.reader.ZimFileReader.Factory
@ -50,6 +51,7 @@ class StorageObserverTest {
private val file: File = mockk() private val file: File = mockk()
private val readerFactory: Factory = mockk() private val readerFactory: Factory = mockk()
private val zimFileReader: ZimFileReader = mockk() private val zimFileReader: ZimFileReader = mockk()
private val libkiwixBookmarks: LibkiwixBookmarks = mockk()
private val scanningProgressListener: ScanningProgressListener = mockk() private val scanningProgressListener: ScanningProgressListener = mockk()
private val files: PublishProcessor<List<File>> = PublishProcessor.create() private val files: PublishProcessor<List<File>> = PublishProcessor.create()
@ -71,8 +73,9 @@ class StorageObserverTest {
every { sharedPreferenceUtil.prefStorage } returns "a" every { sharedPreferenceUtil.prefStorage } returns "a"
every { fileSearch.scan(scanningProgressListener) } returns files every { fileSearch.scan(scanningProgressListener) } returns files
every { downloadDao.downloads() } returns downloads every { downloadDao.downloads() } returns downloads
every { zimFileReader.jniKiwixReader } returns mockk()
every { runBlocking { readerFactory.create(file) } } returns zimFileReader every { runBlocking { readerFactory.create(file) } } returns zimFileReader
storageObserver = StorageObserver(downloadDao, fileSearch, readerFactory) storageObserver = StorageObserver(downloadDao, fileSearch, readerFactory, libkiwixBookmarks)
} }
@Test @Test