Skip to main content

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.

Illustration: an Android device captures a barcode and decoded data flows into your application

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.
Android scan workflow from app capture through Vision Scan processing to a validated observation
Your app owns capture and lifecycle; Vision Scan processes the barcode and returns observations your app can render.

Requirements

Before you start

Verified host requirements (android-sdk-compatibility-matrix.md §2, developer guide prerequisites)
ParameterMinimumRecommended / tested
Android OSAndroid 8.0 (Oreo, API 26)Android 11.0 – 14.0 (API 30–34)
Language toolchainJava 8 / Kotlin 1.8+Java 17 / Kotlin 1.9+
BuildJDK 17, Gradle 8.x + AGP 8.xAndroid API 35 SDK installed
CameraCamera2 API level LIMITED or higher, YUV_420_888 outputCamera2 FULL / LEVEL_3, continuous autofocus, torch
Native toolchainNDK r25c (25.2.9519653), pinnedSame — the release gate pins the C ABI
CameraXandroidx.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.

  1. Install the verified AAR

    Release coordinate: com.visionscan.sdk:vision-scan-android-sdk:0.1.0-RC4

    app/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.

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

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

  4. Create a session

    val session = VisionScanSdk.createSession(
        VisionSessionConfig(
            sessionId = UUID.randomUUID().toString(),
            tenantId = tenantId,
            siteId = siteId,
            scanMode = ScanMode.LIVE_STREAM
        )
    )

    Details: Scan & Session.

  5. Submit CameraX frames safely

    Camera planes can contain row padding. Use YPlaneExtractor, keep only the latest live frame, and close each ImageProxy exactly 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.

  6. 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 via StillImageBatchScanProfile.liveSweep(); requires the barcode-intelligence feature.
  • HIGH_DENSITY_RESCUE — tap or focused rescue via stillImageDense(1); requires multi-barcode-dense.
  • STILL_IMAGE_BATCH — batch receiving from images via stillImageDense(expectedCount); requires multi-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:

AndroidManifest.xml
<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 VisionScanEntitlementDeniedException when 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.
  • isOcrRescue marks 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().

First-scan recovery (android-sdk-developer-guide.md)
If thisDo that
License refresh deniedHTTPS origin, authenticated tenant, activated device, trial/subscription state, device binding, server time.
Session creation deniedJWS verification, expiry/revocation, offline grace, tenant binding, feature entitlement. Creation throws VisionScanEntitlementDeniedException — never a degraded session.
Frames accepted but no GTINRead getLatestObservations(); check focus, motion blur, glare, working distance, Y-plane extraction, symbology.
Distorted or intermittent decodeConfirm row/pixel stride handling; never reuse an ImageProxy after close().
Live preview stallsKEEP_ONLY_LATEST, close every proxy in finally, no work on the main thread.
Batch mode deniedConfirm multi-barcode-dense entitlement; use a batch/rescue session.
Legacy applySignedLicense overloadPermanently denied — never embed a signing key or caller-created license object in the app.

Beyond this table: Troubleshooting.