Android quickstart
Install the signed Android RC2 evaluation kit, activate a tenant-bound license, and submit CameraX frames.
Introduction
What you build
A tenant-licensed Android evaluation host that submits stride-safe CameraX frames and renders only native-derived observations returned by the public session API.

Two supported paths
Android and Zebra Android are evaluation paths delivered through the same signed tenant kit. iOS and Linux are source previews, cloud is experimental, and Windows is unsupported.
How the pieces fit
- Tenant and device
- The control plane owns trial state, device seats, activation, features, limits, and revocation.
- Signed license
- A backend JWS binds the active tenant, device, plan, entitlements, expiry, and offline grace.
- VisionSession
- An AutoCloseable scanning lifecycle that accepts frames and exposes native-derived observations.
- Frame source
- Your host owns CameraX preview, focus, torch, permissions, image selection, and lifecycle.

Requirements
Before you start
- DeviceAndroid 8.0 or newer, API 26 minimum, with JDK 17.
- CameraA rear camera with YUV_420_888 output; continuous autofocus and torch are recommended.
- Tenant and activationAn authenticated evaluation tenant, site, reserved device seat, and activated device.
- SDK download grantA tenant download grant for the signed Android evaluation kit.
| Parameter | Minimum | Recommended / tested |
|---|---|---|
| Android OS | Android 8.0 (Oreo, API 26) | Android 11.0 – 14.0 (API 30–34) |
| Language toolchain | Java 8 / Kotlin 1.8+ | Java 17 / Kotlin 1.9+ |
| Build | JDK 17, Gradle 8.x + AGP 8.x | Android API 35 SDK installed |
| Camera | Camera2 API level LIMITED or higher, YUV_420_888 output | Camera2 FULL / LEVEL_3, continuous autofocus, torch |
| Native toolchain | NDK r25c (25.2.9519653), pinned | Same — the release gate pins the C ABI |
| CameraX | androidx.camera:camera-*:1.4.1 (core, camera2, lifecycle, view) | |
Quickstart: first scan in six steps
The linear path from an empty host app to a first verified observation. Each step links to its deep section below.
Install the verified AAR
Release coordinate:
com.visionscan.sdk:vision-scan-android-sdk:0.1.0-RC4app/build.gradle.kts dependencies { implementation(files("libs/vision-scan-android-sdk-0.1.0-RC2.aar")) implementation("androidx.camera:camera-core:1.4.1") implementation("androidx.camera:camera-camera2:1.4.1") implementation("androidx.camera:camera-lifecycle:1.4.1") implementation("androidx.camera:camera-view:1.4.1") }Details: Installation.
Activate the trial and device
Follow the authenticated control-plane sequence: sign in, resolve the current tenant, activate the evaluation, redeem the tenant-scoped SDK download grant, reserve the device seat, activate the device, then refresh its signed license.
The complete operation reference (method and path for each step) lives in the AAR installation guide.
Verify the signed device license
Run the blocking refresh on an I/O worker. The SDK fetches the fixed license and JWKS routes, verifies the JWS and tenant/device claims, persists the lease, and only then enables scanning.
VisionScanSdk.initializeLeaseStore( File(context.noBackupFilesDir, "visionscan-license"), deviceId) val refresh = VisionScanSdk.refreshPaidLicense( "https://api.example.com", tenantId, deviceId, shortLivedAccessToken) check(refresh.activated) { "Activation failed: " + refresh.errorCode } // Use this only when the authenticated host already holds the compact JWS. val evaluation = VisionScanSdk.applySignedLicenseJws(jws, tenantId, deviceId) check(evaluation.isAllowed) { evaluation.denialReason ?: "License denied" }Details: License & Entitlement.
Create a session
val session = VisionScanSdk.createSession( VisionSessionConfig( sessionId = UUID.randomUUID().toString(), tenantId = tenantId, siteId = siteId, scanMode = ScanMode.LIVE_STREAM ) )Details: Scan & Session.
Submit CameraX frames safely
Camera planes can contain row padding. Use
YPlaneExtractor, keep only the latest live frame, and close eachImageProxyexactly once.val analysis = ImageAnalysis.Builder() .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .build() analysis.setAnalyzer(executor) { image -> try { val plane = image.planes.first() val luma = YPlaneExtractor.extractYPlane( plane.buffer, image.width, image.height, plane.rowStride, plane.pixelStride) session.submitFrame( image.width, image.height, ImageFormat.YUV_420_888, luma, StillImageBatchScanProfile.liveSweep()) } finally { image.close() } }Details: Scan & Session.
Render native-derived results
The submit return value is a native status code, not a barcode count. Read observations after an accepted frame; an empty list is a valid no-decode result.
val gtins = session.getLatestObservations() .filterNot { it.isOcrRescue } .mapNotNull { it.gtin } .distinct()Expected output: the synthetic EAN-13 test label supplied with the evaluation kit is a non-production test value that normalizes to GTIN
00012345678905.Expected decode output (synthetic test label) // Scanning the synthetic EAN-13 label from the evaluation kit: session.getLatestObservations() .filterNot { it.isOcrRescue } .mapNotNull { it.gtin } .distinct() // -> ["00012345678905"]Details: Result Handling.
Installation
The AAR is distributed through a tenant-scoped download grant — it is never on public Maven. Verify the release before installing, place the AAR in app/libs/, and add CameraX 1.4.1 (see the journey step above).
The full grant → verify → add → confirm sequence and kit contents: AAR installation detail.
Configuration
Pick one scan mode per session. The mode is set on VisionSessionConfig.scanMode and paired with a frame profile:
LIVE_STREAM— continuous live sweep viaStillImageBatchScanProfile.liveSweep(); requires thebarcode-intelligencefeature.HIGH_DENSITY_RESCUE— tap or focused rescue viastillImageDense(1); requiresmulti-barcode-dense.STILL_IMAGE_BATCH— batch receiving from images viastillImageDense(expectedCount); requiresmulti-barcode-dense.
The full mode reference table lives in the Android stack guides: Kotlin · Java · AAR.
Permissions
Declare camera and network access in the manifest:
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />Request camera permission at runtime before binding CameraX. Handle denial by keeping scan controls disabled — a denied camera is a fail-closed state, not an error to retry silently.
License & Entitlement
The SDK verifies a backend-issued compact JWS before scanning. Refresh is event-based — run it at login, on foreground, on device reassignment, on entitlement revision change, and before lease expiry. No refresh interval is published; do not invent a timer cadence.
- Offline scanning works only through signed grace claims, and fails closed when grace expires or anti-rollback checks fail.
- Bearer tokens stay in host-owned memory or secure credential storage — the lease store holds verified leases, never access tokens.
- Stop scanning after entitlement denial; keep redacted diagnostics for support. Never log JWS values, bearer tokens, raw images, full serial numbers, or tenant-identifying payloads.
The entitlement lifecycle in full: License refresh.
Scan & Session
Session creation is the entitlement gate: a denied session never starts scanning. Close the session on stop, logout, or workflow completion — it is leak-safe through AutoCloseable, and closed sessions fail deterministically on submitFrame().
- Session creation requires a verified matching license and all mode features.
- Creation throws
VisionScanEntitlementDeniedExceptionwhen the verified lease is missing, expired, revoked, outside offline grace, bound to another tenant/device, or missing the required feature.
Scan modes and the production checklist live in the Android stack guides: Kotlin · Java · AAR.
Result Handling
Read observations only after an accepted frame. Render only verified observations — never render a submit status as a result, and never fabricate a GTIN from a failed or empty decode.
The full observation contract: Kotlin guide.
GS1 / GTIN
The full decode contract — deterministic path, observation fields, and review rules — lives in the Android stack guides. For the first scan, two rules are enough:
- GTIN is the primary identity; lot, expiration, and serial remain GTIN-scoped dependent evidence.
isOcrRescuemarks OCR-derived evidence — OCR-only identity remains review-required and must never auto-commit.
Worn or partial labels that will not decode cleanly are the Focused Rescue case — rescue is constrained and never overwrites valid decoded data. The contract reference: Kotlin · Java · AAR.
Errors
The SDK fails closed: entitlement problems disable scanning instead of degrading it, and closed sessions or invalid buffers fail deterministically on submitFrame().
| If this | Do that |
|---|---|
| License refresh denied | HTTPS origin, authenticated tenant, activated device, trial/subscription state, device binding, server time. |
| Session creation denied | JWS verification, expiry/revocation, offline grace, tenant binding, feature entitlement. Creation throws VisionScanEntitlementDeniedException — never a degraded session. |
| Frames accepted but no GTIN | Read getLatestObservations(); check focus, motion blur, glare, working distance, Y-plane extraction, symbology. |
| Distorted or intermittent decode | Confirm row/pixel stride handling; never reuse an ImageProxy after close(). |
| Live preview stalls | KEEP_ONLY_LATEST, close every proxy in finally, no work on the main thread. |
| Batch mode denied | Confirm multi-barcode-dense entitlement; use a batch/rescue session. |
Legacy applySignedLicense overload | Permanently denied — never embed a signing key or caller-created license object in the app. |
Beyond this table: Troubleshooting.