diff --git a/app/app/build.gradle.kts b/app/app/build.gradle.kts index 798d6028c..0a0d0a258 100644 --- a/app/app/build.gradle.kts +++ b/app/app/build.gradle.kts @@ -34,8 +34,8 @@ android { applicationId = "de.mm20.launcher2" minSdk = libs.versions.minSdk.get().toInt() targetSdk = libs.versions.targetSdk.get().toInt() - versionCode = System.getenv("VERSION_CODE_OVERRIDE")?.toIntOrNull() ?: 2026091002 - versionName = "1.40.2-typing.3" + versionCode = System.getenv("VERSION_CODE_OVERRIDE")?.toIntOrNull() ?: 2026091003 + versionName = "1.40.2-typing.4" signingConfig = signingConfigs.getByName("debug") } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/LauncherScaffold.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/LauncherScaffold.kt index 4ee10e18a..cdfe921f5 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/LauncherScaffold.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/scaffold/LauncherScaffold.kt @@ -52,6 +52,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.SideEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -1074,14 +1075,25 @@ internal fun LauncherScaffold( // Open the search when the user starts typing on a physical keyboard. The handler is // installed on the activity, so it also receives key events while no text field is focused. - val typeToSearchHandler = rememberTypeToSearchHandler(state, searchVM) - DisposableEffect(typeToSearchHandler) { - val launcherActivity = activity as? SharedLauncherActivity - launcherActivity?.launcherKeyEventHandler = typeToSearchHandler + // + // The scaffold state is recreated when one of its keys changes - the window size after a + // rotation, or the state that rememberSaveable restores after the activity was recreated - + // so the handler must not keep a reference to one particular instance: it reads the current + // state through this holder, which the composition keeps up to date, and the activity's + // handler is refreshed on every composition. Without that, the handler can end up opening + // the search on a state that is no longer the one that is rendered, which looks as if the + // key press did nothing. + val currentState = remember { mutableStateOf(state) } + SideEffect { + if (currentState.value !== state) currentState.value = state + } + val typeToSearchHandler = rememberTypeToSearchHandler({ currentState.value }, searchVM) + SideEffect { + (activity as? SharedLauncherActivity)?.launcherKeyEventHandler = typeToSearchHandler + } + DisposableEffect(Unit) { onDispose { - if (launcherActivity?.launcherKeyEventHandler === typeToSearchHandler) { - launcherActivity.launcherKeyEventHandler = null - } + (activity as? SharedLauncherActivity)?.launcherKeyEventHandler = null } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/TypeToSearchHandler.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/TypeToSearchHandler.kt index 7706f373f..415a3fb41 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/TypeToSearchHandler.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/TypeToSearchHandler.kt @@ -58,6 +58,9 @@ internal fun KeyEvent.isEnter(): Boolean { * [SharedLauncherActivity.launcherKeyEventHandler]. It is called for every key event before the * event is dispatched to the view hierarchy, so it also works while no text field has focus. * + * The scaffold state the handler works on is passed as a provider rather than as a value, because + * the state is replaced when the window size changes or the launcher is recreated. + * * While the search is driven by the physical keyboard, the search bar's text field is deliberately * left unfocused: a focused text field would make the system show the on-screen keyboard, and the * user is typing on a physical keyboard anyway. The handler therefore inserts and deletes the @@ -68,7 +71,7 @@ internal fun KeyEvent.isEnter(): Boolean { */ @Composable internal fun rememberTypeToSearchHandler( - state: LauncherScaffoldState, + stateProvider: () -> LauncherScaffoldState, searchVM: SearchVM, ): (KeyEvent) -> Boolean { val scope = rememberCoroutineScope() @@ -76,8 +79,12 @@ internal fun rememberTypeToSearchHandler( val sheetManager = LocalBottomSheetManager.current val overlayManager = LocalOverlayManager.current - return remember(state, searchVM, activity, sheetManager, overlayManager) { + // Deliberately not keyed on the scaffold state: the state is recreated when the window is + // resized or the launcher is recreated, and a key must always be handled by the state that is + // rendered at that moment. stateProvider hands out that state. + return remember(searchVM, activity, sheetManager, overlayManager) { fun handle(event: KeyEvent): Boolean { + val state = stateProvider() // Read the preference here rather than capturing it in the composition, so that the // current value is always used. if (!searchVM.searchOnTyping.value || state.isLocked) return false