Add hidden on-screen keyboard toggle (5 taps top-right corner)
All checks were successful
APK-BUILD/pipeline/head This commit looks good

Tap the top-right 60dp corner 5 times within 3 seconds to toggle
the soft keyboard on or off. Default is off (suppressed). The toggle
controls all suppression layers: JS inputmode injection, the
MutationObserver, InputMethodManager hide calls, and the WebView's
EditorInfo flags. A toast confirms the current state in Spanish.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
caspar 2026-10-08 00:39:53 -04:00
parent b7cf7256ac
commit 619fa4f048
4 changed files with 81 additions and 14 deletions

View File

@ -12,6 +12,9 @@ object AppConfig {
"joykoo.mx",
)
/** Size in dp of the corner regions used for hidden tap gestures. */
const val GESTURE_CORNER_DP = 60
/** Number of rapid Back presses required to exit the app when there is no WebView history. */
const val BACK_PRESS_EXIT_COUNT = 3
@ -23,4 +26,10 @@ object AppConfig {
/** Time window (ms) for admin tap sequence. */
const val ADMIN_TAP_WINDOW_MS = 3000L
/** Number of taps on the top-right corner to toggle the on-screen keyboard. */
const val KEYBOARD_TOGGLE_TAP_COUNT = 5
/** Time window (ms) for keyboard toggle tap sequence. */
const val KEYBOARD_TOGGLE_TAP_WINDOW_MS = 3000L
}

View File

@ -41,6 +41,10 @@ class MainActivity : AppCompatActivity() {
// Admin exit: rapid taps in the top-left corner
private var adminTapTimestamps = mutableListOf<Long>()
// Keyboard toggle: rapid taps in the top-right corner
private var keyboardToggleTapTimestamps = mutableListOf<Long>()
private var isKeyboardSuppressed = true
// Back-press exit: rapid presses when no WebView history
private var backPressTimestamps = mutableListOf<Long>()
@ -213,11 +217,14 @@ class MainActivity : AppCompatActivity() {
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
// Top-left 60x60 dp region as the hidden admin area
val threshold = (60 * resources.displayMetrics.density).toInt()
val threshold = (AppConfig.GESTURE_CORNER_DP * resources.displayMetrics.density).toInt()
if (ev.x < threshold && ev.y < threshold) {
handleAdminTap()
}
val screenWidth = resources.displayMetrics.widthPixels
if (ev.x > screenWidth - threshold && ev.y < threshold) {
handleKeyboardToggleTap()
}
}
return super.dispatchTouchEvent(ev)
}
@ -234,7 +241,6 @@ class MainActivity : AppCompatActivity() {
}
private fun showAdminExitDialog() {
// Structured so a PIN check can be added here before calling finish()
AlertDialog.Builder(this)
.setTitle("Salir de la aplicación")
.setMessage("¿Desea cerrar la aplicación del escáner?")
@ -243,6 +249,35 @@ class MainActivity : AppCompatActivity() {
.show()
}
// ── Keyboard Toggle (hidden tap gesture, top-right corner) ──────────
private fun handleKeyboardToggleTap() {
val now = System.currentTimeMillis()
keyboardToggleTapTimestamps.add(now)
keyboardToggleTapTimestamps.removeAll {
now - it > AppConfig.KEYBOARD_TOGGLE_TAP_WINDOW_MS
}
if (keyboardToggleTapTimestamps.size >= AppConfig.KEYBOARD_TOGGLE_TAP_COUNT) {
keyboardToggleTapTimestamps.clear()
toggleKeyboard()
}
}
private fun toggleKeyboard() {
isKeyboardSuppressed = !isKeyboardSuppressed
webView.keyboardSuppressed = isKeyboardSuppressed
if (isKeyboardSuppressed) {
injectKeyboardSuppression(webView)
hideKeyboard()
Toast.makeText(this, R.string.keyboard_disabled, Toast.LENGTH_SHORT).show()
} else {
removeKeyboardSuppression(webView)
Toast.makeText(this, R.string.keyboard_enabled, Toast.LENGTH_SHORT).show()
}
}
// ── Lifecycle ───────────────────────────────────────────────────────
override fun onResume() {
@ -291,15 +326,15 @@ class MainActivity : AppCompatActivity() {
}
private fun hideKeyboard() {
if (!isKeyboardSuppressed) return
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(webView.windowToken, 0)
}
private fun injectKeyboardSuppression(view: WebView) {
if (!isKeyboardSuppressed) return
view.evaluateJavascript("""
(function() {
if (window.__jkKbSuppressed) return;
window.__jkKbSuppressed = true;
function suppress(el) {
if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA'
|| el.isContentEditable) {
@ -309,7 +344,8 @@ class MainActivity : AppCompatActivity() {
}
document.querySelectorAll('input, textarea, [contenteditable]')
.forEach(suppress);
new MutationObserver(function(muts) {
if (window.__jkKbObserver) window.__jkKbObserver.disconnect();
window.__jkKbObserver = new MutationObserver(function(muts) {
muts.forEach(function(m) {
m.addedNodes.forEach(function(n) {
if (n.nodeType === 1) {
@ -320,12 +356,27 @@ class MainActivity : AppCompatActivity() {
}
});
});
}).observe(document.body || document.documentElement,
});
window.__jkKbObserver.observe(document.body || document.documentElement,
{ childList: true, subtree: true });
})();
""".trimIndent(), null)
}
private fun removeKeyboardSuppression(view: WebView) {
view.evaluateJavascript("""
(function() {
if (window.__jkKbObserver) {
window.__jkKbObserver.disconnect();
window.__jkKbObserver = null;
}
document.querySelectorAll('[inputmode="none"]').forEach(function(el) {
el.removeAttribute('inputmode');
});
})();
""".trimIndent(), null)
}
// ── WebViewClient ───────────────────────────────────────────────────
private inner class ScannerWebViewClient : WebViewClient() {

View File

@ -26,6 +26,8 @@ import android.webkit.WebView
*/
class NoSoftKeyboardWebView : WebView {
var keyboardSuppressed = true
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) :
@ -33,17 +35,20 @@ class NoSoftKeyboardWebView : WebView {
override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection? {
val ic = super.onCreateInputConnection(outAttrs)
if (keyboardSuppressed) {
outAttrs.imeOptions = outAttrs.imeOptions or
EditorInfo.IME_FLAG_NO_EXTRACT_UI or
EditorInfo.IME_FLAG_NO_FULLSCREEN
post {
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE)
as InputMethodManager
imm.hideSoftInputFromWindow(windowToken, 0)
}
}
return ic
}
override fun onCheckIsTextEditor(): Boolean {
return false
return !keyboardSuppressed
}
}

View File

@ -4,4 +4,6 @@
<string name="error_no_connection">No se puede conectar</string>
<string name="error_check_wifi">Verifique la conexión Wi-Fi e intente de nuevo.</string>
<string name="retry">Reintentar</string>
<string name="keyboard_enabled">Teclado en pantalla activado</string>
<string name="keyboard_disabled">Teclado en pantalla desactivado</string>
</resources>