> ## Documentation Index
> Fetch the complete documentation index at: https://docs.go.gbgplc.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Capture Screens

> Build a capture surface for document and selfie requests — no stub views required.

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`](https://github.com/gbgplc/gbg-go-android-reference) — 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.

<Warning>
  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.
</Warning>

## 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:

| 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. |

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:

```kotlin theme={null}
internal class CameraCaptureHandler(
  override val action: String,
) : BridgeCapabilityHandler {

  private var activeResponder: BridgeResponder? = null

  /** Set by the host UI — invoked on the main thread when a request arrives. */
  var onActivate: (() -> Unit)? = null
  var onDeactivate: (() -> Unit)? = null

  override fun handle(request: BridgeMessage, responder: BridgeResponder) {
    if (activeResponder != null) {
      // The SDK doesn't enforce single-flight for raw handlers, so do it here.
      responder.respond(
        status = BridgeResponseStatus.ERROR,
        error = BridgeErrorPayload(
          code = "BUSY",
          message = "A $action request is already active",
          recoverable = true,
        ),
      )
      return
    }
    activeResponder = responder
    onActivate?.invoke()
  }

  fun onCameraResult(bitmap: Bitmap?) {
    val responder = activeResponder ?: return
    if (bitmap == null) {
      cancelled(responder, "User cancelled camera capture")
      return
    }
    success(responder, bitmap)
  }

  fun cancelActive(reason: String) {
    val responder = activeResponder ?: return
    cancelled(responder, reason)
  }

  // success(...) / cancelled(...) respond on the BridgeResponder, clear
  // activeResponder, and invoke onDeactivate — see "Returning the Captured
  // Image" below for the response shape.
}
```

The journey screen mirrors the handler's activate / deactivate callbacks into Compose state, and shows `CaptureStubScreen` while a request is active:

```kotlin theme={null}
var activeCapture by remember { mutableStateOf<CaptureMode?>(null) }

val pickImage = rememberLauncherForActivityResult(
  ActivityResultContracts.PickVisualMedia(),
) { uri -> activeHandler.onPickerResult(context, uri) }

LaunchedEffect(controller) {
  controller.documentHandler.onActivate = { activeCapture = CaptureMode.Document }
  controller.documentHandler.onDeactivate = {
    if (activeCapture == CaptureMode.Document) activeCapture = null
  }
  // ...same pair for selfieHandler with CaptureMode.Selfie
}

activeCapture?.let { mode ->
  CaptureStubScreen(
    mode = mode,
    onCapture = { bitmap -> activeHandler.onCameraResult(bitmap) },
    onPickFromLibrary = {
      pickImage.launch(
        PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly),
      )
    },
    onUseStubImage = { activeHandler.completeWithStubImage() },
    onCancel = { activeHandler.cancelActive("User dismissed capture") },
  )
}
```

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.

<Note>
  **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.
</Note>

## 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:

```kotlin theme={null}
val documentHandler = remember {
  CameraCaptureHandler(action = "camera.document.capture")
}

val takePicture = rememberLauncherForActivityResult(
  ActivityResultContracts.TakePicturePreview(),
) { bitmap ->
  // null bitmap = user backed out of the camera app → cancelled response.
  documentHandler.onCameraResult(bitmap)
}

LaunchedEffect(documentHandler) {
  documentHandler.onActivate = { takePicture.launch(null) }
}
```

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.

<Note>
  `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.
</Note>

## 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.

```kotlin theme={null}
private fun success(responder: BridgeResponder, bitmap: Bitmap) {
  val base64 = bitmapToBase64Jpeg(bitmap)
  responder.respond(
    status = BridgeResponseStatus.SUCCESS,
    data = mapOf(
      "imageBase64" to JsonPrimitive(base64),
      "imageWidth" to JsonPrimitive(bitmap.width),
      "imageHeight" to JsonPrimitive(bitmap.height),
      "mimeType" to JsonPrimitive("image/jpeg"),
    ),
  )
}

private fun bitmapToBase64Jpeg(bitmap: Bitmap, quality: Int = 85): String {
  val output = ByteArrayOutputStream()
  bitmap.compress(Bitmap.CompressFormat.JPEG, quality, output)
  return Base64.encodeToString(output.toByteArray(), Base64.NO_WRAP)
}
```

| 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:

```kotlin theme={null}
// Setup: the handler shows your capture UI, then suspends until complete().
host.documentCapture.handler = { request ->
  showCaptureUi()
  host.documentCapture.awaitCompletion()
}

// Capture UI, once a Bitmap is in hand:
val output = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG, 85, output)
host.documentCapture.complete(
  CaptureResult.Document(
    imageData = output.toByteArray(),
    width = bitmap.width,
    height = bitmap.height,
    mimeType = "image/jpeg",
  ),
)
```

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](/docs/go-v2/developer-integration/sdks/android/capability-handling) 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.

<Note>
  [Tutorial Part 2: Integrate Smart Capture SDKs](/docs/go-v2/developer-integration/sdks/android/tutorial-smart-capture) 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](/docs/go-v2/developer-integration/sdks/ios/stub-camera-views) swap).
</Note>

## Next Steps

* [Tutorial Part 2: Integrate Smart Capture SDKs](/docs/go-v2/developer-integration/sdks/android/tutorial-smart-capture) — Swap the stub surface for production document scanning and face capture with liveness detection
* [Capability Handling Guide](/docs/go-v2/developer-integration/sdks/android/capability-handling) — Raw handlers, typed slots, and the dispatch lifecycle
* [Integration Checklist](/docs/go-v2/developer-integration/sdks/android/integration-checklist) — End-to-end setup walkthrough
* [Embedding Guide](/docs/go-v2/developer-integration/sdks/android/embedding) — Compose and View-system WebView integration patterns
* [Advanced Integration Example](/docs/go-v2/developer-integration/sdks/android/examples/advanced-integration) — The full reference-app pattern in one place
