Fixed: Pressing the back button no longer reopens the Search fragment when you’re on the Reader fragment and navigated there from Search (which was happening before).

* Fixed: Pressing the back button now correctly closes the left drawer when it’s open on the Local Library or Online fragments.
* Introduced: A common mechanism to support back press handling across all fragments and the activity, and added support for "Periodic back navigation".
This commit is contained in:
MohitMaliFtechiz 2025-08-01 17:54:39 +05:30
parent 51bd9e7908
commit 8112632c80
14 changed files with 175 additions and 209 deletions

View File

@ -51,11 +51,9 @@ import org.kiwix.kiwixmobile.core.main.CoreMainActivity
import org.kiwix.kiwixmobile.core.utils.LanguageUtils.Companion.handleLocaleChange import org.kiwix.kiwixmobile.core.utils.LanguageUtils.Companion.handleLocaleChange
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
import org.kiwix.kiwixmobile.core.utils.TestingUtils.COMPOSE_TEST_RULE_ORDER import org.kiwix.kiwixmobile.core.utils.TestingUtils.COMPOSE_TEST_RULE_ORDER
import org.kiwix.kiwixmobile.core.utils.TestingUtils.RETRY_RULE_ORDER
import org.kiwix.kiwixmobile.main.KiwixMainActivity import org.kiwix.kiwixmobile.main.KiwixMainActivity
import org.kiwix.kiwixmobile.main.topLevel import org.kiwix.kiwixmobile.main.topLevel
import org.kiwix.kiwixmobile.nav.destination.library.library import org.kiwix.kiwixmobile.nav.destination.library.library
import org.kiwix.kiwixmobile.testutils.RetryRule
import org.kiwix.kiwixmobile.testutils.TestUtils import org.kiwix.kiwixmobile.testutils.TestUtils
import org.kiwix.kiwixmobile.testutils.TestUtils.closeSystemDialogs import org.kiwix.kiwixmobile.testutils.TestUtils.closeSystemDialogs
import org.kiwix.kiwixmobile.testutils.TestUtils.isSystemUINotRespondingDialogVisible import org.kiwix.kiwixmobile.testutils.TestUtils.isSystemUINotRespondingDialogVisible
@ -65,9 +63,9 @@ import java.util.concurrent.TimeUnit
@LargeTest @LargeTest
class DownloadTest : BaseActivityTest() { class DownloadTest : BaseActivityTest() {
@Rule(order = RETRY_RULE_ORDER) // @Rule(order = RETRY_RULE_ORDER)
@JvmField // @JvmField
val retryRule = RetryRule() // val retryRule = RetryRule()
@get:Rule(order = COMPOSE_TEST_RULE_ORDER) @get:Rule(order = COMPOSE_TEST_RULE_ORDER)
val composeTestRule = createComposeRule() val composeTestRule = createComposeRule()

View File

@ -18,6 +18,8 @@
package org.kiwix.kiwixmobile.main package org.kiwix.kiwixmobile.main
import androidx.activity.compose.BackHandler
import androidx.activity.compose.LocalActivity
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@ -75,6 +77,7 @@ fun KiwixMainActivityScreen(
val navBackStackEntry by navController.currentBackStackEntryAsState() val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route val currentRoute = navBackStackEntry?.destination?.route
val shouldShowBottomBar = currentRoute in topLevelDestinationsRoute && shouldShowBottomAppBar val shouldShowBottomBar = currentRoute in topLevelDestinationsRoute && shouldShowBottomAppBar
OnUserBackPressed(leftDrawerState, uiCoroutineScope, currentRoute, navController)
KiwixTheme { KiwixTheme {
ModalNavigationDrawer( ModalNavigationDrawer(
drawerState = leftDrawerState, drawerState = leftDrawerState,
@ -119,6 +122,33 @@ fun KiwixMainActivityScreen(
} }
} }
@Composable
private fun OnUserBackPressed(
leftDrawerState: DrawerState,
uiCoroutineScope: CoroutineScope,
currentRoute: String?,
navController: NavHostController
) {
val activity = LocalActivity.current
BackHandler(enabled = true) {
when {
leftDrawerState.isOpen -> uiCoroutineScope.launch { leftDrawerState.close() }
currentRoute == KiwixDestination.Reader.route &&
navController.previousBackStackEntry?.destination?.route != KiwixDestination.Search.route -> {
activity?.finish()
}
else -> {
val popped = navController.popBackStack()
if (!popped) {
activity?.finish()
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun BottomNavigationBar( fun BottomNavigationBar(

View File

@ -66,6 +66,7 @@ import org.kiwix.kiwixmobile.cachedComponent
import org.kiwix.kiwixmobile.core.R.string import org.kiwix.kiwixmobile.core.R.string
import org.kiwix.kiwixmobile.core.base.BaseActivity import org.kiwix.kiwixmobile.core.base.BaseActivity
import org.kiwix.kiwixmobile.core.base.BaseFragment import org.kiwix.kiwixmobile.core.base.BaseFragment
import org.kiwix.kiwixmobile.core.base.FragmentActivityExtensions
import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.isManageExternalStoragePermissionGranted import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.isManageExternalStoragePermissionGranted
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.navigate import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.navigate
@ -200,7 +201,9 @@ class LocalLibraryFragment : BaseFragment(), CopyMoveFileHandler.FileCopyMoveCal
onMultiSelect = { offerAction(RequestSelect(it)) }, onMultiSelect = { offerAction(RequestSelect(it)) },
onRefresh = { onSwipeRefresh() }, onRefresh = { onSwipeRefresh() },
onDownloadButtonClick = { downloadBookButtonClick() }, onDownloadButtonClick = { downloadBookButtonClick() },
bottomAppBarScrollBehaviour = (requireActivity() as CoreMainActivity).bottomAppBarScrollBehaviour bottomAppBarScrollBehaviour = (requireActivity() as CoreMainActivity).bottomAppBarScrollBehaviour,
navHostController = (requireActivity() as CoreMainActivity).navController,
onUserBackPressed = { onUserBackPressed() }
) { ) {
NavigationIcon( NavigationIcon(
iconItem = IconItem.Vector(Icons.Filled.Menu), iconItem = IconItem.Vector(Icons.Filled.Menu),
@ -213,12 +216,16 @@ class LocalLibraryFragment : BaseFragment(), CopyMoveFileHandler.FileCopyMoveCal
} }
} }
private fun onUserBackPressed(): FragmentActivityExtensions.Super {
val coreMainActivity = (activity as? CoreMainActivity)
if (coreMainActivity?.navigationDrawerIsOpen() == true) {
coreMainActivity.closeNavigationDrawer()
return FragmentActivityExtensions.Super.ShouldNotCall
}
return FragmentActivityExtensions.Super.ShouldCall
}
private fun navigationIconClick() { private fun navigationIconClick() {
// Manually handle the navigation open/close.
// Since currently we are using the view based navigation drawer in other screens.
// Once we fully migrate to jetpack compose we will refactor this code to use the
// compose navigation.
// TODO Replace with compose based navigation when migration is done.
val activity = activity as CoreMainActivity val activity = activity as CoreMainActivity
if (activity.navigationDrawerIsOpen()) { if (activity.navigationDrawerIsOpen()) {
activity.closeNavigationDrawer() activity.closeNavigationDrawer()

View File

@ -47,9 +47,12 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.navigation.NavHostController
import org.kiwix.kiwixmobile.R.string import org.kiwix.kiwixmobile.R.string
import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.base.FragmentActivityExtensions
import org.kiwix.kiwixmobile.core.main.reader.CONTENT_LOADING_PROGRESSBAR_TESTING_TAG import org.kiwix.kiwixmobile.core.main.reader.CONTENT_LOADING_PROGRESSBAR_TESTING_TAG
import org.kiwix.kiwixmobile.core.main.reader.OnBackPressed
import org.kiwix.kiwixmobile.core.ui.components.ContentLoadingProgressBar import org.kiwix.kiwixmobile.core.ui.components.ContentLoadingProgressBar
import org.kiwix.kiwixmobile.core.ui.components.KiwixAppBar import org.kiwix.kiwixmobile.core.ui.components.KiwixAppBar
import org.kiwix.kiwixmobile.core.ui.components.KiwixButton import org.kiwix.kiwixmobile.core.ui.components.KiwixButton
@ -73,7 +76,7 @@ const val BOOK_LIST_TESTING_TAG = "bookListTestingTag"
const val SELECT_FILE_BUTTON_TESTING_TAG = "selectFileButtonTestingTag" const val SELECT_FILE_BUTTON_TESTING_TAG = "selectFileButtonTestingTag"
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Suppress("ComposableLambdaParameterNaming") @Suppress("ComposableLambdaParameterNaming", "LongParameterList")
@Composable @Composable
fun LocalLibraryScreen( fun LocalLibraryScreen(
state: LocalLibraryScreenState, state: LocalLibraryScreenState,
@ -85,6 +88,8 @@ fun LocalLibraryScreen(
onLongClick: ((BookOnDisk) -> Unit)? = null, onLongClick: ((BookOnDisk) -> Unit)? = null,
onMultiSelect: ((BookOnDisk) -> Unit)? = null, onMultiSelect: ((BookOnDisk) -> Unit)? = null,
bottomAppBarScrollBehaviour: BottomAppBarScrollBehavior?, bottomAppBarScrollBehaviour: BottomAppBarScrollBehavior?,
onUserBackPressed: () -> FragmentActivityExtensions.Super,
navHostController: NavHostController,
navigationIcon: @Composable () -> Unit navigationIcon: @Composable () -> Unit
) { ) {
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
@ -117,6 +122,7 @@ fun LocalLibraryScreen(
.fillMaxSize() .fillMaxSize()
.padding(contentPadding) .padding(contentPadding)
) { ) {
OnBackPressed(onUserBackPressed, navHostController)
if (state.scanningProgressItem.first) { if (state.scanningProgressItem.first) {
ContentLoadingProgressBar( ContentLoadingProgressBar(
modifier = Modifier.testTag(CONTENT_LOADING_PROGRESSBAR_TESTING_TAG), modifier = Modifier.testTag(CONTENT_LOADING_PROGRESSBAR_TESTING_TAG),

View File

@ -29,7 +29,6 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
@ -271,7 +270,9 @@ class OnlineLibraryFragment : BaseFragment(), FragmentActivityExtensions {
onClick = { navigationIconClick(onlineLibraryScreenState.value.value.isSearchActive) } onClick = { navigationIconClick(onlineLibraryScreenState.value.value.isSearchActive) }
) )
}, },
bottomAppBarScrollBehaviour = (requireActivity() as CoreMainActivity).bottomAppBarScrollBehaviour bottomAppBarScrollBehaviour = (requireActivity() as CoreMainActivity).bottomAppBarScrollBehaviour,
navHostController = (requireActivity() as CoreMainActivity).navController,
onUserBackPressed = { onUserBackPressed() }
) )
DialogHost(alertDialogShower) DialogHost(alertDialogShower)
} }
@ -460,8 +461,13 @@ class OnlineLibraryFragment : BaseFragment(), FragmentActivityExtensions {
composeView = null composeView = null
} }
override fun onBackPressed(activity: AppCompatActivity): FragmentActivityExtensions.Super { @Suppress("ReturnCount")
if (isKeyboardVisible() || onlineLibraryScreenState.value.value.isSearchActive) { private fun onUserBackPressed(): FragmentActivityExtensions.Super {
val coreMainActivity = (activity as? CoreMainActivity)
if (coreMainActivity?.navigationDrawerIsOpen() == true) {
coreMainActivity.closeNavigationDrawer()
return FragmentActivityExtensions.Super.ShouldNotCall
} else if (isKeyboardVisible() || onlineLibraryScreenState.value.value.isSearchActive) {
closeKeyboard() closeKeyboard()
closeSearch() closeSearch()
return FragmentActivityExtensions.Super.ShouldNotCall return FragmentActivityExtensions.Super.ShouldNotCall

View File

@ -56,12 +56,15 @@ import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.navigation.NavHostController
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import org.kiwix.kiwixmobile.core.R.string import org.kiwix.kiwixmobile.core.R.string
import org.kiwix.kiwixmobile.core.base.FragmentActivityExtensions
import org.kiwix.kiwixmobile.core.downloader.downloadManager.FIVE import org.kiwix.kiwixmobile.core.downloader.downloadManager.FIVE
import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO
import org.kiwix.kiwixmobile.core.extensions.hideKeyboardOnLazyColumnScroll import org.kiwix.kiwixmobile.core.extensions.hideKeyboardOnLazyColumnScroll
import org.kiwix.kiwixmobile.core.main.reader.OnBackPressed
import org.kiwix.kiwixmobile.core.ui.components.ContentLoadingProgressBar import org.kiwix.kiwixmobile.core.ui.components.ContentLoadingProgressBar
import org.kiwix.kiwixmobile.core.ui.components.KiwixAppBar import org.kiwix.kiwixmobile.core.ui.components.KiwixAppBar
import org.kiwix.kiwixmobile.core.ui.components.KiwixSearchView import org.kiwix.kiwixmobile.core.ui.components.KiwixSearchView
@ -99,6 +102,8 @@ fun OnlineLibraryScreen(
actionMenuItems: List<ActionMenuItem>, actionMenuItems: List<ActionMenuItem>,
listState: LazyListState, listState: LazyListState,
bottomAppBarScrollBehaviour: BottomAppBarScrollBehavior?, bottomAppBarScrollBehaviour: BottomAppBarScrollBehavior?,
onUserBackPressed: () -> FragmentActivityExtensions.Super,
navHostController: NavHostController,
navigationIcon: @Composable () -> Unit, navigationIcon: @Composable () -> Unit,
) { ) {
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
@ -135,6 +140,7 @@ fun OnlineLibraryScreen(
end = paddingValues.calculateEndPadding(LocalLayoutDirection.current), end = paddingValues.calculateEndPadding(LocalLayoutDirection.current),
) )
) { ) {
OnBackPressed(onUserBackPressed, navHostController)
OnlineLibraryScreenContent(state, listState) OnlineLibraryScreenContent(state, listState)
} }
} }

View File

@ -52,7 +52,7 @@
<application <application
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="false" android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:hasFragileUserData="true" android:hasFragileUserData="true"

View File

@ -24,7 +24,6 @@ import android.os.Process
import android.view.ActionMode import android.view.ActionMode
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.material3.BottomAppBarScrollBehavior import androidx.compose.material3.BottomAppBarScrollBehavior
@ -38,7 +37,6 @@ import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavDirections import androidx.navigation.NavDirections
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.NavOptions import androidx.navigation.NavOptions
import com.google.android.material.navigation.NavigationView
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -47,7 +45,6 @@ import org.kiwix.kiwixmobile.core.CoreApp
import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.base.BaseActivity import org.kiwix.kiwixmobile.core.base.BaseActivity
import org.kiwix.kiwixmobile.core.base.FragmentActivityExtensions import org.kiwix.kiwixmobile.core.base.FragmentActivityExtensions
import org.kiwix.kiwixmobile.core.base.FragmentActivityExtensions.Super.ShouldCall
import org.kiwix.kiwixmobile.core.data.remote.ObjectBoxToLibkiwixMigrator import org.kiwix.kiwixmobile.core.data.remote.ObjectBoxToLibkiwixMigrator
import org.kiwix.kiwixmobile.core.data.remote.ObjectBoxToRoomMigrator import org.kiwix.kiwixmobile.core.data.remote.ObjectBoxToRoomMigrator
import org.kiwix.kiwixmobile.core.di.components.CoreActivityComponent import org.kiwix.kiwixmobile.core.di.components.CoreActivityComponent
@ -144,6 +141,11 @@ abstract class CoreMainActivity : BaseActivity(), WebViewProvider {
*/ */
val enableLeftDrawer = mutableStateOf(true) val enableLeftDrawer = mutableStateOf(true)
/**
* For managing the back press of fragments.
*/
val customBackHandler = mutableStateOf<(() -> FragmentActivityExtensions.Super)?>(null)
/** /**
* For managing the the showing/hiding the bottomAppBar when scrolling. * For managing the the showing/hiding the bottomAppBar when scrolling.
*/ */
@ -197,7 +199,6 @@ abstract class CoreMainActivity : BaseActivity(), WebViewProvider {
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
createApplicationShortcuts() createApplicationShortcuts()
} }
handleBackPressed()
leftDrawerMenu.addAll(leftNavigationDrawerMenuItems) leftDrawerMenu.addAll(leftNavigationDrawerMenuItems)
} }
@ -231,11 +232,6 @@ abstract class CoreMainActivity : BaseActivity(), WebViewProvider {
} }
} }
override fun onDestroy() {
onBackPressedCallBack.remove()
super.onDestroy()
}
override fun onStop() { override fun onStop() {
startMonitoringDownloads() startMonitoringDownloads()
downloadMonitor.stopListeningDownloads() downloadMonitor.stopListeningDownloads()
@ -253,11 +249,6 @@ abstract class CoreMainActivity : BaseActivity(), WebViewProvider {
} }
} }
@Suppress("UnusedParameter")
private fun NavigationView.setLockMode(lockMode: Int) {
// drawerContainerLayout.setDrawerLockMode(lockMode, this)
}
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
override fun onRequestPermissionsResult( override fun onRequestPermissionsResult(
requestCode: Int, requestCode: Int,
@ -339,41 +330,6 @@ abstract class CoreMainActivity : BaseActivity(), WebViewProvider {
externalLinkOpener.openExternalUrl(KIWIX_SUPPORT_URL.toUri().browserIntent(), false) externalLinkOpener.openExternalUrl(KIWIX_SUPPORT_URL.toUri().browserIntent(), false)
} }
private fun handleBackPressed() {
onBackPressedDispatcher.addCallback(this, onBackPressedCallBack)
}
private val onBackPressedCallBack =
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (navigationDrawerIsOpen()) {
closeNavigationDrawer()
return
}
if (activeFragments().filterIsInstance<FragmentActivityExtensions>().isEmpty()) {
isEnabled = false
return onBackPressedDispatcher.onBackPressed().also {
isEnabled = true
}
}
activeFragments().filterIsInstance<FragmentActivityExtensions>().forEach {
if (it.onBackPressed(this@CoreMainActivity) == ShouldCall) {
if (navController.currentDestination?.route?.equals(readerFragmentRoute) == true &&
navController.previousBackStackEntry?.destination
?.route?.equals(searchFragmentRoute) == false
) {
drawerToggle = null
finish()
} else {
isEnabled = false
onBackPressedDispatcher.onBackPressed()
isEnabled = true
}
}
}
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
if (activeFragments().filterIsInstance<FragmentActivityExtensions>().isEmpty()) { if (activeFragments().filterIsInstance<FragmentActivityExtensions>().isEmpty()) {
return super.onCreateOptionsMenu(menu) return super.onCreateOptionsMenu(menu)

View File

@ -1,125 +0,0 @@
/*
* Kiwix Android
* Copyright (c) 2020 Kiwix <android.kiwix.org>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.kiwix.kiwixmobile.core.main
import android.graphics.Typeface
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView.Adapter
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.base.adapter.BaseViewHolder
import org.kiwix.kiwixmobile.core.databinding.SectionListBinding
class TableDrawerAdapter constructor(
private val listener: TableClickListener
) : Adapter<ViewHolder>() {
private var title: String = ""
private val sections: MutableList<DocumentSection> = mutableListOf()
fun setSections(sections: List<DocumentSection>) {
this.sections.clear()
this.sections.addAll(sections)
}
fun setTitle(title: String) {
this.title = title
}
override fun getItemViewType(position: Int): Int {
return if (position == 0) {
0
} else {
1
}
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): ViewHolder {
val binding = SectionListBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return if (viewType == 0) {
HeaderTableDrawerViewHolder(binding)
} else {
SectionTableDrawerViewHolder(binding)
}
}
override fun getItemCount(): Int = sections.size + 1
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
when (holder) {
is HeaderTableDrawerViewHolder -> {
holder.bind(title)
holder.itemView.setOnClickListener(listener::onHeaderClick)
}
is SectionTableDrawerViewHolder -> {
val titleAdjustedPosition = position - 1
holder.bind(sections[titleAdjustedPosition])
holder.itemView.setOnClickListener {
listener.onSectionClick(it, titleAdjustedPosition)
}
}
else -> {
throw IllegalStateException("Unknown ViewHolder $holder found")
}
}
}
interface TableClickListener {
fun onHeaderClick(view: View?)
fun onSectionClick(view: View?, position: Int)
}
class HeaderTableDrawerViewHolder(private val sectionListBinding: SectionListBinding) :
BaseViewHolder<String>(sectionListBinding.root) {
override fun bind(item: String) {
val context = itemView.context
sectionListBinding.titleText.typeface = Typeface.DEFAULT_BOLD
sectionListBinding.titleText.text =
when {
item.isNotEmpty() -> item
context is WebViewProvider ->
context.getCurrentWebView()?.title
?: context.getString(R.string.no_section_info)
else -> context.getString(R.string.no_section_info)
}
}
}
class SectionTableDrawerViewHolder(private val sectionListBinding: SectionListBinding) :
BaseViewHolder<TableDrawerAdapter.DocumentSection>(sectionListBinding.root) {
override fun bind(
item: TableDrawerAdapter.DocumentSection
) {
val context = itemView.context
val padding =
((item.level - 1) * context.resources.getDimension(R.dimen.title_text_padding)).toInt()
sectionListBinding.titleText.setPadding(padding, 0, 0, 0)
sectionListBinding.titleText.text = item.title
}
}
data class DocumentSection(var title: String, var id: String, var level: Int)
}

View File

@ -474,7 +474,9 @@ abstract class CoreReaderFragment :
}, },
mainActivityBottomAppBarScrollBehaviour = (requireActivity() as CoreMainActivity).bottomAppBarScrollBehaviour, mainActivityBottomAppBarScrollBehaviour = (requireActivity() as CoreMainActivity).bottomAppBarScrollBehaviour,
documentSections = documentSections, documentSections = documentSections,
showTableOfContentDrawer = shouldTableOfContentDrawer showTableOfContentDrawer = shouldTableOfContentDrawer,
onUserBackPressed = { onUserBackPressed(requireActivity() as CoreMainActivity) },
navHostController = (requireActivity() as CoreMainActivity).navController
) )
DialogHost(alertDialogShower as AlertDialogShower) DialogHost(alertDialogShower as AlertDialogShower)
} }
@ -842,9 +844,16 @@ abstract class CoreReaderFragment :
shouldTableOfContentDrawer.update { true } shouldTableOfContentDrawer.update { true }
} }
@Suppress("ReturnCount", "NestedBlockDepth") @Suppress("ReturnCount", "NestedBlockDepth", "LongMethod", "CyclomaticComplexMethod")
override fun onBackPressed(activity: AppCompatActivity): FragmentActivityExtensions.Super { private fun onUserBackPressed(coreMainActivity: CoreMainActivity): FragmentActivityExtensions.Super {
when { when {
coreMainActivity.leftDrawerState.isOpen -> {
coreMainActivity.uiCoroutineScope.launch {
coreMainActivity.leftDrawerState.close()
}
return FragmentActivityExtensions.Super.ShouldNotCall
}
readerScreenState.value.showTabSwitcher -> { readerScreenState.value.showTabSwitcher -> {
selectTab( selectTab(
if (currentWebViewIndex < webViewList.size) { if (currentWebViewIndex < webViewList.size) {
@ -893,7 +902,12 @@ abstract class CoreReaderFragment :
isHomePageOfServiceWorkerZimFiles(url, webViewBackWordHistoryList) isHomePageOfServiceWorkerZimFiles(url, webViewBackWordHistoryList)
) { ) {
// If it is the last page that is showing to the user, then exit the application. // If it is the last page that is showing to the user, then exit the application.
return@onBackPressed FragmentActivityExtensions.Super.ShouldCall if (coreMainActivity.navController.previousBackStackEntry?.destination?.route !=
coreMainActivity.searchFragmentRoute
) {
activity?.finish()
}
return FragmentActivityExtensions.Super.ShouldCall
} }
} }
// Otherwise, go to the previous page. // Otherwise, go to the previous page.

View File

@ -22,6 +22,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
@ -114,9 +115,11 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import androidx.navigation.NavHostController
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.base.FragmentActivityExtensions
import org.kiwix.kiwixmobile.core.downloader.downloadManager.HUNDERED import org.kiwix.kiwixmobile.core.downloader.downloadManager.HUNDERED
import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO
import org.kiwix.kiwixmobile.core.extensions.update import org.kiwix.kiwixmobile.core.extensions.update
@ -177,13 +180,15 @@ const val READER_BOTTOM_BAR_TABLE_CONTENT_BUTTON_TESTING_TAG =
"readerBottomBarTableContentButtonTestingTag" "readerBottomBarTableContentButtonTestingTag"
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Suppress("ComposableLambdaParameterNaming", "LongMethod") @Suppress("ComposableLambdaParameterNaming", "LongMethod", "LongParameterList")
@Composable @Composable
fun ReaderScreen( fun ReaderScreen(
state: ReaderScreenState, state: ReaderScreenState,
actionMenuItems: List<ActionMenuItem>, actionMenuItems: List<ActionMenuItem>,
showTableOfContentDrawer: MutableState<Boolean>, showTableOfContentDrawer: MutableState<Boolean>,
documentSections: MutableList<DocumentSection>?, documentSections: MutableList<DocumentSection>?,
onUserBackPressed: () -> FragmentActivityExtensions.Super,
navHostController: NavHostController,
mainActivityBottomAppBarScrollBehaviour: BottomAppBarScrollBehavior?, mainActivityBottomAppBarScrollBehaviour: BottomAppBarScrollBehavior?,
navigationIcon: @Composable () -> Unit navigationIcon: @Composable () -> Unit
) { ) {
@ -215,6 +220,7 @@ fun ReaderScreen(
} }
.semantics { testTag = READER_SCREEN_TESTING_TAG } .semantics { testTag = READER_SCREEN_TESTING_TAG }
) { paddingValues -> ) { paddingValues ->
OnBackPressed(onUserBackPressed, navHostController)
ReaderContentLayout( ReaderContentLayout(
state, state,
Modifier.padding(paddingValues), Modifier.padding(paddingValues),
@ -251,6 +257,19 @@ fun ReaderScreen(
} }
} }
@Composable
fun OnBackPressed(
onUserBackPressed: () -> FragmentActivityExtensions.Super,
navHostController: NavHostController
) {
BackHandler(enabled = true) {
val result = onUserBackPressed()
if (result == FragmentActivityExtensions.Super.ShouldCall) {
navHostController.popBackStack()
}
}
}
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Suppress("ComposableLambdaParameterNaming") @Suppress("ComposableLambdaParameterNaming")
@Composable @Composable
@ -731,7 +750,8 @@ private fun BottomAppBarButtonIcon(
onClick = onClick, onClick = onClick,
onLongClick = onLongClick, onLongClick = onLongClick,
enabled = shouldEnable enabled = shouldEnable
).testTag(testingTag), )
.testTag(testingTag),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Icon( Icon(

View File

@ -22,7 +22,7 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.activity.OnBackPressedCallback import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
@ -39,6 +39,7 @@ import kotlinx.coroutines.withContext
import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.base.BaseActivity import org.kiwix.kiwixmobile.core.base.BaseActivity
import org.kiwix.kiwixmobile.core.base.BaseFragment import org.kiwix.kiwixmobile.core.base.BaseFragment
import org.kiwix.kiwixmobile.core.base.FragmentActivityExtensions
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.cachedComponent import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.cachedComponent
import org.kiwix.kiwixmobile.core.extensions.closeKeyboard import org.kiwix.kiwixmobile.core.extensions.closeKeyboard
import org.kiwix.kiwixmobile.core.extensions.coreMainActivity import org.kiwix.kiwixmobile.core.extensions.coreMainActivity
@ -123,6 +124,12 @@ class SearchFragment : BaseFragment() {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
composeView?.apply { composeView?.apply {
setContent { setContent {
DisposableEffect(Unit) {
(activity as CoreMainActivity).customBackHandler.value = { handleBackPress() }
onDispose {
(activity as CoreMainActivity).customBackHandler.value = null
}
}
SearchScreen( SearchScreen(
searchScreenState.value, searchScreenState.value,
actionMenuItems(), actionMenuItems(),
@ -134,7 +141,6 @@ class SearchFragment : BaseFragment() {
searchViewModel.setAlertDialogShower(dialogShower as AlertDialogShower) searchViewModel.setAlertDialogShower(dialogShower as AlertDialogShower)
observeViewModelData() observeViewModelData()
handleSearchArgument() handleSearchArgument()
handleBackPress()
} }
private fun handleSearchArgument() { private fun handleSearchArgument() {
@ -203,15 +209,9 @@ class SearchFragment : BaseFragment() {
} }
} }
private fun handleBackPress() { private fun handleBackPress(): FragmentActivityExtensions.Super {
activity?.onBackPressedDispatcher?.addCallback( goBack()
viewLifecycleOwner, return FragmentActivityExtensions.Super.ShouldCall
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
goBack()
}
}
)
} }
override fun onDestroyView() { override fun onDestroyView() {

View File

@ -71,7 +71,9 @@ class CustomMainActivity : CoreMainActivity() {
leftDrawerContent = leftDrawerMenu, leftDrawerContent = leftDrawerMenu,
topLevelDestinationsRoute = topLevelDestinationsRoute, topLevelDestinationsRoute = topLevelDestinationsRoute,
leftDrawerState = leftDrawerState, leftDrawerState = leftDrawerState,
enableLeftDrawer = enableLeftDrawer.value enableLeftDrawer = enableLeftDrawer.value,
uiCoroutineScope = uiCoroutineScope,
customBackHandler = customBackHandler
) )
DialogHost(alertDialogShower) DialogHost(alertDialogShower)
} }

View File

@ -18,6 +18,8 @@
package org.kiwix.kiwixmobile.custom.main package org.kiwix.kiwixmobile.custom.main
import androidx.activity.compose.BackHandler
import androidx.activity.compose.LocalActivity
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@ -27,10 +29,14 @@ import androidx.compose.material3.DrawerState
import androidx.compose.material3.ModalNavigationDrawer import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.currentBackStackEntryAsState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.kiwix.kiwixmobile.core.base.FragmentActivityExtensions
import org.kiwix.kiwixmobile.core.main.DrawerMenuGroup import org.kiwix.kiwixmobile.core.main.DrawerMenuGroup
import org.kiwix.kiwixmobile.core.main.LeftDrawerMenu import org.kiwix.kiwixmobile.core.main.LeftDrawerMenu
import org.kiwix.kiwixmobile.core.ui.theme.KiwixTheme import org.kiwix.kiwixmobile.core.ui.theme.KiwixTheme
@ -42,9 +48,18 @@ fun CustomMainActivityScreen(
topLevelDestinationsRoute: Set<String>, topLevelDestinationsRoute: Set<String>,
leftDrawerState: DrawerState, leftDrawerState: DrawerState,
enableLeftDrawer: Boolean, enableLeftDrawer: Boolean,
customBackHandler: MutableState<(() -> FragmentActivityExtensions.Super)?>,
uiCoroutineScope: CoroutineScope
) { ) {
val navBackStackEntry by navController.currentBackStackEntryAsState() val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route val currentRoute = navBackStackEntry?.destination?.route
OnUserBackPressed(
leftDrawerState,
uiCoroutineScope,
currentRoute,
navController,
customBackHandler
)
KiwixTheme { KiwixTheme {
ModalNavigationDrawer( ModalNavigationDrawer(
drawerState = leftDrawerState, drawerState = leftDrawerState,
@ -76,3 +91,34 @@ fun CustomMainActivityScreen(
} }
} }
} }
@Composable
private fun OnUserBackPressed(
leftDrawerState: DrawerState,
uiCoroutineScope: CoroutineScope,
currentRoute: String?,
navController: NavHostController,
customBackHandler: MutableState<(() -> FragmentActivityExtensions.Super)?>,
) {
val activity = LocalActivity.current
BackHandler(enabled = true) {
when {
leftDrawerState.isOpen -> uiCoroutineScope.launch { leftDrawerState.close() }
customBackHandler.value?.invoke() == FragmentActivityExtensions.Super.ShouldNotCall -> {
// do nothing since fragment handles the back press.
}
currentRoute == CustomDestination.Reader.route &&
navController.previousBackStackEntry?.destination?.route != CustomDestination.Search.route -> {
activity?.finish()
}
else -> {
val popped = navController.popBackStack()
if (!popped) {
activity?.finish()
}
}
}
}
}