Every field keeps inputmode="none" and the forced hide. A finger tap (pointerdown, also through its label) gives that field the site's own inputmode back before Chromium focuses it, so numeric fields get a numeric keypad; it is suppressed again on blur or a cancelled tap, so scans and programmatic focus never open the keyboard. Native code checks the focused field before hiding the keyboard. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7.1 KiB
JoykooScanner — Android Barcode Scanner Kiosk App
What This Is
Native Android app (Kotlin) that wraps https://smart.joykoo.mx/ in a fullscreen WebView for dedicated Chinese industrial handheld barcode scanners. The scanner operates in HID/keyboard-wedge mode — it sends Android key events like a physical keyboard.
Critical Design Decision
The Android soft keyboard must NEVER appear for scanner fields. The only exception is a field a person taps with a finger (see Keyboard Policy below). Keyboard suppression uses multiple layers — do not remove any of them:
NoSoftKeyboardWebView.onCheckIsTextEditor()→falseEditorInfoflagsIME_FLAG_NO_EXTRACT_UI | IME_FLAG_NO_FULLSCREENinonCreateInputConnection()InputMethodManager.hideSoftInputFromWindow()posted after each InputConnection request and on page load / window focus — skipped only when the focused field carriesdata-jk-kb="1"- JavaScript injection (
KeyboardPolicy) settinginputmode="none"on every<input>,<textarea>, andcontenteditableviaMutationObserver, except the field currently tapped - Manifest
windowSoftInputMode="stateAlwaysHidden|adjustNothing"
The real InputConnection from super.onCreateInputConnection() must be returned — returning null breaks hardware key input from the barcode scanner on devices like the Urovo CT58C.
Keyboard Policy
KeyboardPolicy.kt: every field is suppressed (inputmode="none") until a finger taps it. On pointerdown (also via its <label>) the field gets the site's own inputmode back — numeric fields get a numeric keypad — and data-jk-kb="1", before Chromium focuses it. On focus loss, or a tap that never focuses it (scroll / pointercancel), it is suppressed again, so programmatic focus (scans, the site's own .focus()) never shows the keyboard. A field whose own inputmode is none stays suppressed even when tapped.
The site reads scans with a page-wide keydown listener (bursts ≤ 60 ms apart), so scanning does not need a focused field. The admin "Mostrar teclado" toggle still turns all suppression off.
Project Structure
app/src/main/java/mx/joykoo/scanner/
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 layers 1–3 (real InputConnection is returned)
KeyboardPolicy.kt — Injected rule: all fields inputmode="none" except the one a finger tapped
ScannerIntegration.kt — Scanner lifecycle interface
BroadcastScannerIntegration.kt — Intent-output scans → JS injection (React-safe value setter)
KioskController.kt — Lock Task kiosk toggle (session-only until KIOSK_PERSISTED); device-owner full lockdown + HOME alias, else screen pinning
KioskAdminReceiver.kt — DeviceAdminReceiver for `dpm set-device-owner`
StatusStrip.kt — App-drawn status strip (Wi-Fi + battery only); system bars stay hidden
Permissions.kt — Runtime permission prompts (camera, storage ≤ Android 9); asked at launch and on demand
FileTransfers.kt — Downloads → Downloads folder; file uploads with camera option; photos kept in Pictures/Joykoo
Build
- Language: Kotlin, targeting JVM 1.8 bytecode
- SDK: compileSdk 34, minSdk 24, targetSdk 34
- Dependencies: androidx core-ktx, appcompat, webkit, swiperefreshlayout
- Gradle: 8.7 with Kotlin DSL, AGP 8.5.2, Kotlin 1.9.24
- Package:
mx.joykoo.scanner
Build prerequisites (not yet installed on this server)
sudo dnf install java-17-openjdk-devel— only the JRE is installed, need javac- Android SDK cmdline-tools — download to
~/Android/Sdk/, then install:platform-toolsplatforms;android-34build-tools;34.0.0
- Set env vars:
JAVA_HOME=/usr/lib/jvm/java-17-openjdk-17.0.20.1.1-1.2.el9.x86_64 ANDROID_HOME=~/Android/Sdk ./gradlew assembleDebug
Server environment (RHEL 9.8)
- JRE 17 installed at
/usr/lib/jvm/java-17-openjdk-17.0.20.1.1-1.2.el9.x86_64 - 323 GB free on /home, 5.5 GB free on /
- curl, wget, unzip, zip available
- No Android SDK present anywhere
Configuration
All configurable values are in AppConfig.kt:
HOME_URL— the webpage to loadALLOWED_HOSTS— domains that stay inside the WebViewBACK_PRESS_EXIT_COUNT/BACK_PRESS_WINDOW_MS— back-button exit guardADMIN_VOLUME_HOLD_MS— hold Volume Up + Volume Down this long to open the admin dialogADMIN_BARCODE— scanning this value (Intent mode) opens the admin dialogKIOSK_BARCODE— scanning this value (Intent mode) toggles kiosk lockdownRENDERER_CRASH_LIMIT/RENDERER_CRASH_WINDOW_MS— WebView crash auto-recovery limit
Navigation Rules
- URLs on
joykoo.mxorsmart.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 dialog (exit / toggle soft keyboard): hold both volume keys 3 s, or scan
ADMIN_BARCODE. Volume keys are consumed and never reach the page. No touch gestures — corner taps leaked through to site UI
Kiosk & Errors
- Kiosk mode (
KioskController): off by default and not persisted while it is being tested (AppConfig.KIOSK_PERSISTED = false) — each launch starts unlocked and clears any leftover lockdown; scanningKIOSK_BARCODEtoggles it for the session. When on, admin dialog hides "Salir" and Back never exits. Device owner required for real lockdown; the app can't be uninstalled while device owner, so set it only with the release-signed APK. - Error overlay: per-cause messages (no Wi-Fi with auto-retry on reconnect, unreachable, 5xx, SSL, repeated renderer crash). 4xx responses are left to the site.
- Renderer crash (
onRenderProcessGone): the WebView is replaced and the last URL (tracked viadoUpdateVisitedHistory) reloaded.
Files & Permissions
- Permissions are requested at first launch (camera; storage on Android ≤ 9) and again when a feature needs one.
- Downloads: https on allowed hosts via
DownloadManager(session cookie forwarded);data:decoded natively;blob:read in-page and returned viaevaluateJavascriptpolling (no JS interface). A document-start hook keeps blobs readable if the site revokes the URL immediately. Saved to Downloads, with a toast on completion. - Uploads:
<input type=file>image fields show an in-app "Tomar foto / Elegir de la galería" dialog (the system chooser hid the camera behind the gallery picker);captureopens the camera directly. If camera permission is denied, a dialog offers app Settings or the file picker. Photos are saved to Pictures/Joykoo. - In-page camera (
getUserMedia) is granted to allowed hosts only, video only.
Security
- HTTPS only, no cleartext, mixed content never allowed, no SSL error bypass
- No JavaScript interfaces exposed
- No file:// or content:// access
- Network security config enforces system trust anchors only
- Domain allowlist in WebViewClient
UI Strings
Spanish (es) — this is for a Mexico-based company.