From b56da63fe7776b14bc2cff4fe3c63ae2dd65ac73 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Thu, 6 Aug 2026 16:35:50 +0000 Subject: Widget: fix QuickAdd keyboard-focus race, force widget re-render on save QuickAddActivity's autofocus used delay(150) then requestFocus() + keyboard.show() -- a fixed delay racing against the window actually gaining focus. This is a translucent, separate-taskAffinity popup activity, so window focus transfer is variable/device-dependent; a show() call made before the window is focused is silently dropped by the IME service, no error, no retry -- the classic cause of "keyboard doesn't appear until I tap out and back in" (reported 2026-08-06, "more consistent here" than the same general Android quirk elsewhere). Fixed to react to LocalWindowInfo.isWindowFocused instead of guessing a timing window, plus windowSoftInputMode="adjustResize|stateVisible" as an OS-level second line of defense. Tried to verify live and hit a real limitation worth recording: this box's AVDs run -no-window (headless), and in that mode mInputShown never reports true via dumpsys input_method even for a manual, deliberate tap on the field -- confirmed by testing a plain tap directly, independent of any app code. The harness can't observe IME visibility here, so this fix is verified by code-level reasoning (LocalWindowInfo-driven focus is the standard, documented fix for this exact bug class) and confirmed window-focus DOES transfers correctly (mServedView moves to the bottom sheet's window), not by watching the keyboard actually appear. Real confirmation has to happen on-device. Also: SettingsActivity.saveAndFinish() only ever called DootWidget().updateAll() indirectly, as a side effect of RefreshWorker succeeding its network fetch -- so a slow or failing request could delay or block the widget from reflecting a setting the user just saved, even though every setting saved there (theme, text size, background, checkboxes) is already fully local and needs no network round trip to take effect. Now calls updateAll() directly and immediately after writing prefs; RefreshWorker still runs afterward to separately pull fresh server data. Verified installable and crash-free on a real API 36 emulator. Deployed as doot-widget.apk. --- .../org/terst/doot/widget/ui/QuickAddActivity.kt | 22 +++++++++++++++++----- .../org/terst/doot/widget/ui/SettingsActivity.kt | 14 ++++++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) (limited to 'android/app/src/main/java/org') diff --git a/android/app/src/main/java/org/terst/doot/widget/ui/QuickAddActivity.kt b/android/app/src/main/java/org/terst/doot/widget/ui/QuickAddActivity.kt index 980f286..5d658e6 100644 --- a/android/app/src/main/java/org/terst/doot/widget/ui/QuickAddActivity.kt +++ b/android/app/src/main/java/org/terst/doot/widget/ui/QuickAddActivity.kt @@ -13,12 +13,12 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.glance.appwidget.updateAll import androidx.lifecycle.lifecycleScope -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import okhttp3.OkHttpClient @@ -153,10 +153,22 @@ fun QuickAddSheet( val focusRequester = remember { FocusRequester() } val keyboard = LocalSoftwareKeyboardController.current - LaunchedEffect(Unit) { - delay(150) - focusRequester.requestFocus() - keyboard?.show() + // A fixed delay() here (the original approach) races against the window + // actually gaining focus -- this is a translucent, separately-task-affinity'd + // popup activity, so its window can take a variable, device-dependent amount + // of time to become focused after launch. Requesting focus / calling show() + // before that happens is the classic cause of "keyboard doesn't appear until + // I tap out and back in": the IME service silently drops a show() request + // made against a window that isn't focused yet, no error, no retry. Reacting + // to the real isWindowFocused signal instead of guessing a timing window + // fixes the race at its source (reported 2026-08-06 as "more consistent + // here" than the same general Android quirk in other apps). + val windowInfo = LocalWindowInfo.current + LaunchedEffect(windowInfo.isWindowFocused) { + if (windowInfo.isWindowFocused) { + focusRequester.requestFocus() + keyboard?.show() + } } val datePickerState = rememberDatePickerState(initialSelectedDateMillis = System.currentTimeMillis()) diff --git a/android/app/src/main/java/org/terst/doot/widget/ui/SettingsActivity.kt b/android/app/src/main/java/org/terst/doot/widget/ui/SettingsActivity.kt index 44a7a20..9b77621 100644 --- a/android/app/src/main/java/org/terst/doot/widget/ui/SettingsActivity.kt +++ b/android/app/src/main/java/org/terst/doot/widget/ui/SettingsActivity.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.dp import androidx.datastore.preferences.core.edit +import androidx.glance.appwidget.updateAll import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch @@ -109,6 +110,19 @@ class SettingsActivity : ComponentActivity() { prefs[Keys.BACKGROUND_ALPHA] = backgroundAlpha prefs[Keys.HIDE_CHECKBOXES] = hideCheckboxes } + + // Re-render immediately and unconditionally, not just as a side effect of + // RefreshWorker succeeding. Every rendering-affecting setting saved above + // (theme, text size, background, checkboxes) is already fully local -- + // none of it needs a network round trip to take effect. Before this, + // updateAll() only ever got called from inside RefreshWorker.doWork(), + // AFTER fetchAndPersist() succeeded -- so a slow or failing network + // request could delay or entirely block the widget from reflecting a + // setting the user just saved (reported 2026-08-06). RefreshWorker below + // still runs too, to also pull fresh server data, but that's a separate + // concern from making local preference changes visible. + DootWidget().updateAll(this) + RefreshWorker.schedule(this) RefreshWorker.runOnce(this) -- cgit v1.2.3