diff --git a/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/reader/KiwixReaderFragment.kt b/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/reader/KiwixReaderFragment.kt index eaf1c41311..6a6d0f8118 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/reader/KiwixReaderFragment.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/reader/KiwixReaderFragment.kt @@ -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 currentTab Index of the tab to be restored as the currently active one. + * @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, 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) } } } diff --git a/core/detekt_baseline.xml b/core/detekt_baseline.xml index 6415a52a83..0d97ca687a 100644 --- a/core/detekt_baseline.xml +++ b/core/detekt_baseline.xml @@ -12,7 +12,7 @@ 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 ) LongParameterList:MainMenu.kt$MainMenu.Factory$( menu: Menu, webViews: MutableList<KiwixWebView>, urlIsValid: Boolean, menuClickListener: MenuClickListener, disableReadAloud: Boolean, disableTabs: Boolean ) 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" ) - 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 ) + 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 ) 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 ) MagicNumber:ArticleCount.kt$ArticleCount$3 MagicNumber:CompatFindActionModeCallback.kt$CompatFindActionModeCallback$100 diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/HistoryRoomDao.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/HistoryRoomDao.kt index 79ab8bebbd..1c6aa3a474 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/HistoryRoomDao.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/HistoryRoomDao.kt @@ -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 diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/WebViewHistoryRoomDao.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/WebViewHistoryRoomDao.kt new file mode 100644 index 0000000000..19431f9687 --- /dev/null +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/WebViewHistoryRoomDao.kt @@ -0,0 +1,49 @@ +/* + * Kiwix Android + * Copyright (c) 2024 Kiwix + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package org.kiwix.kiwixmobile.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) + + @Query("SELECT * FROM WebViewHistoryEntity ORDER BY webViewIndex ASC") + abstract fun getAllWebViewPagesHistory(): Flowable> + + @Query("Delete from WebViewHistoryEntity") + abstract fun clearWebViewPagesHistory() + + fun clearPageHistoryWithPrimaryKey() { + clearWebViewPagesHistory() + } + + @Query("DELETE FROM sqlite_sequence WHERE name='PageHistoryRoomEntity'") + abstract fun resetPrimaryKey() +} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/entities/WebViewHistoryEntity.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/entities/WebViewHistoryEntity.kt new file mode 100644 index 0000000000..1e868b133b --- /dev/null +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/entities/WebViewHistoryEntity.kt @@ -0,0 +1,68 @@ +/* + * Kiwix Android + * Copyright (c) 2024 Kiwix + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package org.kiwix.kiwixmobile.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 + } +} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/data/DataSource.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/data/DataSource.kt index 0b897b0393..45713cb45a 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/data/DataSource.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/data/DataSource.kt @@ -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): Completable + + suspend fun insertWebViewPageHistoryItems(webViewHistoryEntityList: List) + fun getAllWebViewPagesHistory(): Single> + suspend fun clearWebViewPagesHistory() } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/data/KiwixRoomDatabase.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/data/KiwixRoomDatabase.kt index 44a50b6801..d918a82b91 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/data/KiwixRoomDatabase.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/data/KiwixRoomDatabase.kt @@ -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 } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/data/Repository.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/data/Repository.kt index de4c7884d4..8938cf27bd 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/data/Repository.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/data/Repository.kt @@ -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 + ) { + 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) diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/di/components/CoreComponent.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/di/components/CoreComponent.kt index 54a0405abf..367f47be06 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/di/components/CoreComponent.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/di/components/CoreComponent.kt @@ -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 diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/di/modules/DatabaseModule.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/di/modules/DatabaseModule.kt index c7b374e5aa..31eb596cb7 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/di/modules/DatabaseModule.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/di/modules/DatabaseModule.kt @@ -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() diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/main/CoreMainActivity.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/main/CoreMainActivity.kt index eb33f72812..1d2ba9acc1 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/main/CoreMainActivity.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/main/CoreMainActivity.kt @@ -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 ) } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/main/CoreReaderFragment.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/main/CoreReaderFragment.kt index 6ad2b4cef1..3d49580f3f 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/main/CoreReaderFragment.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/main/CoreReaderFragment.kt @@ -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( + FIND_IN_PAGE_SEARCH_STRING, + viewLifecycleOwner, + Observer(::storeFindInPageTitle) + ) + requireActivity().observeNavigationResult( + 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 { - loadUrl(url, it) + 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 - openArticle(UNINITIALISER_ADDRESS) + 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( - 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) - } - 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: " + - "${zimReaderContainer?.zimReaderSource?.toDatabase()}" - ) + /** + * 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 webViewHistoryEntityList = arrayListOf() + 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.putInt(TAG_CURRENT_TAB, currentWebViewIndex) + editor.apply() + Log.d( + TAG_KIWIX, + "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, + 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) + selectTab(currentTab) + 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( - FIND_IN_PAGE_SEARCH_STRING, - viewLifecycleOwner, - Observer(::findInPage) - ) - requireActivity().observeNavigationResult( - 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, 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 { diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/main/MainMenu.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/main/MainMenu.kt index 6123f1cfb8..277b8a2fef 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/main/MainMenu.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/main/MainMenu.kt @@ -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 } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/main/MainRepositoryActions.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/main/MainRepositoryActions.kt index 5557c2b23c..1b9b60cd71 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/main/MainRepositoryActions.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/main/MainRepositoryActions.kt @@ -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) { + dataSource.insertWebViewPageHistoryItems(webViewHistoryEntityList) + } + + suspend fun clearWebViewPageHistory() { + dataSource.clearWebViewPagesHistory() + } + + fun loadWebViewPagesHistory(): Single> = + 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() } } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/page/history/adapter/WebViewHistoryItem.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/page/history/adapter/WebViewHistoryItem.kt new file mode 100644 index 0000000000..6d711e7a51 --- /dev/null +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/page/history/adapter/WebViewHistoryItem.kt @@ -0,0 +1,51 @@ +/* + * Kiwix Android + * Copyright (c) 2024 Kiwix + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package org.kiwix.kiwixmobile.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 + ) +} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/Constants.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/Constants.kt index b1b0361bed..6d1139feb7 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/Constants.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/Constants.kt @@ -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" diff --git a/custom/src/androidTest/java/org/kiwix/kiwixmobile/custom/search/SearchFragmentTestForCustomApp.kt b/custom/src/androidTest/java/org/kiwix/kiwixmobile/custom/search/SearchFragmentTestForCustomApp.kt index c0307a1ceb..0cc7a9d850 100644 --- a/custom/src/androidTest/java/org/kiwix/kiwixmobile/custom/search/SearchFragmentTestForCustomApp.kt +++ b/custom/src/androidTest/java/org/kiwix/kiwixmobile/custom/search/SearchFragmentTestForCustomApp.kt @@ -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 diff --git a/custom/src/main/java/org/kiwix/kiwixmobile/custom/main/CustomReaderFragment.kt b/custom/src/main/java/org/kiwix/kiwixmobile/custom/main/CustomReaderFragment.kt index a5b5730ad0..9e3602498a 100644 --- a/custom/src/main/java/org/kiwix/kiwixmobile/custom/main/CustomReaderFragment.kt +++ b/custom/src/main/java/org/kiwix/kiwixmobile/custom/main/CustomReaderFragment.kt @@ -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, 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,15 +229,19 @@ class CustomReaderFragment : CoreReaderFragment() { val bookOnDisk = BookOnDisk(zimFileReader) repositoryActions?.saveBook(bookOnDisk) } - // Open the previous loaded pages after ZIM file loads. - manageExternalLaunchAndRestoringViewState() + if (shouldManageExternalLaunch) { + // Open the previous loaded pages after ZIM file loads. + manageExternalLaunchAndRestoringViewState() + } } is ValidationState.HasBothFiles -> { it.zimFile.delete() - openZimFile(ZimReaderSource(it.obbFile), true) - // Open the previous loaded pages after ZIM file loads. - manageExternalLaunchAndRestoringViewState() + openZimFile(ZimReaderSource(it.obbFile), true, shouldManageExternalLaunch) + if (shouldManageExternalLaunch) { + // Open the previous loaded pages after ZIM file loads. + manageExternalLaunchAndRestoringViewState() + } } else -> {}