Replace corner-tap admin gesture with volume-key hold and admin barcode
All checks were successful
APK-BUILD/pipeline/head This commit looks good

The 7-tap top-left corner gesture passed every tap through to the page,
triggering site UI underneath it. Remove it entirely and open the admin
dialog (exit / toggle soft keyboard) by either:

- holding Volume Up + Volume Down together for 5 seconds; volume keys
  are consumed so they never reach the page, or
- scanning AppConfig.ADMIN_BARCODE in Intent output mode, which opens
  the dialog instead of being injected into the page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
caspar 2026-10-08 13:28:45 -04:00
parent 2f88432776
commit 84035e7107
4 changed files with 42 additions and 34 deletions

View File

@ -20,7 +20,7 @@ The real `InputConnection` from `super.onCreateInputConnection()` **must be retu
``` ```
app/src/main/java/mx/joykoo/scanner/ app/src/main/java/mx/joykoo/scanner/
AppConfig.kt — URL, allowed hosts, gesture thresholds (single config point) AppConfig.kt — URL, allowed hosts, admin triggers, scanner broadcasts (single config point)
MainActivity.kt — Fullscreen kiosk activity, WebView setup, navigation, lifecycle MainActivity.kt — Fullscreen kiosk activity, WebView setup, navigation, lifecycle
NoSoftKeyboardWebView.kt — IME suppression via null InputConnection NoSoftKeyboardWebView.kt — IME suppression via null InputConnection
ScannerIntegration.kt — Scanner lifecycle interface ScannerIntegration.kt — Scanner lifecycle interface
@ -62,14 +62,15 @@ All configurable values are in `AppConfig.kt`:
- `HOME_URL` — the webpage to load - `HOME_URL` — the webpage to load
- `ALLOWED_HOSTS` — domains that stay inside the WebView - `ALLOWED_HOSTS` — domains that stay inside the WebView
- `BACK_PRESS_EXIT_COUNT` / `BACK_PRESS_WINDOW_MS` — back-button exit guard - `BACK_PRESS_EXIT_COUNT` / `BACK_PRESS_WINDOW_MS` — back-button exit guard
- `ADMIN_TAP_COUNT` / `ADMIN_TAP_WINDOW_MS` — hidden admin exit gesture (7 taps top-left corner) - `ADMIN_VOLUME_HOLD_MS` — hold Volume Up + Volume Down this long to open the admin dialog
- `ADMIN_BARCODE` — scanning this value (Intent mode) opens the admin dialog
## Navigation Rules ## Navigation Rules
- URLs on `joykoo.mx` or `smart.joykoo.mx` (and subdomains) load inside the WebView - URLs on `joykoo.mx` or `smart.joykoo.mx` (and subdomains) load inside the WebView
- All other URLs are silently blocked - All other URLs are silently blocked
- Back button goes back in WebView history; 3 rapid presses exits when no history - Back button goes back in WebView history; 3 rapid presses exits when no history
- Admin exit: 7 rapid taps in top-left 60dp corner → confirmation dialog (PIN-ready stub) - Admin dialog (exit / toggle soft keyboard): hold both volume keys 5 s, or scan `ADMIN_BARCODE`. Volume keys are consumed and never reach the page. No touch gestures — corner taps leaked through to site UI
## Security ## Security

View File

@ -9,7 +9,7 @@ Aplicación nativa de Android que funciona como navegador dedicado de pantalla c
- **Modo kiosco** — Pantalla siempre encendida, modo inmersivo, navegación restringida al dominio de la empresa - **Modo kiosco** — Pantalla siempre encendida, modo inmersivo, navegación restringida al dominio de la empresa
- **Persistencia de sesión** — Cookies y almacenamiento local persisten entre reinicios de la aplicación - **Persistencia de sesión** — Cookies y almacenamiento local persisten entre reinicios de la aplicación
- **Manejo de errores de red** — Pantalla de reintento cuando no hay conexión Wi-Fi - **Manejo de errores de red** — Pantalla de reintento cuando no hay conexión Wi-Fi
- **Salida protegida** — Se requieren 3 pulsaciones rápidas del botón Atrás para salir; gesto oculto de administrador (7 toques en esquina superior izquierda) con diálogo de confirmación - **Salida protegida** — Se requieren 3 pulsaciones rápidas del botón Atrás para salir; menú de administrador (salir / mostrar u ocultar teclado) al mantener presionados Subir y Bajar volumen 5 segundos, o al escanear el código de administrador
## Requisitos ## Requisitos
@ -54,7 +54,8 @@ Todos los valores configurables están en un solo archivo:
| `HOME_URL` | URL que se carga al iniciar | `https://smart.joykoo.mx/` | | `HOME_URL` | URL que se carga al iniciar | `https://smart.joykoo.mx/` |
| `ALLOWED_HOSTS` | Dominios permitidos dentro del WebView | `smart.joykoo.mx`, `joykoo.mx` | | `ALLOWED_HOSTS` | Dominios permitidos dentro del WebView | `smart.joykoo.mx`, `joykoo.mx` |
| `BACK_PRESS_EXIT_COUNT` | Pulsaciones rápidas de Atrás para salir | 3 | | `BACK_PRESS_EXIT_COUNT` | Pulsaciones rápidas de Atrás para salir | 3 |
| `ADMIN_TAP_COUNT` | Toques en esquina superior izquierda para menú de administrador | 7 | | `ADMIN_VOLUME_HOLD_MS` | Tiempo (ms) con ambos botones de volumen presionados para abrir el menú de administrador | 5000 |
| `ADMIN_BARCODE` | Código que, al escanearse (modo Intent), abre el menú de administrador | `JOYKOO-ADMIN-…` |
## Uso del escáner ## Uso del escáner

View File

@ -12,20 +12,18 @@ 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
/** Time window (ms) in which BACK_PRESS_EXIT_COUNT presses must occur to exit. */ /** Time window (ms) in which BACK_PRESS_EXIT_COUNT presses must occur to exit. */
const val BACK_PRESS_WINDOW_MS = 2000L const val BACK_PRESS_WINDOW_MS = 2000L
/** Number of taps on the hidden admin area required to trigger the admin exit. */ /** How long (ms) Volume Up + Volume Down must be held together to open the admin dialog. */
const val ADMIN_TAP_COUNT = 7 const val ADMIN_VOLUME_HOLD_MS = 5000L
/** Time window (ms) for admin tap sequence. */ /** Scanning a barcode with exactly this value opens the admin dialog instead of
const val ADMIN_TAP_WINDOW_MS = 3000L * being typed into the page (Intent output mode only). Print it as a QR/Code128. */
const val ADMIN_BARCODE = "JOYKOO-ADMIN-O4306IFH9AUJ"
/** Broadcast actions sent by scanners in "Intent output" mode (Urovo first, then /** Broadcast actions sent by scanners in "Intent output" mode (Urovo first, then
* other common Chinese vendors). Add the device's action here if it differs. */ * other common Chinese vendors). Add the device's action here if it differs. */

View File

@ -11,7 +11,6 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.KeyEvent import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.WindowInsets import android.view.WindowInsets
import android.view.WindowInsetsController import android.view.WindowInsetsController
@ -41,8 +40,9 @@ class MainActivity : AppCompatActivity() {
private var fileUploadCallback: ValueCallback<Array<Uri>>? = null private var fileUploadCallback: ValueCallback<Array<Uri>>? = null
private var isShowingError = false private var isShowingError = false
// Admin exit: rapid taps in the top-left corner // Admin dialog: hold Volume Up + Volume Down, or scan AppConfig.ADMIN_BARCODE
private var adminTapTimestamps = mutableListOf<Long>() private val volumeKeysDown = mutableSetOf<Int>()
private val adminHoldRunnable = Runnable { showAdminDialog() }
private var isKeyboardSuppressed = true private var isKeyboardSuppressed = true
// Back-press exit: rapid presses when no WebView history // Back-press exit: rapid presses when no WebView history
@ -57,7 +57,7 @@ class MainActivity : AppCompatActivity() {
setupWebView() setupWebView()
setupErrorOverlay() setupErrorOverlay()
setupBackNavigation() setupBackNavigation()
scanner = BroadcastScannerIntegration(this, ::injectScannedText) scanner = BroadcastScannerIntegration(this, ::onBarcodeScanned)
if (savedInstanceState == null) { if (savedInstanceState == null) {
webView.loadUrl(AppConfig.HOME_URL) webView.loadUrl(AppConfig.HOME_URL)
@ -214,30 +214,29 @@ class MainActivity : AppCompatActivity() {
} }
} }
// ── Admin Exit (hidden tap gesture) ───────────────────────────────── // ── Admin Dialog ────────────────────────────────────────────────────
// Opened by holding both volume keys (see dispatchKeyEvent) or by scanning
// AppConfig.ADMIN_BARCODE. Neither touches the page, unlike a tap gesture.
override fun dispatchTouchEvent(ev: MotionEvent): Boolean { private fun handleVolumeKey(event: KeyEvent) {
if (ev.actionMasked == MotionEvent.ACTION_DOWN) { when (event.action) {
val threshold = (AppConfig.GESTURE_CORNER_DP * resources.displayMetrics.density).toInt() KeyEvent.ACTION_DOWN -> if (event.repeatCount == 0) {
if (ev.x < threshold && ev.y < threshold) { volumeKeysDown.add(event.keyCode)
handleAdminTap() if (volumeKeysDown.size == 2) {
webView.postDelayed(adminHoldRunnable, AppConfig.ADMIN_VOLUME_HOLD_MS)
} }
} }
return super.dispatchTouchEvent(ev) KeyEvent.ACTION_UP -> {
volumeKeysDown.remove(event.keyCode)
webView.removeCallbacks(adminHoldRunnable)
} }
private fun handleAdminTap() {
val now = System.currentTimeMillis()
adminTapTimestamps.add(now)
adminTapTimestamps.removeAll { now - it > AppConfig.ADMIN_TAP_WINDOW_MS }
if (adminTapTimestamps.size >= AppConfig.ADMIN_TAP_COUNT) {
adminTapTimestamps.clear()
showAdminDialog()
} }
} }
private fun showAdminDialog() { private fun showAdminDialog() {
// The dialog takes focus, so the volume key-ups never reach us.
volumeKeysDown.clear()
webView.removeCallbacks(adminHoldRunnable)
val keyboardLabel = if (isKeyboardSuppressed) val keyboardLabel = if (isKeyboardSuppressed)
getString(R.string.keyboard_show) else getString(R.string.keyboard_hide) getString(R.string.keyboard_show) else getString(R.string.keyboard_hide)
AlertDialog.Builder(this) AlertDialog.Builder(this)
@ -311,6 +310,11 @@ class MainActivity : AppCompatActivity() {
override fun dispatchKeyEvent(event: KeyEvent): Boolean { override fun dispatchKeyEvent(event: KeyEvent): Boolean {
Log.d(TAG, "Key ${event.action} code=${event.keyCode} char=${event.unicodeChar} " + Log.d(TAG, "Key ${event.action} code=${event.keyCode} char=${event.unicodeChar} " +
"chars=${event.characters} dev=${event.deviceId} focus=${currentFocus?.javaClass?.simpleName}") "chars=${event.characters} dev=${event.deviceId} focus=${currentFocus?.javaClass?.simpleName}")
// Volume keys are reserved for the admin combo and never reach the page.
if (event.keyCode == KeyEvent.KEYCODE_VOLUME_UP || event.keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
handleVolumeKey(event)
return true
}
return super.dispatchKeyEvent(event) return super.dispatchKeyEvent(event)
} }
@ -320,6 +324,10 @@ class MainActivity : AppCompatActivity() {
// which is what React-controlled inputs need to pick up the change — // which is what React-controlled inputs need to pick up the change —
// assigning el.value directly is silently discarded by React. // assigning el.value directly is silently discarded by React.
private fun onBarcodeScanned(text: String) {
if (text == AppConfig.ADMIN_BARCODE) showAdminDialog() else injectScannedText(text)
}
private fun injectScannedText(text: String) { private fun injectScannedText(text: String) {
if (!::webView.isInitialized) return if (!::webView.isInitialized) return
ensureWebViewFocus() ensureWebViewFocus()