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

119 lines
7.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.