diff --git a/CLAUDE.md b/CLAUDE.md index dc153fb..0b3bf06 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,7 +20,7 @@ The real `InputConnection` from `super.onCreateInputConnection()` **must be retu ``` 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 NoSoftKeyboardWebView.kt — IME suppression via null InputConnection ScannerIntegration.kt — Scanner lifecycle interface @@ -62,14 +62,15 @@ All configurable values are in `AppConfig.kt`: - `HOME_URL` — the webpage to load - `ALLOWED_HOSTS` — domains that stay inside the WebView - `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 - URLs on `joykoo.mx` or `smart.joykoo.mx` (and subdomains) load inside the WebView - All other URLs are silently blocked - 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 diff --git a/README.md b/README.md index 4e9bf93..d0459bd 100644 --- a/README.md +++ b/README.md @@ -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 - **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 -- **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 @@ -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/` | | `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 | -| `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 diff --git a/app/src/main/java/mx/joykoo/scanner/AppConfig.kt b/app/src/main/java/mx/joykoo/scanner/AppConfig.kt index 00e10b6..b4b86b2 100644 --- a/app/src/main/java/mx/joykoo/scanner/AppConfig.kt +++ b/app/src/main/java/mx/joykoo/scanner/AppConfig.kt @@ -12,20 +12,18 @@ 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 /** Time window (ms) in which BACK_PRESS_EXIT_COUNT presses must occur to exit. */ const val BACK_PRESS_WINDOW_MS = 2000L - /** Number of taps on the hidden admin area required to trigger the admin exit. */ - const val ADMIN_TAP_COUNT = 7 + /** How long (ms) Volume Up + Volume Down must be held together to open the admin dialog. */ + const val ADMIN_VOLUME_HOLD_MS = 5000L - /** Time window (ms) for admin tap sequence. */ - const val ADMIN_TAP_WINDOW_MS = 3000L + /** Scanning a barcode with exactly this value opens the admin dialog instead of + * 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 * other common Chinese vendors). Add the device's action here if it differs. */ diff --git a/app/src/main/java/mx/joykoo/scanner/MainActivity.kt b/app/src/main/java/mx/joykoo/scanner/MainActivity.kt index efadf0f..304e542 100644 --- a/app/src/main/java/mx/joykoo/scanner/MainActivity.kt +++ b/app/src/main/java/mx/joykoo/scanner/MainActivity.kt @@ -11,7 +11,6 @@ import android.os.Build import android.os.Bundle import android.util.Log import android.view.KeyEvent -import android.view.MotionEvent import android.view.View import android.view.WindowInsets import android.view.WindowInsetsController @@ -41,8 +40,9 @@ class MainActivity : AppCompatActivity() { private var fileUploadCallback: ValueCallback>? = null private var isShowingError = false - // Admin exit: rapid taps in the top-left corner - private var adminTapTimestamps = mutableListOf() + // Admin dialog: hold Volume Up + Volume Down, or scan AppConfig.ADMIN_BARCODE + private val volumeKeysDown = mutableSetOf() + private val adminHoldRunnable = Runnable { showAdminDialog() } private var isKeyboardSuppressed = true // Back-press exit: rapid presses when no WebView history @@ -57,7 +57,7 @@ class MainActivity : AppCompatActivity() { setupWebView() setupErrorOverlay() setupBackNavigation() - scanner = BroadcastScannerIntegration(this, ::injectScannedText) + scanner = BroadcastScannerIntegration(this, ::onBarcodeScanned) if (savedInstanceState == null) { 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 { - if (ev.actionMasked == MotionEvent.ACTION_DOWN) { - val threshold = (AppConfig.GESTURE_CORNER_DP * resources.displayMetrics.density).toInt() - if (ev.x < threshold && ev.y < threshold) { - handleAdminTap() + private fun handleVolumeKey(event: KeyEvent) { + when (event.action) { + KeyEvent.ACTION_DOWN -> if (event.repeatCount == 0) { + volumeKeysDown.add(event.keyCode) + if (volumeKeysDown.size == 2) { + webView.postDelayed(adminHoldRunnable, AppConfig.ADMIN_VOLUME_HOLD_MS) + } + } + KeyEvent.ACTION_UP -> { + volumeKeysDown.remove(event.keyCode) + webView.removeCallbacks(adminHoldRunnable) } - } - return super.dispatchTouchEvent(ev) - } - - 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() { + // The dialog takes focus, so the volume key-ups never reach us. + volumeKeysDown.clear() + webView.removeCallbacks(adminHoldRunnable) val keyboardLabel = if (isKeyboardSuppressed) getString(R.string.keyboard_show) else getString(R.string.keyboard_hide) AlertDialog.Builder(this) @@ -311,6 +310,11 @@ class MainActivity : AppCompatActivity() { override fun dispatchKeyEvent(event: KeyEvent): Boolean { Log.d(TAG, "Key ${event.action} code=${event.keyCode} char=${event.unicodeChar} " + "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) } @@ -320,6 +324,10 @@ class MainActivity : AppCompatActivity() { // which is what React-controlled inputs need to pick up the change — // 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) { if (!::webView.isInitialized) return ensureWebViewFocus()