Skip to main content
The iOS SDK ships stub camera views (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.
Development and testing only. Both approaches take a plain photo (or generate a placeholder bitmap). They do not perform document detection, auto-cropping, liveness checks, or biometric encryption, and placeholder images will not pass server-side verification. Do not ship the placeholder path in production.

When You Need a Capture Surface

The web journey requests native capture through the camera.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’s CaptureStubScreen 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: The screen requests the runtime 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 pairs CaptureStubScreen 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:
The journey screen mirrors the handler’s activate / deactivate callbacks into Compose state, and shows CaptureStubScreen while a request is active:
Dismissing the capture surface (the close button or the system back press) cancels the active request, which sends a 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:
This works on real devices and on the emulator (which renders a synthetic camera scene), with no in-app camera UI to build. The trade-offs: the returned bitmap is a low-resolution preview, you get no framing guidance, and you cannot brand the capture experience.
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 the Bitmap 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.

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:
Note that 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, and TakePicturePreview 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, the BridgeResponder (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