General
Common questions about supported platforms, dependencies, and how the SDK is distributed.
What Android versions does GBGBridge support?
Android 7.0 (API 24) and later. The SDK is compiled against compile/target SDK 34.
Does GBGBridge have any external dependencies?
A small set: androidx.annotation, kotlinx-serialization-json, and kotlinx-coroutines-android. Coroutines is exposed as an api dependency because coroutine types appear on the public surface of CaptureCapability (suspend handlers, StateFlow); the others are implementation details. Unlike the iOS SDK, the dependency count is not zero — but all three are standard AndroidX/Kotlin libraries most apps already include.
What Kotlin version is required?
Kotlin 2.x and JDK 17. The SDK is built with Kotlin 2.2.10.
How is GBGBridge distributed?
As an AAR via Maven Central. No repository setup is needed beyond mavenCentral():
dependencies {
implementation("com.gbg:gbgbridge-sdk:0.1.0-alpha01")
}
Does it work on the emulator?
Yes. The emulator provides a synthetic camera image to camera APIs, and the reference app’s capture surface includes a placeholder-bitmap path that needs no camera at all. For networking to a local journey server, use 10.0.2.2 (the host machine as seen from the emulator — localhost is the emulator itself), or run adb reverse tcp:3000 tcp:3000.
Integration
Questions about wiring GBGBridge into your app’s UI layer and capability setup.
Does GBGBridge work with both Jetpack Compose and the View system?
Yes. The SDK has no Compose dependency — integration is a plain WebView plus host.attach(webView). In Compose, create the WebView inside AndroidView(factory = { WebView(it) }); in the View system, create or inflate the WebView in your Activity or Fragment. See the Embedding Guide for details.
Can I use multiple BridgeHost instances?
Each BridgeHost should be associated with one WebView. If you have multiple WebViews (e.g., multiple journey tabs), create a separate BridgeHost for each.
Can I use my own WebViewClient?
Yes. Subclass BootstrapInjectingWebViewClient (it is public open), add your navigation policy or error logging, and pass it to attach:
class MyWebViewClient(bootstrapScript: String) :
BootstrapInjectingWebViewClient(bootstrapScript) {
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
return !isAllowedHost(request.url)
}
}
host.attach(webView, client = MyWebViewClient(bootstrap))
If you override onPageStarted, call super.onPageStarted(...) so the bootstrap script is still injected.
Can I change capabilities after initialization?
With typed slots (BridgeHost(hostVersion)), capabilities are inherently dynamic. Set or clear handler to change support, toggle isEnabled to temporarily disable, and update permissionState as permissions change. The capability query response is built dynamically on each query.
With the configuration-based constructor, the capabilities property is a read-only merged snapshot (unlike iOS, where the map is mutable). For dynamic capabilities in that path, pass a capabilitiesProvider lambda — an Android-only addition that is re-evaluated every time capabilities are read or queried:
val host = BridgeHost(
configuration = BridgeConfiguration(hostVersion = "1.0.0"),
capabilitiesProvider = { currentCapabilities() }
)
Do I need to handle the capability.query action myself?
No. BridgeHost automatically registers a CapabilityQueryHandler that responds to capability.query requests, built from the typed slots, custom capabilities, and the static configuration map or capabilitiesProvider. The handler class is public, so if you need different behavior you can unregister("capability.query") and register a replacement.
What is the difference between typed slots and custom capabilities?
Typed slots (host.documentCapture, host.selfieCapture) are built-in CaptureCapability properties for well-known capture operations. They handle result encoding, busy rejection, and permission state automatically. Handlers are suspend lambdas that return CaptureResult values.
Custom capabilities (registerCustomCapability()) support any action. Handlers receive a BridgeResponder and build responses manually using JsonElement maps.
Both appear in capability.query responses. If both are registered for the same ID, the typed slot takes precedence — but only when its handler is set; an unused slot never shadows a custom registration.
How does permission state work?
Each typed slot has a permissionState property (default: NOT_DETERMINED). Populate it using CameraDetector.check(context):
val camera = CameraDetector.check(context)
host.documentCapture.permissionState = camera.permissionState
This is included in the capability.query response as a permissionState field, allowing the web journey to detect permission issues before attempting capture.
CameraDetector only reports GRANTED or NOT_DETERMINED — Android cannot distinguish “never asked” from “permanently denied” without app-side state. After running your own permission flow, set the richer DENIED or RESTRICTED values on the slot yourself.
What happens if the web journey sends a request for an action I haven’t registered?
The request is added to host.pendingRequests and onUnhandledRequest(host, request) is called on the delegate. You can respond to it manually via the lookup overload host.respond(to = correlationId, status = ..., data = ...).
If you don’t respond, the request sits in pendingRequests indefinitely. The web journey may implement its own timeout.
Messaging
Questions about message delivery, ordering, and the wire protocol.
Is the wire protocol the same as the iOS SDK?
Yes. The message envelope (version, correlationId, type, timestamp, payload), response statuses, and the capability.query response shape are identical — the environment field reports "android" instead of "ios". The one platform difference is the web-to-native entry point: Android exposes window.GBGBridge.postMessage(jsonString), while iOS uses window.webkit.messageHandlers.gbgBridge.postMessage(...). Web code targeting both platforms must feature-detect which is present. The native-to-web direction (window.GBGBridge.receive) is identical on both.
Are messages guaranteed to be delivered?
Messages sent via the bridge are in-process calls through the WebView. They are delivered reliably as long as:
- The WebView is attached to the host.
- The web page has not navigated away.
window.GBGBridge.receive is defined on the web side.
There is no built-in retry or delivery confirmation mechanism. Note one Android-specific behavior: calling respond() or sendEvent() while no WebView is attached still fires the delegate’s onMessageSent (as an intent trace), but the message is silently dropped at the transport — no lastError is recorded, unlike iOS.
What is the maximum message size?
There is no hard limit imposed by GBGBridge. However, very large messages (e.g., multi-megabyte Base64 images) can cause memory pressure. For large data transfers, consider passing file paths instead of inline data.
Can I send messages before the web page loads?
Messages sent before the page loads (or before the web journey sets up its receive() function) will be lost. The default bootstrap script creates a no-op receive(), so the call won’t error — but the data won’t be processed.
Wait for a signal from the web journey (e.g., a journey.ready event) before sending messages.
Is the message order preserved?
Messages are processed in the order they arrive: inbound messages come in on the WebView render thread and are posted to the main looper in sequence. Responses are sent in the order respond() is called, and evaluateJavascript calls execute sequentially.
What happens to events — does anyone respond to them?
Events are fire-and-forget. They have no expected response, and they don’t enter the request dispatch path — observe them in the delegate’s onMessage. Both the native host and the web journey can send events. Use events for notifications, state updates, and lifecycle signals.
Capabilities
Questions about declaring capabilities and handling hardware availability.
How does the web journey know what capabilities are available?
The web journey sends a capability.query request. The built-in handler responds with the environment ("android"), host version, and a map of all declared capabilities with their supported status, version, and optionally permissionState.
What if a capability is hardware-dependent?
For camera capabilities, use CameraDetector.check(context) and conditionally set the handler:
val camera = CameraDetector.check(context)
if (camera.hardwareAvailable) {
host.documentCapture.handler = { request ->
// Show capture UI and await result
}
host.documentCapture.permissionState = camera.permissionState
}
For other hardware-dependent features exposed via registerCustomCapability(), run your availability check first and only register the capability when the hardware is present — capabilities that are never declared simply don’t appear as supported in the query response.
What manifest entries do I need?
GBGBridge’s own library manifest forces nothing on your app. Your host app needs:
<uses-permission android:name="android.permission.INTERNET" /> — always.
- If hosting camera capture:
<uses-permission android:name="android.permission.CAMERA" /> plus <uses-feature android:name="android.hardware.camera" android:required="false" />, and request the runtime CAMERA permission at the point of use.
Security
Questions about the trust boundary between the host app and the web journey.
Is communication between the host and web journey encrypted?
Communication uses the WebView’s JavaScript interface and evaluateJavascript within your app’s process. It does not traverse the network, so TLS is not applicable. Messages are memory-to-memory within the device.
Can other apps intercept bridge messages?
No. The WebView runs within your app’s sandbox, and the GBGBridge JavaScript interface is private to your app process. To additionally guard against messages arriving from an unexpected page origin (e.g., after a rogue navigation), you can set allowedOrigins on BridgeConfiguration — an opt-in, Android-only message-level origin gate. Treat it as defence-in-depth, not a security boundary: pair it with navigation-level filtering in shouldOverrideUrlLoading.
Should I validate incoming message data?
Yes. Treat data from the web journey the same way you would treat user input. Validate required fields, check value ranges, and reject malformed requests.
Debugging
Questions about observing bridge traffic and diagnosing common issues.
How do I see what messages are being exchanged?
Use the BridgeHostDelegate to log traffic in both directions — onMessageSent is an Android-only callback that fires for every outbound message:
class LoggingDelegate : BridgeHostDelegate {
override fun onMessage(host: BridgeHost, message: BridgeMessage) {
Log.d("Bridge", "in [${message.type}] ${message.payload.action}")
}
override fun onMessageSent(host: BridgeHost, message: BridgeMessage) {
Log.d("Bridge", "out [${message.type}] ${message.payload.action}")
}
}
You can also read the host.receivedMessages and host.pendingRequests snapshots at any time.
Can I inspect the WebView?
Yes. Call WebView.setWebContentsDebuggingEnabled(true) (gate it on debuggable builds), then open chrome://inspect in desktop Chrome and inspect the WebView. You can examine window.GBGBridge and test messages from the console. To surface page console output in logcat, set a WebChromeClient with an onConsoleMessage override — after calling attach(), since attach installs its own clients.
Why is my delegate not firing?
BridgeHost.delegate is backed by a weak reference — the host does not keep your delegate alive. An inline assignment like host.delegate = LoggingDelegate() with no other strong reference will be garbage collected and silently stop firing. Hold the delegate in a property whose lifetime matches the host (e.g., on your controller or ViewModel).
Next Steps
Continue with these guides for deeper coverage of the topics above.