JoykooSmartApp/CLAUDE.md
caspar 4b23724639 Allow the soft keyboard on fields the site marks as typed
Scan fields keep inputmode="none" and the forced hide; fields the site's markup
marks as manual (data-jk, its own inputmode, textarea, autocapitalize, or a
typing input type) keep the keyboard and the site's inputmode, so numeric
fields get a numeric keypad. Native code checks the focused field before
hiding the keyboard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 15:02:45 -04:00

7.1 KiB
Raw Blame History

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. Fields that the site's markup marks as typed by a person are the only exception (see Keyboard Policy below). Keyboard suppression uses multiple layers — do not remove any of them:

  1. NoSoftKeyboardWebView.onCheckIsTextEditor() → false
  2. EditorInfo flags IME_FLAG_NO_EXTRACT_UI | IME_FLAG_NO_FULLSCREEN in onCreateInputConnection()
  3. InputMethodManager.hideSoftInputFromWindow() posted after each InputConnection request and on page load / window focus — skipped only when the focused field carries data-jk-kb="1"
  4. JavaScript injection (KeyboardPolicy) setting inputmode="none" on every scan <input>, <textarea>, and contenteditable via MutationObserver
  5. 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 decides per field from the site's markup, first match wins:

  1. data-jk="manual" / data-jk="scan" on the field or an ancestor (the site should use this for anything the hints get wrong)
  2. the site's own inputmode (none → scan, anything else → manual, and it is kept, e.g. numeric keypad)
  3. <textarea> / contenteditable → manual
  4. autocapitalize other than off/none → manual
  5. type tel / number / email / url / password → manual
  6. otherwise → scan (keyboard suppressed)

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 per-field rule: scan fields get inputmode="none", manual fields keep the keyboard
  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)

  1. sudo dnf install java-17-openjdk-devel — only the JRE is installed, need javac
  2. Android SDK cmdline-tools — download to ~/Android/Sdk/, then install:
    • platform-tools
    • platforms;android-34
    • build-tools;34.0.0
  3. 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
    
  4. ./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 load
  • ALLOWED_HOSTS — domains that stay inside the WebView
  • BACK_PRESS_EXIT_COUNT / BACK_PRESS_WINDOW_MS — back-button exit guard
  • 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
  • KIOSK_BARCODE — scanning this value (Intent mode) toggles kiosk lockdown
  • RENDERER_CRASH_LIMIT / RENDERER_CRASH_WINDOW_MS — WebView crash auto-recovery limit

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 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; scanning KIOSK_BARCODE toggles 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 via doUpdateVisitedHistory) 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 via evaluateJavascript polling (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); capture opens 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.