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>
107 lines
6.6 KiB
Markdown
107 lines
6.6 KiB
Markdown
# 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:
|
|
|
|
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
|
|
4. JavaScript injection setting `inputmode="none"` on all `<input>`, `<textarea>`, and `contenteditable` elements 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.
|
|
|
|
## 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)
|
|
|
|
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
|
|
- `FILE_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.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, and records the `<a download="name">` file name (WebView does not pass it on; without it files save as `downloadfile.csv`). Saved to Downloads, then an "Archivo guardado" dialog offers **Abrir**: the app registered for the type, else the app in `AppConfig.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); `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.
|