fix(android): back focus lost after tabbing out — re-assert overlay back callbacks on resume

With NowPlaying (or an ABS player) open, backgrounding and resuming the app made the next back
press pop the NavHost invisibly to Home before dismissing the overlay. Returning from the
background re-registers the NavController's own OnBackPressedCallback on top of the dispatcher,
stealing back-priority from the overlays.

AppNavigation now bumps a resumeTick on Lifecycle.Event.ON_RESUME and keys every overlay
DisposableEffect (NowPlaying, ABS player, ABS item, Erkunden overlay) on resumeTick +
currentBackEntry, so each overlay re-adds its callback above the NavController after a resume or
navigation. Needs on-device verification.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Opus 4.8 committed 2026-08-06 10:00:04 +02:00
1 parent 17bb527e1d
commit e69fbaf75e
3 files changed
+21 -5

No files matched your search

@@ -23,6 +23,8 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LifecycleEventEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.*
import androidx.navigation.compose.*
@@ -112,7 +114,15 @@ fun AppNavigation() {
var exploreOverlayBack by remember { mutableStateOf<(() -> Unit)?>(null) }
val backDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
DisposableEffect(showNowPlaying, backDispatcher) {
val currentBackEntry by navController.currentBackStackEntryAsState()
// Returning from the background re-registers the NavController's OWN back callback on top of the
// dispatcher, which stole back-priority from the NowPlaying/ABS overlays: pressing back then popped
// the NavHost (invisibly, all the way to Home) before dismissing the overlay. Bump this tick on
// ON_RESUME (and re-key on navigation) so the overlay callbacks below re-add above it again.
var resumeTick by remember { mutableIntStateOf(0) }
LifecycleEventEffect(Lifecycle.Event.ON_RESUME) { resumeTick++ }
DisposableEffect(showNowPlaying, backDispatcher, currentBackEntry, resumeTick) {
if (!showNowPlaying || backDispatcher == null) return@DisposableEffect onDispose {}
val cb = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() { showNowPlaying = false }
@@ -120,7 +130,7 @@ fun AppNavigation() {
backDispatcher.addCallback(cb)
onDispose { cb.remove() }
}
DisposableEffect(showAbsPlayer, backDispatcher) {
DisposableEffect(showAbsPlayer, backDispatcher, currentBackEntry, resumeTick) {
if (!showAbsPlayer || backDispatcher == null) return@DisposableEffect onDispose {}
val cb = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() { showAbsPlayer = false }
@@ -128,7 +138,7 @@ fun AppNavigation() {
backDispatcher.addCallback(cb)
onDispose { cb.remove() }
}
DisposableEffect(selectedAbsItem, backDispatcher) {
DisposableEffect(selectedAbsItem, backDispatcher, currentBackEntry, resumeTick) {
if (selectedAbsItem == null || backDispatcher == null) return@DisposableEffect onDispose {}
val cb = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() { selectedAbsItem = null }
@@ -148,7 +158,6 @@ fun AppNavigation() {
var homeRefreshKey by remember { mutableIntStateOf(0) }
val currentBackEntry by navController.currentBackStackEntryAsState()
val currentRoute = currentBackEntry?.destination?.route
val isTopLevel = TopLevel.entries.any { it.route == currentRoute }
@@ -156,7 +165,7 @@ fun AppNavigation() {
// callback is re-added (bumped to the top of the OnBackPressedDispatcher) after every navigation
// — otherwise, once the NavHost re-registers its own callback during an album drill-down or tab
// switch, it could regain priority and pop Erkunden → Home on the next back ("sometimes" bug).
DisposableEffect(exploreOverlayBack, backDispatcher, currentBackEntry) {
DisposableEffect(exploreOverlayBack, backDispatcher, currentBackEntry, resumeTick) {
val action = exploreOverlayBack
if (action == null || backDispatcher == null) return@DisposableEffect onDispose {}
val cb = object : OnBackPressedCallback(true) {