Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion lib/src/main/java/org/connectbot/terminal/ImeInputView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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())
}
}

Expand Down Expand Up @@ -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
}
87 changes: 87 additions & 0 deletions lib/src/test/java/org/connectbot/terminal/ImeShowFlagsTest.kt
Original file line number Diff line number Diff line change
@@ -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,
),
)
}
}
Loading