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 aWebView 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 aWebView and your native code.
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
documentCapture and selfieCapture slots. A CapabilityQueryHandler is registered automatically and builds its response dynamically from typed slots and custom capabilities, including permissionState metadata.
Example:
BridgeHost(configuration:, capabilitiesProvider:) β Configuration-based
BridgeConfiguration map, or dynamically via capabilitiesProvider. A built-in CapabilityQueryHandler is registered automatically for "capability.query" requests.
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.Companion constants
These constants describe the protocol identity and the hostβs internal buffer limits. The buffer caps surface through public behaviour β eviction fromreceivedMessages, leak warnings in lastError, and the response dedupe window.
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
delegate
capabilities
- Runtime
registerCustomCapabilityregistrations. - 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
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
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
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
camera.document). Set its handler to declare support. Routes requests for the "camera.document.capture" action.
selfieCapture
camera.selfie). Set its handler to declare support. Routes requests for the "camera.selfie.capture" action.
Methods
All methods are main-thread-only and returnUnit unless noted. After dispose(), the state-mutating methods throw IllegalStateException β see dispose() for the full post-dispose contract.
attach(webView:, client:)
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.
Example:
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()
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()
Activity.onDestroy, ViewModel.onCleared).
After dispose():
attach,register,unregister,registerCustomCapability, bothrespondoverloads, andsendEventthrowIllegalStateException.detach(),dispose(),clearError(), all property getters (which return empty/null), and thedelegatesetter 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).
register(handler:)
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).
Example:
unregister(action:)
pendingRequests instead.
"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:)
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.
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.sendEvent(action:, data:)
android-event-{uuid} (iOS uses ios-event-{uuid}).
Example:
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:)
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.
Example:
respond(to:, action:, status:, data:, error:)
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.
clearError()
lastError to null. Safe to call after dispose().
Request dispatch behaviour
When a request arrives with no registered handler, it is appended topendingRequests 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 aWebView with the bridge infrastructure. BridgeHost.attach() calls it for you β you only interact with it directly when subclassing its WebViewClient for custom navigation policy.
configure(webView:, bootstrapScript:, client:)
WebViewClient that injects the bootstrap script on each page load:
- Sets
javaScriptEnabled = trueanddomStorageEnabled = true. - Installs a
BootstrapInjectingWebViewClient(the suppliedclient, or a default built frombootstrapScript). - Installs a plain
WebChromeClient.
The default bootstrap script is:
BootstrapInjectingWebViewClient
This nested class is the SDKβsWebViewClient. 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.
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:
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 ahandler declares support; the SDK handles routing, result encoding, and busy rejection automatically.
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
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βsactionId, the slot decides as follows:
- No handler set β responds
UNSUPPORTED(error codeUNSUPPORTED). - A request is already in flight β responds
ERRORwith codeBUSY(recoverable = true). - Otherwise,
activeRequestis set and the handler is launched on the main dispatcher. - Handler throws β
HANDLER_FAILUREresponse (recoverable = false) plusdelegate.onError. ACancellationExceptionis translated to acancelledresponse instead. - Handler returns a
CaptureResultβ the result is encoded onto the wire andactiveRequestis 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()
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()
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:
complete(result:)
cancelIfBusy(reason:)
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.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.
Bridge response data (status
success):
CaptureResult.Selfie
A successful selfie capture, carrying preview image bytes and biometric blob bytes (encrypted and unencrypted, integrator-supplied).
Bridge response data (status
success):
CaptureResult.Cancelled
The capture was cancelled by the user or system. Produces acancelled response with error code CANCELLED and recoverable = true.
CaptureResult.Failed
The capture failed. Produces anerror response carrying the supplied code, message, and recoverability flag in a BridgeErrorPayload.
PermissionState
The current authorisation status for a native capability. The wire representation β the string surfaced incapability.query responses β uses the same tokens as iOS, exposed via the wireValue property.
CameraDetector
A utility for detecting camera hardware availability and permission status.permissionState on capture slots without writing PackageManager boilerplate.
check(context:)
PackageManager.FEATURE_CAMERA_ANY and permission status using Context.checkSelfPermission(Manifest.permission.CAMERA).
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.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.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.BridgeHost uses to initialise capability state, configure the WebView bootstrap, and (optionally) gate inbound messages by origin.
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, inboundpostMessage 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
nullto 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).
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.
BridgeCapabilityInfo
Metadata about a single capability.
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 aBridgeMessage 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.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 aBridgeMessage.
BridgeErrorPayload
Structured error information included in error responses.recoverable flag that tells the web journey whether it should retry or fail.
BridgeRequestOptions
Optional per-request options the web journey can attach to a request payload.BridgeMessageType
The type of a bridge message.BridgeResponseStatus
The status of a response message.JsonElement
Arbitrary JSON values in payloads are represented withkotlinx.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.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.action
"camera.document.capture", "nfc.read"). Must be unique per BridgeHost β registering a handler with the same action replaces the previous one.
handle(request:, responder:)
responder.respond(...) when done.
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.BridgeResponder
A callback interface used to send a response from a capability handler back to the web journey.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.
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.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 tohost.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.
BridgeHost so the web journey can always discover available capabilities.
Action: "capability.query"
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.Response format
When the web journey sends acapability.query request, the response data contains:
constraints is never emitted, on either platform.