Add hidden on-screen keyboard toggle (5 taps top-right corner)
All checks were successful
APK-BUILD/pipeline/head This commit looks good
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:
parent
b7cf7256ac
commit
619fa4f048
@ -12,6 +12,9 @@ object AppConfig {
|
|||||||
"joykoo.mx",
|
"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. */
|
/** Number of rapid Back presses required to exit the app when there is no WebView history. */
|
||||||
const val BACK_PRESS_EXIT_COUNT = 3
|
const val BACK_PRESS_EXIT_COUNT = 3
|
||||||
|
|
||||||
@ -23,4 +26,10 @@ object AppConfig {
|
|||||||
|
|
||||||
/** Time window (ms) for admin tap sequence. */
|
/** Time window (ms) for admin tap sequence. */
|
||||||
const val ADMIN_TAP_WINDOW_MS = 3000L
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,6 +41,10 @@ class MainActivity : AppCompatActivity() {
|
|||||||
// Admin exit: rapid taps in the top-left corner
|
// Admin exit: rapid taps in the top-left corner
|
||||||
private var adminTapTimestamps = mutableListOf<Long>()
|
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
|
// Back-press exit: rapid presses when no WebView history
|
||||||
private var backPressTimestamps = mutableListOf<Long>()
|
private var backPressTimestamps = mutableListOf<Long>()
|
||||||
|
|
||||||
@ -213,11 +217,14 @@ class MainActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
|
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
|
||||||
if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
|
if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
|
||||||
// Top-left 60x60 dp region as the hidden admin area
|
val threshold = (AppConfig.GESTURE_CORNER_DP * resources.displayMetrics.density).toInt()
|
||||||
val threshold = (60 * resources.displayMetrics.density).toInt()
|
|
||||||
if (ev.x < threshold && ev.y < threshold) {
|
if (ev.x < threshold && ev.y < threshold) {
|
||||||
handleAdminTap()
|
handleAdminTap()
|
||||||
}
|
}
|
||||||
|
val screenWidth = resources.displayMetrics.widthPixels
|
||||||
|
if (ev.x > screenWidth - threshold && ev.y < threshold) {
|
||||||
|
handleKeyboardToggleTap()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return super.dispatchTouchEvent(ev)
|
return super.dispatchTouchEvent(ev)
|
||||||
}
|
}
|
||||||
@ -234,7 +241,6 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun showAdminExitDialog() {
|
private fun showAdminExitDialog() {
|
||||||
// Structured so a PIN check can be added here before calling finish()
|
|
||||||
AlertDialog.Builder(this)
|
AlertDialog.Builder(this)
|
||||||
.setTitle("Salir de la aplicación")
|
.setTitle("Salir de la aplicación")
|
||||||
.setMessage("¿Desea cerrar la aplicación del escáner?")
|
.setMessage("¿Desea cerrar la aplicación del escáner?")
|
||||||
@ -243,6 +249,35 @@ class MainActivity : AppCompatActivity() {
|
|||||||
.show()
|
.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 ───────────────────────────────────────────────────────
|
// ── Lifecycle ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
@ -291,15 +326,15 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun hideKeyboard() {
|
private fun hideKeyboard() {
|
||||||
|
if (!isKeyboardSuppressed) return
|
||||||
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
imm.hideSoftInputFromWindow(webView.windowToken, 0)
|
imm.hideSoftInputFromWindow(webView.windowToken, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun injectKeyboardSuppression(view: WebView) {
|
private fun injectKeyboardSuppression(view: WebView) {
|
||||||
|
if (!isKeyboardSuppressed) return
|
||||||
view.evaluateJavascript("""
|
view.evaluateJavascript("""
|
||||||
(function() {
|
(function() {
|
||||||
if (window.__jkKbSuppressed) return;
|
|
||||||
window.__jkKbSuppressed = true;
|
|
||||||
function suppress(el) {
|
function suppress(el) {
|
||||||
if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA'
|
if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA'
|
||||||
|| el.isContentEditable) {
|
|| el.isContentEditable) {
|
||||||
@ -309,7 +344,8 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
document.querySelectorAll('input, textarea, [contenteditable]')
|
document.querySelectorAll('input, textarea, [contenteditable]')
|
||||||
.forEach(suppress);
|
.forEach(suppress);
|
||||||
new MutationObserver(function(muts) {
|
if (window.__jkKbObserver) window.__jkKbObserver.disconnect();
|
||||||
|
window.__jkKbObserver = new MutationObserver(function(muts) {
|
||||||
muts.forEach(function(m) {
|
muts.forEach(function(m) {
|
||||||
m.addedNodes.forEach(function(n) {
|
m.addedNodes.forEach(function(n) {
|
||||||
if (n.nodeType === 1) {
|
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 });
|
{ childList: true, subtree: true });
|
||||||
})();
|
})();
|
||||||
""".trimIndent(), null)
|
""".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 ───────────────────────────────────────────────────
|
// ── WebViewClient ───────────────────────────────────────────────────
|
||||||
|
|
||||||
private inner class ScannerWebViewClient : WebViewClient() {
|
private inner class ScannerWebViewClient : WebViewClient() {
|
||||||
|
|||||||
@ -26,6 +26,8 @@ import android.webkit.WebView
|
|||||||
*/
|
*/
|
||||||
class NoSoftKeyboardWebView : WebView {
|
class NoSoftKeyboardWebView : WebView {
|
||||||
|
|
||||||
|
var keyboardSuppressed = true
|
||||||
|
|
||||||
constructor(context: Context) : super(context)
|
constructor(context: Context) : super(context)
|
||||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||||
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) :
|
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) :
|
||||||
@ -33,17 +35,20 @@ class NoSoftKeyboardWebView : WebView {
|
|||||||
|
|
||||||
override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection? {
|
override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection? {
|
||||||
val ic = super.onCreateInputConnection(outAttrs)
|
val ic = super.onCreateInputConnection(outAttrs)
|
||||||
outAttrs.imeOptions = outAttrs.imeOptions or
|
if (keyboardSuppressed) {
|
||||||
EditorInfo.IME_FLAG_NO_EXTRACT_UI or
|
outAttrs.imeOptions = outAttrs.imeOptions or
|
||||||
EditorInfo.IME_FLAG_NO_FULLSCREEN
|
EditorInfo.IME_FLAG_NO_EXTRACT_UI or
|
||||||
post {
|
EditorInfo.IME_FLAG_NO_FULLSCREEN
|
||||||
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
post {
|
||||||
imm.hideSoftInputFromWindow(windowToken, 0)
|
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE)
|
||||||
|
as InputMethodManager
|
||||||
|
imm.hideSoftInputFromWindow(windowToken, 0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return ic
|
return ic
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCheckIsTextEditor(): Boolean {
|
override fun onCheckIsTextEditor(): Boolean {
|
||||||
return false
|
return !keyboardSuppressed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,4 +4,6 @@
|
|||||||
<string name="error_no_connection">No se puede conectar</string>
|
<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="error_check_wifi">Verifique la conexión Wi-Fi e intente de nuevo.</string>
|
||||||
<string name="retry">Reintentar</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>
|
</resources>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user