diff --git a/lib/src/main/java/org/connectbot/terminal/ImeInputView.kt b/lib/src/main/java/org/connectbot/terminal/ImeInputView.kt index 555562a..4c03ac3 100644 --- a/lib/src/main/java/org/connectbot/terminal/ImeInputView.kt +++ b/lib/src/main/java/org/connectbot/terminal/ImeInputView.kt @@ -17,6 +17,7 @@ package org.connectbot.terminal import android.content.Context +import android.content.res.Configuration import android.view.KeyEvent import android.view.View import android.view.inputmethod.BaseInputConnection @@ -68,9 +69,11 @@ internal class ImeInputView( * Show the IME forcefully. This is more reliable than SoftwareKeyboardController. */ @Suppress("DEPRECATION") + private fun showImeFlags(): Int = imeShowFlags(resources.configuration.keyboard, resources.configuration.hardKeyboardHidden) + fun showIme() { if (requestFocus()) { - inputMethodManager.showSoftInput(this, InputMethodManager.SHOW_FORCED) + inputMethodManager.showSoftInput(this, showImeFlags()) } } @@ -377,3 +380,29 @@ internal class ImeInputView( private const val MAX_DELETE_SURROUNDING = 4096 } } + +/** + * Flags for `showSoftInput` given the current [Configuration] keyboard state. + * + * SHOW_FORCED is documented to hold the IME open "until the user explicitly + * closes it", which overrides the platform's own rule that a usable hardware + * keyboard suppresses the soft one. On a device with a keyboard attached that + * puts the on-screen keyboard over the terminal on every keypress — reported + * against a Meta Quest 3 with a Bluetooth keyboard, where it covers the session. + * + * Passing no flags hands the decision back to the platform, which still honours + * the user's "show virtual keyboard" setting for anyone who wants both. + * + * [hardKeyboardHidden] is part of the test, not just [Configuration.keyboard]: a + * folded or undocked device reports KEYBOARD_QWERTY while the keys are + * physically unreachable, and there the soft keyboard is the only way to type, + * so SHOW_FORCED still earns its keep. + * + * Top-level and internal so the policy can be tested without a Context. + */ +@Suppress("DEPRECATION") +internal fun imeShowFlags(keyboard: Int, hardKeyboardHidden: Int): Int { + val physicalKeyboardAttached = keyboard != Configuration.KEYBOARD_NOKEYS && + hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO + return if (physicalKeyboardAttached) 0 else InputMethodManager.SHOW_FORCED +} diff --git a/lib/src/test/java/org/connectbot/terminal/ImeShowFlagsTest.kt b/lib/src/test/java/org/connectbot/terminal/ImeShowFlagsTest.kt new file mode 100644 index 0000000..65bd910 --- /dev/null +++ b/lib/src/test/java/org/connectbot/terminal/ImeShowFlagsTest.kt @@ -0,0 +1,87 @@ +/* + * ConnectBot Terminal + * Copyright 2025 Kenny Root + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.connectbot.terminal + +import android.content.res.Configuration +import android.view.inputmethod.InputMethodManager +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * Reported on a Meta Quest 3 with a Bluetooth keyboard: the virtual keyboard rose + * over the session on every keypress. + * + * SHOW_FORCED is documented to hold the IME open until the user explicitly closes + * it, which overrides the platform rule that a usable hardware keyboard suppresses + * the soft one. The policy here is only about which flags to pass; whether the + * platform then shows the keyboard is its call, and still respects the user's + * "show virtual keyboard" setting. + */ +@Suppress("DEPRECATION") +class ImeShowFlagsTest { + + @Test + fun `no physical keyboard keeps SHOW_FORCED`() { + assertEquals( + "a touch-only device still needs the forced show that made this reliable", + InputMethodManager.SHOW_FORCED, + imeShowFlags( + keyboard = Configuration.KEYBOARD_NOKEYS, + hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_YES, + ), + ) + } + + @Test + fun `an attached usable keyboard drops the forced show`() { + assertEquals( + "SHOW_FORCED here is what puts the virtual keyboard over the session", + 0, + imeShowFlags( + keyboard = Configuration.KEYBOARD_QWERTY, + hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_NO, + ), + ) + } + + /** + * A folded or undocked device reports a keyboard whose keys cannot be + * reached. The soft keyboard is the only way to type, so it must still be + * forced up. + */ + @Test + fun `a hidden physical keyboard keeps SHOW_FORCED`() { + assertEquals( + InputMethodManager.SHOW_FORCED, + imeShowFlags( + keyboard = Configuration.KEYBOARD_QWERTY, + hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_YES, + ), + ) + } + + @Test + fun `a 12-key pad counts as physical`() { + assertEquals( + 0, + imeShowFlags( + keyboard = Configuration.KEYBOARD_12KEY, + hardKeyboardHidden = Configuration.HARDKEYBOARDHIDDEN_NO, + ), + ) + } +}