All checks were successful
APK-BUILD/pipeline/head This commit looks good
WebView drops the <a download> name of page-made files, so downloads such as the Verificar entrega Odoo CSV were saved as downloadfile.csv. The download hook now records the name when the link is clicked and the app asks the page for it. After saving, a dialog shows the file name and offers Abrir, which uses the registered viewer or, for types nothing opens (CSV on the scanners), launches the app set in AppConfig.FILE_OPEN_FALLBACK_APPS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6.6 KiB
6.6 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. 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- JavaScript injection setting
inputmode="none"on all<input>,<textarea>, andcontenteditableelements viaMutationObserver - 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.
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 via null InputConnection
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 limitFILE_OPEN_FALLBACK_APPS— app launched by a download's "Abrir" when nothing opens that file type (also add it to<queries>in the manifest)
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, and records the<a download="name">file name (WebView does not pass it on; without it files save asdownloadfile.csv). Saved to Downloads, then an "Archivo guardado" dialog offers Abrir: the app registered for the type, else the app inAppConfig.FILE_OPEN_FALLBACK_APPS(CSV → CSV Editor from F-Droid, which has no open-with support) is launched. Fallback apps must also be declared under<queries>in the manifest (Android 11+ package visibility). - 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.