A server that registers only the normal scanner callback offers the E2E package no sign-in, so the fixture could not be used against it. The new `emu` build type (initWith debug, no applicationIdSuffix, versionNameSuffix -emu, label "QQQ Mobile EMULATOR TEST") reuses the E2E fixture driver source under io.qrun.mobile.scanner. It does not take the E2E manifest or network security config: cleartext stays off and only system trust is used. Because it carries the normal application ID, the fixture driver is now created only for the disposable .e2e package (unchanged behaviour) or when android.os.Build describes an SDK emulator image: HARDWARE goldfish or ranchu, PRODUCT sdk_*, MODEL sdk_* or "Android SDK built for *", and a FINGERPRINT naming that same product. Otherwise there is no driver. Debug, qa and release source sets are unchanged and contain no ingress. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
11 KiB
Emulator testing
An Android emulator verifies login, enrollment, native navigation and manual barcode submission. The separate E2E variant also supports synthetic scan injection. An emulator does not reproduce Zebra's EMDK scan engine or physical trigger.
Prepared Windows workstation
The QQQ_Mobile_API35 virtual device is installed under %LOCALAPPDATA%/Android/Avd, with Android 15/API35 and WHPX acceleration. Local emulator testing is currently paused: the operator confirmed whole-workstation stuttering stopped when the emulator closed. Do not restart it as part of automatic local tests. The next deliberate diagnostic launch should compare software rendering while preserving the other settings:
$env:ANDROID_AVD_HOME = Join-Path $env:LOCALAPPDATA 'Android/Avd'
& "$env:LOCALAPPDATA/Android/Sdk/emulator/emulator.exe" -avd QQQ_Mobile_API35 -gpu software -no-snapshot -no-audio
The previous host-GPU run logged OpenGL context restoration and external-memory import failures. Sampled host resources had ample headroom and WHPX was operational; graphics interoperability is the leading hypothesis, not a proven cause. The saved AVD now has hw.gpu.mode=software, with the previous config backed up locally; that renderer has not yet been tested. Software graphics retains WHPX CPU acceleration. A cold start avoids restoring old graphics state; -no-snapshot preserves installed apps and their data. The Android acceleration guide recommends software rendering when host/auto graphics have problems. Preserve other settings during the comparison, observe desktop responsiveness as well as guest frame times, and stop if workstation stalls recur. Use the physical TC22 or hosted emulator CI for acceptance while local testing is paused.
The local demo uses port57832 on this workstation because other services occupy the conventional ports. Start it from the sibling qbit-mobile-scanners directory in another terminal, then forward the emulator port:
pwsh -File scripts/Start-Demo.ps1 -SkipBuild -Port 57832
& "$env:LOCALAPPDATA/Android/Sdk/platform-tools/adb.exe" -s emulator-5554 reverse tcp:8080 tcp:57832
Keep the Android server URL http://127.0.0.1:8080/. The browser administration URL is http://127.0.0.1:57832/. The demo's local guide explains the scanner/admin accounts and password-copy helper. Open QQQ Mobile in the emulator. Tap the connection footer to view or copy diagnostics; it distinguishes server reachability from login and approval.
Local interactive test
- Create/start an API35 Android emulator in Android Studio, or use the SDK emulator installed on the development machine.
- Build/install
scanner-host/build/outputs/apk/debug/scanner-host-debug.apkwithadb install -rtargeting the emulator's serial if multiple devices are connected. - Start the companion mobile scanner demo on port8080.
- Run
adb reverse tcp:8080 tcp:8080and usehttp://127.0.0.1:8080/in QQQ Mobile. - Sign in with the demo operator account. Approve the displayed device from the administrator session, then tap Check approval.
- Select the mobile app, open its scan process, and submit a synthetic barcode manually. Verify Saved scan, and verify the server's stored record.
- Revoke the device on the server and verify subsequent submissions are denied. Sign out and verify a new login is required.
Only the disposable E2E APK accepts synthetic broadcasts while its scanner screen is visible. Normal debug/release builds ignore this ingress and use EMDK or manual entry:
adb shell am broadcast -a io.qrun.mobile.scanner.SCAN -c android.intent.category.DEFAULT -p io.qrun.mobile.scanner.e2e --es com.symbol.datawedge.data_string DEMO-001 --es com.symbol.datawedge.label_type LABEL-TYPE-CODE128
am broadcast cannot send a byte-array extra. A scan that needs fidelity metadata (for example GS1-128) adds the AIM identifier and the raw bytes as canonical padded Base64 text, both quoted for the device shell:
--es io.qrun.mobile.scanner.e2e.aim_identifier ']C1' --es io.qrun.mobile.scanner.e2e.raw_data_base64 'XUMxMDEwOTUwMTEwMTUzMDAwMzExMjAxMDE0MjFURVNUMDAwMTIz'
The byte-array extra io.qrun.mobile.scanner.e2e.raw_data wins when both are present. A broadcast with malformed Base64 or an invalid AIM identifier is ignored and the read stays pending. am to-intent-uri with the same arguments prints how the device parsed them.
This validates intent parsing only; the server treats source/symbology as client-reported metadata. The E2E package signs in with its own callback (io.qrun.mobile.scanner.e2e.oauth://callback/oauth2redirect, see AUTHENTICATION.md), so a server that advertises only the normal scanner callback offers it no browser sign-in.
Emulator test build under the normal application ID
For such a server, :scanner-host:assembleEmu builds QQQ Mobile EMULATOR TEST: the same fixture driver under the normal application ID io.qrun.mobile.scanner, so it signs in with the normal callback. Its version name ends in -emu. Send it the broadcast above with -p io.qrun.mobile.scanner.
- It is debug-signed, so it can never be installed beside or over the production-signed application.
adb uninstall io.qrun.mobile.scannermust come first, which removes that installation's session and device enrollment; going back means uninstalling the test build and reinstalling the signed APK. - It does not use the E2E manifest or network security config: cleartext stays off and only the system trust store is used.
- The fixture driver is created only when
android.os.Builddescribes an SDK emulator image, and every signal must agree:HARDWAREisgoldfishorranchu,PRODUCTstarts withsdk_,MODELstarts withsdk_orAndroid SDK built for, andFINGERPRINTnames that same product. Anywhere else there is no driver and the scan control reports hardware scanning as unavailable. The disposable.e2epackage is exempt and behaves as before. - Never install it on a handheld. It is a local test aid, not a release artifact, and CI does not build it.
Automated checks
The GitHub Scanner Android workflow runs JVM tests, lint, APK assembly, the shared native UI suite, and the real-server scanner E2E on pushes and pull requests. The emulator job checks out exact core and companion qbit commits, verifies them in a fresh private Maven repository, and starts a fresh isolated demo database. Fixture provenance describes the source pins, runtime checks and distinction from published release dependencies. Sanitized instrumentation results are uploaded as workflow artifacts. Hosted execution is verified separately from a local run.
The scanner E2E installs QQQ Mobile E2E, a separate application (io.qrun.mobile.scanner.e2e). It drives server selection, username/password login, pending enrollment, approval, app selection, Scan Capture, manual barcode submission, server acknowledgment, independent persisted-record verification, and revocation of an already-open scan process. Administrative setup uses the real QQQ process endpoints; the companion qbit's browser suite covers their single-record and selected-bulk UI entry points. No repository or server mocks are used. Random test credentials travel through standard input into the test application's private files, are removed after reading, and are redacted from reports. The runner preserves the normal scanner package and its port forwarding.
The runner uses a test-only forwarding proxy that checks observed requests against the real server's published OpenAPI operations. It modifies only the version response for incompatible-major, missing-extension, missing-Android-channel and additive-future-version scenarios. Rejected servers must receive no credentials or requests beyond public discovery. The eight scenarios also cover the real scan flow, native administrative forms for approval/revocation/session termination/cancellation, session activity and actual screen mirroring with forced logout, and a PixelCopy pixel-content regression. Compatible scenarios use the real backend, processes and database. The audit report contains operation IDs and counts, never request headers or bodies. SESSION-STATE.md records actual passing runs and revision boundaries. The runner wakes/unlocks the selected emulator, temporarily disables animation scales and restores their original values during cleanup. UI conditions have a 60-second deadline and instrumentation has a 300-second deadline. Set E2E_SCENARIO=session-monitor or E2E_SCENARIO=pixel-copy for focused verification.
The default suite also runs bundled-license rendering, local orientation policy checks and the diagnostics filtering/pause/clear/return UI test. Set E2E_SCENARIO=licenses, E2E_SCENARIO=orientation or E2E_SCENARIO=diagnostics to run one independently; these scenarios need only the scanner E2E APKs and do not start a server or establish USB forwarding. After installation and ordinary scenarios' data resets, the runner grants coarse and fine location permission only to io.qrun.mobile.scanner.e2e, keeping the default-on reporting permission dialog from interrupting unrelated scanner/UI automation. The session-monitor scenario instead explicitly revokes both permissions and seeds only the already-requested marker, preserving default-on reporting and its real denied-location telemetry assertion without an OS dialog. This fixture setup does not exercise the real Android permission prompt or user denial interaction and does not change permissions for the normal scanner or separate QA package.
To reproduce with Node 22+ and Java 21, first prepare the source-pinned fixture. Use its qualified server directory below (the example assumes .e2e-server); an ordinary server build against the default RC.20 core does not include the nullable process fix:
./gradlew.bat :scanner-host:assembleE2e :scanner-host:assembleE2eAndroidTest
$env:ANDROID_SERIAL = 'emulator-5554'
node scripts/ci/scanner-e2e.mjs .e2e-server
Set ANDROID_HOME (or ANDROID_SDK_ROOT) if adb is not on PATH. Linux/macOS use ./mvnw, ./gradlew, and their shell's environment assignment syntax. The runner refuses physical devices, uses a free local port and temporary database, and cleans up its server, forwarding and both isolated test APKs. The older setup-only scripts/ci/emulator-smoke.sh is retained for disposable-emulator troubleshooting; it clears the normal scanner package and should not be used on your interactive demo emulator.
Physical acceptance still required
Test the TC22 EMDK trigger, rotation/backgrounding, retry after a lost response, revoked approval, leading-zero barcodes and repeated physical scans. Confirm DataWedge scanning works again after QQQ releases ownership. An uncertain submission must retain its event ID; a repeated physical scan receives a new event ID. See trusted scanner acceptance. Never use sensitive production barcodes in test screenshots or CI artifacts.