Saving and Restoring the Web View Navigation History Across Sessions
This commit is contained in:
Kelson 2025-02-08 09:17:43 +01:00 committed by GitHub
commit 51aca061b9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 651 additions and 170 deletions

View File

@ -55,6 +55,7 @@ import org.kiwix.kiwixmobile.core.main.RestoreOrigin
import org.kiwix.kiwixmobile.core.main.RestoreOrigin.FromExternalLaunch
import org.kiwix.kiwixmobile.core.main.RestoreOrigin.FromSearchScreen
import org.kiwix.kiwixmobile.core.main.ToolbarScrollingKiwixWebView
import org.kiwix.kiwixmobile.core.page.history.adapter.WebViewHistoryItem
import org.kiwix.kiwixmobile.core.reader.ZimReaderSource
import org.kiwix.kiwixmobile.core.reader.ZimReaderSource.Companion.fromDatabaseValue
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
@ -242,40 +243,40 @@ class KiwixReaderFragment : CoreReaderFragment() {
}
}
override fun restoreViewStateOnInvalidJSON() {
override fun restoreViewStateOnInvalidWebViewHistory() {
Log.d(TAG_KIWIX, "Kiwix normal start, no zimFile loaded last time -> display home page")
exitBook()
}
/**
* Restores the view state based on the provided JSON data and restore origin.
* Restores the view state based on the provided webViewHistoryItemList data and restore origin.
*
* Depending on the `restoreOrigin`, this method either restores the last opened ZIM file
* (if the launch is external) or skips re-opening the ZIM file when coming from the search screen,
* as the ZIM file is already set in the reader. The method handles setting up the ZIM file and bookmarks,
* and restores the tabs and positions from the provided data.
*
* @param zimArticles JSON string representing the list of articles to be restored.
* @param zimPositions JSON string representing the positions of the restored articles.
* @param webViewHistoryItemList WebViewHistoryItem list representing the list of articles to be restored.
* @param currentTab Index of the tab to be restored as the currently active one.
* @param restoreOrigin Indicates whether the restoration is triggered from an external launch or the search screen.
* @param onComplete Callback to be invoked upon completion of the restoration process.
*/
override fun restoreViewStateOnValidJSON(
zimArticles: String?,
zimPositions: String?,
override fun restoreViewStateOnValidWebViewHistory(
webViewHistoryItemList: List<WebViewHistoryItem>,
currentTab: Int,
restoreOrigin: RestoreOrigin
restoreOrigin: RestoreOrigin,
onComplete: () -> Unit
) {
when (restoreOrigin) {
FromExternalLaunch -> {
coreReaderLifeCycleScope?.launch {
if (!isAdded) return@launch
val settings =
requireActivity().getSharedPreferences(SharedPreferenceUtil.PREF_KIWIX_MOBILE, 0)
val zimReaderSource = fromDatabaseValue(settings.getString(TAG_CURRENT_FILE, null))
activity?.getSharedPreferences(SharedPreferenceUtil.PREF_KIWIX_MOBILE, 0)
val zimReaderSource = fromDatabaseValue(settings?.getString(TAG_CURRENT_FILE, null))
if (zimReaderSource?.canOpenInLibkiwix() == true) {
if (zimReaderContainer?.zimReaderSource == null) {
openZimFile(zimReaderSource)
openZimFile(zimReaderSource, isFromManageExternalLaunch = true)
Log.d(
TAG_KIWIX,
"Kiwix normal start, Opened last used zimFile: -> ${zimReaderSource.toDatabase()}"
@ -283,7 +284,7 @@ class KiwixReaderFragment : CoreReaderFragment() {
} else {
zimReaderContainer?.zimFileReader?.let(::setUpBookmarks)
}
restoreTabs(zimArticles, zimPositions, currentTab)
restoreTabs(webViewHistoryItemList, currentTab, onComplete)
} else {
getCurrentWebView()?.snack(string.zim_not_opened)
exitBook() // hide the options for zim file to avoid unexpected UI behavior
@ -292,7 +293,7 @@ class KiwixReaderFragment : CoreReaderFragment() {
}
FromSearchScreen -> {
restoreTabs(zimArticles, zimPositions, currentTab)
restoreTabs(webViewHistoryItemList, currentTab, onComplete)
}
}
}

View File

@ -12,7 +12,7 @@
<ID>LongParameterList:MainMenu.kt$MainMenu$( private val activity: Activity, zimFileReader: ZimFileReader?, menu: Menu, webViews: MutableList&lt;KiwixWebView>, urlIsValid: Boolean, disableReadAloud: Boolean = false, disableTabs: Boolean = false, private val menuClickListener: MenuClickListener )</ID>
<ID>LongParameterList:MainMenu.kt$MainMenu.Factory$( menu: Menu, webViews: MutableList&lt;KiwixWebView>, urlIsValid: Boolean, menuClickListener: MenuClickListener, disableReadAloud: Boolean, disableTabs: Boolean )</ID>
<ID>LongParameterList:PageTestHelpers.kt$( bookmarkTitle: String = "bookmarkTitle", isSelected: Boolean = false, id: Long = 2, zimId: String = "zimId", zimName: String = "zimName", zimFilePath: String = "zimFilePath", bookmarkUrl: String = "bookmarkUrl", favicon: String = "favicon" )</ID>
<ID>LongParameterList:Repository.kt$Repository$( @param:IO private val ioThread: Scheduler, @param:MainThread private val mainThread: Scheduler, private val bookDao: NewBookDao, private val libkiwixBookmarks: LibkiwixBookmarks, private val historyRoomDao: HistoryRoomDao, private val notesRoomDao: NotesRoomDao, private val languageDao: NewLanguagesDao, private val recentSearchRoomDao: RecentSearchRoomDao, private val zimReaderContainer: ZimReaderContainer )</ID>
<ID>LongParameterList:Repository.kt$Repository$( @param:IO private val ioThread: Scheduler, @param:MainThread private val mainThread: Scheduler, private val bookDao: NewBookDao, private val libkiwixBookmarks: LibkiwixBookmarks, private val historyRoomDao: HistoryRoomDao, private val webViewHistoryRoomDao: WebViewHistoryRoomDao, private val notesRoomDao: NotesRoomDao, private val languageDao: NewLanguagesDao, private val recentSearchRoomDao: RecentSearchRoomDao, private val zimReaderContainer: ZimReaderContainer )</ID>
<ID>LongParameterList:ToolbarScrollingKiwixWebView.kt$ToolbarScrollingKiwixWebView$( context: Context, callback: WebViewCallback, attrs: AttributeSet, nonVideoView: ViewGroup, videoView: ViewGroup, webViewClient: CoreWebViewClient, private val toolbarView: View, private val bottomBarView: View, sharedPreferenceUtil: SharedPreferenceUtil, private val parentNavigationBar: View? = null )</ID>
<ID>MagicNumber:ArticleCount.kt$ArticleCount$3</ID>
<ID>MagicNumber:CompatFindActionModeCallback.kt$CompatFindActionModeCallback$100</ID>

View File

@ -69,7 +69,7 @@ abstract class HistoryRoomDao : PageDao {
historyItem.dateString
)?.let {
it.apply {
// update the exiting entity
// update the existing entity
historyUrl = historyItem.historyUrl
historyTitle = historyItem.title
timeStamp = historyItem.timeStamp

View File

@ -0,0 +1,49 @@
/*
* 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.core.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import io.reactivex.Flowable
import org.kiwix.kiwixmobile.core.dao.entities.WebViewHistoryEntity
@Dao
abstract class WebViewHistoryRoomDao {
fun insertWebViewPageHistoryItem(webViewHistoryEntity: WebViewHistoryEntity) {
insertWebViewPageHistoryItems(listOf(webViewHistoryEntity))
}
@Insert
abstract fun insertWebViewPageHistoryItems(webViewHistoryEntityList: List<WebViewHistoryEntity>)
@Query("SELECT * FROM WebViewHistoryEntity ORDER BY webViewIndex ASC")
abstract fun getAllWebViewPagesHistory(): Flowable<List<WebViewHistoryEntity>>
@Query("Delete from WebViewHistoryEntity")
abstract fun clearWebViewPagesHistory()
fun clearPageHistoryWithPrimaryKey() {
clearWebViewPagesHistory()
}
@Query("DELETE FROM sqlite_sequence WHERE name='PageHistoryRoomEntity'")
abstract fun resetPrimaryKey()
}

View File

@ -0,0 +1,68 @@
/*
* 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.core.dao.entities
import android.os.Bundle
import android.os.Parcel
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.TypeConverter
import androidx.room.TypeConverters
import org.kiwix.kiwixmobile.core.page.history.adapter.WebViewHistoryItem
@Entity
data class WebViewHistoryEntity(
@PrimaryKey(autoGenerate = true) var id: Long = 0L,
val zimId: String,
val webViewIndex: Int,
val webViewCurrentPosition: Int,
@TypeConverters(BundleRoomConverter::class)
val webViewBackForwardListBundle: Bundle?
) {
constructor(webViewHistoryItem: WebViewHistoryItem) : this(
webViewHistoryItem.databaseId,
webViewHistoryItem.zimId,
webViewHistoryItem.webViewIndex,
webViewHistoryItem.webViewCurrentPosition,
webViewHistoryItem.webViewBackForwardListBundle,
)
}
class BundleRoomConverter {
@TypeConverter
fun convertToDatabaseValue(bundle: Bundle?): ByteArray? {
if (bundle == null) return null
val parcel = Parcel.obtain()
parcel.writeBundle(bundle)
val bytes = parcel.marshall()
parcel.recycle()
return bytes
}
@TypeConverter
fun convertToEntityProperty(byteArray: ByteArray?): Bundle? {
if (byteArray == null) return null
val parcel = Parcel.obtain()
parcel.unmarshall(byteArray, 0, byteArray.size)
parcel.setDataPosition(0)
val bundle = parcel.readBundle(Bundle::class.java.classLoader)
parcel.recycle()
return bundle
}
}

View File

@ -20,6 +20,7 @@ package org.kiwix.kiwixmobile.core.data
import io.reactivex.Completable
import io.reactivex.Flowable
import io.reactivex.Single
import org.kiwix.kiwixmobile.core.dao.entities.WebViewHistoryEntity
import org.kiwix.kiwixmobile.core.page.bookmark.adapter.LibkiwixBookmarkItem
import org.kiwix.kiwixmobile.core.page.history.adapter.HistoryListItem
import org.kiwix.kiwixmobile.core.page.history.adapter.HistoryListItem.HistoryItem
@ -53,4 +54,8 @@ interface DataSource {
fun saveNote(noteListItem: NoteListItem): Completable
fun deleteNote(noteTitle: String): Completable
fun deleteNotes(noteList: List<NoteListItem>): Completable
suspend fun insertWebViewPageHistoryItems(webViewHistoryEntityList: List<WebViewHistoryEntity>)
fun getAllWebViewPagesHistory(): Single<List<WebViewHistoryEntity>>
suspend fun clearWebViewPagesHistory()
}

View File

@ -30,10 +30,13 @@ import org.kiwix.kiwixmobile.core.dao.HistoryRoomDao
import org.kiwix.kiwixmobile.core.dao.HistoryRoomDaoCoverts
import org.kiwix.kiwixmobile.core.dao.NotesRoomDao
import org.kiwix.kiwixmobile.core.dao.RecentSearchRoomDao
import org.kiwix.kiwixmobile.core.dao.WebViewHistoryRoomDao
import org.kiwix.kiwixmobile.core.dao.entities.BundleRoomConverter
import org.kiwix.kiwixmobile.core.dao.entities.DownloadRoomEntity
import org.kiwix.kiwixmobile.core.dao.entities.HistoryRoomEntity
import org.kiwix.kiwixmobile.core.dao.entities.NotesRoomEntity
import org.kiwix.kiwixmobile.core.dao.entities.RecentSearchRoomEntity
import org.kiwix.kiwixmobile.core.dao.entities.WebViewHistoryEntity
import org.kiwix.kiwixmobile.core.dao.entities.ZimSourceRoomConverter
@Suppress("UnnecessaryAbstractClass")
@ -42,17 +45,23 @@ import org.kiwix.kiwixmobile.core.dao.entities.ZimSourceRoomConverter
RecentSearchRoomEntity::class,
HistoryRoomEntity::class,
NotesRoomEntity::class,
DownloadRoomEntity::class
DownloadRoomEntity::class,
WebViewHistoryEntity::class
],
version = 7,
version = 8,
exportSchema = false
)
@TypeConverters(HistoryRoomDaoCoverts::class, ZimSourceRoomConverter::class)
@TypeConverters(
HistoryRoomDaoCoverts::class,
ZimSourceRoomConverter::class,
BundleRoomConverter::class
)
abstract class KiwixRoomDatabase : RoomDatabase() {
abstract fun recentSearchRoomDao(): RecentSearchRoomDao
abstract fun historyRoomDao(): HistoryRoomDao
abstract fun notesRoomDao(): NotesRoomDao
abstract fun downloadRoomDao(): DownloadRoomDao
abstract fun webViewHistoryRoomDao(): WebViewHistoryRoomDao
companion object {
private var db: KiwixRoomDatabase? = null
@ -68,7 +77,8 @@ abstract class KiwixRoomDatabase : RoomDatabase() {
MIGRATION_3_4,
MIGRATION_4_5,
MIGRATION_5_6,
MIGRATION_6_7
MIGRATION_6_7,
MIGRATION_7_8
)
.build().also { db = it }
}
@ -271,6 +281,23 @@ abstract class KiwixRoomDatabase : RoomDatabase() {
}
}
@Suppress("MagicNumber")
private val MIGRATION_7_8 = object : Migration(7, 8) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS `WebViewHistoryEntity` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
`zimId` TEXT NOT NULL,
`webViewIndex` INTEGER NOT NULL,
`webViewCurrentPosition` INTEGER NOT NULL,
`webViewBackForwardListBundle` BLOB NULL
)
"""
)
}
}
fun destroyInstance() {
db = null
}

View File

@ -29,6 +29,8 @@ import org.kiwix.kiwixmobile.core.dao.NewBookDao
import org.kiwix.kiwixmobile.core.dao.NewLanguagesDao
import org.kiwix.kiwixmobile.core.dao.NotesRoomDao
import org.kiwix.kiwixmobile.core.dao.RecentSearchRoomDao
import org.kiwix.kiwixmobile.core.dao.WebViewHistoryRoomDao
import org.kiwix.kiwixmobile.core.dao.entities.WebViewHistoryEntity
import org.kiwix.kiwixmobile.core.di.qualifiers.IO
import org.kiwix.kiwixmobile.core.di.qualifiers.MainThread
import org.kiwix.kiwixmobile.core.extensions.HeaderizableList
@ -55,6 +57,7 @@ class Repository @Inject internal constructor(
private val bookDao: NewBookDao,
private val libkiwixBookmarks: LibkiwixBookmarks,
private val historyRoomDao: HistoryRoomDao,
private val webViewHistoryRoomDao: WebViewHistoryRoomDao,
private val notesRoomDao: NotesRoomDao,
private val languageDao: NewLanguagesDao,
private val recentSearchRoomDao: RecentSearchRoomDao,
@ -144,6 +147,22 @@ class Repository @Inject internal constructor(
Completable.fromAction { notesRoomDao.deleteNotes(noteList) }
.subscribeOn(ioThread)
override suspend fun insertWebViewPageHistoryItems(
webViewHistoryEntityList: List<WebViewHistoryEntity>
) {
webViewHistoryRoomDao.insertWebViewPageHistoryItems(webViewHistoryEntityList)
}
override fun getAllWebViewPagesHistory() =
webViewHistoryRoomDao.getAllWebViewPagesHistory()
.first(emptyList())
.subscribeOn(ioThread)
.observeOn(mainThread)
override suspend fun clearWebViewPagesHistory() {
webViewHistoryRoomDao.clearWebViewPagesHistory()
}
override fun deleteNote(noteTitle: String): Completable =
Completable.fromAction { notesRoomDao.deleteNote(noteTitle) }
.subscribeOn(ioThread)

View File

@ -37,6 +37,7 @@ import org.kiwix.kiwixmobile.core.dao.NewLanguagesDao
import org.kiwix.kiwixmobile.core.dao.NewNoteDao
import org.kiwix.kiwixmobile.core.dao.NewRecentSearchDao
import org.kiwix.kiwixmobile.core.dao.NotesRoomDao
import org.kiwix.kiwixmobile.core.dao.WebViewHistoryRoomDao
import org.kiwix.kiwixmobile.core.dao.RecentSearchRoomDao
import org.kiwix.kiwixmobile.core.data.DataModule
import org.kiwix.kiwixmobile.core.data.DataSource
@ -105,6 +106,7 @@ interface CoreComponent {
fun libkiwixBookmarks(): LibkiwixBookmarks
fun recentSearchRoomDao(): RecentSearchRoomDao
fun historyRoomDao(): HistoryRoomDao
fun webViewHistoryRoomDao(): WebViewHistoryRoomDao
fun noteRoomDao(): NotesRoomDao
fun objectBoxToRoomMigrator(): ObjectBoxToRoomMigrator
fun context(): Context

View File

@ -86,6 +86,10 @@ open class DatabaseModule {
@Singleton
fun provideHistoryDao(db: KiwixRoomDatabase) = db.historyRoomDao()
@Provides
@Singleton
fun provideWebViewHistoryRoomDao(db: KiwixRoomDatabase) = db.webViewHistoryRoomDao()
@Singleton
@Provides
fun provideNoteRoomDao(db: KiwixRoomDatabase) = db.notesRoomDao()

View File

@ -40,6 +40,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.NavDirections
import androidx.navigation.NavOptions
import com.google.android.material.navigation.NavigationView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -393,6 +394,10 @@ abstract class CoreMainActivity : BaseActivity(), WebViewProvider {
navController.navigate(fragmentId, bundle)
}
fun navigate(fragmentId: Int, bundle: Bundle, navOptions: NavOptions) {
navController.navigate(fragmentId, bundle, navOptions)
}
private fun openSettings() {
handleDrawerOnNavigation()
navigate(settingsFragmentResId)
@ -435,13 +440,18 @@ abstract class CoreMainActivity : BaseActivity(), WebViewProvider {
if (zimReaderSource != null) {
zimFileUri = zimReaderSource.toDatabase()
}
val navOptions = NavOptions.Builder()
.setLaunchSingleTop(true)
.setPopUpTo(readerFragmentResId, inclusive = true)
.build()
navigate(
readerFragmentResId,
bundleOf(
PAGE_URL_KEY to pageUrl,
ZIM_FILE_URI_KEY to zimFileUri,
SHOULD_OPEN_IN_NEW_TAB to shouldOpenInNewTab
)
),
navOptions
)
}

View File

@ -99,15 +99,18 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import org.json.JSONArray
import org.json.JSONException
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.kiwix.kiwixmobile.core.BuildConfig
import org.kiwix.kiwixmobile.core.CoreApp
import org.kiwix.kiwixmobile.core.DarkModeConfig
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.StorageObserver
import org.kiwix.kiwixmobile.core.base.BaseFragment
import org.kiwix.kiwixmobile.core.base.FragmentActivityExtensions
import org.kiwix.kiwixmobile.core.dao.LibkiwixBookmarks
import org.kiwix.kiwixmobile.core.dao.entities.WebViewHistoryEntity
import org.kiwix.kiwixmobile.core.databinding.FragmentReaderBinding
import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.consumeObservable
@ -135,6 +138,7 @@ import org.kiwix.kiwixmobile.core.page.history.NavigationHistoryClickListener
import org.kiwix.kiwixmobile.core.page.history.NavigationHistoryDialog
import org.kiwix.kiwixmobile.core.page.history.adapter.HistoryListItem.HistoryItem
import org.kiwix.kiwixmobile.core.page.history.adapter.NavigationHistoryListItem
import org.kiwix.kiwixmobile.core.page.history.adapter.WebViewHistoryItem
import org.kiwix.kiwixmobile.core.read_aloud.ReadAloudCallbacks
import org.kiwix.kiwixmobile.core.read_aloud.ReadAloudService
import org.kiwix.kiwixmobile.core.read_aloud.ReadAloudService.Companion.ACTION_PAUSE_OR_RESUME_TTS
@ -157,14 +161,11 @@ import org.kiwix.kiwixmobile.core.utils.REQUEST_POST_NOTIFICATION_PERMISSION
import org.kiwix.kiwixmobile.core.utils.REQUEST_STORAGE_PERMISSION
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
import org.kiwix.kiwixmobile.core.utils.StyleUtils.getAttributes
import org.kiwix.kiwixmobile.core.utils.TAG_CURRENT_ARTICLES
import org.kiwix.kiwixmobile.core.utils.TAG_CURRENT_FILE
import org.kiwix.kiwixmobile.core.utils.TAG_CURRENT_POSITIONS
import org.kiwix.kiwixmobile.core.utils.TAG_CURRENT_TAB
import org.kiwix.kiwixmobile.core.utils.TAG_FILE_SEARCHED
import org.kiwix.kiwixmobile.core.utils.TAG_FILE_SEARCHED_NEW_TAB
import org.kiwix.kiwixmobile.core.utils.TAG_KIWIX
import org.kiwix.kiwixmobile.core.utils.UpdateUtils.reformatProviderUrl
import org.kiwix.kiwixmobile.core.utils.dialog.DialogShower
import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog
import org.kiwix.kiwixmobile.core.utils.dialog.UnsupportedMimeTypeHandler
@ -286,6 +287,10 @@ abstract class CoreReaderFragment :
private var bottomToolbarToc: ImageView? = null
private var isFirstTimeMainPageLoaded = true
private var isFromManageExternalLaunch = false
private val savingTabsMutex = Mutex()
private var searchItemToOpen: SearchItemToOpen? = null
private var findInPageTitle: String? = null
@JvmField
@Inject
@ -505,6 +510,16 @@ abstract class CoreReaderFragment :
readAloudService?.registerCallBack(this@CoreReaderFragment)
}
}
requireActivity().observeNavigationResult<String>(
FIND_IN_PAGE_SEARCH_STRING,
viewLifecycleOwner,
Observer(::storeFindInPageTitle)
)
requireActivity().observeNavigationResult<SearchItemToOpen>(
TAG_FILE_SEARCHED,
viewLifecycleOwner,
Observer(::storeSearchItem)
)
handleClicks()
}
@ -988,6 +1003,9 @@ abstract class CoreReaderFragment :
override fun clearHistory() {
getCurrentWebView()?.clearHistory()
CoroutineScope(Dispatchers.IO).launch {
repositoryActions?.clearWebViewPageHistory()
}
updateBottomToolbarArrowsAlpha()
toast(R.string.navigation_history_cleared)
}
@ -1303,7 +1321,16 @@ abstract class CoreReaderFragment :
}
}
private fun initalizeWebView(url: String): KiwixWebView? {
/**
* Initializes a new instance of `KiwixWebView` with the specified URL.
*
* @param url The URL to load in the web view. This is ignored if `shouldLoadUrl` is false.
* @param shouldLoadUrl A flag indicating whether to load the specified URL in the web view.
* When restoring tabs, this should be set to false to avoid loading
* an extra page, as the previous web view history will be restored directly.
* @return The initialized `KiwixWebView` instance, or null if initialization fails.
*/
private fun initalizeWebView(url: String, shouldLoadUrl: Boolean = true): KiwixWebView? {
if (isAdded) {
val attrs = requireActivity().getAttributes(R.xml.webview)
val webView: KiwixWebView? = try {
@ -1316,7 +1343,9 @@ abstract class CoreReaderFragment :
null
}
webView?.let {
if (shouldLoadUrl) {
loadUrl(url, it)
}
setUpWithTextToSpeech(it)
documentParser?.initInterface(it)
ServiceWorkerUninitialiser(::openMainPage).initInterface(it)
@ -1349,8 +1378,23 @@ abstract class CoreReaderFragment :
newTab(url, false)
}
private fun newTab(url: String, selectTab: Boolean = true): KiwixWebView? {
val webView = initalizeWebView(url)
/**
* Creates a new instance of `KiwixWebView` and adds it to the list of web views.
*
* @param url The URL to load in the newly created web view.
* @param selectTab A flag indicating whether to select the newly created tab immediately.
* Defaults to true, which means the new tab will be selected.
* @param shouldLoadUrl A flag indicating whether to load the specified URL in the web view.
* If set to false, the web view will be created without loading the URL,
* which is useful when restoring tabs.
* @return The newly created `KiwixWebView` instance, or null if the initialization fails.
*/
private fun newTab(
url: String,
selectTab: Boolean = true,
shouldLoadUrl: Boolean = true
): KiwixWebView? {
val webView = initalizeWebView(url, shouldLoadUrl)
webView?.let {
webViewList.add(it)
if (selectTab) {
@ -1520,6 +1564,14 @@ abstract class CoreReaderFragment :
}
}
override fun onSearchMenuClickedMenuClicked() {
saveTabStates {
// Pass this function to saveTabStates so that after saving
// the tab state in the database, it will open the search fragment.
openSearch("", isOpenedFromTabView = isInTabSwitcher, false)
}
}
@Suppress("NestedBlockDepth")
override fun onReadAloudMenuClicked() {
if (requireActivity().hasNotificationPermission(sharedPreferenceUtil)) {
@ -1705,7 +1757,12 @@ abstract class CoreReaderFragment :
)
}
suspend fun openZimFile(zimReaderSource: ZimReaderSource, isCustomApp: Boolean = false) {
suspend fun openZimFile(
zimReaderSource: ZimReaderSource,
isCustomApp: Boolean = false,
isFromManageExternalLaunch: Boolean = false
) {
this.isFromManageExternalLaunch = isFromManageExternalLaunch
if (hasPermission(Manifest.permission.READ_EXTERNAL_STORAGE) || isCustomApp) {
if (zimReaderSource.canOpenInLibkiwix()) {
// Show content if there is `Open Library` button showing
@ -1751,7 +1808,9 @@ abstract class CoreReaderFragment :
val zimFileReader = zimReaderContainer.zimFileReader
zimFileReader?.let { zimFileReader ->
// uninitialized the service worker to fix https://github.com/kiwix/kiwix-android/issues/2561
if (!isFromManageExternalLaunch) {
openArticle(UNINITIALISER_ADDRESS)
}
mainMenu?.onFileOpened(urlIsValid())
setUpBookmarks(zimFileReader)
} ?: kotlin.run {
@ -2087,6 +2146,27 @@ abstract class CoreReaderFragment :
openSearch("", isOpenedFromTabView = false, isVoice)
}
/**
* Stores the specified search item to be opened later.
*
* This method saves the provided `SearchItemToOpen` object, which will be used to
* open the searched item after the tabs have been restored.
*
* @param item The search item to be opened after restoring the tabs.
*/
private fun storeSearchItem(item: SearchItemToOpen) {
searchItemToOpen = item
}
/**
* Opens a search item based on its properties.
*
* If the item should open in a new tab, a new tab is created.
*
* The method attempts to load the page URL directly. If the page URL is not available,
* it attempts to convert the page title to a URL using the ZIM reader container. The
* resulting URL is then loaded in the current web view.
*/
private fun openSearchItem(item: SearchItemToOpen) {
if (item.shouldOpenInNewTab) {
createNewTab()
@ -2285,6 +2365,23 @@ abstract class CoreReaderFragment :
}
}
/**
* Stores the given title for a "find in page" search operation.
* This title is used later when triggering the "find in page" functionality.
*
* @param title The title or keyword to search for within the current WebView content.
*/
private fun storeFindInPageTitle(title: String) {
findInPageTitle = title
}
/**
* Initiates the "find in page" UI for searching within the current WebView content.
* If the `compatCallback` is active, it sets up the WebView to search for the
* specified title and displays the search input UI.
*
* @param title The search term or keyword to locate within the page. If null, no action is taken.
*/
private fun findInPage(title: String?) {
// if the search is localized trigger find in page UI.
compatCallback?.apply {
@ -2344,34 +2441,114 @@ abstract class CoreReaderFragment :
updateNightMode()
}
private fun saveTabStates() {
val settings = requireActivity().getSharedPreferences(
/**
* Saves the current state of tabs and web view history to persistent storage.
*
* This method is designed to be called when the fragment is about to pause,
* ensuring that the current tab states are preserved. It performs the following steps:
*
* 1. Clears any previous web view page history stored in the database.
* 2. Retrieves the current activity's shared preferences to store the tab states.
* 3. Iterates over the currently opened web views, creating a list of
* `WebViewHistoryEntity` objects based on their URLs.
* 4. Saves the collected web view history entities to the database.
* 5. Updates the shared preferences with the current ZIM file and tab index.
* 6. Logs the current ZIM file being saved for debugging purposes.
* 7. Calls the provided `onComplete` callback function once all operations are finished.
*
* Note: This method runs on the main thread and performs database operations
* in a background thread to avoid blocking the UI.
*
* @param onComplete A lambda function to be executed after the tab states have
* been successfully saved. This is optional and defaults to
* an empty function.
*
* Example usage:
* ```
* saveTabStates {
* openSearch("", isOpenedFromTabView = isInTabSwitcher, false)
* }
*/
private fun saveTabStates(onComplete: () -> Unit = {}) {
CoroutineScope(Dispatchers.Main).launch {
savingTabsMutex.withLock {
// clear the previous history saved in database
withContext(Dispatchers.IO) {
repositoryActions?.clearWebViewPageHistory()
}
val coreApp = sharedPreferenceUtil?.context as CoreApp
val settings = coreApp.getMainActivity().getSharedPreferences(
SharedPreferenceUtil.PREF_KIWIX_MOBILE,
0
)
val editor = settings.edit()
val urls = JSONArray()
val positions = JSONArray()
for (view in webViewList) {
if (view.url == null) continue
urls.put(view.url)
positions.put(view.scrollY)
val webViewHistoryEntityList = arrayListOf<WebViewHistoryEntity>()
webViewList.forEachIndexed { index, view ->
if (view.url == null) return@forEachIndexed
getWebViewHistoryEntity(view, index)?.let(webViewHistoryEntityList::add)
}
withContext(Dispatchers.IO) {
repositoryActions?.saveWebViewPageHistory(webViewHistoryEntityList)
}
editor.putString(TAG_CURRENT_FILE, zimReaderContainer?.zimReaderSource?.toDatabase())
editor.putString(TAG_CURRENT_ARTICLES, "$urls")
editor.putString(TAG_CURRENT_POSITIONS, "$positions")
editor.putInt(TAG_CURRENT_TAB, currentWebViewIndex)
editor.apply()
}
override fun onPause() {
super.onPause()
saveTabStates()
Log.d(
TAG_KIWIX,
"onPause Save current zim file to preferences: " +
"Save current zim file to preferences: " +
"${zimReaderContainer?.zimReaderSource?.toDatabase()}"
)
onComplete.invoke()
}
}
}
/**
* Retrieves a `WebViewHistoryEntity` from the given `KiwixWebView` instance.
*
* This method captures the current state of the specified web view, including its
* scroll position and back-forward list, and creates a `WebViewHistoryEntity`
* if the necessary conditions are met. The steps involved are as follows:
*
* 1. Initializes a `Bundle` to store the state of the web view.
* 2. Calls `saveState` on the provided `webView`, which populates the bundle
* with the current state of the web view's back-forward list.
* 3. Retrieves the ID of the currently loaded ZIM file from the `zimReaderContainer`.
* 4. Checks if the ZIM ID is not null and if the web back-forward list contains any entries:
* - If both conditions are satisfied, it creates and returns a `WebViewHistoryEntity`
* containing a `WebViewHistoryItem` with the following data:
* - `zimId`: The ID of the current ZIM file.
* - `webViewIndex`: The index of the web view in the list of opened views.
* - `webViewPosition`: The current vertical scroll position of the web view.
* - `webViewBackForwardList`: The bundle containing the saved state of the
* web view's back-forward list.
* 5. If the ZIM ID is null or the web back-forward list is empty, the method returns null.
*
* @param webView The `KiwixWebView` instance from which to retrieve the history entity.
* @param webViewIndex The index of the web view in the list of opened web views,
* used to identify the position of this web view in the history.
* @return A `WebViewHistoryEntity` containing the state information of the web view,
* or null if the necessary conditions for creating the entity are not met.
*/
private suspend fun getWebViewHistoryEntity(
webView: KiwixWebView,
webViewIndex: Int
): WebViewHistoryEntity? {
val bundle = Bundle()
val webBackForwardList = webView.saveState(bundle)
val zimId = zimReaderContainer?.zimFileReader?.id
if (zimId != null && webBackForwardList != null && webBackForwardList.size > 0) {
return WebViewHistoryEntity(
WebViewHistoryItem(
zimId = zimId,
webViewIndex = webViewIndex,
webViewPosition = webView.scrollY,
webViewBackForwardList = bundle
)
)
}
return null
}
override fun webViewUrlLoading() {
@ -2392,9 +2569,9 @@ abstract class CoreReaderFragment :
// it will not remove the service worker from the history, so it will remain in the history.
// To clear this, we are clearing the history when the main page is loaded for the first time.
val mainPageUrl = zimReaderContainer?.mainPage
if (mainPageUrl != null &&
isFirstTimeMainPageLoaded &&
getCurrentWebView()?.url?.endsWith(mainPageUrl) == true
if (isFirstTimeMainPageLoaded &&
!isFromManageExternalLaunch &&
mainPageUrl?.let { getCurrentWebView()?.url?.endsWith(it) } == true
) {
// Set isFirstTimeMainPageLoaded to false. This ensures that if the user clicks
// on the home menu after visiting multiple pages, the history will not be erased.
@ -2460,6 +2637,7 @@ abstract class CoreReaderFragment :
showProgressBarWithProgress(progress)
if (progress == 100) {
hideProgressBar()
saveTabStates()
Log.d(TAG_KIWIX, "Loaded URL: " + getCurrentWebView()?.url)
}
(webView.context as AppCompatActivity).invalidateOptionsMenu()
@ -2553,9 +2731,7 @@ abstract class CoreReaderFragment :
)
}
private fun isInvalidJson(jsonString: String?): Boolean =
jsonString == null || jsonString == "[]"
@SuppressLint("CheckResult")
protected fun manageExternalLaunchAndRestoringViewState(
restoreOrigin: RestoreOrigin = FromExternalLaunch
) {
@ -2563,72 +2739,112 @@ abstract class CoreReaderFragment :
SharedPreferenceUtil.PREF_KIWIX_MOBILE,
0
)
val zimArticles = settings.getString(TAG_CURRENT_ARTICLES, null)
val zimPositions = settings.getString(TAG_CURRENT_POSITIONS, null)
val currentTab = safelyGetCurrentTab(settings)
if (isInvalidJson(zimArticles) || isInvalidJson(zimPositions)) {
restoreViewStateOnInvalidJSON()
} else {
restoreViewStateOnValidJSON(zimArticles, zimPositions, currentTab, restoreOrigin)
repositoryActions?.loadWebViewPagesHistory()
?.subscribe({ webViewHistoryItemList ->
if (webViewHistoryItemList.isEmpty()) {
restoreViewStateOnInvalidWebViewHistory()
return@subscribe
}
restoreViewStateOnValidWebViewHistory(
webViewHistoryItemList,
currentTab,
restoreOrigin
) {
// This lambda is executed after the tabs have been restored. It checks if there is a
// search item to open. If `searchItemToOpen` is not null, it calls `openSearchItem`
// to open the specified item, then sets `searchItemToOpen` to null to prevent
// any unexpected behavior on future calls. Similarly, if `findInPageTitle` is set,
// it invokes `findInPage` and resets `findInPageTitle` to null.
searchItemToOpen?.let(::openSearchItem)
searchItemToOpen = null
findInPageTitle?.let(::findInPage)
findInPageTitle = null
}
}, {
Log.e(
TAG_KIWIX,
"Could not restore tabs. Original exception = ${it.printStackTrace()}"
)
restoreViewStateOnInvalidWebViewHistory()
})
}
private fun safelyGetCurrentTab(settings: SharedPreferences): Int =
max(settings.getInt(TAG_CURRENT_TAB, 0), 0)
/* This method restores tabs state in new launches, do not modify it
unless it is explicitly mentioned in the issue you're fixing */
/**
* Restores the tabs based on the provided webViewHistoryItemList.
*
* This method performs the following actions:
* - Resets the current web view index to zero.
* - Removes the first tab from the webViewList and updates the tabs adapter.
* - Iterates over the provided webViewHistoryItemList, creating new tabs and restoring
* their states based on the historical data.
* - Selects the specified tab to make it the currently active one.
* - Invokes the onComplete callback once the restoration is finished.
*
* If any error occurs during the restoration process, it logs a warning and displays
* a toast message to inform the user that the tabs could not be restored.
*
* @param webViewHistoryItemList List of WebViewHistoryItem representing the historical data for restoring tabs.
* @param currentTab Index of the tab to be set as the currently active tab after restoration.
* @param onComplete Callback to be invoked upon successful restoration of the tabs.
*
* @Warning: This method restores tabs state in new launches, do not modify it
* unless it is explicitly mentioned in the issue you're fixing.
*/
protected fun restoreTabs(
zimArticles: String?,
zimPositions: String?,
currentTab: Int
webViewHistoryItemList: List<WebViewHistoryItem>,
currentTab: Int,
onComplete: () -> Unit
) {
try {
val urls = JSONArray(zimArticles)
val positions = JSONArray(zimPositions)
isFromManageExternalLaunch = true
currentWebViewIndex = 0
tabsAdapter?.apply {
webViewList.removeAt(0)
notifyItemRemoved(0)
notifyDataSetChanged()
}
var cursor = 0
getCurrentWebView()?.let { kiwixWebView ->
kiwixWebView.loadUrl(reformatProviderUrl(urls.getString(cursor)))
kiwixWebView.scrollY = positions.getInt(cursor)
cursor++
while (cursor < urls.length()) {
newTab(reformatProviderUrl(urls.getString(cursor)))
kiwixWebView.scrollY = positions.getInt(cursor)
cursor++
webViewHistoryItemList.forEach { webViewHistoryItem ->
newTab("", shouldLoadUrl = false)?.let {
restoreTabState(it, webViewHistoryItem)
}
}
selectTab(currentTab)
}
} catch (e: JSONException) {
Log.w(TAG_KIWIX, "Kiwix shared preferences corrupted", e)
onComplete.invoke()
} catch (ignore: Exception) {
Log.w(TAG_KIWIX, "Kiwix shared preferences corrupted", ignore)
activity.toast(R.string.could_not_restore_tabs, Toast.LENGTH_LONG)
}
// After restoring the tabs, observe any search actions that the user might have triggered.
// Since the ZIM file opening functionality has been moved to a background thread,
// we ensure that all necessary actions are completed before observing these search actions.
observeSearchActions()
}
/**
* Observes any search-related actions triggered by the user, such as "Find in Page" or
* opening a specific search item.
* This method sets up observers for navigation results related to search functionality.
* Restores the state of the specified KiwixWebView based on the provided WebViewHistoryItem.
*
* This method retrieves the back-forward list from the WebViewHistoryItem and
* uses it to restore the web view's state. It also sets the vertical scroll position
* of the web view to the position stored in the WebViewHistoryItem.
*
* If the provided WebViewHistoryItem is null, the method instead loads the main page
* of the currently opened ZIM file. This fallback behavior is triggered, for example,
* when opening a note in the notes screen, where the webViewHistoryList is intentionally
* set to null to indicate that the main page of the newly opened ZIM file should be loaded.
*
* @param webView The KiwixWebView instance whose state is to be restored.
* @param webViewHistoryItem The WebViewHistoryItem containing the saved state and scroll position,
* or null if the main page should be loaded.
*/
private fun observeSearchActions() {
requireActivity().observeNavigationResult<String>(
FIND_IN_PAGE_SEARCH_STRING,
viewLifecycleOwner,
Observer(::findInPage)
)
requireActivity().observeNavigationResult<SearchItemToOpen>(
TAG_FILE_SEARCHED,
viewLifecycleOwner,
Observer(::openSearchItem)
)
private fun restoreTabState(webView: KiwixWebView, webViewHistoryItem: WebViewHistoryItem?) {
webViewHistoryItem?.webViewBackForwardListBundle?.let { bundle ->
webView.restoreState(bundle)
webView.scrollY = webViewHistoryItem.webViewCurrentPosition
} ?: kotlin.run {
zimReaderContainer?.zimFileReader?.let {
webView.loadUrl(redirectOrOriginal(contentUrl("${it.mainPage}")))
}
}
}
override fun onReadAloudPauseOrResume(isPauseTTS: Boolean) {
@ -2697,29 +2913,29 @@ abstract class CoreReaderFragment :
}
/**
* Restores the view state after successfully reading valid JSON from shared preferences.
* Restores the view state after successfully reading valid webViewHistory from room database.
* Developers modifying this method in subclasses, such as CustomReaderFragment and
* KiwixReaderFragment, should review and consider the implementations in those subclasses
* (e.g., CustomReaderFragment.restoreViewStateOnValidJSON,
* KiwixReaderFragment.restoreViewStateOnValidJSON) to ensure consistent behavior
* when handling valid JSON scenarios.
* (e.g., CustomReaderFragment.restoreViewStateOnValidWebViewHistory,
* KiwixReaderFragment.restoreViewStateOnValidWebViewHistory) to ensure consistent behavior
* when handling valid webViewHistory scenarios.
*/
protected abstract fun restoreViewStateOnValidJSON(
zimArticles: String?,
zimPositions: String?,
protected abstract fun restoreViewStateOnValidWebViewHistory(
webViewHistoryItemList: List<WebViewHistoryItem>,
currentTab: Int,
restoreOrigin: RestoreOrigin
restoreOrigin: RestoreOrigin,
onComplete: () -> Unit
)
/**
* Restores the view state when the attempt to read JSON from shared preferences fails
* due to invalid or corrupted data. Developers modifying this method in subclasses, such as
* Restores the view state when the attempt to read webViewHistory from room database fails
* due to the absence of any history records. Developers modifying this method in subclasses, such as
* CustomReaderFragment and KiwixReaderFragment, should review and consider the implementations
* in those subclasses (e.g., CustomReaderFragment.restoreViewStateOnInvalidJSON,
* KiwixReaderFragment.restoreViewStateOnInvalidJSON) to ensure consistent behavior
* in those subclasses (e.g., CustomReaderFragment.restoreViewStateOnInvalidWebViewHistory,
* KiwixReaderFragment.restoreViewStateOnInvalidWebViewHistory) to ensure consistent behavior
* when handling invalid JSON scenarios.
*/
abstract fun restoreViewStateOnInvalidJSON()
abstract fun restoreViewStateOnInvalidWebViewHistory()
}
enum class RestoreOrigin {

View File

@ -61,6 +61,7 @@ class MainMenu(
fun onRandomArticleMenuClicked()
fun onReadAloudMenuClicked()
fun onFullscreenMenuClicked()
fun onSearchMenuClickedMenuClicked()
}
init {
@ -154,7 +155,7 @@ class MainMenu(
}
private fun navigateToSearch(): Boolean {
(activity as CoreMainActivity).openSearch(isOpenedFromTabView = isInTabSwitcher)
menuClickListener.onSearchMenuClickedMenuClicked()
return true
}

View File

@ -17,11 +17,14 @@
*/
package org.kiwix.kiwixmobile.core.main
import io.reactivex.Single
import io.reactivex.disposables.Disposable
import org.kiwix.kiwixmobile.core.dao.entities.WebViewHistoryEntity
import org.kiwix.kiwixmobile.core.data.DataSource
import org.kiwix.kiwixmobile.core.di.ActivityScope
import org.kiwix.kiwixmobile.core.page.bookmark.adapter.LibkiwixBookmarkItem
import org.kiwix.kiwixmobile.core.page.history.adapter.HistoryListItem.HistoryItem
import org.kiwix.kiwixmobile.core.page.history.adapter.WebViewHistoryItem
import org.kiwix.kiwixmobile.core.page.notes.adapter.NoteListItem
import org.kiwix.kiwixmobile.core.utils.files.Log
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.adapter.BooksOnDiskListItem.BookOnDisk
@ -36,6 +39,9 @@ class MainRepositoryActions @Inject constructor(private val dataSource: DataSour
private var saveNoteDisposable: Disposable? = null
private var saveBookDisposable: Disposable? = null
private var deleteNoteDisposable: Disposable? = null
private var saveWebViewHistoryDisposable: Disposable? = null
private var clearWebViewHistoryDisposable: Disposable? = null
private var getWebViewHistoryDisposable: Disposable? = null
fun saveHistory(history: HistoryItem) {
saveHistoryDisposable = dataSource.saveHistory(history)
@ -68,11 +74,32 @@ class MainRepositoryActions @Inject constructor(private val dataSource: DataSour
.subscribe({}, { e -> Log.e(TAG, "Unable to save book", e) })
}
suspend fun saveWebViewPageHistory(webViewHistoryEntityList: List<WebViewHistoryEntity>) {
dataSource.insertWebViewPageHistoryItems(webViewHistoryEntityList)
}
suspend fun clearWebViewPageHistory() {
dataSource.clearWebViewPagesHistory()
}
fun loadWebViewPagesHistory(): Single<List<WebViewHistoryItem>> =
dataSource.getAllWebViewPagesHistory()
.map { roomEntities ->
roomEntities.map(::WebViewHistoryItem)
}
.onErrorReturn {
Log.e(TAG, "Unable to load page history", it)
emptyList()
}
fun dispose() {
saveHistoryDisposable?.dispose()
saveBookmarkDisposable?.dispose()
saveNoteDisposable?.dispose()
deleteNoteDisposable?.dispose()
saveBookDisposable?.dispose()
saveWebViewHistoryDisposable?.dispose()
clearWebViewHistoryDisposable?.dispose()
getWebViewHistoryDisposable?.dispose()
}
}

View File

@ -0,0 +1,51 @@
/*
* 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.core.page.history.adapter
import android.os.Bundle
import org.kiwix.kiwixmobile.core.dao.entities.WebViewHistoryEntity
data class WebViewHistoryItem(
val databaseId: Long = 0L,
val zimId: String,
val webViewIndex: Int,
val webViewCurrentPosition: Int,
val webViewBackForwardListBundle: Bundle?
) {
constructor(
zimId: String,
webViewIndex: Int,
webViewPosition: Int,
webViewBackForwardList: Bundle?
) : this(
0L,
zimId,
webViewIndex,
webViewPosition,
webViewBackForwardList
)
constructor(webViewHistoryEntity: WebViewHistoryEntity) : this(
webViewHistoryEntity.id,
webViewHistoryEntity.zimId,
webViewHistoryEntity.webViewIndex,
webViewHistoryEntity.webViewCurrentPosition,
webViewHistoryEntity.webViewBackForwardListBundle
)
}

View File

@ -29,8 +29,6 @@ const val REQUEST_POST_NOTIFICATION_PERMISSION = 4
const val TAG_FILE_SEARCHED = "searchedarticle"
const val TAG_FILE_SEARCHED_NEW_TAB = "searchedarticlenewtab"
const val TAG_CURRENT_FILE = "currentzimfile"
const val TAG_CURRENT_ARTICLES = "currentarticles"
const val TAG_CURRENT_POSITIONS = "currentpositions"
const val TAG_CURRENT_TAB = "currenttab"
const val TAG_FROM_TAB_SWITCHER = "fromtabswitcher"

View File

@ -21,7 +21,7 @@ package org.kiwix.kiwixmobile.custom.search
import android.Manifest
import android.content.Context
import android.content.res.AssetFileDescriptor
import android.os.ParcelFileDescriptor
import androidx.core.content.ContextCompat
import androidx.core.content.edit
import androidx.lifecycle.Lifecycle
import androidx.navigation.fragment.NavHostFragment
@ -61,7 +61,6 @@ import org.kiwix.kiwixmobile.custom.testutils.TestUtils.closeSystemDialogs
import org.kiwix.kiwixmobile.custom.testutils.TestUtils.isSystemUINotRespondingDialogVisible
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.net.URI
import java.util.concurrent.TimeUnit
import javax.inject.Singleton
@ -225,7 +224,7 @@ class SearchFragmentTestForCustomApp {
UiThreadStatement.runOnUiThread {
customMainActivity.navigate(customMainActivity.readerFragmentResId)
}
openZimFileInReaderWithAssetFileDescriptor(downloadingZimFile)
openZimFileInReader(zimFile = downloadingZimFile)
openSearchWithQuery(searchTerms[0])
// wait for searchFragment become visible on screen.
delay(2000)
@ -304,12 +303,6 @@ class SearchFragmentTestForCustomApp {
}
}
private fun openZimFileInReaderWithAssetFileDescriptor(downloadingZimFile: File) {
getAssetFileDescriptorFromFile(downloadingZimFile)?.let(::openZimFileInReader) ?: run {
throw RuntimeException("Unable to get fileDescriptor from file. Original exception")
}
}
private fun openZimFileInReader(
assetFileDescriptor: AssetFileDescriptor? = null,
zimFile: File? = null
@ -338,23 +331,6 @@ class SearchFragmentTestForCustomApp {
}
}
private fun getAssetFileDescriptorFromFile(file: File): AssetFileDescriptor? {
val parcelFileDescriptor = getFileDescriptor(file)
if (parcelFileDescriptor != null) {
return AssetFileDescriptor(parcelFileDescriptor, 0, file.length())
}
return null
}
private fun getFileDescriptor(file: File?): ParcelFileDescriptor? {
try {
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
} catch (e: IOException) {
e.printStackTrace()
return null
}
}
private fun writeZimFileData(responseBody: ResponseBody, file: File) {
FileOutputStream(file).use { outputStream ->
responseBody.byteStream().use { inputStream ->
@ -374,7 +350,7 @@ class SearchFragmentTestForCustomApp {
.build()
private fun getDownloadingZimFile(): File {
val zimFile = File(context.cacheDir, "ray_charles.zim")
val zimFile = File(ContextCompat.getExternalFilesDirs(context, null)[0], "ray_charles.zim")
if (zimFile.exists()) zimFile.delete()
zimFile.createNewFile()
return zimFile

View File

@ -40,6 +40,7 @@ import org.kiwix.kiwixmobile.core.extensions.isFileExist
import org.kiwix.kiwixmobile.core.main.CoreReaderFragment
import org.kiwix.kiwixmobile.core.main.MainMenu
import org.kiwix.kiwixmobile.core.main.RestoreOrigin
import org.kiwix.kiwixmobile.core.page.history.adapter.WebViewHistoryItem
import org.kiwix.kiwixmobile.core.reader.ZimReaderSource
import org.kiwix.kiwixmobile.core.utils.LanguageUtils
import org.kiwix.kiwixmobile.core.utils.dialog.DialogShower
@ -143,32 +144,32 @@ class CustomReaderFragment : CoreReaderFragment() {
// See https://github.com/kiwix/kiwix-android/issues/3541
zimReaderContainer?.zimFileReader?.let(::setUpBookmarks)
} else {
openObbOrZim()
openObbOrZim(true)
}
requireArguments().clear()
}
/**
* Restores the view state when the attempt to read JSON from shared preferences fails
* due to invalid or corrupted data. In this case, it opens the homepage of the zim file,
* as custom apps always have the zim file available.
* Restores the view state when the attempt to read web view history from the room database fails
* due to the absence of any history records. In this case, it navigates to the homepage of the
* ZIM file, as custom apps are expected to have the ZIM file readily available.
*/
override fun restoreViewStateOnInvalidJSON() {
override fun restoreViewStateOnInvalidWebViewHistory() {
openHomeScreen()
}
/**
* Restores the view state when the JSON data is valid. This method restores the tabs
* and loads the last opened article in the specified tab.
* Restores the view state when the webViewHistory data is valid.
* This method restores the tabs with webView pages history.
*/
override fun restoreViewStateOnValidJSON(
zimArticles: String?,
zimPositions: String?,
override fun restoreViewStateOnValidWebViewHistory(
webViewHistoryItemList: List<WebViewHistoryItem>,
currentTab: Int,
// Unused in custom apps as there is only one ZIM file that is already set.
restoreOrigin: RestoreOrigin
restoreOrigin: RestoreOrigin,
onComplete: () -> Unit
) {
restoreTabs(zimArticles, zimPositions, currentTab)
restoreTabs(webViewHistoryItemList, currentTab, onComplete)
}
/**
@ -183,7 +184,28 @@ class CustomReaderFragment : CoreReaderFragment() {
)
}
private fun openObbOrZim() {
/**
* Opens a ZIM file or an OBB file based on the validation of available files.
*
* This method uses the `customFileValidator` to check for the presence of required files.
* Depending on the validation results, it performs the following actions:
*
* - If a valid ZIM file is found:
* - It opens the ZIM file and creates a `ZimReaderSource` for it.
* - Saves the book information in the database to be displayed in the `ZimHostFragment`.
* - Manages the external launch and restores the view state if specified.
*
* - If both ZIM and OBB files are found:
* - The ZIM file is deleted, and the OBB file is opened instead.
* - Manages the external launch and restores the view state if specified.
*
* If no valid files are found and the app is not in test mode, the user is navigated to
* the `customDownloadFragment` to facilitate downloading the required files.
*
* @param shouldManageExternalLaunch Indicates whether to manage external launch and
* restore the view state after opening the file. Default is false.
*/
private fun openObbOrZim(shouldManageExternalLaunch: Boolean = false) {
customFileValidator.validate(
onFilesFound = {
coreReaderLifeCycleScope?.launch {
@ -195,7 +217,8 @@ class CustomReaderFragment : CoreReaderFragment() {
null,
it.assetFileDescriptorList
),
true
true,
shouldManageExternalLaunch
)
// Save book in the database to display it in `ZimHostFragment`.
zimReaderContainer?.zimFileReader?.let { zimFileReader ->
@ -206,16 +229,20 @@ class CustomReaderFragment : CoreReaderFragment() {
val bookOnDisk = BookOnDisk(zimFileReader)
repositoryActions?.saveBook(bookOnDisk)
}
if (shouldManageExternalLaunch) {
// Open the previous loaded pages after ZIM file loads.
manageExternalLaunchAndRestoringViewState()
}
}
is ValidationState.HasBothFiles -> {
it.zimFile.delete()
openZimFile(ZimReaderSource(it.obbFile), true)
openZimFile(ZimReaderSource(it.obbFile), true, shouldManageExternalLaunch)
if (shouldManageExternalLaunch) {
// Open the previous loaded pages after ZIM file loads.
manageExternalLaunchAndRestoringViewState()
}
}
else -> {}
}