Skip to main content

Android Paper Integration

This page covers building your own camera screen around RecognizerView. If you want the ready-made camera screen instead, use the blinkreceipt-camera-ui module — see Camera UI Customization and the Jetpack Compose Integration guide.

Before you start, complete Android Paper Installation: the SDK must be initialized before you set up a RecognizerView.

1. Implement RecognizerCallback​

RecognizerCallback delivers scan results and error events:

public interface RecognizerCallback {
// Called when scanning is complete and results are ready.
void onRecognizerDone(@NonNull ScanResults results, @NonNull Media media);

// Called if an exception occurs while processing a frame.
void onRecognizerException(@NonNull Throwable e);

// Called each time an intermediate result is produced during scanning.
void onRecognizerResultsChanged(@NonNull RecognizerResult result);
}

Intermediate Result Types​

onRecognizerResultsChanged fires throughout the scan session. Check the RecognizerResult against the specific type you need:

TypeWhat it carries
PreliminaryResultA snapshot of the receipt so far — results(), media() — produced when you call preliminaryResults()
OcrRawResultRaw OCR output for a frame
EdgeDetectionResultFrame-quality feedback: how much of the frame the receipt fills, and whether its edges were found
MerchantRecognizerResultThe merchant name, as soon as the winning merchant is identified during the scan
BitmapResultA processed frame

Preliminary results — partial data before the scan ends:

@Override
public void onRecognizerResultsChanged(@NonNull RecognizerResult result) {
if (result instanceof PreliminaryResult) {
PreliminaryResult preliminary = (PreliminaryResult) result;
ScanResults partial = preliminary.results();
// Use partial receipt data
}
}

From 2.5.0, a snapshot you requested with preliminaryResults(true) reports preliminary.tripResolutionRequested() == true — see Finishing a Scan.

Raw OCR results:

@Override
public void onRecognizerResultsChanged(@NonNull RecognizerResult result) {
if (result instanceof OcrRawResult) {
OcrRawResult ocrRawResult = (OcrRawResult) result;
}
}

Edge detection results — frame quality feedback:

@Override
public void onRecognizerResultsChanged(@NonNull RecognizerResult result) {
if (result instanceof EdgeDetectionResult) {
EdgeDetectionResult edges = (EdgeDetectionResult) result;

float filled = edges.contentPercent(); // how much of the frame the receipt fills
if (edges.state().state() == EdgeDetectionState.ABOVE_THRESHOLD) {
// The receipt fills enough of the frame
}
}
}

Edge detection states (EdgeDetectionState constants):

StateMeaning
ABOVE_THRESHOLDReceipt fills at least the configured minimum percentage of the frame
BELOW_THRESHOLDReceipt is too small in the frame
CONSECUTIVE_ABOVE_THRESHOLD_LIMIT_REACHEDN consecutive frames above threshold — ready to capture
CONSECUTIVE_BELOW_THRESHOLD_LIMIT_REACHEDN consecutive frames below threshold

Camera Callback​

With RecognizerView, implement CameraRecognizerCallback. It extends RecognizerCallback, so one object receives both the scan results and the camera events:

public interface CameraRecognizerCallback extends RecognizerCallback {
// Called when a confirmed frame has been written to storage.
void onConfirmPicture(@NonNull File file);

// Called when the CAMERA permission has not been granted.
void onPermissionDenied();

// Called when the camera preview has started.
void onPreviewStarted();

// Called when the camera or a capture fails.
void onException(@NonNull Throwable throwable);
}

Register it with recognizerView.recognizerCallback(callback) — see Setting Up the View. onConfirmPicture fires only when frame storage is on, through ScanOptions.Builder.frameCharacteristics(FrameCharacteristics.newBuilder().storeFrames(true).build()).


2. RecognizerView (Custom Camera UI)​

RecognizerView gives you a camera preview surface you can embed in any layout. It observes your screen's lifecycle itself, so you don't forward onResume(), onPause(), or other lifecycle events to it — register the lifecycle owner once instead.

Setting Up the View​

Request the CAMERA runtime permission first; without it, the view reports onPermissionDenied() and shows no preview. Then set the view up in this order:

// 1. Callbacks first, so no early event is missed.
recognizerView.recognizerCallback(cameraRecognizerCallback);
recognizerView.cameraCaptureListener(cameraCaptureListener);

// 2. Start the scan session with your scan configuration.
ScanOptions scanOptions = ScanOptions.newBuilder()
.countryCode("US")
.detectDuplicates(true)
.build();
recognizerView.initialize(scanOptions);

// 3. Register the lifecycle owner exactly once. This is what starts the camera.
recognizerView.lifecycle(this);

initialize(ScanOptions) throws if the SDK has not been initialized or the license is invalid. Call lifecycle(...) after initialize(...), not before.

Capturing Frames​

Take a picture with takePicture(). The photo arrives in your CameraCaptureListener:

recognizerView.takePicture();
CameraCaptureListener cameraCaptureListener = new CameraCaptureListener() {
@Override
public void onCaptured(@NonNull BitmapResult results) {
// Show the photo, or confirm it right away:
recognizerView.confirmPicture(results);
}

@Override
public void onException(@NonNull Throwable e) {
// The capture failed.
}
};

confirmPicture(bitmapResult) adds the photo to the scan. Confirmed frames are returned in the Media object passed to onRecognizerDone, and — when frame storage is on — each one is also reported through onConfirmPicture(File).

Finishing a Scan​

recognizerView.finishedScanning();

This compiles and finalizes results, delivering them through onRecognizerDone(ScanResults results, Media media). onRecognizerDone, onRecognizerException, and the intermediate results are delivered on the main thread; MerchantRecognizerResult can arrive on a background thread, so switch to the main thread before touching your UI with it.

From 2.5.0, when a US scan ends with no merchant identified, the SDK queries the Trip Resolution API as a last-resort merchant lookup before delivering results. This can add up to 4 seconds to finishedScanning(); a merchant found this way fills ScanResults.merchantName(). To include Trip Resolution in a snapshot before you finish — for example, the snapshot you will treat as final — call recognizerView.preliminaryResults(true). Request it once: each request can make a network call and wait up to 4 seconds for it.

To cancel mid-scan:

recognizerView.terminate();

Guarding Asynchronous Capture Callbacks​

Avoid acting on a terminated session on screen re-entry

RecognizerView is a per-screen view, but the underlying Recognizer is a process-wide singleton. Calling terminate() on any RecognizerView reference tears down that shared session for every view. If a stale or older RecognizerView — for example one left over when the user re-enters the scan screen — calls terminate() after a new session has been initialized, the live session is poisoned. Any in-flight capture callback (e.g. onCaptured) that then re-enters the view acts on a session that no longer exists, and frames may be silently dropped (empty scan results).

Follow both rules below to prevent this.

Rule 1 — Keep exactly one live scan session. Ordering is the root cause: never let an old session's teardown run after a new one has initialized.

  • Terminate the old session with terminate() before calling initialize(...) on a new one.
  • Register the lifecycle owner exactly once for the live view.
  • Never keep a RecognizerView reference in a longer-lived holder (ViewModel, singleton) and terminate it later — a late terminate() will kill an unrelated active session.

Rule 2 — Guard the async capture callback. The capture callback can fire after the screen has started tearing down, or after the shared session was terminated. Before touching the view, verify the screen is at least STARTED and the session is still initialized:

@Override
public void onCaptured(@NonNull BitmapResult results) {
// Bail if the screen is tearing down or the shared session was terminated.
if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)
|| !recognizerView.initialized()) {
return;
}

recognizerView.confirmPicture(results);
}

initialized() returns false both when this view was terminated and when another view terminated the shared session, which is what makes it the reliable check. Without it, the failure signal is not uniform:

CallThis view was terminatedAnother view terminated the shared session
takePicture()Throws IllegalStateExceptionStill captures a frame
confirmPicture(...)Never throws — reported through CameraRecognizerCallback.onExceptionProcesses the frame against the torn-down session
preliminaryResults()Throws IllegalStateExceptionDelivers an empty PreliminaryResult
finishedScanning()Throws IllegalStateExceptionDoes nothing

So apply the same guard to every call that re-enters the view from an async callback, and wrap takePicture(), preliminaryResults(), and finishedScanning() in a try/catch for the window between the guard and the call:

if (!recognizerView.initialized()) {
return;
}

try {
recognizerView.finishedScanning();
} catch (IllegalStateException e) {
// Session was terminated between the guard and this call — nothing left to do.
}
note

recognizerView.initialized() is available from com.microblink.blinkreceipt:blinkreceipt-bom:2.3.0 onwards. Confirm your BOM version before relying on it.

Earlier SDK versions (before BOM 2.3.0). recognizerView.initialized() does not exist yet, so pre-check the session with a client-managed flag instead — set it when your session initializes and clear it on terminate() (and on navigation away from the scan screen), then check that flag in place of recognizerView.initialized(). On these versions, calls on a terminated session can throw RecognizerNotInitializedException, so keep a try/catch around them:

private volatile boolean recognizerSessionActive = false;

private void initializeRecognizer() {
recognizerView.initialize(scanOptions);
recognizerSessionActive = true;
}

private void terminateRecognizer() {
recognizerView.terminate();
recognizerSessionActive = false;
}

@Override
public void onCaptured(@NonNull BitmapResult results) {
// Bail if the screen is tearing down or the shared session was terminated.
if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)
|| !recognizerSessionActive) {
return;
}

try {
recognizerView.confirmPicture(results);
} catch (RecognizerException | IllegalStateException e) {
// Session was terminated between the guard and this call — nothing left to do.
return;
}
}

This flag is a fallback, not a substitute for Rule 1 — it only protects the callback; overlapping RecognizerView lifetimes must still be avoided.

If you use Jetpack Compose, see the Jetpack Compose Integration guide for the Compose-specific ordering rules.


3. AndroidManifest Keys​

Add the SDK's keys to AndroidManifest.xml inside the <application> element.

License key — required. The SDK reads it when it initializes (see Android Paper Installation):

<meta-data
android:name="com.microblink.LicenseKey"
android:value="YOUR_LICENSE_KEY" />

The remaining keys are optional.

Product Intelligence — enriches receipt line items with product data:

<meta-data
android:name="com.microblink.ProductIntelligence"
android:value="YOUR_PRODUCT_INTELLIGENCE_KEY" />

Google Places — merchant location enrichment:

<meta-data
android:name="com.microblink.GooglePlacesKey"
android:value="YOUR_GOOGLE_PLACES_KEY" />

Yelp — merchant data enrichment:

<meta-data
android:name="com.microblink.YelpKey"
android:value="YOUR_YELP_KEY" />

Client User ID — associates scan sessions with your user identifiers:

<meta-data
android:name="com.microblink.ClientUserId"
android:value="YOUR_CLIENT_USER_ID" />

The optional keys can also be set in code with BlinkReceiptSdk.productIntelligenceKey(...), googleApiKey(...), yelpApiKey(...), and clientUserId(...); a value set in code takes precedence over the manifest. Because a client user ID identifies one user, setting it at sign-in with BlinkReceiptSdk.clientUserId(id) usually fits better than a fixed manifest value.