Complete reference for every public type, method, and property in the GBGBridge Android SDK.
Artifact: com.gbg:gbgbridge-sdk:0.1.0-alpha01 (Maven Central)
Packages: com.gbg.gbgbridge.core, com.gbg.gbgbridge.models, com.gbg.gbgbridge.capabilities, com.gbg.gbgbridge.webview
Platforms: Android API 24+ (Android 7.0)
Kotlin: 2.x
Table of contents
Primary entry points
These are the types youβll interact with directly to wire the bridge into your app: a host that routes messages, and a configurator that prepares a WebView for bridge traffic.
Differs from iOS: there is no
BridgeWebView composable or
makeWebView factory on Android. Integration is a plain
WebView(context) (or
AndroidView { WebView(it) } in Compose) plus
host.attach(webView) plus
webView.loadUrl(journeyUrl). See
Embedding the WebView for the full pattern.
BridgeHost
The central coordinator that manages message routing between a WebView and your native code.
package com.gbg.gbgbridge.core
public class BridgeHost(
public val configuration: BridgeConfiguration,
capabilitiesProvider: (() -> Map<String, BridgeCapabilityInfo>)? = null,
)
Why it exists: BridgeHost is the main object you interact with. It decodes incoming messages from the WebView, routes requests to registered handlers, tracks pending requests, and sends responses and events back to the web journey.
Threading: Main-thread-only. Every public state-mutating method asserts the main thread at runtime and throws IllegalStateException if called from a background thread; the @MainThread annotation also enforces this statically via lint. Inbound JavaScript messages arrive on the WebView render thread and are posted to the main looper before any handler or delegate runs.
Differs from iOS: iOS enforces main-thread isolation through @MainActor and the Swift concurrency system. Android relies on a documented main-thread contract enforced with runtime assertions and lint β the compiler will not stop you, but the SDK will fail loudly rather than corrupt state silently.
Constructors
BridgeHost offers two constructors. Most apps should use the hostVersion convenience constructor, which wires up typed capability slots automatically. Use the primary constructor when you need to declare capabilities explicitly or supply a dynamic provider.
BridgeHost(hostVersion:) β Recommended
public constructor(hostVersion: String)
Creates a new bridge host with typed capability slots and an empty static capability map. Capability support is declared by setting handlers on the documentCapture and selfieCapture slots. A CapabilityQueryHandler is registered automatically and builds its response dynamically from typed slots and custom capabilities, including permissionState metadata.
| Parameter | Type | Description |
|---|
hostVersion | String | A version string for the host app (e.g., "1.0.0"). |
Example:
val host = BridgeHost(hostVersion = "1.0.0")
// Declare support by setting handlers
host.documentCapture.handler = { request ->
host.documentCapture.awaitCompletion()
}
BridgeHost(configuration:, capabilitiesProvider:) β Configuration-based
public constructor(
configuration: BridgeConfiguration,
capabilitiesProvider: (() -> Map<String, BridgeCapabilityInfo>)? = null,
)
Creates a new bridge host with explicit configuration. Capabilities are declared statically in the BridgeConfiguration map, or dynamically via capabilitiesProvider. A built-in CapabilityQueryHandler is registered automatically for "capability.query" requests.
| Parameter | Type | Default | Description |
|---|
configuration | BridgeConfiguration | β | Declares the host version, capability map, bootstrap script, and origin allowlist. |
capabilitiesProvider | (() -> Map<String, BridgeCapabilityInfo>)? | null | Optional dynamic capability provider. When supplied, it replaces the static configuration map and is re-evaluated on every capability read, so capability state can change without polling. |
Differs from iOS: capabilitiesProvider is an Android-only addition. On iOS, dynamic capability state is expressed by mutating the published capabilities map; on Android the merged capabilities property is read-only, and dynamic state belongs in the provider. Also unlike iOS β where init(configuration:) does not wire typed slots into the query handler β both Android constructors merge configured typed slots into capability.query responses automatically.
Example:
val host = BridgeHost(
configuration = BridgeConfiguration(
hostVersion = "1.0.0",
capabilities = mapOf(
"camera.document" to BridgeCapabilityInfo(supported = true, version = "1.0"),
),
),
)
Example β dynamic provider:
val host = BridgeHost(
configuration = BridgeConfiguration(hostVersion = "1.0.0"),
capabilitiesProvider = {
mapOf(
"nfc.read" to BridgeCapabilityInfo(
supported = nfcAdapter?.isEnabled == true,
version = "1.0",
),
)
},
)
The static configuration.capabilities map is defensively snapshotted at host construction. Mutating a MutableMap you passed in does not change query responses β use capabilitiesProvider for dynamic state. If your provider lambda can throw, wrap direct reads of host.capabilities; a provider throw propagates to the caller on direct reads (the inbound capability.query path is protected separately by the SDKβs handler catch).
Companion constants
These constants describe the protocol identity and the hostβs internal buffer limits. The buffer caps surface through public behaviour β eviction from receivedMessages, leak warnings in lastError, and the response dedupe window.
| Constant | Value | Description |
|---|
PROTOCOL_VERSION | "1.0" | The bridge protocol version stamped on every outbound message. |
ENVIRONMENT | "android" | The environment token reported in capability.query responses. |
MAX_RECEIVED_MESSAGES | 200 | Cap on the receivedMessages inbound buffer. Older entries are evicted from the head when exceeded. |
PENDING_REQUEST_LEAK_THRESHOLD | 50 | When pendingRequests grows beyond this size, lastError is set with a leak warning. |
MAX_RESPONDED_TRACKED | 200 | How many recently responded correlation IDs the host remembers to deduplicate explicit-action respond calls. Oldest entries are evicted in insertion order. |
Properties
These properties expose the hostβs configuration and observable state. The list-valued properties return immutable snapshots β each read copies the underlying buffer.
configuration
public val configuration: BridgeConfiguration
The configuration passed at construction. Read-only.
delegate
public var delegate: BridgeHostDelegate?
An optional delegate that receives notifications about inbound messages, outbound messages, unhandled requests, and errors. Set this to observe bridge activity without registering handlers. Read and assigned on the main thread.
Differs from iOS: the delegate is backed by a WeakReference β the host does not keep your delegate alive. The common pattern (the delegate is your Activity, Fragment, or ViewModel, which the platform holds strongly) is unaffected, but an inline assignment like host.delegate = SomeImpl() with no other strong reference will be garbage-collected, after which callbacks silently stop firing. Always hold your own strong reference to the delegate.
capabilities
public val capabilities: Map<String, BridgeCapabilityInfo>
A read-only merged snapshot of the hostβs capability map, computed on each read. The merge combines, from lowest to highest precedence (later entries win on key collision):
- Runtime
registerCustomCapability registrations.
- The static configuration map or the dynamic
capabilitiesProvider β explicit configuration is authoritative over custom registrations.
- Typed slots (
documentCapture, selfieCapture) with a non-null handler. An unused slot never shadows integrator-supplied capability info for the same ID.
Differs from iOS: iOS exposes a mutable @Published var capabilities: [String: Bool] that hosts write directly. The Android property is read-only and richer (BridgeCapabilityInfo values, not booleans); dynamic state belongs in capabilitiesProvider.
receivedMessages
public val receivedMessages: List<BridgeMessage>
An ordered snapshot of all messages received from the web journey (requests, responses, and events), capped at MAX_RECEIVED_MESSAGES (200) with oldest entries evicted first. Each read returns an immutable copy. Useful for debugging or building a message log UI. Read on the main thread.
pendingRequests
public val pendingRequests: List<BridgeMessage>
A snapshot of requests from the web journey that have no registered handler. These are waiting for a manual response via the lookup overload of respond. When a response is sent for a pending request, it is removed from this list. If the list grows beyond PENDING_REQUEST_LEAK_THRESHOLD (50), lastError is set with a leak warning. Read on the main thread.
lastError
public var lastError: String? // private set
The most recent error message, if any. Set when message decoding fails, outbound encoding fails, a handler throws before responding, an origin-gated message is rejected, or the pending-request buffer exceeds its leak threshold. Call clearError() to reset.
Differs from iOS: the setter is private β host apps cannot write
lastError themselves; only the SDK records errors there. Also, sending while no WebView is attached does
not set
lastError on Android (see
sendEvent), whereas iOS records
"WebView not attached".
documentCapture
public val documentCapture: CaptureCapability
The typed slot for document capture (capability ID camera.document). Set its handler to declare support. Routes requests for the "camera.document.capture" action.
selfieCapture
public val selfieCapture: CaptureCapability
The typed slot for selfie capture (capability ID camera.selfie). Set its handler to declare support. Routes requests for the "camera.selfie.capture" action.
Methods
All methods are main-thread-only and return Unit unless noted. After dispose(), the state-mutating methods throw IllegalStateException β see dispose() for the full post-dispose contract.
attach(webView:, client:)
public fun attach(
webView: WebView,
client: BridgeWebViewConfigurator.BootstrapInjectingWebViewClient? = null,
)
Associates a WebView with this host. Internally calls BridgeWebViewConfigurator.configure(...) (enabling JavaScript and DOM storage, and installing the bootstrap-injecting WebViewClient), then installs the SDKβs JavaScript interface on the WebView under the name GBGBridge. If the host is already attached, it detaches first.
| Parameter | Type | Default | Description |
|---|
webView | WebView | β | The WebView to attach. The host holds a weak reference. |
client | BootstrapInjectingWebViewClient? | null | Optional WebViewClient subclass to install alongside the JS interface. Hosts that need their own navigation policy (URL allowlisting, error handling) must pass their subclass here β assigning a different WebViewClient after attach works but loses the bootstrap injection. |
Example:
val webView = WebView(context)
host.attach(webView)
webView.loadUrl(journeyUrl)
attach() calls BridgeWebViewConfigurator.configure() for you β do not call configure() separately before attach(), as it would be re-run and clobbered. If you need a custom WebChromeClient, set it after attach().
detach()
Disconnects the host from its WebView. Cancels any in-flight typed-slot capture (so the web side receives a cancelled response), removes the JavaScript interface, and resets the per-attach-session state: the response dedupe set, pendingRequests, and receivedMessages are all cleared. Idempotent β safe to call when not attached.
Differs from iOS: iOS preserves pendingRequests and receivedMessages across detach. Android clears both, because a Compose host driving a list off these snapshots would otherwise show ghost entries from a previous attach-session after re-attaching to a different WebView.
dispose()
Terminal teardown: detaches from the WebView and cancels the typed-slot coroutine scopes so future capture launches refuse rather than hang. Idempotent on repeat calls. Call once from the ownerβs terminal lifecycle hook (Activity.onDestroy, ViewModel.onCleared).
After dispose():
attach, register, unregister, registerCustomCapability, both respond overloads, and sendEvent throw IllegalStateException.
detach(), dispose(), clearError(), all property getters (which return empty/null), and the delegate setter remain safe to call.
- The host must not be re-attached β hosts that just want to swap WebViews should use
detach()/attach(), which keep the slot scopes live.
Differs from iOS: there is no iOS equivalent β ARC handles terminal cleanup there. On Android, dispose explicitly cancels the coroutine scopes that pin handler closures (and through them, Activity context).
detach() failures propagate out of dispose(). The rare path is removeJavascriptInterface throwing on a WebView already in shutdown. Wrap the call when wiring it into onDestroy:
override fun onDestroy() {
try {
bridgeHost.dispose()
} catch (e: Exception) {
Log.w("Bridge", "dispose() failed during teardown", e)
}
super.onDestroy()
}
register(handler:)
public fun register(handler: BridgeCapabilityHandler)
Registers a capability handler. When a request arrives with an action matching the handlerβs action property, the handlerβs handle(request, responder) method is called on the main thread.
If a handler is already registered for the same action, it is replaced β including the SDKβs own auto-registered handlers (the CapabilityQueryHandler for "capability.query", and the typed-slot handlers for the capture actions).
| Parameter | Type | Description |
|---|
handler | BridgeCapabilityHandler | The handler to register. |
Example:
host.register(DocumentCaptureHandler())
unregister(action:)
public fun unregister(action: String)
Removes the handler registered for the given action, along with any custom capability registered under the same name. Subsequent requests for that action are added to pendingRequests instead.
| Parameter | Type | Description |
|---|
action | String | The action identifier to unregister. |
"capability.query" is the action of the SDKβs auto-registered CapabilityQueryHandler. Unregistering it makes capability queries fall through to the unhandled-request path; re-register a replacement (CapabilityQueryHandler is public for exactly this purpose) if you want the action to keep working.
registerCustomCapability(action:, version:, handler:)
public fun registerCustomCapability(
action: String,
version: String? = null,
handler: (BridgeMessage, BridgeResponder) -> Unit,
)
Registers a lightweight custom capability backed by a lambda. The capability automatically appears in capability.query responses with supported = true and the supplied version (defaulting to "1.0"). The lambda is invoked on the main thread when a matching request arrives. Exceptions thrown from the lambda are caught by the SDK and routed to delegate.onError, with a best-effort error response dispatched to the web side.
If a typed slot with a handler uses the same capability ID, the typed slotβs info wins in query responses.
| Parameter | Type | Default | Description |
|---|
action | String | β | The capability/action identifier (e.g., "nfc.read"). |
version | String? | null | Optional version string. Defaults to "1.0" in query responses. |
handler | (BridgeMessage, BridgeResponder) -> Unit | β | The lambda called when a matching request arrives. |
Differs from iOS: the handler lambda is synchronous, not
async. For asynchronous work, retain the responder, complete the work, hop back to the main thread, then call
responder.respond(...) β see
BridgeResponder.
Example:
host.registerCustomCapability("nfc.read", version = "1.0") { request, responder ->
// Handle NFC read request
responder.respond(
status = BridgeResponseStatus.SUCCESS,
data = mapOf("chipRead" to JsonPrimitive(true)),
)
}
sendEvent(action:, data:)
public fun sendEvent(action: String, data: Map<String, JsonElement>? = null)
Sends a one-way event message to the web journey. Events are fire-and-forget β no response is expected. Each event is stamped with an auto-generated correlation ID of the form android-event-{uuid} (iOS uses ios-event-{uuid}).
| Parameter | Type | Default | Description |
|---|
action | String | β | The event action identifier (e.g., "journey.progress"). |
data | Map<String, JsonElement>? | null | Optional event payload data. |
Example:
host.sendEvent(
action = "host.ready",
data = mapOf("timestamp" to JsonPrimitive(System.currentTimeMillis())),
)
Error cases: if outbound JSON encoding fails, lastError is set, delegate.onError fires, and the message is dropped.
Differs from iOS: if no WebView is attached, the message fires delegate.onMessageSent (recording intent for tracing) and is then silently dropped at transport β lastError is not set. iOS records lastError = "WebView not attached" in the same situation.
respond(to:, status:, data:, error:)
public fun respond(
to: String,
status: BridgeResponseStatus,
data: Map<String, JsonElement>? = null,
error: BridgeErrorPayload? = null,
)
Sends a response to a pending request, looking up its action from pendingRequests. The matching pending request is removed before dispatch; the responseβs action is automatically set to the original requestβs action. If no pending request matches the correlation ID, the call silently no-ops.
| Parameter | Type | Default | Description |
|---|
to | String | β | The correlation ID of the request being responded to. |
status | BridgeResponseStatus | β | The response status. |
data | Map<String, JsonElement>? | null | Optional response data. |
error | BridgeErrorPayload? | null | Optional error details (typically set when status is ERROR). |
Example:
host.respond(
to = request.correlationId,
status = BridgeResponseStatus.SUCCESS,
data = mapOf("imageBase64" to JsonPrimitive(base64Image)),
)
Retry asymmetry: the pending entry is consumed before sending. If the send then fails (encode error, or no WebView attached), a retry through this lookup overload silently no-ops because the entry is gone. To retry, use the explicit-action overload below β you can recover the original action from the request you captured via delegate.onUnhandledRequest.
respond(to:, action:, status:, data:, error:)
public fun respond(
to: String,
action: String,
status: BridgeResponseStatus,
data: Map<String, JsonElement>? = null,
error: BridgeErrorPayload? = null,
)
Sends a response with an explicit action, without requiring a matching pending request. Use this from contexts that have already consumed the request, or when retrying after a failed send.
This overload deduplicates by correlation ID: a second call with the same ID silently no-ops (the dedupe window holds the most recent MAX_RESPONDED_TRACKED = 200 IDs and is reset on detach()). If the send fails β encode error or no WebView attached β the dedupe entry is rolled back so a retry with the same correlation ID works once the underlying problem is resolved.
| Parameter | Type | Default | Description |
|---|
to | String | β | The correlation ID of the request. |
action | String | β | The action identifier for the response. |
status | BridgeResponseStatus | β | The response status. |
data | Map<String, JsonElement>? | null | Optional response data. |
error | BridgeErrorPayload? | null | Optional error details. |
clearError()
Resets lastError to null. Safe to call after dispose().
Request dispatch behaviour
When a request arrives with no registered handler, it is appended to pendingRequests and delegate.onUnhandledRequest fires; respond later via the lookup respond overload. When a registered handler throws, the SDK catches the exception and routes it to delegate.onError. If the handler had not yet responded, lastError is also set and an ERROR response with code HANDLER_FAILURE (recoverable = false) is dispatched to the web side; a handler that responded successfully and then threw is reported via onError only β the request itself still succeeded.
See Messaging for the full message flow and Capability handling for handler patterns.
BridgeWebViewConfigurator
A utility object that configures a WebView with the bridge infrastructure. BridgeHost.attach() calls it for you β you only interact with it directly when subclassing its WebViewClient for custom navigation policy.
package com.gbg.gbgbridge.webview
public object BridgeWebViewConfigurator
Why it exists: Separates WebView setup (JavaScript settings, bootstrap injection, client installation) from the hostβs message routing.
public fun configure(
webView: WebView,
bootstrapScript: String? = null,
client: BootstrapInjectingWebViewClient? = null,
)
Applies the SDKβs required WebView defaults and installs a WebViewClient that injects the bootstrap script on each page load:
- Sets
javaScriptEnabled = true and domStorageEnabled = true.
- Installs a
BootstrapInjectingWebViewClient (the supplied client, or a default built from bootstrapScript).
- Installs a plain
WebChromeClient.
| Parameter | Type | Default | Description |
|---|
webView | WebView | β | The WebView to configure. |
bootstrapScript | String? | null | Optional override of the JS bootstrap injected on every page load. null uses the default script (below). Ignored when client is non-null β the subclass owns its script; pass the script to the subclass constructor instead. |
client | BootstrapInjectingWebViewClient? | null | Optional pre-built WebViewClient subclass, installed as-is. |
The default bootstrap script is:
window.GBGBridge = window.GBGBridge || {}; window.GBGBridge.receive = window.GBGBridge.receive || function(){};
configure() unconditionally overwrites any existing webViewClient and webChromeClient on the WebView (the API-26+ getters that would allow reading an existing client are unavailable at the SDKβs minSdk = 24 floor). Since BridgeHost.attach() calls configure() internally, set a custom WebChromeClient only after attach(), and supply custom WebViewClient behaviour by subclassing BootstrapInjectingWebViewClient and passing it to attach(webView, client = ...).
BootstrapInjectingWebViewClient
This nested class is the SDKβs WebViewClient. It is public open so security-conscious hosts can subclass it to layer their own navigation policy without losing the bootstrap injection that makes the bridge work.
public open class BootstrapInjectingWebViewClient(
private val bootstrapScript: String,
) : WebViewClient()
The client injects the bootstrap via evaluateJavascript in onPageStarted (main frame only β sub-frames receive no bootstrap, matching iOSβs forMainFrameOnly: true). Override any callback you need β shouldOverrideUrlLoading allowlists, error logging, SSL handling β and call super.onPageStarted(...) to preserve the bootstrap.
Example β restricting navigation to a single origin:
class JourneyClient(bootstrap: String) : BootstrapInjectingWebViewClient(bootstrap) {
override fun shouldOverrideUrlLoading(
view: WebView, request: WebResourceRequest
): Boolean {
val url = request.url ?: return true
return url.host != "journey.example.com"
}
}
host.attach(webView, client = JourneyClient(myBootstrap))
Differs from iOS: iOS injects the bootstrap with WKUserScript(.atDocumentStart), which is guaranteed to run before any page JavaScript. Androidβs onPageStarted + evaluateJavascript is best-effort β a <script> in the document head that synchronously calls window.GBGBridge.receive could race the injection. Hosts that install their own client must also supply the bootstrap literal themselves (via the subclass constructor), since the default script constant is not public.
Capabilities
The capability types implement the native side of capability negotiation: typed slots that declare and route capture requests, a strongly-typed result hierarchy, and helpers for permission state.
CaptureCapability
A typed capability slot that represents a capture operation (document or selfie). Setting a handler declares support; the SDK handles routing, result encoding, and busy rejection automatically.
package com.gbg.gbgbridge.capabilities
public class CaptureCapability(
public val id: String,
public val actionId: String,
initialVersion: String? = null,
)
Why it exists: Eliminates the need to manually build BridgeCapabilityInfo maps, encode capture results into JsonElement, and keep capability declarations in sync with handler registrations. Setting a handler is the declaration.
You typically donβt create CaptureCapability instances directly β use the built-in slots on BridgeHost (documentCapture, selfieCapture). All mutable state is main-thread-only, like the rest of the SDK.
Properties
| Property | Type | Description |
|---|
id | String | The capability identifier (e.g., "camera.document"). |
actionId | String | The bridge action this slot handles (e.g., "camera.document.capture"). |
handler | (suspend (BridgeMessage) -> CaptureResult)? | The handler suspend lambda, invoked on Dispatchers.Main. Setting this declares support. |
isEnabled | Boolean | Runtime toggle. When false, the slot reports as unsupported in capability.query even if a handler is set. Default: true. Advisory only β it does not gate dispatch; a request arriving while isEnabled is false still routes to the handler (matches iOS). |
isSupported | Boolean | Computed: handler != null && isEnabled. |
activeRequest | StateFlow<BridgeMessage?> | The currently in-flight request, or null when idle. In Compose, observe with collectAsState(); in the View system, launchIn a lifecycle scope. |
permissionState | PermissionState | The current permission state for this capability, included in query responses. Default: PermissionState.NOT_DETERMINED. |
version | String? | Optional version string. Defaults to "1.0" in query responses when supported. |
Differs from iOS: the handler is a Kotlin suspend lambda rather than a Swift async closure, and activeRequest is a StateFlow rather than an @Published property β the observation idiom changes but the semantics match.
Request dispatch
When a request arrives for the slotβs actionId, the slot decides as follows:
- No handler set β responds
UNSUPPORTED (error code UNSUPPORTED).
- A request is already in flight β responds
ERROR with code BUSY (recoverable = true).
- Otherwise,
activeRequest is set and the handler is launched on the main dispatcher.
- Handler throws β
HANDLER_FAILURE response (recoverable = false) plus delegate.onError. A CancellationException is translated to a cancelled response instead.
- Handler returns a
CaptureResult β the result is encoded onto the wire and activeRequest is cleared.
Methods
These methods drive capability handling at runtime β building query responses, awaiting capture completion, and signalling completion or cancellation from the UI layer.
buildCapabilityInfo()
public fun buildCapabilityInfo(): BridgeCapabilityInfo
Returns a BridgeCapabilityInfo reflecting the slotβs current state: supported mirrors isSupported, version is null when unsupported (else falls back to "1.0"), and permissionState carries the slotβs current wire token. Used internally to build capability.query responses.
awaitCompletion()
public suspend fun awaitCompletion(): CaptureResult
Suspends the current handler until complete(...) is called (or returns CaptureResult.Cancelled if cancelIfBusy runs first). Use this in handlers that present capture UI declaratively and complete from a callback.
Example:
host.documentCapture.handler = { request ->
showCaptureUi()
host.documentCapture.awaitCompletion()
}
complete(result:)
public fun complete(result: CaptureResult)
Resumes the pending handler with a capture result. Call from your capture UIβs callback. Main-thread-only; silently no-ops when no handler is pending.
Example:
// In a camera capture callback (after hopping to the main thread):
host.documentCapture.complete(
CaptureResult.Document(
imageData = imageBytes,
width = 1920,
height = 1080,
),
)
cancelIfBusy(reason:)
public fun cancelIfBusy(reason: String = "Request cancelled")
Cancels the in-flight request, if any: resumes the pending handler with CaptureResult.Cancelled(reason) and clears activeRequest immediately. Main-thread-only; safe to call when idle. Call this from teardown paths (Activity destruction, dialog dismissal) and when the user backs out of your capture UI.
See Capture screens for complete capture-UI patterns built on this slot.
CaptureResult
A strongly-typed result returned by capture handlers.
public sealed class CaptureResult
Why it exists: Eliminates manual JsonElement map construction. The SDK converts CaptureResult values to the bridge protocol format automatically.
Differs from iOS: iOS models this as an enum with separate DocumentCaptureResult / SelfieCaptureResult wrapper structs. Kotlin uses a sealed class whose subclasses carry the data directly β construct CaptureResult.Document(...) rather than .document(DocumentCaptureResult(...)). Image payloads are ByteArray instead of Data; the Document and Selfie data classes override equals/hashCode to compare byte-array content. The wire format is identical on both platforms.
CaptureResult.Document
A successful document capture, carrying the encoded image bytes. The SDK performs no compression or conversion β the bytes are emitted as supplied.
public data class Document(
val imageData: ByteArray,
val width: Int,
val height: Int,
val mimeType: String = "image/png",
) : CaptureResult()
| Parameter | Type | Default | Description |
|---|
imageData | ByteArray | β | The captured image data. Base64-encoded in the bridge response. |
width | Int | β | Image width in pixels. |
height | Int | β | Image height in pixels. |
mimeType | String | "image/png" | The MIME type of the image data. Supply "image/jpeg" if you compress a Bitmap to JPEG. |
Bridge response data (status success):
{
"imageBase64": "<base64-encoded image>",
"imageWidth": 1920,
"imageHeight": 1080,
"mimeType": "image/png"
}
CaptureResult.Selfie
A successful selfie capture, carrying preview image bytes and biometric blob bytes (encrypted and unencrypted, integrator-supplied).
public data class Selfie(
val previewImageData: ByteArray,
val width: Int,
val height: Int,
val mimeType: String = "image/jpeg",
val encryptedBlob: ByteArray,
val unencryptedBlob: ByteArray,
) : CaptureResult()
| Parameter | Type | Default | Description |
|---|
previewImageData | ByteArray | β | Preview image data. Base64-encoded in the bridge response. |
width | Int | β | Image width in pixels. |
height | Int | β | Image height in pixels. |
mimeType | String | "image/jpeg" | The MIME type of the preview image. |
encryptedBlob | ByteArray | β | Encrypted biometric data blob. Base64-encoded in the bridge response. |
unencryptedBlob | ByteArray | β | Unencrypted biometric data blob. Base64-encoded in the bridge response. |
Bridge response data (status success):
{
"imageBase64": "<base64>",
"imageWidth": 640,
"imageHeight": 480,
"mimeType": "image/jpeg",
"encryptedBlobBase64": "<base64>",
"unencryptedBlobBase64": "<base64>"
}
CaptureResult.Cancelled
The capture was cancelled by the user or system. Produces a cancelled response with error code CANCELLED and recoverable = true.
public data class Cancelled(val reason: String) : CaptureResult()
CaptureResult.Failed
The capture failed. Produces an error response carrying the supplied code, message, and recoverability flag in a BridgeErrorPayload.
public data class Failed(
val code: String,
val message: String,
val recoverable: Boolean,
) : CaptureResult()
PermissionState
The current authorisation status for a native capability. The wire representation β the string surfaced in capability.query responses β uses the same tokens as iOS, exposed via the wireValue property.
public enum class PermissionState(public val wireValue: String)
| Case | Wire value | Description |
|---|
GRANTED | "granted" | Permission has been granted. |
DENIED | "denied" | Permission has been explicitly denied. |
NOT_DETERMINED | "notDetermined" | Permission has not been requested yet. |
RESTRICTED | "restricted" | Permission is restricted by device policy (e.g., a work profile). |
NOT_APPLICABLE | "notApplicable" | Permission is not applicable for this capability. |
CameraDetector
A utility for detecting camera hardware availability and permission status.
public object CameraDetector
Why it exists: Provides a simple way to populate permissionState on capture slots without writing PackageManager boilerplate.
check(context:)
public fun check(context: Context): CameraDetector.Result
Checks camera hardware availability using PackageManager.FEATURE_CAMERA_ANY and permission status using Context.checkSelfPermission(Manifest.permission.CAMERA).
| Parameter | Type | Description |
|---|
context | Context | Any context β no Activity required. |
Returns: A CameraDetector.Result with hardware and permission info.
CameraDetector.Result
The nested result type carries the two facts a host needs to seed its capture slots.
public data class Result(
val hardwareAvailable: Boolean,
val permissionState: PermissionState,
)
| Property | Type | Description |
|---|
hardwareAvailable | Boolean | Whether a camera device is physically present. Typically false on emulators without a virtual camera. |
permissionState | PermissionState | The current camera permission state β see the caveat below. |
Differs from iOS: the Android runtime permission API cannot reliably distinguish βnever requestedβ from βpermanently deniedβ without an Activity and integrator-tracked state, so check() reports only GRANTED vs NOT_DETERMINED. Integrators who have run their own permission flow should set the richer DENIED/RESTRICTED state on the slot directly. iOS reports the full range from AVAuthorizationStatus.
Example:
val camera = CameraDetector.check(context)
host.documentCapture.permissionState = camera.permissionState
host.selfieCapture.permissionState = camera.permissionState
Configuration
The configuration types describe what the host declares to the web journey: its version, its capability map, and the optional WebView bootstrap and origin allowlist.
BridgeConfiguration
Declares the host applicationβs version, supported capabilities, bootstrap script, and an optional origin allowlist.
package com.gbg.gbgbridge.core
public data class BridgeConfiguration(
val hostVersion: String,
val capabilities: Map<String, BridgeCapabilityInfo> = emptyMap(),
val bootstrapScript: String? = null,
val allowedOrigins: List<String>? = null,
)
Why it exists: Provides a single configuration object that BridgeHost uses to initialise capability state, configure the WebView bootstrap, and (optionally) gate inbound messages by origin.
| Parameter | Type | Default | Description |
|---|
hostVersion | String | β | A version string for the host app (e.g., "1.0.0"). Reported to the web journey via capability queries. |
capabilities | Map<String, BridgeCapabilityInfo> | emptyMap() | A map of capability identifiers to their support status and metadata. Defensively snapshotted at host construction. |
bootstrapScript | String? | null | Optional custom JavaScript injected on every page load. If null, the default script is used. |
allowedOrigins | List<String>? | null | Optional allowlist of origins (scheme://host[:port]) permitted to drive the bridge. null disables enforcement. |
Differs from iOS: allowedOrigins is an Android-only addition, and capabilities has a default value (emptyMap()), so configuration-only hosts can construct BridgeConfiguration(hostVersion = "1.0.0") directly.
allowedOrigins
When non-null, inbound postMessage calls are checked against the normalised origin of the WebViewβs main-frame URL. On rejection the message is dropped, lastError is set, and delegate.onError fires with a SecurityException.
The constructor validates the list up front and throws IllegalArgumentException for:
- An empty list (which would be a kill-switch with no opt-out β pass
null to disable enforcement instead).
- Any malformed entry: missing host, non-http(s) scheme,
% in the host, or a raw internationalised domain label (use punycode A-labels).
Entries and the live URL are both normalised to scheme://host[:port] β http/https only, case-insensitive, default ports elided, single trailing host dot stripped β so "HTTPS://APP.example.com/", "https://app.example.com", and "https://app.example.com:443" are equivalent.
allowedOrigins is
not a security boundary. Androidβs
addJavascriptInterface injects into every frame, but only the main-frame URL can be checked β a hostile sub-frame under an allowlisted page can still drive the bridge, and the URL read is subject to time-of-check races. Treat it as defence-in-depth against host misconfiguration, layered on top of navigation-level filtering via
shouldOverrideUrlLoading. See
Security.
BridgeCapabilityInfo
Metadata about a single capability.
@Serializable
public data class BridgeCapabilityInfo(
val supported: Boolean,
val version: String? = null,
val constraints: Map<String, JsonElement>? = null,
val permissionState: String? = null,
)
Why it exists: Allows the host to express not just whether a capability is supported, but also its version and permission state.
| Parameter | Type | Default | Description |
|---|
supported | Boolean | β | Whether the capability is available. |
version | String? | null | The capability implementation version. |
constraints | Map<String, JsonElement>? | null | Arbitrary key-value constraints. Carried for cross-platform parity but never emitted in capability.query responses (on either platform). |
permissionState | String? | null | The native permission status (e.g., "granted", "denied"). Included in capability query responses when non-null. |
As a Kotlin data class with val properties, instances are immutable β derive variants with .copy(...).
Message models
Every value that crosses the bridge is wrapped in a BridgeMessage envelope, which carries a typed payload, a correlation ID, and a status. The types in this section describe that envelope and the shapes youβll embed inside it. All of them live in com.gbg.gbgbridge.models and are @Serializable via kotlinx.serialization; the wire format is identical to iOS.
BridgeMessage
A structured envelope for all bridge communication.
@Serializable
public data class BridgeMessage(
val version: String,
val correlationId: String,
val type: BridgeMessageType,
val timestamp: Long,
val payload: BridgePayload,
)
| Property | Type | Description |
|---|
version | String | Protocol version (currently "1.0"). |
correlationId | String | Unique identifier for request-response correlation. |
type | BridgeMessageType | Message type: REQUEST, RESPONSE, or EVENT. |
timestamp | Long | Milliseconds since the Unix epoch. Decoding is lenient: fractional millisecond values (as iOS may emit) are rounded to a whole Long. |
payload | BridgePayload | The message payload. |
Differs from iOS: there is no Identifiable conformance or computed id property. When rendering messages in a Compose LazyColumn, key items by correlationId (combined with timestamp if you expect to render multiple messages per correlation ID).
BridgePayload
The payload carried within a BridgeMessage.
@Serializable
public data class BridgePayload(
val action: String,
val data: Map<String, JsonElement>? = null,
val status: BridgeResponseStatus? = null,
val error: BridgeErrorPayload? = null,
val options: BridgeRequestOptions? = null,
)
| Property | Type | Default | Description |
|---|
action | String | β | The action identifier (e.g., "camera.document.capture", "capability.query"). |
data | Map<String, JsonElement>? | null | Arbitrary typed JSON data. |
status | BridgeResponseStatus? | null | Response status (only present in response messages). |
error | BridgeErrorPayload? | null | Error details (only present when the status indicates failure). |
options | BridgeRequestOptions? | null | Per-request options supplied by the web journey (only present on inbound requests that carry them). |
BridgeErrorPayload
Structured error information included in error responses.
@Serializable
public data class BridgeErrorPayload(
val code: String,
val message: String,
val recoverable: Boolean,
)
Why it exists: Provides machine-readable error codes alongside human-readable messages, plus a recoverable flag that tells the web journey whether it should retry or fail.
| Property | Type | Description |
|---|
code | String | A machine-readable error code (e.g., "CAMERA_DENIED", "HANDLER_FAILURE"). |
message | String | A human-readable error description. |
recoverable | Boolean | Whether the error is recoverable (e.g., the user can retry). |
BridgeRequestOptions
Optional per-request options the web journey can attach to a request payload.
@Serializable
public data class BridgeRequestOptions(
val timeout: Double? = null,
val fallbackAllowed: Boolean? = null,
)
| Property | Type | Default | Description |
|---|
timeout | Double? | null | A timeout hint supplied by the web journey. |
fallbackAllowed | Boolean? | null | Whether the web journey can fall back to a web-based flow if the native capability declines. |
BridgeMessageType
The type of a bridge message.
@Serializable
public enum class BridgeMessageType
| Case | Wire value | Description |
|---|
REQUEST | "request" | A request from the web journey to the native host. |
RESPONSE | "response" | A response from the native host to the web journey. |
EVENT | "event" | An asynchronous notification (either direction). |
BridgeResponseStatus
The status of a response message.
@Serializable
public enum class BridgeResponseStatus
| Case | Wire value | When to use |
|---|
SUCCESS | "success" | The operation completed successfully. |
ERROR | "error" | The operation failed. Include a BridgeErrorPayload. |
CANCELLED | "cancelled" | The user cancelled the operation. |
UNSUPPORTED | "unsupported" | The requested capability is not available. |
ACKNOWLEDGED | "acknowledged" | The request was received; the result will follow. |
JsonElement
Arbitrary JSON values in payloads are represented with kotlinx.serialization.json.JsonElement β the standard kotlinx.serialization JSON tree type β wherever the iOS SDK uses its custom JSONValue enum.
Differs from iOS: there is no SDK-defined JSON type. JsonElement (with its subtypes JsonPrimitive, JsonObject, JsonArray, and JsonNull) comes from the kotlinx-serialization-json dependency, so you get the full ecosystem of builders and accessors for free.
Example β constructing a payload:
import kotlinx.serialization.json.*
val data: Map<String, JsonElement> = mapOf(
"name" to JsonPrimitive("John Doe"),
"age" to JsonPrimitive(30),
"verified" to JsonPrimitive(true),
"documents" to buildJsonArray {
addJsonObject {
put("type", "passport")
put("number", "AB123456")
}
},
"metadata" to JsonNull,
)
Example β reading request parameters:
val side = request.payload.data?.get("side")?.jsonPrimitive?.contentOrNull
val attempts = request.payload.data?.get("attempts")?.jsonPrimitive?.intOrNull
Interfaces
Where the iOS SDK defines protocols, the Android SDK defines Kotlin interfaces with the same roles: a handler contract for capabilities, a responder for sending results, and a delegate for observing host activity.
BridgeCapabilityHandler
The interface for objects that handle specific bridge request actions.
public interface BridgeCapabilityHandler {
public val action: String
@MainThread
public fun handle(request: BridgeMessage, responder: BridgeResponder)
}
Why it exists: Defines the contract for capability implementations. Each handler is responsible for a single action and receives a responder to send its result back to the web journey.
public val action: String
The action identifier this handler responds to (e.g., "camera.document.capture", "nfc.read"). Must be unique per BridgeHost β registering a handler with the same action replaces the previous one.
handle(request:, responder:)
public fun handle(request: BridgeMessage, responder: BridgeResponder)
Called on the main thread when a request arrives with a matching action. Perform your native operation and call responder.respond(...) when done.
| Parameter | Type | Description |
|---|
request | BridgeMessage | The incoming request message. Access request.payload.data for request parameters. |
responder | BridgeResponder | A callback to send the response. Call exactly once. |
Differs from iOS: handle is
synchronous, not
async. For asynchronous work (camera capture, network calls, pickers), retain the
responder, kick off the work, and when it completes hop back to the main thread and call
responder.respond(...). See
BridgeResponder for the threading contract and hop patterns.
Example:
class DocumentCaptureHandler : BridgeCapabilityHandler {
override val action = "camera.document.capture"
override fun handle(request: BridgeMessage, responder: BridgeResponder) {
// Extract parameters from the request
val side = request.payload.data?.get("side")?.jsonPrimitive?.contentOrNull
// Launch asynchronous capture (your implementation), retaining the responder
captureDocument(
onSuccess = { imageBytes ->
Handler(Looper.getMainLooper()).post {
responder.respond(
status = BridgeResponseStatus.SUCCESS,
data = mapOf(
"imageBase64" to JsonPrimitive(
Base64.encodeToString(imageBytes, Base64.NO_WRAP)
),
),
)
}
},
onFailure = { error ->
Handler(Looper.getMainLooper()).post {
responder.respond(
status = BridgeResponseStatus.ERROR,
error = BridgeErrorPayload(
code = "CAPTURE_FAILED",
message = error.message ?: "Capture failed",
recoverable = true,
),
)
}
},
)
}
}
BridgeResponder
A callback interface used to send a response from a capability handler back to the web journey.
public interface BridgeResponder {
@MainThread
public fun respond(
status: BridgeResponseStatus,
data: Map<String, JsonElement>? = null,
error: BridgeErrorPayload? = null,
)
}
Why it exists: Decouples the handler from BridgeHost internals. Handlers donβt need to know about WebViews or JavaScript evaluation β they simply call respond(). Implementations are provided by the SDK; you never implement this interface yourself for the built-in dispatch path.
| Parameter | Type | Default | Description |
|---|
status | BridgeResponseStatus | β | The response status. |
data | Map<String, JsonElement>? | null | Optional response payload. |
error | BridgeErrorPayload? | null | Optional error details. |
Threading: Main-thread-only, like the rest of the SDK. Handlers that defer to an asynchronous worker (an executor, OkHttp callback, CameraX callback) must hop back to the main thread before responding:
- Coroutines:
withContext(Dispatchers.Main) { responder.respond(...) }
- Handler:
Handler(Looper.getMainLooper()).post { responder.respond(...) }
- Activity:
runOnUiThread { responder.respond(...) }
Differs from iOS: the iOS responder is safe to call from any thread (it dispatches to the main actor internally). The Android responder throws IllegalStateException when called off the main thread β the hop is your responsibility. Call respond exactly once per request; subsequent calls silently no-op.
BridgeHostDelegate
An observer interface for monitoring bridge activity. All methods have default no-op implementations, so you only override what you need.
public interface BridgeHostDelegate {
public fun onMessage(host: BridgeHost, message: BridgeMessage) {}
public fun onMessageSent(host: BridgeHost, message: BridgeMessage) {}
public fun onUnhandledRequest(host: BridgeHost, request: BridgeMessage) {}
public fun onError(host: BridgeHost, error: Throwable) {}
}
Why it exists: Provides a way to observe all bridge messages and react to unhandled requests and errors without registering a handler. Useful for logging, analytics, or building custom request handling.
Differs from iOS: Kotlin uses distinct method names rather than Swift argument labels β onMessage corresponds to bridgeHost(_:didReceive:) and onUnhandledRequest to bridgeHost(_:unhandledRequest:). onMessageSent and onError are Android-only additions. Remember that BridgeHost.delegate is WeakReference-backed: keep your own strong reference to the delegate.
onMessage(host:, message:)
Called for every message received from the web journey, regardless of type. One-way journey events (e.g. journey.completed) are observed here β events do not go through the request dispatch path.
onMessageSent(host:, message:)
Called for every outbound envelope, before transport. This records intent, not delivery β it also fires for messages that are subsequently dropped because no WebView is attached, or that fail to encode. Android-only.
onUnhandledRequest(host:, request:)
Called when a request arrives with no registered handler. The request is also added to host.pendingRequests; respond later via the lookup respond overload.
onError(host:, error:)
The Throwable-level error channel, Android-only. Fires for inbound decode failures, handler exceptions, outbound encode failures, and origin-gate rejections (SecurityException). Pairs with the string-valued lastError property for hosts that want full exception objects.
Built-in handlers
The SDK auto-registers one handler on every host so capability negotiation works out of the box.
CapabilityQueryHandler
A built-in handler that responds to "capability.query" requests with the hostβs declared capabilities.
public class CapabilityQueryHandler(
private val capabilitiesProvider: () -> Map<String, BridgeCapabilityInfo>,
private val hostVersion: String,
) : BridgeCapabilityHandler
Why it exists: Capability negotiation is fundamental to the bridge protocol. This handler is automatically registered by BridgeHost so the web journey can always discover available capabilities.
Action: "capability.query"
| Parameter | Type | Description |
|---|
capabilitiesProvider | () -> Map<String, BridgeCapabilityInfo> | A lambda that returns the current capability map. Called on each query. |
hostVersion | String | The host version string to include in the response. |
Differs from iOS: there is a single constructor taking a rich BridgeCapabilityInfo provider β no separate βsimpleβ boolean-map initialiser. The class is public (iOS keeps its equivalent internal) so hosts that call unregister("capability.query") can register a replacement instance with their own provider.
When the web journey sends a capability.query request, the response data contains:
{
"environment": "android",
"hostVersion": "1.0.0",
"capabilities": {
"camera.document": {
"supported": true,
"version": "1.0",
"permissionState": "granted"
},
"camera.selfie": {
"supported": false,
"version": null
}
}
}
| Field | Type | Description |
|---|
environment | String | Always "android" for this SDK. |
hostVersion | String | The host version from configuration. |
capabilities | Object | Map of capability ID β { supported, version, permissionState? }. |
capabilities[id].supported | Boolean | Always present. |
capabilities[id].version | String? | Always present; JSON null when unset. |
capabilities[id].permissionState | String? | Present only when the capability provides permission metadata. |
constraints is never emitted, on either platform.