From 5ec6030503f7d08da12ec41acedca530f7292140 Mon Sep 17 00:00:00 2001 From: darken Date: Fri, 17 Apr 2026 07:40:53 +0200 Subject: [PATCH 1/2] General: Fix Settings toolbar title getting stuck on Android 16 back On Android 16 with predictive back, childFragmentManager's back-stack-changed listener fires while the new fragment is already RESUMED but backStackEntryCount has not yet been decremented. The old implementation read the count during the callback and ended up re-applying the previous sub-screen's title over the restored index content. Drive the toolbar from FragmentLifecycleCallbacks.onFragmentResumed instead, and derive title from the child fragment's own arguments rather than a parallel screens list. No back-stack-count lookups means no stale-value race. Closes #2386 --- .../main/ui/settings/SettingsFragment.kt | 127 ++++++++---------- 1 file changed, 56 insertions(+), 71 deletions(-) diff --git a/app/src/main/java/eu/darken/sdmse/main/ui/settings/SettingsFragment.kt b/app/src/main/java/eu/darken/sdmse/main/ui/settings/SettingsFragment.kt index 00790e1e7..41791782f 100644 --- a/app/src/main/java/eu/darken/sdmse/main/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/eu/darken/sdmse/main/ui/settings/SettingsFragment.kt @@ -1,9 +1,10 @@ package eu.darken.sdmse.main.ui.settings import android.os.Bundle -import android.os.Parcelable import android.view.View import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager import androidx.fragment.app.viewModels import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat @@ -14,7 +15,6 @@ import eu.darken.sdmse.common.uix.Fragment2 import eu.darken.sdmse.common.uix.ToolbarHost import eu.darken.sdmse.common.viewbinding.viewBinding import eu.darken.sdmse.databinding.SettingsFragmentBinding -import kotlinx.parcelize.Parcelize @AndroidEntryPoint class SettingsFragment : Fragment2(R.layout.settings_fragment), @@ -27,14 +27,24 @@ class SettingsFragment : Fragment2(R.layout.settings_fragment), override val toolbar: Toolbar get() = ui.toolbar - private val screens = ArrayList() - - @Parcelize - data class Screen( - val fragmentClass: String, - val screenTitle: String?, - val screenSubtitle: String? = null, - ) : Parcelable + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + // Drive the toolbar from fragment lifecycle + fragment arguments. + // We can't trust childFragmentManager.backStackEntryCount inside the resumed + // callback: on Android 16's predictive-back path, the new fragment becomes + // RESUMED before the back stack has actually been decremented, so reading + // the count here would give a stale value. + childFragmentManager.registerFragmentLifecycleCallbacks( + object : FragmentManager.FragmentLifecycleCallbacks() { + override fun onFragmentResumed(fm: FragmentManager, f: Fragment) { + if (f.id == R.id.content_frame && view != null) { + syncToolbarForFragment(f) + } + } + }, + false, + ) + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { EdgeToEdgeHelper(requireActivity()).apply { @@ -42,78 +52,42 @@ class SettingsFragment : Fragment2(R.layout.settings_fragment), insetsPadding(ui.appbarlayout, top = true) } - childFragmentManager.addOnBackStackChangedListener { - val backStackCnt = childFragmentManager.backStackEntryCount - val newScreenInfo = when { - backStackCnt < screens.size -> { - // We popped the backstack, restore the underlying screen infos - // If there are none left, we are at the index again - screens.removeLastOrNull() - screens.lastOrNull() ?: Screen( - fragmentClass = SettingsIndexFragment::class.qualifiedName!!, - screenTitle = getString(eu.darken.sdmse.common.R.string.general_settings_title) - ) - } - - else -> { - // We added the current fragment to the stack, the new fragment's infos were already set, do nothing. - null - } - } - - newScreenInfo?.let { setCurrentScreenInfo(it) } + if (savedInstanceState == null + && childFragmentManager.findFragmentById(R.id.content_frame) == null + ) { + childFragmentManager + .beginTransaction() + .add(R.id.content_frame, SettingsIndexFragment()) + .commit() } - if (savedInstanceState == null) { - val currentFragment = childFragmentManager.findFragmentById(R.id.content_frame) - if (currentFragment == null) { - childFragmentManager - .beginTransaction() - .add(R.id.content_frame, SettingsIndexFragment()) - .commit() - } - } else { - @Suppress("DEPRECATION") - savedInstanceState.getParcelableArrayList(BKEY_SCREEN_INFOS)?.let { - screens.addAll(it) - } - } - - // Always restore toolbar title from current screen state (handles NavComponent view recreation) - screens.lastOrNull()?.let { setCurrentScreenInfo(it) } + // Sync on first view creation AND on NavComponent view recreation. + childFragmentManager.findFragmentById(R.id.content_frame)?.let { syncToolbarForFragment(it) } ui.toolbar.setNavigationOnClickListener { requireActivity().onBackPressedDispatcher.onBackPressed() } super.onViewCreated(view, savedInstanceState) } - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - outState.putParcelableArrayList(BKEY_SCREEN_INFOS, screens) - } - override fun onPreferenceStartFragment(caller: PreferenceFragmentCompat, pref: Preference): Boolean { - val screenInfo = Screen( - fragmentClass = pref.fragment!!, - screenTitle = pref.title?.toString(), - screenSubtitle = ui.toolbar.title?.toString(), - ) - - val args = Bundle().apply { - putAll(pref.extras) - putString(BKEY_SCREEN_TITLE, screenInfo.screenTitle) - } + val title = pref.title?.toString().orEmpty() @Suppress("DEPRECATION") val fragment = childFragmentManager.fragmentFactory .instantiate(this::class.java.classLoader!!, pref.fragment!!) .apply { - arguments = args + arguments = Bundle().apply { + putAll(pref.extras) + putString(BKEY_SCREEN_TITLE, title) + } setTargetFragment(caller, 0) } - setCurrentScreenInfo(screenInfo) - screens.add(screenInfo) + // Update toolbar synchronously before commit so the title matches the transition. + setToolbar( + title = title, + subtitle = getString(eu.darken.sdmse.common.R.string.general_settings_title), + ) childFragmentManager.beginTransaction().apply { replace(R.id.content_frame, fragment) @@ -123,16 +97,27 @@ class SettingsFragment : Fragment2(R.layout.settings_fragment), return true } - - private fun setCurrentScreenInfo(info: Screen) { - ui.toolbar.apply { - title = info.screenTitle - subtitle = info.screenSubtitle + private fun syncToolbarForFragment(f: Fragment) { + val screenTitle = f.arguments?.getString(BKEY_SCREEN_TITLE) + if (screenTitle != null) { + setToolbar( + title = screenTitle, + subtitle = getString(eu.darken.sdmse.common.R.string.general_settings_title), + ) + } else { + setToolbar( + title = getString(eu.darken.sdmse.common.R.string.general_settings_title), + subtitle = null, + ) } } + private fun setToolbar(title: String?, subtitle: String?) { + ui.toolbar.title = title + ui.toolbar.subtitle = subtitle + } + companion object { private const val BKEY_SCREEN_TITLE = "preferenceScreenTitle" - private const val BKEY_SCREEN_INFOS = "preferenceScreenInfos" } } From 866e5cce9363582c6b90869f0175fcd3b285d58f Mon Sep 17 00:00:00 2001 From: darken Date: Fri, 17 Apr 2026 07:49:23 +0200 Subject: [PATCH 2/2] Git: Ignore .claude/scheduled_tasks.lock --- .claude/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.claude/.gitignore b/.claude/.gitignore index 6443c37cd..a6d63ff6f 100644 --- a/.claude/.gitignore +++ b/.claude/.gitignore @@ -1,3 +1,4 @@ settings.local.json tmp/ worktrees/ +scheduled_tasks.lock \ No newline at end of file