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 0318c7a393..82da05e718 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 @@ -33,7 +33,11 @@ import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import androidx.core.net.toFile import androidx.drawerlayout.widget.DrawerLayout +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.google.android.material.bottomnavigation.BottomNavigationView +import kotlinx.coroutines.launch import org.kiwix.kiwixmobile.R import org.kiwix.kiwixmobile.cachedComponent import org.kiwix.kiwixmobile.core.R.anim @@ -71,44 +75,52 @@ class KiwixReaderFragment : CoreReaderFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - - val activity = activity as CoreMainActivity - noOpenBookButton?.setOnClickListener { - activity.navigate( - KiwixReaderFragmentDirections.actionNavigationReaderToNavigationLibrary() - ) + lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + val activity = activity as CoreMainActivity + noOpenBookButton?.setOnClickListener { + activity.navigate( + KiwixReaderFragmentDirections.actionNavigationReaderToNavigationLibrary() + ) + } + activity.supportActionBar?.setDisplayHomeAsUpEnabled(true) + toolbar?.let(activity::setupDrawerToggle) + setFragmentContainerBottomMarginToSizeOfNavBar() + openPageInBookFromNavigationArguments() + } } - activity.supportActionBar?.setDisplayHomeAsUpEnabled(true) - toolbar?.let(activity::setupDrawerToggle) - setFragmentContainerBottomMarginToSizeOfNavBar() - openPageInBookFromNavigationArguments() } - private fun openPageInBookFromNavigationArguments() { + @Suppress("TooGenericExceptionCaught") + private suspend fun openPageInBookFromNavigationArguments() { val args = KiwixReaderFragmentArgs.fromBundle(requireArguments()) - - if (args.pageUrl.isNotEmpty()) { - if (args.zimFileUri.isNotEmpty()) { - tryOpeningZimFile(args.zimFileUri) - } else { - // Set up bookmarks for the current book when opening bookmarks from the Bookmark screen. - // This is necessary because we are not opening the ZIM file again; the bookmark is - // inside the currently opened book. Bookmarks are set up when opening the ZIM file. - // See https://github.com/kiwix/kiwix-android/issues/3541 - zimReaderContainer?.zimFileReader?.let(::setUpBookmarks) - } - loadUrlWithCurrentWebview(args.pageUrl) - } else { - if (args.zimFileUri.isNotEmpty()) { - tryOpeningZimFile(args.zimFileUri) + try { + if (args.pageUrl.isNotEmpty()) { + if (args.zimFileUri.isNotEmpty()) { + tryOpeningZimFile(args.zimFileUri) + } else { + // Set up bookmarks for the current book when opening bookmarks from the Bookmark screen. + // This is necessary because we are not opening the ZIM file again; the bookmark is + // inside the currently opened book. Bookmarks are set up when opening the ZIM file. + // See https://github.com/kiwix/kiwix-android/issues/3541 + zimReaderContainer?.zimFileReader?.let(::setUpBookmarks) + } + loadUrlWithCurrentWebview(args.pageUrl) } else { - manageExternalLaunchAndRestoringViewState() + if (args.zimFileUri.isNotEmpty()) { + tryOpeningZimFile(args.zimFileUri) + } else { + manageExternalLaunchAndRestoringViewState() + } } + } catch (error: Exception) { + Log.e("KiwixReader", "Error opening ZIM file", error) + } finally { + requireArguments().clear() } - requireArguments().clear() } - private fun tryOpeningZimFile(zimFileUri: String) { + private suspend fun tryOpeningZimFile(zimFileUri: String) { val filePath = FileUtils.getLocalFilePathByUri( requireActivity().applicationContext, Uri.parse(zimFileUri) ) @@ -155,14 +167,16 @@ class KiwixReaderFragment : CoreReaderFragment() { contentFrame?.visibility = View.VISIBLE } mainMenu?.showWebViewOptions(true) - if (webViewList.isEmpty()) { - exitBook() - } else { - // Reset the top margin of web views to 0 to remove any previously set margin - // This ensures that the web views are displayed without any additional - // top margin for kiwix main app. - setTopMarginToWebViews(0) - selectTab(currentWebViewIndex) + lifecycleScope.launch { + if (webViewList.isEmpty()) { + exitBook() + } else { + // Reset the top margin of web views to 0 to remove any previously set margin + // This ensures that the web views are displayed without any additional + // top margin for kiwix main app. + setTopMarginToWebViews(0) + selectTab(currentWebViewIndex) + } } } } @@ -199,17 +213,19 @@ class KiwixReaderFragment : CoreReaderFragment() { override fun onResume() { super.onResume() - if (zimReaderContainer?.zimFile == null && - zimReaderContainer?.zimFileReader?.assetFileDescriptorList?.isEmpty() == true - ) { - exitBook() - } - if (isFullScreenVideo || isInFullScreenMode()) { - hideNavBar() + lifecycleScope.launch { + if (zimReaderContainer?.zimFile == null && + zimReaderContainer?.zimFileReader?.assetFileDescriptorList?.isEmpty() == true + ) { + exitBook() + } + if (isFullScreenVideo || isInFullScreenMode()) { + hideNavBar() + } } } - override fun restoreViewStateOnInvalidJSON() { + override suspend fun restoreViewStateOnInvalidJSON() { Log.d(TAG_KIWIX, "Kiwix normal start, no zimFile loaded last time -> display home page") exitBook() } @@ -222,20 +238,22 @@ class KiwixReaderFragment : CoreReaderFragment() { val settings = requireActivity().getSharedPreferences(SharedPreferenceUtil.PREF_KIWIX_MOBILE, 0) val zimFile = settings.getString(TAG_CURRENT_FILE, null) - if (zimFile != null && File(zimFile).isFileExist()) { - if (zimReaderContainer?.zimFile == null) { - openZimFile(File(zimFile)) - Log.d( - TAG_KIWIX, - "Kiwix normal start, Opened last used zimFile: -> $zimFile" - ) + lifecycleScope.launch { + if (zimFile != null && File(zimFile).isFileExist()) { + if (zimReaderContainer?.zimFile == null) { + openZimFile(File(zimFile)) + Log.d( + TAG_KIWIX, + "Kiwix normal start, Opened last used zimFile: -> $zimFile" + ) + } else { + zimReaderContainer?.zimFileReader?.let(::setUpBookmarks) + } } else { - zimReaderContainer?.zimFileReader?.let(::setUpBookmarks) + getCurrentWebView()?.snack(R.string.zim_not_opened) + exitBook() // hide the options for zim file to avoid unexpected UI behavior + return@launch // book not found so don't need to restore the tabs for this file } - } else { - getCurrentWebView()?.snack(R.string.zim_not_opened) - exitBook() // hide the options for zim file to avoid unexpected UI behavior - return // book not found so don't need to restore the tabs for this file } restoreTabs(zimArticles, zimPositions, currentTab) } @@ -304,10 +322,12 @@ class KiwixReaderFragment : CoreReaderFragment() { when (it.scheme) { "file" -> { Handler(Looper.getMainLooper()).postDelayed({ - openZimFile(it.toFile()).also { - // if used once then clear it to avoid affecting any other functionality - // of the application. - requireActivity().intent.action = null + lifecycleScope.launch { + openZimFile(it.toFile()).also { + // if used once then clear it to avoid affecting any other functionality + // of the application. + requireActivity().intent.action = null + } } }, 300) } @@ -315,7 +335,9 @@ class KiwixReaderFragment : CoreReaderFragment() { "content" -> { Handler(Looper.getMainLooper()).postDelayed({ getZimFileFromUri(it)?.let { zimFile -> - openZimFile(zimFile) + lifecycleScope.launch { + openZimFile(zimFile) + } }.also { requireActivity().intent.action = null } diff --git a/app/src/main/java/org/kiwix/kiwixmobile/zimManager/ZimManageViewModel.kt b/app/src/main/java/org/kiwix/kiwixmobile/zimManager/ZimManageViewModel.kt index 275a2311a2..a3e9c08137 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/zimManager/ZimManageViewModel.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/zimManager/ZimManageViewModel.kt @@ -23,6 +23,7 @@ import android.net.ConnectivityManager import androidx.annotation.VisibleForTesting import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import io.reactivex.Flowable import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.Disposable @@ -164,7 +165,7 @@ class ZimManageViewModel @Inject constructor( when (it) { is RequestNavigateTo -> OpenFileWithNavigation(it.bookOnDisk) is RequestMultiSelection -> startMultiSelectionAndSelectBook(it.bookOnDisk) - RequestDeleteMultiSelection -> DeleteFiles(selectionsFromState()) + RequestDeleteMultiSelection -> DeleteFiles(selectionsFromState(), viewModelScope) RequestShareMultiSelection -> ShareFiles(selectionsFromState()) MultiModeFinished -> noSideEffectAndClearSelectionState() is RequestSelect -> noSideEffectSelectBook(it.bookOnDisk) diff --git a/app/src/main/java/org/kiwix/kiwixmobile/zimManager/fileselectView/effects/DeleteFiles.kt b/app/src/main/java/org/kiwix/kiwixmobile/zimManager/fileselectView/effects/DeleteFiles.kt index 4c75a9c8e3..faf456612a 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/zimManager/fileselectView/effects/DeleteFiles.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/zimManager/fileselectView/effects/DeleteFiles.kt @@ -19,6 +19,8 @@ package org.kiwix.kiwixmobile.zimManager.fileselectView.effects import androidx.appcompat.app.AppCompatActivity +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import org.kiwix.kiwixmobile.R import org.kiwix.kiwixmobile.cachedComponent import org.kiwix.kiwixmobile.core.base.BaseActivity @@ -33,7 +35,10 @@ import org.kiwix.kiwixmobile.core.utils.files.FileUtils import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.adapter.BooksOnDiskListItem.BookOnDisk import javax.inject.Inject -data class DeleteFiles(private val booksOnDiskListItems: List) : +data class DeleteFiles( + private val booksOnDiskListItems: List, + private val coroutineScope: CoroutineScope +) : SideEffect { @Inject lateinit var dialogShower: DialogShower @@ -42,21 +47,22 @@ data class DeleteFiles(private val booksOnDiskListItems: List) : override fun invokeWith(activity: AppCompatActivity) { (activity as BaseActivity).cachedComponent.inject(this) - val name = booksOnDiskListItems.joinToString(separator = "\n") { it.book.title } dialogShower.show(DeleteZims(name), { - activity.toast( - if (booksOnDiskListItems.deleteAll()) { - R.string.delete_zims_toast - } else { - R.string.delete_zim_failed - } - ) + coroutineScope.launch { + activity.toast( + if (booksOnDiskListItems.deleteAll()) { + R.string.delete_zims_toast + } else { + R.string.delete_zim_failed + } + ) + } }) } - private fun List.deleteAll(): Boolean { + private suspend fun List.deleteAll(): Boolean { return fold(true) { acc, book -> acc && deleteSpecificZimFile(book).also { if (it && book.file.canonicalPath == zimReaderContainer.zimCanonicalPath) { diff --git a/app/src/test/java/org/kiwix/kiwixmobile/zimManager/ZimManageViewModelTest.kt b/app/src/test/java/org/kiwix/kiwixmobile/zimManager/ZimManageViewModelTest.kt index 4810efdc5f..596914b914 100644 --- a/app/src/test/java/org/kiwix/kiwixmobile/zimManager/ZimManageViewModelTest.kt +++ b/app/src/test/java/org/kiwix/kiwixmobile/zimManager/ZimManageViewModelTest.kt @@ -22,6 +22,7 @@ import android.app.Application import android.net.ConnectivityManager import android.net.NetworkCapabilities import android.net.NetworkCapabilities.TRANSPORT_WIFI +import androidx.lifecycle.viewModelScope import com.jraska.livedata.test import io.mockk.clearAllMocks import io.mockk.every @@ -470,7 +471,7 @@ class ZimManageViewModelTest { FileSelectListState(listOf(selectedBook, bookOnDisk()), NORMAL) viewModel.sideEffects.test() .also { viewModel.fileSelectActions.offer(RequestDeleteMultiSelection) } - .assertValues(DeleteFiles(listOf(selectedBook))) + .assertValues(DeleteFiles(listOf(selectedBook), viewModel.viewModelScope)) } @Test 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 8cbd228a5a..d649c65ac0 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 @@ -77,6 +77,7 @@ import androidx.core.widget.ContentLoadingProgressBar import androidx.drawerlayout.widget.DrawerLayout import androidx.lifecycle.Lifecycle import androidx.lifecycle.Observer +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -95,6 +96,7 @@ import io.reactivex.Flowable import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.processors.BehaviorProcessor +import kotlinx.coroutines.launch import org.json.JSONArray import org.json.JSONException import org.kiwix.kiwixmobile.core.BuildConfig @@ -1292,7 +1294,7 @@ abstract class CoreReaderFragment : mainMenu?.showBookSpecificMenuItems() } - protected fun exitBook() { + protected suspend fun exitBook() { showNoBookOpenViews() bottomToolbar?.visibility = View.GONE actionBar?.title = getString(R.string.reader) @@ -1301,7 +1303,7 @@ abstract class CoreReaderFragment : closeZimBook() } - private fun closeZimBook() { + private suspend fun closeZimBook() { zimReaderContainer?.setZimFile(null) } @@ -1320,7 +1322,6 @@ abstract class CoreReaderFragment : LinearLayout.LayoutParams.MATCH_PARENT ) } - zimReaderContainer?.setZimFile(tempZimFileForUndo) webViewList.add(index, it) tabsAdapter?.notifyDataSetChanged() snackBarRoot?.let { root -> @@ -1561,7 +1562,7 @@ abstract class CoreReaderFragment : unsupportedMimeTypeHandler?.showSaveOrOpenUnsupportedFilesDialog(url, documentType) } - fun openZimFile( + suspend fun openZimFile( file: File?, isCustomApp: Boolean = false, assetFileDescriptorList: List = emptyList(), @@ -1610,11 +1611,12 @@ abstract class CoreReaderFragment : ) } - private fun openAndSetInContainer( + private suspend fun openAndSetInContainer( file: File? = null, assetFileDescriptorList: List = emptyList(), filePath: String? = null ) { + progressBar?.visibility = View.VISIBLE try { if (isNotPreviouslyOpenZim(file?.canonicalPath)) { webViewList.clear() @@ -1641,6 +1643,7 @@ abstract class CoreReaderFragment : } ?: kotlin.run { requireActivity().toast(R.string.error_file_invalid, Toast.LENGTH_LONG) } + progressBar?.visibility = View.GONE } } @@ -1675,18 +1678,22 @@ abstract class CoreReaderFragment : ) { when (requestCode) { REQUEST_STORAGE_PERMISSION -> { - if (hasPermission(Manifest.permission.READ_EXTERNAL_STORAGE)) { - file?.let(::openZimFile) - } else { - snackBarRoot?.let { snackBarRoot -> - Snackbar.make(snackBarRoot, R.string.request_storage, Snackbar.LENGTH_LONG) - .setAction(R.string.menu_settings) { - val intent = Intent() - intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS - val uri = Uri.fromParts("package", requireActivity().packageName, null) - intent.data = uri - startActivity(intent) - }.show() + lifecycleScope.launch { + if (hasPermission(Manifest.permission.READ_EXTERNAL_STORAGE)) { + file?.let { file -> + openZimFile(file) + } + } else { + snackBarRoot?.let { snackBarRoot -> + Snackbar.make(snackBarRoot, R.string.request_storage, Snackbar.LENGTH_LONG) + .setAction(R.string.menu_settings) { + val intent = Intent() + intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS + val uri = Uri.fromParts("package", requireActivity().packageName, null) + intent.data = uri + startActivity(intent) + }.show() + } } } } @@ -1733,7 +1740,6 @@ abstract class CoreReaderFragment : private fun restoreDeletedTabs() { if (tempWebViewListForUndo.isNotEmpty()) { - zimReaderContainer?.setZimFile(tempZimFileForUndo) webViewList.addAll(tempWebViewListForUndo) tabsAdapter?.notifyDataSetChanged() snackBarRoot?.let { root -> @@ -2281,7 +2287,7 @@ abstract class CoreReaderFragment : private fun isInvalidJson(jsonString: String?): Boolean = jsonString == null || jsonString == "[]" - protected fun manageExternalLaunchAndRestoringViewState() { + protected suspend fun manageExternalLaunchAndRestoringViewState() { val settings = requireActivity().getSharedPreferences( SharedPreferenceUtil.PREF_KIWIX_MOBILE, 0 @@ -2415,5 +2421,5 @@ abstract class CoreReaderFragment : * KiwixReaderFragment.restoreViewStateOnInvalidJSON) to ensure consistent behavior * when handling invalid JSON scenarios. */ - abstract fun restoreViewStateOnInvalidJSON() + abstract suspend fun restoreViewStateOnInvalidJSON() } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/page/PageFragment.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/page/PageFragment.kt index 825f9f56f4..5a9fb51b99 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/page/PageFragment.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/page/PageFragment.kt @@ -195,14 +195,22 @@ abstract class PageFragment : OnItemClickListener, BaseFragment(), FragmentActiv pageAdapter.items = state.visiblePageItems fragmentPageBinding?.pageSwitch?.isEnabled = !state.isInSelectionState fragmentPageBinding?.noPage?.visibility = if (state.pageItems.isEmpty()) VISIBLE else GONE - if (state.isInSelectionState) { - if (actionMode == null) { - actionMode = - (requireActivity() as AppCompatActivity).startSupportActionMode(actionModeCallback) + when { + state.isInSelectionState -> { + if (actionMode == null) { + actionMode = + (requireActivity() as AppCompatActivity).startSupportActionMode(actionModeCallback) + } + actionMode?.title = getString(R.string.selected_items, state.numberOfSelectedItems()) + } + + state.isLoading -> { + fragmentPageBinding?.loadingZimfileContent?.visibility = View.VISIBLE + } + + else -> { + actionMode?.finish() } - actionMode?.title = getString(R.string.selected_items, state.numberOfSelectedItems()) - } else { - actionMode?.finish() } } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/page/bookmark/viewmodel/BookmarkState.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/page/bookmark/viewmodel/BookmarkState.kt index aea012598e..09ed196162 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/page/bookmark/viewmodel/BookmarkState.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/page/bookmark/viewmodel/BookmarkState.kt @@ -26,7 +26,8 @@ data class BookmarkState( override val pageItems: List, override val showAll: Boolean, override val currentZimId: String?, - override val searchTerm: String = "" + override val searchTerm: String = "", + override val isLoading: Boolean ) : PageState() { override val visiblePageItems: List = filteredPageItems diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/page/bookmark/viewmodel/BookmarkViewModel.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/page/bookmark/viewmodel/BookmarkViewModel.kt index f491ddc782..cee6592faa 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/page/bookmark/viewmodel/BookmarkViewModel.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/page/bookmark/viewmodel/BookmarkViewModel.kt @@ -40,7 +40,15 @@ class BookmarkViewModel @Inject constructor( ) { override fun initialState(): BookmarkState = - BookmarkState(emptyList(), sharedPreferenceUtil.showBookmarksAllBooks, zimReaderContainer.id) + BookmarkState( + emptyList(), + sharedPreferenceUtil.showBookmarksAllBooks, + zimReaderContainer.id, + isLoading = false + ) + + override fun loadData(state: BookmarkState, action: Action.LoadingData): BookmarkState = + state.copy(isLoading = action.isLoading) override fun updatePagesBasedOnFilter( state: BookmarkState, diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/page/history/viewmodel/HistoryState.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/page/history/viewmodel/HistoryState.kt index 10ea0b37a8..fa925723b2 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/page/history/viewmodel/HistoryState.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/page/history/viewmodel/HistoryState.kt @@ -28,7 +28,8 @@ data class HistoryState( override val pageItems: List, override val showAll: Boolean, override val currentZimId: String?, - override val searchTerm: String = "" + override val searchTerm: String = "", + override val isLoading: Boolean ) : PageState() { override val visiblePageItems: List = HeaderizableList(filteredPageItems) diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/page/history/viewmodel/HistoryViewModel.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/page/history/viewmodel/HistoryViewModel.kt index 5a9493cdd4..04892bb173 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/page/history/viewmodel/HistoryViewModel.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/page/history/viewmodel/HistoryViewModel.kt @@ -36,7 +36,15 @@ class HistoryViewModel @Inject constructor( ) : PageViewModel(historyRoomDao, sharedPrefs, zimReaderContainer) { override fun initialState(): HistoryState = - HistoryState(emptyList(), sharedPreferenceUtil.showHistoryAllBooks, zimReaderContainer.id) + HistoryState( + emptyList(), + sharedPreferenceUtil.showHistoryAllBooks, + zimReaderContainer.id, + isLoading = false + ) + + override fun loadData(state: HistoryState, action: Action.LoadingData): HistoryState = + state.copy(isLoading = action.isLoading) override fun updatePagesBasedOnFilter( state: HistoryState, diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/page/notes/viewmodel/NotesState.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/page/notes/viewmodel/NotesState.kt index 948926b77e..286e35ca40 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/page/notes/viewmodel/NotesState.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/page/notes/viewmodel/NotesState.kt @@ -26,7 +26,8 @@ data class NotesState( override val pageItems: List, override val showAll: Boolean, override val currentZimId: String?, - override val searchTerm: String = "" + override val searchTerm: String = "", + override val isLoading: Boolean, ) : PageState() { override val visiblePageItems: List = filteredPageItems diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/page/notes/viewmodel/NotesViewModel.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/page/notes/viewmodel/NotesViewModel.kt index 11c47ecbfc..d4915e548c 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/page/notes/viewmodel/NotesViewModel.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/page/notes/viewmodel/NotesViewModel.kt @@ -1,6 +1,6 @@ /* * Kiwix Android - * Copyright (c) 2020 Kiwix + * 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 @@ -18,6 +18,7 @@ package org.kiwix.kiwixmobile.core.page.notes.viewmodel +import androidx.lifecycle.viewModelScope import kotlinx.coroutines.CoroutineScope import org.kiwix.kiwixmobile.core.dao.NotesRoomDao import org.kiwix.kiwixmobile.core.page.adapter.Page @@ -44,7 +45,15 @@ class NotesViewModel @Inject constructor( } override fun initialState(): NotesState = - NotesState(emptyList(), sharedPreferenceUtil.showNotesAllBooks, zimReaderContainer.id) + NotesState( + emptyList(), + sharedPreferenceUtil.showNotesAllBooks, + zimReaderContainer.id, + isLoading = false + ) + + override fun loadData(state: NotesState, action: Action.LoadingData): NotesState = + state.copy(isLoading = action.isLoading) override fun updatePagesBasedOnFilter(state: NotesState, action: Action.Filter): NotesState = state.copy(searchTerm = action.searchTerm) @@ -70,5 +79,5 @@ class NotesViewModel @Inject constructor( ShowDeleteNotesDialog(effects, state, pageDao, viewModelScope) override fun onItemClick(page: Page) = - ShowOpenNoteDialog(effects, page, zimReaderContainer) + ShowOpenNoteDialog(effects, actions, page, zimReaderContainer, viewModelScope) } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/page/notes/viewmodel/effects/ShowOpenNoteDialog.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/page/notes/viewmodel/effects/ShowOpenNoteDialog.kt index d1ce75018d..637912bfbe 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/page/notes/viewmodel/effects/ShowOpenNoteDialog.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/page/notes/viewmodel/effects/ShowOpenNoteDialog.kt @@ -20,11 +20,14 @@ package org.kiwix.kiwixmobile.core.page.notes.viewmodel.effects import androidx.appcompat.app.AppCompatActivity import io.reactivex.processors.PublishProcessor +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import org.json.JSONArray import org.kiwix.kiwixmobile.core.base.SideEffect import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.cachedComponent import org.kiwix.kiwixmobile.core.page.adapter.Page import org.kiwix.kiwixmobile.core.page.notes.adapter.NoteListItem +import org.kiwix.kiwixmobile.core.page.viewmodel.Action import org.kiwix.kiwixmobile.core.page.viewmodel.effects.OpenNote import org.kiwix.kiwixmobile.core.page.viewmodel.effects.OpenPage import org.kiwix.kiwixmobile.core.reader.ZimFileReader.Companion.CONTENT_PREFIX @@ -41,8 +44,10 @@ import javax.inject.Inject data class ShowOpenNoteDialog( private val effects: PublishProcessor>, + private val actions: PublishProcessor, private val page: Page, - private val zimReaderContainer: ZimReaderContainer + private val zimReaderContainer: ZimReaderContainer, + private val dialogScope: CoroutineScope ) : SideEffect { @Inject lateinit var dialogShower: DialogShower override fun invokeWith(activity: AppCompatActivity) { @@ -51,35 +56,39 @@ data class ShowOpenNoteDialog( ShowNoteDialog, { effects.offer(OpenPage(page, zimReaderContainer)) }, { - val item = page as NoteListItem - // Check if zimFilePath is not null, and then set it in zimReaderContainer. - // For custom apps, we are currently using fileDescriptor, and they only have a single file in them, - // which is already set in zimReaderContainer, so there's no need to set it again. - item.zimFilePath?.let { - val currentZimFilePath = zimReaderContainer.zimCanonicalPath - val file = File(it) - zimReaderContainer.setZimFile(file) - if (zimReaderContainer.zimCanonicalPath != currentZimFilePath) { - // if current zim file is not the same set the main page of that zim file - // so that when we go back it properly loads the article, and do nothing if the - // zim file is same because there might be multiple tabs opened. - val settings = activity.getSharedPreferences( - SharedPreferenceUtil.PREF_KIWIX_MOBILE, - 0 - ) - val editor = settings.edit() - val urls = JSONArray() - val positions = JSONArray() - urls.put(CONTENT_PREFIX + zimReaderContainer.mainPage) - positions.put(0) - editor.putString(TAG_CURRENT_FILE, zimReaderContainer.zimCanonicalPath) - editor.putString(TAG_CURRENT_ARTICLES, "$urls") - editor.putString(TAG_CURRENT_POSITIONS, "$positions") - editor.putInt(TAG_CURRENT_TAB, 0) - editor.apply() + actions.offer(Action.LoadingData(true)) + dialogScope.launch { + val item = page as NoteListItem + // Check if zimFilePath is not null, and then set it in zimReaderContainer. + // For custom apps, we are currently using fileDescriptor, and they only have a single file in them, + // which is already set in zimReaderContainer, so there's no need to set it again. + item.zimFilePath?.let { + val currentZimFilePath = zimReaderContainer.zimCanonicalPath + val file = File(it) + zimReaderContainer.setZimFile(file) + if (zimReaderContainer.zimCanonicalPath != currentZimFilePath) { + // if current zim file is not the same set the main page of that zim file + // so that when we go back it properly loads the article, and do nothing if the + // zim file is same because there might be multiple tabs opened. + val settings = activity.getSharedPreferences( + SharedPreferenceUtil.PREF_KIWIX_MOBILE, + 0 + ) + val editor = settings.edit() + val urls = JSONArray() + val positions = JSONArray() + urls.put(CONTENT_PREFIX + zimReaderContainer.mainPage) + positions.put(0) + editor.putString(TAG_CURRENT_FILE, zimReaderContainer.zimCanonicalPath) + editor.putString(TAG_CURRENT_ARTICLES, "$urls") + editor.putString(TAG_CURRENT_POSITIONS, "$positions") + editor.putInt(TAG_CURRENT_TAB, 0) + editor.apply() + } } + effects.offer(OpenNote(item.noteFilePath, item.zimUrl, item.title)) + actions.offer(Action.LoadingData(false)) } - effects.offer(OpenNote(item.noteFilePath, item.zimUrl, item.title)) } ) } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/page/viewmodel/Action.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/page/viewmodel/Action.kt index 428d02275b..33efc5598b 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/page/viewmodel/Action.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/page/viewmodel/Action.kt @@ -7,7 +7,7 @@ sealed class Action { object ExitActionModeMenu : Action() object UserClickedDeleteButton : Action() object UserClickedDeleteSelectedPages : Action() - + data class LoadingData(val isLoading: Boolean) : Action() data class OnItemClick(val page: Page) : Action() data class OnItemLongClick(val page: Page) : Action() data class UserClickedShowAllToggle(val isChecked: Boolean) : Action() diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/page/viewmodel/PageState.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/page/viewmodel/PageState.kt index 05decb8746..45ab22c72e 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/page/viewmodel/PageState.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/page/viewmodel/PageState.kt @@ -24,6 +24,7 @@ import org.kiwix.kiwixmobile.core.page.bookmark.adapter.LibkiwixBookmarkItem abstract class PageState { abstract val pageItems: List + abstract val isLoading: Boolean val isInSelectionState: Boolean by lazy { pageItems.any(Page::isSelected) } protected val filteredPageItems: List by lazy { pageItems.filter { showAll || it.zimId == currentZimId } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/page/viewmodel/PageViewModel.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/page/viewmodel/PageViewModel.kt index 95379f6c3e..ff9403d978 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/page/viewmodel/PageViewModel.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/page/viewmodel/PageViewModel.kt @@ -88,8 +88,11 @@ abstract class PageViewModel>( is OnItemLongClick -> handleItemLongClick(state, action) is Filter -> updatePagesBasedOnFilter(state, action) is UpdatePages -> updatePages(state, action) + is Action.LoadingData -> loadData(state, action) } + abstract fun loadData(state: S, action: Action.LoadingData): S + abstract fun updatePagesBasedOnFilter(state: S, action: Filter): S abstract fun updatePages(state: S, action: UpdatePages): S diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/reader/ZimFileReader.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/reader/ZimFileReader.kt index 43cda59df5..d4e0fa75c2 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/reader/ZimFileReader.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/reader/ZimFileReader.kt @@ -234,24 +234,29 @@ class ZimFileReader constructor( @Suppress("UnreachableCode", "NestedBlockDepth", "ReturnCount") private fun loadContent(uri: String, extension: String): InputStream? { - val item = getItem(uri) - if (compressedExtensions.any { it != extension }) { - item?.size?.let { - // Check if the item size exceeds 1 MB - if (it / Kb > 1024) { - // Retrieve direct access information for the item - val infoPair = getDirectAccessInfoOfItem(item, uri) - val file = infoPair?.filename?.let(::File) - // If no file found or file does not exist, return input stream from item data - if (infoPair == null || file == null || !file.exists()) { - return@loadContent ByteArrayInputStream(item.data?.data) + try { + val item = getItem(uri) + if (compressedExtensions.any { it != extension }) { + item?.size?.let { + // Check if the item size exceeds 1 MB + if (it / Kb > 1024) { + // Retrieve direct access information for the item + val infoPair = getDirectAccessInfoOfItem(item, uri) + val file = infoPair?.filename?.let(::File) + // If no file found or file does not exist, return input stream from item data + if (infoPair == null || file == null || !file.exists()) { + return@loadContent ByteArrayInputStream(item.data?.data) + } + // Return the input stream from the direct access information + return@loadContent getInputStreamFromDirectAccessInfo(item, file, infoPair) } - // Return the input stream from the direct access information - return@loadContent getInputStreamFromDirectAccessInfo(item, file, infoPair) } } + return loadContent(item, uri) + } catch (exception: Exception) { + Log.e("ZimFileReader", "exception caught while loading content$exception") + return null } - return loadContent(item, uri) } fun getMimeTypeFromUrl(uri: String): String? = getItem(uri)?.mimetype diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/reader/ZimReaderContainer.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/reader/ZimReaderContainer.kt index 46027b840f..a6e95f46d2 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/reader/ZimReaderContainer.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/reader/ZimReaderContainer.kt @@ -19,7 +19,9 @@ package org.kiwix.kiwixmobile.core.reader import android.content.res.AssetFileDescriptor import android.webkit.WebResourceResponse +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext import org.kiwix.kiwixmobile.core.extensions.isFileExist import org.kiwix.kiwixmobile.core.reader.ZimFileReader.Factory import java.io.File @@ -35,21 +37,21 @@ class ZimReaderContainer @Inject constructor(private val zimFileReaderFactory: F field = value } - fun setZimFile(file: File?) { + suspend fun setZimFile(file: File?) { if (file?.canonicalPath == zimFileReader?.zimFile?.canonicalPath) { return } - zimFileReader = runBlocking { + zimFileReader = withContext(Dispatchers.IO) { if (file?.isFileExist() == true) zimFileReaderFactory.create(file) else null } } - fun setZimFileDescriptor( + suspend fun setZimFileDescriptor( assetFileDescriptorList: List, filePath: String? = null ) { - zimFileReader = runBlocking { + zimFileReader = withContext(Dispatchers.IO) { if (assetFileDescriptorList.isNotEmpty() && assetFileDescriptorList[0].parcelFileDescriptor.fileDescriptor.valid() ) diff --git a/core/src/main/res/layout/fragment_page.xml b/core/src/main/res/layout/fragment_page.xml index bdb365b429..6a14b36c8b 100644 --- a/core/src/main/res/layout/fragment_page.xml +++ b/core/src/main/res/layout/fragment_page.xml @@ -38,6 +38,17 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/app_bar" /> + + withContext(Dispatchers.IO) { + try { + openZimFileInReader(assetFileDescriptor) + } catch (e: Exception) { + throw RuntimeException("Unable to get fileDescriptor from file. Original exception", e) + } + } + } ?: throw RuntimeException("Unable to get fileDescriptor from file.") } - private fun openZimFileInReader(assetFileDescriptor: AssetFileDescriptor) { - UiThreadStatement.runOnUiThread { + private suspend fun openZimFileInReader(assetFileDescriptor: AssetFileDescriptor) { + withContext(Dispatchers.Main) { val navHostFragment: NavHostFragment = customMainActivity.supportFragmentManager .findFragmentById( @@ -266,7 +274,9 @@ class SearchFragmentTestForCustomApp { ) as NavHostFragment val customReaderFragment = navHostFragment.childFragmentManager.fragments[0] as CustomReaderFragment - customReaderFragment.openZimFile(null, true, listOf(assetFileDescriptor)) + withContext(Dispatchers.IO) { + customReaderFragment.openZimFile(null, true, listOf(assetFileDescriptor)) + } } } 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 9321cd0858..fd3f6239bd 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 @@ -29,7 +29,11 @@ import android.widget.ImageView import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar import androidx.drawerlayout.widget.DrawerLayout +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.findNavController +import kotlinx.coroutines.launch import org.kiwix.kiwixmobile.core.R.dimen import org.kiwix.kiwixmobile.core.base.BaseActivity import org.kiwix.kiwixmobile.core.extensions.getResizedDrawable @@ -68,23 +72,26 @@ class CustomReaderFragment : CoreReaderFragment() { if (enforcedLanguage()) { return } - - if (isAdded) { - setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED) - if (BuildConfig.DISABLE_SIDEBAR) { - val toolbarToc = activity?.findViewById(R.id.bottom_toolbar_toc) - toolbarToc?.isEnabled = false - } - with(activity as AppCompatActivity) { - supportActionBar?.setDisplayHomeAsUpEnabled(true) - toolbar?.let(::setUpDrawerToggle) - } - loadPageFromNavigationArguments() - if (BuildConfig.DISABLE_EXTERNAL_LINK) { - // If "external links" are disabled in a custom app, - // this sets the shared preference to not show the external link popup - // when opening external links. - sharedPreferenceUtil?.putPrefExternalLinkPopup(false) + lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { + if (isAdded) { + setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED) + if (BuildConfig.DISABLE_SIDEBAR) { + val toolbarToc = activity?.findViewById(R.id.bottom_toolbar_toc) + toolbarToc?.isEnabled = false + } + with(activity as AppCompatActivity) { + supportActionBar?.setDisplayHomeAsUpEnabled(true) + toolbar?.let(::setUpDrawerToggle) + } + loadPageFromNavigationArguments() + if (BuildConfig.DISABLE_EXTERNAL_LINK) { + // If "external links" are disabled in a custom app, + // this sets the shared preference to not show the external link popup + // when opening external links. + sharedPreferenceUtil?.putPrefExternalLinkPopup(false) + } + } } } } @@ -129,7 +136,7 @@ class CustomReaderFragment : CoreReaderFragment() { super.setTabSwitcherVisibility(visibility) } - private fun loadPageFromNavigationArguments() { + private suspend fun loadPageFromNavigationArguments() { val args = CustomReaderFragmentArgs.fromBundle(requireArguments()) if (args.pageUrl.isNotEmpty()) { loadUrlWithCurrentWebview(args.pageUrl) @@ -148,7 +155,7 @@ class CustomReaderFragment : CoreReaderFragment() { * 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. */ - override fun restoreViewStateOnInvalidJSON() { + override suspend fun restoreViewStateOnInvalidJSON() { openHomeScreen() } @@ -179,30 +186,32 @@ class CustomReaderFragment : CoreReaderFragment() { private fun openObbOrZim() { customFileValidator.validate( onFilesFound = { - when (it) { - is ValidationState.HasFile -> { - if (it.assetFileDescriptorList.isNotEmpty()) { - openZimFile(null, true, it.assetFileDescriptorList) - } else { - openZimFile(it.file, true) + lifecycleScope.launch { + when (it) { + is ValidationState.HasFile -> { + if (it.assetFileDescriptorList.isNotEmpty()) { + openZimFile(null, true, it.assetFileDescriptorList) + } else { + openZimFile(it.file, true) + } + // Save book in the database to display it in `ZimHostFragment`. + zimReaderContainer?.zimFileReader?.let { zimFileReader -> + // Check if the file is not null. If the file is null, + // it means we have created zimFileReader with a fileDescriptor, + // so we create a demo file to save it in the database for display on the `ZimHostFragment`. + val file = it.file ?: createDemoFile() + val bookOnDisk = BookOnDisk(file, zimFileReader) + repositoryActions?.saveBook(bookOnDisk) + } } - // Save book in the database to display it in `ZimHostFragment`. - zimReaderContainer?.zimFileReader?.let { zimFileReader -> - // Check if the file is not null. If the file is null, - // it means we have created zimFileReader with a fileDescriptor, - // so we create a demo file to save it in the database for display on the `ZimHostFragment`. - val file = it.file ?: createDemoFile() - val bookOnDisk = BookOnDisk(file, zimFileReader) - repositoryActions?.saveBook(bookOnDisk) + + is ValidationState.HasBothFiles -> { + it.zimFile.delete() + openZimFile(it.obbFile, true) } - } - is ValidationState.HasBothFiles -> { - it.zimFile.delete() - openZimFile(it.obbFile, true) + else -> {} } - - else -> {} } }, onNoFilesFound = {