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

# Tutorial Part 2: Integrate Smart Capture SDKs

> Replace the stub capture surface with the production Smart Capture SDKs for document scanning and face capture with liveness detection.

This tutorial picks up where [Part 1](/docs/go-v2/developer-integration/sdks/android/tutorial) 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`](https://github.com/gbgplc/gbg-go-android-reference/tree/part-2-smart-capture) branch of the reference repository. To see exactly what changed from Part 1, diff the branches:
>
> ```bash theme={null}
> git diff main..part-2-smart-capture
> ```

## Prerequisites

Everything from [Part 1](/docs/go-v2/developer-integration/sdks/android/tutorial), plus:

| Requirement               | Notes                                                                                                                                                                                       |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Smart Capture credentials | The AWS access key / secret for the Smart Capture Maven repository, obtained from your GBG account representative. **The SDKs are not on Maven Central** and will not resolve without them. |
| Physical Android device   | The Smart Capture document scanner and liveness SDK need real camera hardware and sensors. On an emulator the app falls back to the stub surface automatically.                             |

### Required dependencies

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

| Artifact                              | Purpose                                                               |
| ------------------------------------- | --------------------------------------------------------------------- |
| `com.gbg.smartcapture:documentcamera` | Document scanning with guided capture, auto-crop, and quality scoring |
| `com.gbg.smartcapture:facecamera`     | Face capture with liveness detection and an encrypted biometric blob  |
| `com.gbg.smartcapture:commons`        | Shared types (`SmartCaptureException`, theming) used by both          |

## Start from Part 1

Check out the Part 2 branch:

```bash theme={null}
git clone https://github.com/gbgplc/gbg-go-android-reference.git
cd gbg-go-android-reference
git checkout part-2-smart-capture
```

Or if you already have the repo:

```bash theme={null}
git checkout part-2-smart-capture
```

The companion server is unchanged — start it the same way as Part 1:

```bash theme={null}
cd server
npm install
node index.mjs
```

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

```kotlin expandable theme={null}
dependencyResolutionManagement {
  repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
  repositories {
    google()
    mavenCentral()

    val awsAccessKey = providers.gradleProperty("awsAccessKey").orNull
    val awsSecretKey = providers.gradleProperty("awsSecretKey").orNull
    if (awsAccessKey != null && awsSecretKey != null) {
      // Releases hold the published com.gbg.smartcapture artifacts; snapshots
      // hold transitive SNAPSHOT dependencies (e.g. com.gbg.smartcapture:camera).
      listOf("releases", "snapshots").forEach { channel ->
        maven {
          url = uri("s3://maven-mobile-repo/$channel")
          credentials(AwsCredentials::class.java) {
            accessKey = awsAccessKey
            secretKey = awsSecretKey
          }
        }
      }
    }
  }
}
```

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

```properties theme={null}
awsAccessKey=YOUR_ACCESS_KEY_ID
awsSecretKey=YOUR_SECRET_ACCESS_KEY
```

CI environments can instead export them as environment variables, which Gradle maps to the same properties:

```bash theme={null}
export ORG_GRADLE_PROJECT_awsAccessKey=YOUR_ACCESS_KEY_ID
export ORG_GRADLE_PROJECT_awsSecretKey=YOUR_SECRET_ACCESS_KEY
```

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

### 3. Declare the dependencies

In `app/build.gradle.kts`, add the three artifacts alongside the GBGBridge SDK:

```kotlin theme={null}
dependencies {
  implementation("com.gbg:gbgbridge-sdk:0.1.0-alpha01")

  // Smart Capture SDKs — served from the credentialed S3 repo above.
  implementation("com.gbg.smartcapture:commons:1.1.2")
  implementation("com.gbg.smartcapture:documentcamera:1.1.2")
  implementation("com.gbg.smartcapture:facecamera:1.1.2")

  // ... the rest of Part 1's dependencies are unchanged
}
```

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:

```kotlin expandable theme={null}
package com.gbg.go.reference.ui.capture

import android.content.Context
import android.content.pm.PackageManager
import android.os.Build

object DeviceCapabilities {

  /**
   * True when this build should use the Smart Capture SDKs: a real device
   * (not an emulator) that actually has a camera. Otherwise fall back to
   * [CaptureStubScreen].
   */
  fun supportsSmartCapture(context: Context): Boolean =
    hasCamera(context) && !isProbablyEmulator()

  private fun hasCamera(context: Context): Boolean =
    context.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)

  private fun isProbablyEmulator(): Boolean {
    val fingerprint = Build.FINGERPRINT.lowercase()
    val model = Build.MODEL.lowercase()
    val product = Build.PRODUCT.lowercase()
    val hardware = Build.HARDWARE.lowercase()
    val brand = Build.BRAND.lowercase()

    return fingerprint.startsWith("generic") ||
      fingerprint.startsWith("unknown") ||
      fingerprint.contains("emulator") ||
      model.contains("emulator") ||
      model.contains("android sdk built for") ||
      product.contains("sdk_gphone") ||
      product.contains("emulator") ||
      hardware.contains("goldfish") ||
      hardware.contains("ranchu") ||
      (brand.startsWith("generic") && Build.DEVICE.lowercase().startsWith("generic"))
  }
}
```

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

```kotlin expandable theme={null}
package com.gbg.go.reference.ui.capture

import android.app.Activity
import android.content.Context
import android.content.Intent
import androidx.activity.result.ActivityResult
import com.gbg.go.reference.ui.journey.CameraCaptureHandler
import com.gbg.smartcapture.commons.SmartCaptureException
import com.gbg.smartcapture.documentcamera.DocumentCameraActivity
import com.gbg.smartcapture.documentcamera.DocumentProcessingState
import com.gbg.smartcapture.documentcamera.DocumentScannerConfig
import com.gbg.smartcapture.facecamera.FaceCameraActivity

internal object SmartCaptureLauncher {

  /** Intent that launches the Smart Capture document scanner. */
  fun documentIntent(context: Context): Intent =
    DocumentCameraActivity.getIntent(context, DocumentScannerConfig())

  /** Intent that launches the Smart Capture face/liveness camera. */
  fun selfieIntent(context: Context): Intent =
    Intent(context, FaceCameraActivity::class.java)

  /** Map a document-scanner result onto the handler. */
  fun handleDocumentResult(result: ActivityResult, handler: CameraCaptureHandler) {
    when (result.resultCode) {
      Activity.RESULT_OK -> when (val state = DocumentCameraActivity.latestResult) {
        is DocumentProcessingState.Success ->
          handler.completeWithSmartCaptureImage(state.image.toBitmap(true))
        is DocumentProcessingState.Failure ->
          handler.failActive(state.message ?: "Document capture failed")
        else ->
          handler.failActive("Document capture returned no result")
      }
      Activity.RESULT_CANCELED -> handler.cancelActive("User cancelled document capture")
      else -> handler.failActive("Document capture error (code ${result.resultCode})")
    }
  }

  /** Map a face-camera result onto the handler. */
  fun handleSelfieResult(result: ActivityResult, handler: CameraCaptureHandler) {
    when (result.resultCode) {
      Activity.RESULT_OK -> {
        val faceResult = FaceCameraActivity.getResult()
        if (faceResult != null) {
          handler.completeWithSmartCaptureSelfie(
            previewPhoto = faceResult.previewPhoto,
            encryptedBlob = faceResult.encryptedBlob,
          )
        } else {
          handler.failActive("Face capture returned no result")
        }
      }
      Activity.RESULT_CANCELED -> handler.cancelActive("User cancelled face capture")
      FaceCameraActivity.RESULT_ERROR -> {
        @Suppress("DEPRECATION")
        val exception =
          result.data?.getSerializableExtra(FaceCameraActivity.ERROR_OBJECT) as? SmartCaptureException
        handler.failActive(exception?.message ?: "Face capture failed")
      }
      else -> handler.failActive("Face capture error (code ${result.resultCode})")
    }
  }
}
```

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

| Feature                              | CaptureStubScreen | Smart Capture document |
| ------------------------------------ | ----------------- | ---------------------- |
| Document edge detection              | No                | Yes                    |
| Auto-crop and perspective correction | No                | Yes                    |
| Blur / glare / resolution scoring    | No                | Yes                    |
| Guided capture overlay               | No                | Yes                    |
| Auto-capture on quality threshold    | No                | Yes                    |

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

| Feature                         | CaptureStubScreen  | Smart Capture face  |
| ------------------------------- | ------------------ | ------------------- |
| Face detection and positioning  | No                 | Yes                 |
| Liveness detection              | No                 | Yes (passive)       |
| Guided selfie overlay           | No                 | Yes                 |
| Encrypted biometric blob        | No (raw JPEG only) | Real encrypted data |
| Server-side liveness validation | Fails              | Passes              |

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

```kotlin expandable theme={null}
/** Smart Capture document result: a (rotation-corrected) document image. */
fun completeWithSmartCaptureImage(bitmap: Bitmap) {
  val responder = activeResponder ?: return
  success(responder, bitmap)
}

/**
 * Smart Capture selfie result: the preview photo plus the encrypted biometric
 * blob from the liveness check, forwarded base64-encoded under `encryptedBlob`.
 */
fun completeWithSmartCaptureSelfie(previewPhoto: Bitmap, encryptedBlob: ByteArray) {
  val responder = activeResponder ?: return
  success(
    responder,
    previewPhoto,
    extraData = mapOf(
      "encryptedBlob" to JsonPrimitive(Base64.encodeToString(encryptedBlob, Base64.NO_WRAP)),
    ),
  )
}

/** Smart Capture failure (SDK error / no result) for the active request. */
fun failActive(message: String) {
  val responder = activeResponder ?: return
  error(responder, "CAPTURE_FAILED", message)
}

private fun success(
  responder: BridgeResponder,
  bitmap: Bitmap,
  extraData: Map<String, JsonElement> = emptyMap(),
) {
  val base64 = bitmapToBase64Jpeg(bitmap)
  responder.respond(
    status = BridgeResponseStatus.SUCCESS,
    data = buildMap {
      put("imageBase64", JsonPrimitive(base64))
      put("imageWidth", JsonPrimitive(bitmap.width))
      put("imageHeight", JsonPrimitive(bitmap.height))
      put("mimeType", JsonPrimitive("image/jpeg"))
      putAll(extraData)
    },
  )
  finish()
}
```

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:

```kotlin expandable theme={null}
// Decide Smart Capture vs stub once for this screen.
val useSmartCapture = remember { DeviceCapabilities.supportsSmartCapture(context) }

// The SDKs present full-screen Activities, launched via StartActivityForResult.
val launchSmartCaptureDocument = rememberLauncherForActivityResult(
  ActivityResultContracts.StartActivityForResult(),
) { result ->
  SmartCaptureLauncher.handleDocumentResult(result, controller.documentHandler)
}

val launchSmartCaptureSelfie = rememberLauncherForActivityResult(
  ActivityResultContracts.StartActivityForResult(),
) { result ->
  SmartCaptureLauncher.handleSelfieResult(result, controller.selfieHandler)
}
```

Then launch the SDK Activity when a capture becomes active. Keying the effect on `activeCapture` makes it fire exactly once per activation:

```kotlin theme={null}
LaunchedEffect(activeCapture, useSmartCapture) {
  if (!useSmartCapture) return@LaunchedEffect
  when (activeCapture) {
    CaptureMode.Document -> launchSmartCaptureDocument.launch(
      SmartCaptureLauncher.documentIntent(context),
    )
    CaptureMode.Selfie -> launchSmartCaptureSelfie.launch(
      SmartCaptureLauncher.selfieIntent(context),
    )
    null -> Unit
  }
}
```

Finally, guard the Part 1 stub surface so it renders only on the fallback path:

```kotlin theme={null}
if (!useSmartCapture) {
  activeCapture?.let { mode ->
    // ... the Part 1 CaptureStubScreen block, unchanged
  }
}
```

### 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 `remember`ed 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](#2-add-your-credentials-never-commit-them)), 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](/docs/go-v2/developer-integration/sdks/android/api-reference)** — Full documentation for `BridgeHost`, `BridgeWebViewConfigurator`, and result types.
* **[Capture Screens](/docs/go-v2/developer-integration/sdks/android/capture-screens)** — The stub capture surface and the swap seam in detail.
* **[Capability Handling](/docs/go-v2/developer-integration/sdks/android/capability-handling)** — Raw handlers, typed slots, custom capabilities, and permission states.
