Skip to main content
This tutorial picks up where Part 1 left off. You already have a working Android app that runs a GBG Go identity journey with the stub capture surface. Now you will replace those stubs with the real Smart Capture SDKs — adding guided document scanning, face capture with liveness detection, and an encrypted biometric blob. The change is small. The bridge architecture you built in Part 1 was designed so that swapping the capture UI is a localised edit. The handler registration, the BridgeResponder.respond(...) contract, and all bridge wiring stay the same. Only the UI that produces the image changes.
Reference app: The complete source code is on the part-2-smart-capture branch of the reference repository. To see exactly what changed from Part 1, diff the branches:

Prerequisites

Everything from Part 1, plus:

Required dependencies

The Smart Capture SDKs are published as Android libraries to a credentialed S3 Maven repository:

Start from Part 1

Check out the Part 2 branch:
Or if you already have the repo:
The companion server is unchanged — start it the same way as Part 1:

Add the Smart Capture dependencies

Unlike iOS — where the SDKs ship as binary frameworks you drop into the project — the Android SDKs are ordinary Maven dependencies. The only wrinkle is that they live in a credentialed S3 repository, so you configure that repository and supply your credentials, then declare the dependencies.

1. Configure the S3 repository

In settings.gradle.kts, register the Smart Capture repository inside dependencyResolutionManagement. It is added only when credentials are present, so a checkout without them produces a clear warning rather than a confusing failure deep in the build:

2. Add your credentials (never commit them)

Put the access key and secret in your user-global Gradle properties — ~/.gradle/gradle.properties, which lives outside any repository and so can never be committed:
CI environments can instead export them as environment variables, which Gradle maps to the same properties:
Do not put these in the repository’s gradle.properties, local.properties, or settings.gradle.kts. The reference repo is public, and committed S3 credentials would leak. The user-global file is the correct home.

3. Declare the dependencies

In app/build.gradle.kts, add the three artifacts alongside the GBGBridge SDK:
Sync Gradle. The Smart Capture libraries — including their bundled native .so liveness libraries — are now on the classpath.

Choose Smart Capture or the stub at runtime

iOS gates Smart Capture behind a compile-time flag (SMART_CAPTURE_ENABLED) and falls back to stubs on the Simulator. Android takes a slightly different approach for two reasons:
  1. The branch is the toggle. Because the SDKs are Maven dependencies (not committed binaries), the part-2-smart-capture branch simply depends on them directly. The “with vs without Smart Capture” choice is the branch you check out — main for stubs, this branch for Smart Capture — so there is no per-build compile flag.
  2. Emulators still need a fallback. The Smart Capture document scanner and liveness SDK need real camera hardware and sensors. The Android emulator’s synthetic camera isn’t enough, so the app decides at runtime whether to use Smart Capture or fall back to the stub surface.
Create DeviceCapabilities.kt in the ui/capture/ package:
This is the Android analogue of iOS’s #if SMART_CAPTURE_ENABLED && !targetEnvironment(simulator) guard.

Document Capture with Smart Capture

The Smart Capture SDKs are Activity-based: you launch DocumentCameraActivity (or FaceCameraActivity) with StartActivityForResult and read the result back when it finishes. This differs from the iOS SDKs, which embed inline SwiftUI views — but it slots into the same handler seam. Create SmartCaptureLauncher.kt in ui/capture/. It is the only file that touches the com.gbg.smartcapture types — it maps the SDK Activities onto the existing CameraCaptureHandler:

How document capture works

  • DocumentCameraActivity.getIntent(context, DocumentScannerConfig()) builds the launch intent. The default DocumentScannerConfig() enables guided auto-capture; you can pass a DocumentSide / DocumentType to constrain it.
  • The result is not returned in the Intent — the SDK exposes it as DocumentCameraActivity.latestResult, a sealed DocumentProcessingState.Result (Success or Failure), read after RESULT_OK.
  • DocumentProcessingState.Success.image is a DocumentImage carrying the JPEG bytes, dimensions, rotation, and quality flags (isGood, isSharp, isGlareFree, isAdequateDpi). image.toBitmap(true) returns a rotation-corrected bitmap, which we forward to the handler.

What you get vs the stub

Face Capture with Liveness

Face capture follows the same pattern — launch FaceCameraActivity, then read the result:
  • On RESULT_OK, FaceCameraActivity.getResult() returns a FaceCameraResult with previewPhoto (a Bitmap) and encryptedBlob (a ByteArray of encrypted biometric data for server-side liveness verification).
  • On FaceCameraActivity.RESULT_ERROR, the ERROR_OBJECT extra carries a SmartCaptureException describing the failure.
  • RESULT_CANCELED means the user dismissed the camera.
The mapping for all three cases is in handleSelfieResult above.

What you get vs the stub

Forward the encrypted blob

The stub handler only sends imageBase64 / dimensions / mimeType. The face SDK additionally produces an encrypted liveness blob, which the journey needs for server-side validation — matching the iOS reference, whose selfie result carries the same blob. Add Smart Capture entry points to CameraCaptureHandler.kt. They build the same response shape as the stub paths, with the selfie path adding encryptedBlob:
The handler stays SDK-agnostic — it deals in plain Bitmap / ByteArray, so SmartCaptureLauncher does all the mapping from the com.gbg.smartcapture types.

The Swap Pattern

Open JourneyScreen.kt. Three additions wire Smart Capture in, and the existing stub block is guarded so it only renders on the fallback path. First, decide once which path this device uses, and register the activity launchers:
Then launch the SDK Activity when a capture becomes active. Keying the effect on activeCapture makes it fire exactly once per activation:
Finally, guard the Part 1 stub surface so it renders only on the fallback path:

What didn’t change

Look at what surrounds these additions — nothing changed:
  • The BridgeController construction (host, delegate, capability map) is identical.
  • The handler registration and the onActivate / onDeactivate callbacks driving activeCapture are identical.
  • The BridgeResponder.respond(...) contract and the wire format are identical (bar the extra encryptedBlob field on selfie success).
  • The companion server is identical.
This is the whole point of the architecture. The bridge integration layer is stable; only the capture UI swaps.

Why runtime instead of a compile flag?

The single-flight activeCapture state already names which capture is in flight; useSmartCapture names how to satisfy it. Because the SDKs are Maven dependencies that this branch always pulls in, there is nothing to conditionally compile — the only real decision is at runtime: does this hardware support Smart Capture, or do we fall back to the stub? That maps cleanly onto a remembered boolean and a guarded composable.

Test on a physical device

  1. Make sure the companion server is running.
  2. Find your machine’s LAN IP (e.g. ipconfig getifaddr en0 on macOS).
  3. Connect a physical Android device and select it in Android Studio.
  4. Press Run (or ./gradlew :app:assembleDebug then install).
  5. On the Setup screen, enter http://<your-ip>:3000 as the server URL.
  6. Tap Start Journey.
  7. When the journey requests a document capture, the Smart Capture document scanner opens — with a guided overlay, edge detection, and auto-capture.
  8. When the journey requests a selfie, the FaceCamera SDK opens — with face positioning and liveness detection, returning the encrypted blob to the journey.
On an emulator the app uses the stub surface automatically. This is by design.

Common Pitfalls

Credentials not configured

If awsAccessKey / awsSecretKey aren’t set, the Smart Capture repository isn’t registered and the build fails at dependency resolution with Could not resolve com.gbg.smartcapture:.... Add the credentials to ~/.gradle/gradle.properties (see Add your credentials), or build the main branch for the stub-only Part 1 app.

Smart Capture views don’t appear on a device

If capture still shows the stub surface on real hardware, DeviceCapabilities.supportsSmartCapture() returned false. Check the emulator-detection heuristic against your device’s Build fingerprints — some devices report unusual values.

Native library stripping warning

The build logs Unable to strip the following libraries ... libFaceIad.so, .... This is a warning, not an error — the SDK’s prebuilt native liveness libraries are packaged as-is. The build still succeeds and the APK runs.

Capture appears to hang

The handler enforces single-flight: a second capture request for the same action while one is active gets a BUSY error. If a capture seems stuck, confirm the previous request resolved (the LaunchedEffect only re-launches when activeCapture transitions, so a handler that never calls back will block the next request).

What’s Next

  • API Reference — Full documentation for BridgeHost, BridgeWebViewConfigurator, and result types.
  • Capture Screens — The stub capture surface and the swap seam in detail.
  • Capability Handling — Raw handlers, typed slots, custom capabilities, and permission states.