Alaqan Mini SDK
for Android

Java SDK for the Alaqan Mini palm-vein scanner. Distributed as an AAR; integrates over USB host on Android 7.0+.

v2.1.0 April 2026 minSdk 24 arm64-v8a · armeabi-v7a

Overview

Three operations: startIdentify(), startEnroll(), startRescan(). Two listener registries: DeviceEventsListener for device state and FrameListener for processed frames. Both bind to your LifecycleOwner. The SDK runs the USB session, camera stream and on-device inference. You provide the UI and an API key.

Public surface at a glance

// Setup
sdk.setApiKey(String);
sdk.initialize(Context);
sdk.addDeviceEventsListener(LifecycleOwner, DeviceEventsListener);
sdk.addFrameListener(LifecycleOwner, FrameListener);          // optional preview

// Operations (one at a time)
sdk.startIdentify(IdentifyCallback);
sdk.startEnroll(externalId, name, metadata, EnrollCallback);
sdk.startRescan(externalId, RescanCallback);
sdk.stopOperation();

// Device
sdk.getDevice().connect() / disconnect() / reboot();

// Shutdown (process exit)
sdk.destroy();

Requirements

ItemValue
Android API24+ (target 34 recommended)
ABIsarm64-v8a, armeabi-v7a
AAR size~21 MB
Steady-state RAM~30 MB native
HardwareUSB host (OTG); Alaqan Mini scanner
NetworkOutbound HTTPS for activation and biometric calls
PermissionsINTERNET (declared by SDK); android.hardware.usb.host feature

Installation

1

Drop the AAR into app/libs/

app/
├── libs/
│   └── alaqan-mini-sdk.aar
└── build.gradle.kts
2

Wire it up in build.gradle.kts

android {
    defaultConfig {
        minSdk = 24
        ndk { abiFilters += listOf("arm64-v8a", "armeabi-v7a") }
    }
}

dependencies {
    implementation(files("libs/alaqan-mini-sdk.aar"))
    implementation("androidx.appcompat:appcompat:1.6.1")
    implementation("androidx.lifecycle:lifecycle-common:2.6.2")
}
3

USB device filter (res/xml/device_filter.xml)

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <usb-device vendor-id="17992" product-id="65280" />
</resources>
4

AndroidManifest.xml: minimum activity setup

<uses-feature android:name="android.hardware.usb.host" android:required="true" />

<activity android:name=".MainActivity"
          android:launchMode="singleTask"
          android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
    <meta-data android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
               android:resource="@xml/device_filter" />
</activity>
Why singleTask?

The manifest USB-attach intent is routed to your existing instance via onNewIntent() instead of spawning a new one. The SDK keeps its USB session.

Quick Start

One AlaqanMiniSDK per process. Hold it in your Application. The launcher Activity initializes the SDK and opens the USB session; from there you gate the UI on connection state. Each operation lives in its own Activity (see Identify).

// App.java: single SDK instance shared across activities.
public final class App extends Application {
    private final AlaqanMiniSDK sdk = new AlaqanMiniSDK();
    public AlaqanMiniSDK getSdk() { return sdk; }
}
// MainActivity.java: bootstrap the SDK and react to its lifecycle events.
public class MainActivity extends AppCompatActivity
        implements DeviceEventsListener {

    private AlaqanMiniSDK sdk;

    @Override
    protected void onCreate(Bundle b) {
        super.onCreate(b);
        sdk = ((App) getApplication()).getSdk();
        sdk.setApiKey(BuildConfig.ALAQAN_API_KEY);
        sdk.addDeviceEventsListener(this, this);   // removed automatically on ON_DESTROY
        sdk.initialize(this);
    }

    @Override public void onInitialized()                                  { sdk.getDevice().connect(); }
    @Override public void onDeviceConnected(@NonNull Device d)        { /* enable operation entry points in your UI */ }
    @Override public void onDeviceDisconnected(@NonNull Device d)     { /* disable operation entry points */ }
    @Override public void onError(@NonNull SdkError e)                  { /* ambient only; operation errors come via the operation callback */ }

    @Override
    protected void onDestroy() {
        if (isFinishing()) sdk.destroy();
        super.onDestroy();
    }
}
Don't ship the API key in the APK.

Inject via BuildConfig, fetch from your backend on first launch, or read from secure storage. The SDK never persists it.

Don't auto-start operations from onDeviceConnected.

The event fires on every reconnect (cable wiggle, soft reboot). Start operations in response to user intent, typically in onResume of a dedicated screen.

Listener Model

Two registries, both multi-subscriber, both lifecycle-aware. Add the same listener once per LifecycleOwner; the SDK removes it on ON_DESTROY.

RegistryDeliversFrequencyLifecycle binding
DeviceEventsListenerSDK init, device connect/disconnect, ambient errorsSparseRemoved on ON_DESTROY
FrameListenerPer-frame FrameResult~30 HzActive only between ON_START and ON_STOP

Sticky replay

If onInitialized() or onDeviceConnected() already fired before you subscribe, the SDK delivers them again to the new listener. An Activity recreated after rotation re-binds and gets current state without polling isInitialized() / isConnected().

Auto-streaming

The frame stream starts when the first FrameListener is added and stops when the last is removed. There is no startStreaming() / stopStreaming(); registration is the switch.

Error scoping

SourceDelivered to
During an active operationThe operation callback's onResult
Outside any operation (init, USB, watchdog)DeviceEventsListener.onError

Errors are never delivered twice. Your operation callback owns the operation outcome.

Threading

Every callback runs on the main thread. All public SDK methods are safe to call from any thread. The only constraint to remember: Device.getSensingRange() blocks on a USB round-trip, so call it from a worker.

stopOperation() is thread-safe. The terminal onResult(OP_CANCELLED) always lands on the main thread; if you call stopOperation() from the main thread it fires inline before your handler returns.

Lifecycle

One SDK instance per process; the lifecycle of each Activity binds its own listeners.

WhereWhat to call
Application.onCreate()new AlaqanMiniSDK() (held in your Application)
First Activity's onCreate()setApiKey(...) · addDeviceEventsListener(this, this) · initialize(this)
Each Activity's onCreate()addDeviceEventsListener(this, this). Sticky replay re-syncs the UI.
Preview screen's onCreate()addFrameListener(this, this). Streams only while STARTED.
Launcher Activity's onDestroy() when isFinishing()destroy()
destroy() is single-shot.

After destroy(), calling initialize() on the same instance throws IllegalStateException. Construct a new AlaqanMiniSDK if you need to reinitialize. Never call destroy() from a non-launcher Activity's onDestroy; it tears down models and forces a cold start.

Operations

Three operations, single-active. Starting a second while one runs fails fast with OP_ALREADY_RUNNING on the new callback.

void           startIdentify(IdentifyCallback cb);
void           startEnroll(String externalId, String name,
                             @Nullable String metadata, EnrollCallback cb);
void           startRescan(String externalId, RescanCallback cb);
void           stopOperation();
void           setOperationTimeout(long ms);   // default 30 000; 0 disables
OperationState getOperationState();        // IDLE | DETECTING | CAPTURING | PROCESSING
OperationType  getOperationType();         // NONE | IDENTIFY | ENROLL | RESCAN

State machine

StateMeaning
IDLENo operation running
DETECTINGWaiting for a hand that passes confidence + quality + steadiness gates
CAPTURINGEnroll/rescan only; collecting up to three frames
PROCESSINGFrames captured, server call in flight

Identify (1:N)

Captures one steady frame and submits it. Returns the matched person or an error. Start in onResume, cancel in onPause; the activity owns the operation lifecycle.

// IdentifyActivity.java: one identify lifecycle, tied to onResume/onPause.
public class IdentifyActivity extends AppCompatActivity
        implements IdentifyCallback {

    private AlaqanMiniSDK sdk;

    @Override
    protected void onCreate(Bundle b) {
        super.onCreate(b);
        sdk = ((App) getApplication()).getSdk();
    }

    @Override protected void onResume() { super.onResume();  sdk.startIdentify(this); }
    @Override protected void onPause()  { sdk.stopOperation(); super.onPause(); }

    @Override public void onPreviewFrame(@NonNull FrameResult r) {
        // r.bitmap is a shared buffer. Draw it here. Never retain it past this call.
    }

    @Override public void onGuidance(@NonNull Guidance g) {
        // map g to your localized prompt; SDK does not localize.
    }

    @Override public void onResult(@NonNull IdentifyResult r) {
        if (isFinishing() || isDestroyed()) return;

        ErrorCode code = r.error != null ? r.error.code : null;
        if (code == ErrorCode.OP_CANCELLED) return;                  // user navigated away
        if (code == ErrorCode.OP_DISCONNECTED
                || code == ErrorCode.OP_NOT_CONNECTED) { finish(); return; }

        // r.isSuccess() → r.externalId, r.name, r.metadata
        // otherwise → r.error
        finish();
    }
}
IdentifyResult fieldTypeNotes
scanIdString?Server scan id; success only
externalIdString?Matched person's external id
nameString?Null if not set on enroll
metadataString?JSON object string set on enroll
errorSdkError?Null on success

Enroll

Captures three steady frames and uploads the highest-quality one. onProgress fires after each capture so you can render an N/3 indicator.

sdk.startEnroll("emp-1024", "Aiman", null, new EnrollCallback() {
    @Override public void onGuidance(@NonNull Guidance g)            { /* update prompt */ }
    @Override public void onProgress(int captured, int total)        { /* render N/total */ }
    @Override public void onResult(@NonNull EnrollResult r) {
        // r.isSuccess() → r.scanId; otherwise r.error.
    }
});
  • name and metadata are optional; pass null to omit.
  • metadata must be a valid JSON object string when provided.
  • EnrollResult exposes scanId and error.

Rescan

Replaces an enrolled person's biometric template, keeping their name and metadata. Same flow and same result type as enroll; the marker interface RescanCallback extends EnrollCallback documents intent at the call site.

sdk.startRescan("emp-1024", new RescanCallback() {
    @Override public void onGuidance(@NonNull Guidance g)            { /* update prompt */ }
    @Override public void onProgress(int captured, int total)        { /* render N/total */ }
    @Override public void onResult(@NonNull EnrollResult r) {
        // r.isSuccess() on success; otherwise r.error.
    }
});

Guidance

UI hints emitted as state changes. Map each value to your own localized strings; the SDK does not localize.

ValueMeaning
SHOW_PALMNo hand in frame
MOVE_CLOSERHand bbox area below threshold
MOVE_FURTHERHand bbox area above threshold
CENTER_PALMHand bbox off-center
HOLD_STEADYDetected but quality or steadiness gate not met
CAPTURINGEnroll/rescan only; fires once per captured frame
PROCESSINGServer call in flight

API Reference

AlaqanMiniSDK

void           setApiKey(@Nullable String key);
void           initialize(@NonNull Context ctx);
boolean        isInitialized();
Device         getDevice();
@Nullable String getNativeVersion();
void           setLedGateEnabled(boolean on);   // default true
void           setConfig(@NonNull SdkConfig cfg);
SdkConfig      getConfig();
void           destroy();
MethodContract
setApiKeyRequired before initialize(). Setting null after init causes server calls to fail with API_KEY_MISSING until a new key is set.
initializeAsync. On success → onInitialized(). On failure → onError(SdkError) with API_KEY_MISSING / SERVER_NETWORK_ERROR / MODEL_LOAD_FAILED. Idempotent until destroy().
setLedGateEnabledWhen true, frames with the device LED off are skipped before inference. Set false for well-lit indoor rigs where the LED never lights.
destroyReleases native memory, USB session and threads. Single-shot per instance.

Device

// Connection
void    connect();              // no-op until SDK is initialized
void    disconnect();
boolean isConnected();

// Identity
@Nullable String getSerialNumber();    // e.g. "GX4TYQWH3D"
@Nullable String getFirmwareVersion(); // e.g. "2026042319"

// Distance sensor
int     getDistance();                       // last reading in mm; -1 if unknown
@Nullable int[] getSensingRange();          // {min,max} mm. BLOCKING.
void    setSensingRange(int min, int max); // async; default 50–200; clamped [0,255]

// Cameras
Cameras cameras();

// Power
void    reboot();   // soft reboot: disconnect, then auto-reconnect

All getters return null / -1 until onDeviceConnected fires. setSensingRange is fire-and-forget; getSensingRange waits for a USB reply, so call it on a worker thread.

Cameras

The scanner has two cameras: CameraType.IR (infrared) and CameraType.VL (visible light). The active camera is the source of preview frames delivered to FrameListener.

Cameras cams = sdk.getDevice().cameras();

cams.setActive(CameraType.VL);   // switch preview source
CameraType active = cams.getActiveType();

long ir = cams.getIrFrameCount();   // frames since connect
long vl = cams.getVlFrameCount();

The detection camera is selected by the SDK while an operation is running and is not partner-controllable.

SdkConfig

Currently exposes one knob: the frame-stall watchdog interval. Apply via setConfig(); takes effect on the next watchdog start.

sdk.setConfig(new SdkConfig.Builder()
        .frameWatchdogMs(15_000L)        // default 30 000; min 1 000
        .build());

If no frame arrives for frameWatchdogMs ms while the device is connected, the SDK fires DEVICE_NOT_RESPONDING on onError once. The connection stays up; this is a signal, not a teardown trigger. Quality and steadiness thresholds are not partner-tunable.

Listeners & Callbacks

DeviceEventsListener

public interface DeviceEventsListener {
    default void onInitialized() {}
    default void onDeviceConnected(@NonNull Device d) {}
    default void onDeviceDisconnected(@NonNull Device d) {}
    default void onError(@NonNull SdkError error) {}
}
sdk.addDeviceEventsListener(listener);                   // manual lifetime
sdk.addDeviceEventsListener(lifecycleOwner, listener);   // removed on ON_DESTROY
sdk.removeDeviceEventsListener(listener);

All methods are default; implement only what you need. Errors from active operations are not delivered here.

FrameListener

public interface FrameListener { void onFrame(@NonNull FrameResult r); }
sdk.addFrameListener(listener);                         // stream starts
sdk.addFrameListener(lifecycleOwner, listener);         // streams only while STARTED
sdk.removeFrameListener(listener);                      // stream stops on last remove
Buffer ownership.

FrameResult.bitmap is shared and reused. Render or Bitmap.copy() inside onFrame; never call recycle() and never keep the reference past the callback.

IdentifyCallback

public interface IdentifyCallback {
    default void onPreviewFrame(@NonNull FrameResult r) {}
    void         onGuidance(@NonNull Guidance g);
    void         onResult(@NonNull IdentifyResult r);
}

onResult fires exactly once. onPreviewFrame is a no-op by default; override it if you render preview straight from the operation callback. Otherwise use a separate FrameListener.

EnrollCallback

public interface EnrollCallback {
    default void onPreviewFrame(@NonNull FrameResult r) {}
    void         onGuidance(@NonNull Guidance g);
    void         onProgress(int captured, int total);   // total currently 3
    void         onResult(@NonNull EnrollResult r);
}

RescanCallback

public interface RescanCallback extends EnrollCallback {}

Marker interface. Methods are identical to EnrollCallback. Use it at the call site so intent is documented; the SDK can later add rescan-specific events without breaking enroll callers.

Data Types

FrameResult

FieldTypeNotes
jpegSizeintSource JPEG size in bytes
width / heightintFrame dimensions in pixels
bitmapBitmap?Decoded preview; shared, do not recycle, do not retain
handsHandDetection[]?null if no hands
moreHandsbooleantrue if more hands than the array's length were found
inferenceMsfloatTotal inference time
ledOnbooleanDevice LED state at capture
steadybooleanBbox stable across consecutive detections

HandDetection

FieldTypeNotes
confidencefloatDetection confidence, 0–1
x / y / w / hfloatBbox top-left + size in pixels
qualityfloatQuality score, 0–100; -1 if not computed

Results

Both result types follow the same shape: check isSuccess() first; on failure read error.

TypeFieldsReturned by
IdentifyResultscanId, externalId, name, metadata, errorstartIdentify()
EnrollResultscanId, errorstartEnroll(), startRescan()

SdkError

public final class SdkError {
    @NonNull  public final ErrorCode code;
    @Nullable public final String    detail;
}

Branch on code (an enum). detail carries a server message for SERVER_REJECTED, otherwise an exception text. Use it for logs, never for control flow.

ErrorCode exposes three things. code() returns a stable 4-digit string for telemetry. category() returns one of DEVICE, AUTH, STREAMING, NATIVE, NETWORK, OPERATION. fromCode(String) does the reverse lookup.

@Override
public void onError(@NonNull SdkError e) {
    switch (e.code) {
        case API_KEY_MISSING:        promptForKey(); break;
        case DEVICE_NOT_RESPONDING:
        case USB_NO_FW_VERSION:      // informational; leave UI alone          break;
        default:
            if (e.code.category() == ErrorCode.Category.NETWORK) showOffline();
            else showError(e.code.code());
    }
}

Error Codes

The full ErrorCode enum. Branch on the enum value; code() returns a stable 4-digit string suitable for logging and back-end reporting.

Device & USB (1xxx)

ConstantMeaning · what to do
1001USB_PERMISSION_DENIEDUser rejected the USB permission dialog. Re-prompt or guide the user to grant access.
1002USB_NO_INTERFACEUSB interface not exposed by the device. Check VID/PID filter and cable.
1003USB_NO_ENDPOINTSRequired USB endpoints missing. Hardware-level issue; reseat or swap the device.
1004USB_OPEN_FAILEDCould not open the USB device. Often follows a kernel/USB-stack hiccup; retry after replug.
1005USB_CLAIM_FAILEDAnother process holds the interface. Close other apps using the device.
1006USB_NO_SERIALSerial number unreadable. Server biometric calls require it; treat as a connect failure.
1007DEVICE_NOT_RESPONDINGNo frames within frameWatchdogMs. Informational; the connection stays up.
1008USB_NO_FW_VERSIONFirmware version probe failed. Informational; the connection stays up.

Authentication (2xxx)

ConstantMeaning · what to do
2001AUTH_CHALLENGE_FAILEDDevice authentication failed at stage 1. Replug or reboot device.
2002AUTH_INVALID_REPLYDevice returned an unexpected response during authentication. Replug or reboot.
2003AUTH_COMPUTE_FAILEDClient-side authentication step failed. Re-call initialize().
2004AUTH_VERIFY_FAILEDDevice authentication failed at stage 2. Replug or reboot device.
2005AUTH_REJECTEDDevice rejected the SDK's credentials. Re-call initialize(); if persistent, contact support.

Streaming (3xxx)

ConstantMeaning · what to do
3001STREAM_START_FAILEDCould not send stream-start command. Replug the device; if persistent, contact support.
3002STREAM_REJECTEDDevice declined the stream-start. Reboot device.

SDK & Native (4xxx)

ConstantMeaning · what to do
4001NATIVE_CREATE_FAILEDNative runtime failed to start. Confirm both ABIs are packaged.
4002MODEL_LOAD_FAILEDModels could not be loaded. Confirm activation succeeded (no preceding SERVER_*).
4004AUTH_PROVIDER_MISSINGInternal misconfiguration; should not occur in released builds.

Network (5xxx)

ConstantMeaning · what to do
5001API_KEY_MISSINGSet the key with setApiKey() before initialize().
5002SERVER_NETWORK_ERRORTransport error. Surface offline UI; offer retry.
5003SERVER_INVALID_RESPONSEMalformed server reply. Capture logs; contact support if persistent.
5004SERVER_REJECTEDServer returned an error message. Read error.detail; the catalog below covers the common values.
5005SERVER_TIMEOUTRequest exceeded 15 s. Offer retry; check network quality.

Operations (6xxx)

ConstantMeaning · what to do
6001OP_ALREADY_RUNNINGAnother operation is active. Call stopOperation() first or wait.
6002OP_CANCELLEDYou called stopOperation(). Treat as a no-op in UI.
6003OP_TIMEOUTOperation timeout elapsed. Show retry UI.
6004OP_DISCONNECTEDDevice disconnected mid-operation. Close the screen.
6005OP_NOT_INITIALIZEDStarted before onInitialized(). Wait for the event or check isInitialized().
6006OP_NOT_CONNECTEDStarted before onDeviceConnected(). Wait or check device.isConnected().
6007OP_INVALID_PARAMSBad arguments. Typically an empty externalId on enroll/rescan.

SERVER_REJECTED.detail catalog

When code == SERVER_REJECTED, error.detail holds the server's message verbatim. Map these to your own copy:

DetailWhenSuggested handling
"no hand detected"anyPrompt user to retry
"quality check failed"anyPrompt user to retry, hand cleaner / closer
"liveness check failed"anyPrompt user to retry with a real palm
"template not found"identifyShow "not enrolled"
"person not found"rescanexternalId is not registered; switch to enroll
"person already exists"enrollAsk whether to rescan instead
"template already enrolled"enroll/rescanThe biometric matches a different person. Review the case.
"duplicate capture detected"anySame image submitted twice; likely a double-tap.
"invalid inference"anyNot retryable. Escalate to support.
"inference unavailable"anyRetry with backoff

Best Practices

One SDK instance, in Application

Construct once, share across activities, destroy() only when the launcher Activity is finishing.

Always bind listeners with a LifecycleOwner

Removes the listener on ON_DESTROY automatically. FrameListener additionally pauses the stream when the screen leaves the foreground.

@Override
protected void onCreate(Bundle b) {
    super.onCreate(b);
    sdk.addDeviceEventsListener(this, this);   // sticky-replay + auto-cleanup
    sdk.addFrameListener(this, this);           // stream while visible
}

Drive UI from onGuidance / onProgress / onResult

You don't need a separate FrameListener while an operation is running. Operation callbacks already deliver preview frames via onPreviewFrame. Pick one source.

Cancel cleanly across navigation

@Override
protected void onPause() {
    sdk.stopOperation();   // emits OP_CANCELLED; handle as a no-op
    super.onPause();
}

Branch on ErrorCode, not its string

Use the enum and its category(). Strings are for logs and back-end telemetry only.

Treat DEVICE_NOT_RESPONDING and USB_NO_FW_VERSION as informational

Both are signals; neither tears the connection down. Real disconnects arrive via onDeviceDisconnected.

Troubleshooting

onDeviceConnected never fires

  • Confirm cable, port, OTG support on the host.
  • device_filter.xml contains vendor-id="17992" and product-id="65280".
  • Activity declares the manifest USB filter and android:launchMode="singleTask".

Init fails with SERVER_NETWORK_ERROR / API_KEY_MISSING / MODEL_LOAD_FAILED

  • API key set before initialize().
  • Network reachable; activation is HTTPS with a 15 s timeout.
  • Both ABIs (arm64-v8a, armeabi-v7a) packaged in the APK.

Guidance stuck on HOLD_STEADY

  • Hand outside the configured sensing range; check device.getDistance().
  • Direct sunlight on the scanner glass; protective film not removed.
  • Indoor rig with the device LED disabled. Call setLedGateEnabled(false).

Frames stop arriving (DEVICE_NOT_RESPONDING)

  • Try device.reboot().
  • If persistent, replug the device or contact support.

Support

Technical

support@alaqan.com

Sales

sales@alaqan.com

What to attach to a bug report

The ErrorCode.code() string. Any error.detail. Steps to reproduce. Device serial number and firmware version. SDK version. Android logcat covering the failure.