StubDocumentCameraView, StubSelfieCameraView) to paper over the ceremony of wrapping UIImagePickerController in SwiftUI and the Simulator’s complete lack of a camera. The GBGBridge Android SDK ships no capture views — deliberately. On Android, ActivityResultContracts.TakePicturePreview() already is a working stub camera: it returns a Bitmap in one line on real devices, and the emulator provides a synthetic camera scene, so there is far less ceremony to hide. Shipping Compose views inside the SDK would also force a Jetpack Compose dependency onto every View-system host.
Instead, this page shows two ways to build your own capture surface and wire it to the bridge:
- The reference app’s
CaptureStubScreen— a full-featured Compose capture surface with a live CameraX preview, system photo picker, and emulator-friendly placeholder. Copy it as-is or use it as a worked example. - A minimal
TakePicturePreview-based handler for hosts that don’t want a CameraX dependency.
When You Need a Capture Surface
The web journey requests native capture through thecamera.document.capture and camera.selfie.capture actions. A development capture surface lets you:
- Verify early integration — Prove the bridge protocol, capability negotiation, and data flow before a production capture SDK is available or configured.
- Test on the emulator / in CI — The emulator’s synthetic camera works with both CameraX and
TakePicturePreview, and a placeholder bitmap path covers environments with no camera at all. - Prototype — Build the host app shell with a capture flow that “just works” while you focus on other parts of the integration.
The Reference Capture Surface
The reference app’sCaptureStubScreen is a full-screen Compose Dialog used for both document and selfie requests — a CaptureMode enum drives the copy, the framing guide (ID-card rectangle for documents, oval for selfies), and the camera selector. It offers three rungs of capture affordance, in priority order:
| Rung | Mechanism | When it applies |
|---|---|---|
| 1. Live preview + shutter | CameraX (Preview + ImageCapture), back camera for document, front camera for selfie | Device has a camera and the CAMERA permission is granted. This is the path a production app replaces with a real capture SDK. |
| 2. Photo picker | ActivityResultContracts.PickVisualMedia, image-only | Choosing an existing image — useful on devices without a usable camera, and the main path on tablets. |
| 3. Placeholder image | Synthetic bitmap drawn in code | A tertiary text button so the screen still reads as a capture surface, but available so the emulator path can exercise the end-to-end bridge round-trip without a real camera. |
CAMERA permission in-place when it opens (the manifest declares the permission with <uses-feature android:name="android.hardware.camera" android:required="false" />, so camera-less devices can still install the app). If permission is denied, the device has no camera, or CameraX fails to bind, the preview area falls back to a static framing guide on a neutral backdrop with an explanatory caption — the photo picker and placeholder rungs carry the user through regardless.
Wiring it to the bridge
The reference app pairsCaptureStubScreen with a raw BridgeCapabilityHandler per action (CameraCaptureHandler). The handler retains the BridgeResponder when a request arrives, signals the UI to present the capture surface, and exposes success / cancel / error completion paths the UI drives once the user finishes:
CaptureStubScreen while a request is active:
cancelled response so the web journey can recover. Also cancel any in-flight request before tearing the screen down — the reference app does this in DisposableEffect.onDispose, calling cancelActive(...) on both handlers before host.detach(), so the JavaScript side never hangs waiting for a response that will never arrive.
Picker back-out does not cancel the capture request. If the user opens the system photo picker and backs out without choosing an image, the picker returns a
null URI. The handler must treat that as “picker dismissed” — not “capture cancelled” — and simply return to the capture surface, leaving the request active. Responding cancelled here would send a spurious CANCELLED to the journey and tear down a capture surface that still offers the camera and placeholder paths.Minimal Alternative: TakePicturePreview
If you don’t want a CameraX dependency,ActivityResultContracts.TakePicturePreview() launches the system camera app and returns a small preview Bitmap (or null if the user backs out). Wire it to the same raw handler — the only change is what onActivate does:
TakePicturePreview itself needs no runtime permission — but if your manifest declares android.permission.CAMERA (for example, because another screen uses CameraX), Android requires the permission to be granted before launching the contract, otherwise it throws a SecurityException. Check and request it first if your manifest declares it.Returning the Captured Image
However theBitmap was produced — shutter, picker, placeholder, or TakePicturePreview — the bridge response is the same: encode it as base64 JPEG and respond with success and the standard image keys.
| Key | Value |
|---|---|
imageBase64 | Base64-encoded JPEG (the reference app uses quality 85, Base64.NO_WRAP) |
imageWidth / imageHeight | Pixel dimensions of the captured image |
mimeType | "image/jpeg" |
Typed-slot equivalent
If your host uses the typed capture slots (host.documentCapture / host.selfieCapture) instead of raw handlers, complete the slot with a CaptureResult.Document and the SDK builds the same wire response for you:
CaptureResult.Document defaults mimeType to "image/png" — pass "image/jpeg" explicitly when you compress to JPEG. On dismissal, call complete(CaptureResult.Cancelled(reason = "User dismissed")) or cancelIfBusy(...). See the Capability Handling Guide for the full slot lifecycle.
Emulator Support
Unlike the iOS Simulator, the Android emulator ships a synthetic camera — a rendered 3D scene that CameraX, the system camera app, andTakePicturePreview all treat as a real camera. That means rungs 1 and 2 of the reference capture surface work on the emulator unmodified. The placeholder-bitmap rung exists for the cases that remain: emulator images without the virtual camera, CI environments, and quick round-trip tests where you just want a deterministic bitmap through the bridge without aiming at the virtual scene.
Swapping for Production Capture SDKs
The capture surface is deliberately swappable. The handler registration, theBridgeResponder (or typed-slot complete()) contract, and the wire format all stay the same — only the UI that produces the Bitmap changes. When you adopt a production capture SDK, replace the CameraX rung (or the whole CaptureStubScreen) with the SDK’s capture view, construct the same response from its output, and delete the placeholder path.
Tutorial Part 2: Integrate Smart Capture SDKs walks through exactly this swap with the GBG Smart Capture SDKs — guided document scanning and face capture with liveness detection — slotting into the same handler seam shown on this page (the Android counterpart of the iOS Stub Camera Views swap).
Next Steps
- Tutorial Part 2: Integrate Smart Capture SDKs — Swap the stub surface for production document scanning and face capture with liveness detection
- Capability Handling Guide — Raw handlers, typed slots, and the dispatch lifecycle
- Integration Checklist — End-to-end setup walkthrough
- Embedding Guide — Compose and View-system WebView integration patterns
- Advanced Integration Example — The full reference-app pattern in one place