Replace corner-tap admin gesture with volume-key hold and admin barcode
All checks were successful
APK-BUILD/pipeline/head This commit looks good
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:
parent
2f88432776
commit
84035e7107
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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. */
|
||||
|
||||
@ -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<Array<Uri>>? = null
|
||||
private var isShowingError = false
|
||||
|
||||
// Admin exit: rapid taps in the top-left corner
|
||||
private var adminTapTimestamps = mutableListOf<Long>()
|
||||
// Admin dialog: hold Volume Up + Volume Down, or scan AppConfig.ADMIN_BARCODE
|
||||
private val volumeKeysDown = mutableSetOf<Int>()
|
||||
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()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user