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 thepart-2-smart-capturebranch 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: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
Insettings.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:
3. Declare the dependencies
Inapp/build.gradle.kts, add the three artifacts alongside the GBGBridge SDK:
.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:
- The branch is the toggle. Because the SDKs are Maven dependencies (not committed binaries), the
part-2-smart-capturebranch simply depends on them directly. The “with vs without Smart Capture” choice is the branch you check out —mainfor stubs, this branch for Smart Capture — so there is no per-build compile flag. - 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.
DeviceCapabilities.kt in the ui/capture/ package:
#if SMART_CAPTURE_ENABLED && !targetEnvironment(simulator) guard.
Document Capture with Smart Capture
The Smart Capture SDKs are Activity-based: you launchDocumentCameraActivity (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 defaultDocumentScannerConfig()enables guided auto-capture; you can pass aDocumentSide/DocumentTypeto constrain it.- The result is not returned in the
Intent— the SDK exposes it asDocumentCameraActivity.latestResult, a sealedDocumentProcessingState.Result(SuccessorFailure), read afterRESULT_OK. DocumentProcessingState.Success.imageis aDocumentImagecarrying 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 — launchFaceCameraActivity, then read the result:
- On
RESULT_OK,FaceCameraActivity.getResult()returns aFaceCameraResultwithpreviewPhoto(aBitmap) andencryptedBlob(aByteArrayof encrypted biometric data for server-side liveness verification). - On
FaceCameraActivity.RESULT_ERROR, theERROR_OBJECTextra carries aSmartCaptureExceptiondescribing the failure. RESULT_CANCELEDmeans the user dismissed the camera.
handleSelfieResult above.
What you get vs the stub
Forward the encrypted blob
The stub handler only sendsimageBase64 / 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:
Bitmap / ByteArray, so SmartCaptureLauncher does all the mapping from the com.gbg.smartcapture types.
The Swap Pattern
OpenJourneyScreen.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:
activeCapture makes it fire exactly once per activation:
What didn’t change
Look at what surrounds these additions — nothing changed:- The
BridgeControllerconstruction (host, delegate, capability map) is identical. - The handler registration and the
onActivate/onDeactivatecallbacks drivingactiveCaptureare identical. - The
BridgeResponder.respond(...)contract and the wire format are identical (bar the extraencryptedBlobfield on selfie success). - The companion server is identical.
Why runtime instead of a compile flag?
The single-flightactiveCapture 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
- Make sure the companion server is running.
- Find your machine’s LAN IP (e.g.
ipconfig getifaddr en0on macOS). - Connect a physical Android device and select it in Android Studio.
- Press Run (or
./gradlew :app:assembleDebugthen install). - On the Setup screen, enter
http://<your-ip>:3000as the server URL. - Tap Start Journey.
- When the journey requests a document capture, the Smart Capture document scanner opens — with a guided overlay, edge detection, and auto-capture.
- When the journey requests a selfie, the FaceCamera SDK opens — with face positioning and liveness detection, returning the encrypted blob to the journey.
Common Pitfalls
Credentials not configured
IfawsAccessKey / 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 logsUnable 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 aBUSY 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.