From 94309775f7c5f489acf54020e413b10603129ee5 Mon Sep 17 00:00:00 2001 From: Manuel Gastelum Date: Tue, 8 Sep 2026 21:17:23 -0600 Subject: [PATCH 1/2] Send what the field asks for instead of typing a newline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ⏎ key committed a literal "\n". Most editors never see that as a submit: they declare an editor action in imeOptions and act on that instead, so Enter looked dead — a WhatsApp message could be dictated but not sent, and a browser search could be typed but not run. Fire the declared action when the field has one, fall back to a real ENTER key event when it does not, and keep committing a newline only for genuinely multi-line editors. On Wear the field is often not the app's own: Galaxy Watch routes WhatsApp and Keep Notes through Samsung's RemoteInput activity, which declares IME_ACTION_DONE and hands the text back to the app when that action fires. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01EBBqERL2Akg9cYDXRaaK7D --- .../dictate/wear/ime/WearImeService.kt | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/wear/src/main/kotlin/net/devemperor/dictate/wear/ime/WearImeService.kt b/wear/src/main/kotlin/net/devemperor/dictate/wear/ime/WearImeService.kt index 99ceaf002..243b706d5 100644 --- a/wear/src/main/kotlin/net/devemperor/dictate/wear/ime/WearImeService.kt +++ b/wear/src/main/kotlin/net/devemperor/dictate/wear/ime/WearImeService.kt @@ -14,8 +14,11 @@ import android.Manifest import android.content.pm.PackageManager import android.inputmethodservice.InputMethodService import android.os.SystemClock +import android.text.InputType import android.util.Log +import android.view.KeyEvent import android.view.View +import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputConnection import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.platform.ComposeView @@ -90,7 +93,7 @@ class WearImeService : WearSettingsStore.load(applicationContext) } - override fun onStartInputView(info: android.view.inputmethod.EditorInfo?, restarting: Boolean) { + override fun onStartInputView(info: EditorInfo?, restarting: Boolean) { super.onStartInputView(info, restarting) // Opening the keyboard is a good moment to reconcile with the phone: read the latest replicated // settings DataItem (accent/provider/key/prompt) and nudge a republish. The cached copy is used @@ -146,7 +149,7 @@ class WearImeService : private val actions = WearImeActions( commitText = { text -> ic()?.commitText(text, 1) }, deleteBackward = { ic()?.deleteSurroundingText(1, 0) }, - performEnter = { ic()?.commitText("\n", 1) }, + performEnter = { performEnter() }, toggleDictation = { toggleDictation() }, togglePause = { togglePause() }, cancelDictation = { cancelDictation() }, @@ -155,6 +158,38 @@ class WearImeService : private fun ic(): InputConnection? = currentInputConnection + /** + * The submit action the focused field declares (SEND / SEARCH / GO / DONE …), or null when it has + * none for the IME to fire — either because it declares no action, or because it asked the IME to + * leave Enter alone with [EditorInfo.IME_FLAG_NO_ENTER_ACTION]. + */ + private fun editorAction(): Int? { + val imeOptions = currentInputEditorInfo?.imeOptions ?: return null + if (imeOptions and EditorInfo.IME_FLAG_NO_ENTER_ACTION != 0) return null + return (imeOptions and EditorInfo.IME_MASK_ACTION).takeUnless { + it == EditorInfo.IME_ACTION_NONE || it == EditorInfo.IME_ACTION_UNSPECIFIED + } + } + + /** + * The ⏎ key. Committing a literal newline is the wrong thing for most fields: WhatsApp's compose box + * and the browser's search bar declare an *editor action* (SEND / SEARCH) and act on that, never on + * "\n", which is why Enter looked dead in those apps (#294). So fire the declared action when there + * is one, fall back to a real ENTER key event for fields that want the key press, and only insert a + * newline into genuinely multi-line editors. + */ + private fun performEnter() { + val ic = ic() ?: return + val action = editorAction() + if (action != null) { + ic.performEditorAction(action) + return + } + val multiline = (currentInputEditorInfo?.inputType ?: 0) and + InputType.TYPE_TEXT_FLAG_MULTI_LINE != 0 + if (multiline) ic.commitText("\n", 1) else sendDownUpKeyEvents(KeyEvent.KEYCODE_ENTER) + } + /** * Voice-page record button. Tap once to start recording, tap again to stop; on stop the audio is * transcribed (tethered via the phone when reachable, else standalone) and committed at the cursor. From 1ea8a3973c644710a5ff3c73c155fa2c86a3c534 Mon Sep 17 00:00:00 2001 From: Manuel Gastelum Date: Tue, 8 Sep 2026 21:17:23 -0600 Subject: [PATCH 2/2] Stay out of the way only once the text has somewhere to go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hiding the keyboard the moment a dictation lands is right when the text is the whole job, as in a note. Over a field that still has to be submitted it does two things wrong: it takes away the ⏎ the user now needs, and its teardown races the commit that just happened — Samsung's browser resolves that race by dropping the transcript, so dictated text never appeared there at all. Keep the keyboard up when the field declares a submit action, and hide once that action has actually fired: the app is showing its result by then, and the input view is opaque enough to cover it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01EBBqERL2Akg9cYDXRaaK7D --- .../devemperor/dictate/wear/ime/WearImeService.kt | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/wear/src/main/kotlin/net/devemperor/dictate/wear/ime/WearImeService.kt b/wear/src/main/kotlin/net/devemperor/dictate/wear/ime/WearImeService.kt index 243b706d5..cce9cac45 100644 --- a/wear/src/main/kotlin/net/devemperor/dictate/wear/ime/WearImeService.kt +++ b/wear/src/main/kotlin/net/devemperor/dictate/wear/ime/WearImeService.kt @@ -182,7 +182,9 @@ class WearImeService : val ic = ic() ?: return val action = editorAction() if (action != null) { - ic.performEditorAction(action) + // The action *is* the submit, so the keyboard is done once it lands: the app is already + // showing its result, and the opaque input view would otherwise sit on top of it. + if (ic.performEditorAction(action)) requestHideSelf(0) return } val multiline = (currentInputEditorInfo?.inputType ?: 0) and @@ -313,9 +315,11 @@ class WearImeService : ic()?.commitText(text, 1) recordingInfo.value = WearRecordingInfo() dictationState.value = WearDictationState.IDLE - // Dictation is the primary action — once the text is in, get out of the way so the - // user sees their field again instead of a keyboard stuck open over it. - requestHideSelf(0) + // Dictation is the primary action, so getting out of the way is the right default — + // but not over a field that declares a submit action. There the dictation is only + // half the job: hiding steals the ⏎ the user still needs, and the teardown races the + // commit we just made, which Samsung's browser loses the text to entirely (#294). + if (editorAction() == null) requestHideSelf(0) } } }