mirror of
https://github.com/kiwix/kiwix-android.git
synced 2025-08-03 18:56:44 -04:00
Merge pull request #3996 from Saifuddin53/Issue#398
Saving and Restoring the Web View Navigation History Across Sessions
This commit is contained in:
commit
51aca061b9
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,7 @@
|
||||
<ID>LongParameterList:MainMenu.kt$MainMenu$( private val activity: Activity, zimFileReader: ZimFileReader?, menu: Menu, webViews: MutableList<KiwixWebView>, urlIsValid: Boolean, disableReadAloud: Boolean = false, disableTabs: Boolean = false, private val menuClickListener: MenuClickListener )</ID>
|
||||
<ID>LongParameterList:MainMenu.kt$MainMenu.Factory$( menu: Menu, webViews: MutableList<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>
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
}
|
@ -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"
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 -> {}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user