PageSourceSearch

https://bambuser.com/docs/assets/js/c65fe0ac.f7c8b293.js

js bambuser.com collected 2026-09-24 08:27:38 UTC 17,804 bytes, 1 lines download raw bytes

1"use strict";(globalThis.webpackChunkbambuser_docs||=[]).push([[1274],{70887(e,n,i){i.r(n),i.d(n,{assets:()=>a,contentTitle:()=>o,default:()=>h,frontMatter:()=>d,metadata:()=>r,toc:()=>l});const r=JSON.parse('{"id":"mobileSDK/android/vc-android-architecture","title":"Android SDK \u2014 Architecture","description":"How the Android Calls-shopper SDK stacks a controller, JS bridge, and Compose overlay on top of a single Android WebView hosting the video call embed.","source":"@site/video-consultation/mobileSDK/android/architecture.mdx","sourceDirName":"mobileSDK/android","slug":"/mobileSDK/android/mobile-sdk/android/architecture","permalink":"/docs/video-consultation/mobileSDK/android/mobile-sdk/android/architecture","draft":false,"unlisted":false,"tags":[],"version":"current","frontMatter":{"id":"vc-android-architecture","title":"Android SDK \u2014 Architecture","description":"How the Android Calls-shopper SDK stacks a controller, JS bridge, and Compose overlay on top of a single Android WebView hosting the video call embed.","sidebar_label":"Architecture","slug":"mobile-sdk/android/architecture"},"sidebar":"someSidebars","previous":{"title":"Installation","permalink":"/docs/video-consultation/mobileSDK/android/mobile-sdk/android/installation"},"next":{"title":"Integration","permalink":"/docs/video-consultation/mobileSDK/android/mobile-sdk/android/integration"}}');var t=i(74848),s=i(28453);const d={id:"vc-android-architecture",title:"Android SDK \u2014 Architecture",description:"How the Android Calls-shopper SDK stacks a controller, JS bridge, and Compose overlay on top of a single Android WebView hosting the video call embed.",sidebar_label:"Architecture",slug:"mobile-sdk/android/architecture"},o="Architecture",a={},l=[{value:"Layer overview",id:"layer-overview",level:2},{value:"Why WebView-as-core",id:"why-webview-as-core",level:2},{value:"The single WebView",id:"the-single-webview",level:2},{value:"JS bridge",id:"js-bridge",level:2},{value:"Iframe sizing safeguard",id:"iframe-sizing-safeguard",level:2},{value:"Data-source handlers",id:"data-source-handlers",level:2},{value:"Fire-and-forget events",id:"fire-and-forget-events",level:2},{value:"Native presentation",id:"native-presentation",level:2}];function c(e){const n={code:"code",h1:"h1",h2:"h2",header:"header",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,s.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.header,{children:(0,t.jsx)(n.h1,{id:"architecture",children:"Architecture"})}),"\n",(0,t.jsxs)(n.p,{children:["The Android Calls-shopper SDK is a thin native shell around the same web-based video-call widget that runs in the browser and on iOS. All video, WebRTC, agent tooling, and UI rendering happens ",(0,t.jsxs)(n.strong,{children:["inside a single Android ",(0,t.jsx)(n.code,{children:"WebView"})]}),". The Kotlin side provides:"]}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["a stable native API surface (",(0,t.jsx)(n.code,{children:"BambuserCallController"}),", ",(0,t.jsx)(n.code,{children:"BambuserCallHandlers"}),", ",(0,t.jsx)(n.code,{children:"BambuserCallDelegate"}),")"]}),"\n",(0,t.jsx)(n.li,{children:"a JS bridge that translates between native calls and the widget's JS event stream"}),"\n",(0,t.jsxs)(n.li,{children:["a Compose overlay (",(0,t.jsx)(n.code,{children:"BambuserCallOverlay"}),") so the WebView can be embedded in any Compose or interop-hosted app without leaking WebView details"]}),"\n"]}),"\n",(0,t.jsx)(n.h2,{id:"layer-overview",children:"Layer overview"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502  Your app (Compose)                                           \u2502\n\u2502    \u2514\u2500 BambuserCallOverlay(controller = ...)                   \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                               \u2502\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502  EmbedWebView (Compose AndroidView)                           \u2502\n\u2502    \u251c\u2500 Android WebView + WebChromeClient + WebViewClient       \u2502\n\u2502    \u251c\u2500 @JavascriptInterface bridge (__bambuserAndroidBridge)   \u2502\n\u2502    \u251c\u2500 Camera + mic auto-grant via onPermissionRequest         \u2502\n\u2502    \u251c\u2500 onCreateWindow popup rout
1ing                            \u2502\n\u2502    \u2514\u2500 Console-log surfacing (Logcat tag BambuserCall.WebView) \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                               \u2502  JS \u2194 native message bridge\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502  BambuserCallController                                       \u2502\n\u2502    \u251c\u2500 Public API: show(), close(), enterPiP(), notify*(),     \u2502\n\u2502    \u2502  updateData(), updateElement(), destroy(),               \u2502\n\u2502    \u2502  updateQueue(), suspend areQueuesOpen(), \u2026               \u2502\n\u2502    \u251c\u2500 Observable state (mutableStateOf isVisible / isPiP /    \u2502\n\u2502    \u2502  callState / connectId / queue / environment)            \u2502\n\u2502    \u2514\u2500 Delegate + Handlers wiring                              \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                               \u2502\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502  EmbedHTMLBuilder + EmbedEventRouter (internal)               \u2502\n\u2502    \u251c\u2500 HTML template with document-start shim                  \u2502\n\u2502    \u251c\u2500 oneToOneEmbed(\u2026) constructor call                       \u2502\n\u2502    \u251c\u2500 Conditional .on(...) subscriptions                      \u2502\n\u2502    \u251c\u2500 MutationObserver forcing widget iframe to fill viewport \u2502\n\u2502    \u2514\u2500 Bidirectional message routing                           \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                               \u2502  loads\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25bc\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502  Bambuser Video Call Widget (Web, WebRTC, LiveKit, \u2026)         \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n"})}),"\n",(0,t.jsx)(n.h2,{id:"why-webview-as-core",children:"Why WebView-as-core"}),"\n",(0,t.jsxs)(n.p,{children:["Bambuser's video-call widget already exists as a mature web application. Rather than reimplementing WebRTC, queue management, agent tooling, chat, tracking, PiP transitions, and every future embed feature on top of ",(0,t.jsx)(n.code,{children:"libwebrtc"})," and native views, the SDK ",(0,t.jsx)(n.strong,{children:"treats the widget as the source of truth"})," and provides:"]}),"\n",(0,t.jsxs)(n.ol,{children:["\n",(0,t.jsxs)(n.li,{children:["A native ",(0,t.jsx)(n.code,{children:"BambuserCallController"})," with Compose-observable state so a Compose UI can drive it declaratively."]}),"\n",(0,t.jsxs)(n.li,{children:["Native handlers (",(0,t.jsx)(n.code,{children:"provideProductData"}),", ",(0,t.jsx)(n.code,{children:"provideSearchData"}),", ",(0,t.jsx)(n.code,{children:"shouldAddToCart"}),", ",(0,t.jsx)(n.code,{children:"shouldUpdateCart"}),") for data merchants need to keep on-device (personalisation, region-locked catalogues, live pricing)."]}),"\n",(0,t.jsx)(n.li,{children:"A native delegate for fire-and-forget events (checkout, navigate-to, tracking, queue status)."}),"\n",(0,t.jsxs)(n.li,{children:["Native presentation control (full-screen \u2194 PiP with a draggable mini-player) via Compose animations and ",(0,t.jsx)(n.code,{children:"pointerInput"}),"."]}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"Merchants get every widget improvement automatically without a new SDK release, and the SDK stays small and stable."}),"\n",(0,t.jsx)(n.h2,{id:"the-single-webview",children:"The single WebView"}),"\n",(0,t.jsxs)(n.p,{children:["There is exactly one ",(0,t.jsx)(n.code,{children:"android.webkit.WebView"})," created by the SDK, inside the ",(0,t.jsx)(n.code,{children:"AndroidView { factory = { \u2026 } }"})," block in ",(0,t.jsx)(n.code,{children:"EmbedWebView.kt"}),". ",(0,t.jsx)(n.code,{children:"BambuserCallOverlay"})," composes that ",(0,t.jsx)(n.code,{children:"EmbedWebView"})," and hands the ",(0,t.jsx)(n.code,{children:"WebView"})," reference to ",(0,t.jsx)(n.code,{children:"BambuserCallController.attach(webView)"})," so the controller can drive JS via ",(0,t.jsx)(n.code,{children:"evaluateJavascript"})," and receive JS-side events through the bridge."]}),"\n",(0,t.jsx)(n.h2,{id:"js-bridge",children:"JS bridge"}),"\n",(0,t.jsxs)(n.p,{children:["The bridge uses Android's native ",(0,t.jsx)(n.code,{children:"@JavascriptInterface"}),", exposed to JS as ",(0,t.jsx)(n.code,{children:"window.__bambuserAndroidBridge"}),". The document-start user script (installed via ",(0,t.jsx)(n.code,{children:"WebViewCompat.addDocumentStartJavaScript"}),") forwards two kinds of things into it:"]}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["Events from the embed's ",(0,t.jsx)(n.code,{children:".on(...)"})," subscriptions."]}),"\n",(0,t.jsxs)(n.li,{children:["Cross-frame ",(0,t.jsx)(n.code,{children:"postMessage"})," events from the widget's own iframe (viewport-mode changes for PiP, viddget:* events)."]}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["Native calls go the other direction via ",(0,t.jsx)(n.code,{children:"WebView.evaluateJavascript(...)"}),". ",(0,t.jsx)(n.code,{children:"BambuserCallController.notify(...)"})," returns callback replies, ",(0,t.jsx)(n.code,{children:".invoke(...)"})," calls methods on the ",(0,t.jsx)(n.code,{children:"oneToOneEmbed"})," instance, ",(0,t.jsx)(n.code,{children:".notifyProductNavigation(...)"})," echoes co-browse navigation, and so on."]}),"\n",(0,t.jsx)(n.h2,{id:"iframe-sizing-safeguard",children:"Iframe sizing safeguard"}),"\n",(0,t.jsxs)(n.p,{children:["The widget's own stylesheet historically clamps its inner iframe with ",(0,t.jsx)(n.code,{children:"max-height: 0"})," on some builds, which would leave the shopper looking at a white screen. The document-start shim installs a ",(0,t.jsx)(n.code,{children:"MutationObserver"})," that force-fills any ",(0,t.jsx)(n.code,{children:"<iframe>"})," under ",(0,t.jsx)(n.code,{children:"<body>"})," with inline ",(0,t.jsx)(n.code,{children:"!important"})," on ",(0,t.jsx)(n.code,{children:"max-height / max-width / min-height / height / width"})," using explicit pixels \u2014 this is defence in depth and keeps the widget rendered edge-to-edge regardless of future embed-side CSS changes."]}),"\n",(0,t.jsx)(n.h2,{id:"data-source-handlers",children:"Data-source handlers"}),"\n",(0,t.jsxs)(n.p,{children:["Anything the widget needs a value back from goes through ",(0,t.jsx)(n.code,{children:"BambuserCallHandlers"}),". Four closures, all ",(0,t.jsx)(n.code,{children:"suspend"}),":"]}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"provideProductData"})," \u2014 return a ",(0,t.jsx)(n.code,{children:"BambuserFactorySpec"})," for a specific product reference (SKU, URL, or barcode)."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"provideSearchData"})," \u2014 return a ",(0,t.jsx)(n.code,{children:"BambuserFactorySpec"})," with paginated search results."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"shouldAddToCart"})," \u2014 accept or reject an add-to-cart intent."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"shouldUpdateCart"})," \u2014 accept or reject a cart-quantity update."]}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["The SDK only installs a JS ",(0,t.jsx)(n.code,{children:".on(...)"})," subscription for handlers that are non-",(0,t.jsx)(n.code,{children:"null"})," \u2014 nothing is sent across the bridge that the host isn't ready to answer."]}
1),"\n",(0,t.jsx)(n.h2,{id:"fire-and-forget-events",children:"Fire-and-forget events"}),"\n",(0,t.jsxs)(n.p,{children:["The ",(0,t.jsx)(n.code,{children:"BambuserCallDelegate"})," interface receives a ",(0,t.jsx)(n.code,{children:"BambuserCallEvent"})," for every event the SDK does not need a reply to: ",(0,t.jsx)(n.code,{children:"Close"}),", ",(0,t.jsx)(n.code,{children:"CallStateChanged"}),", ",(0,t.jsx)(n.code,{children:"PresentationChanged"}),", ",(0,t.jsx)(n.code,{children:"Checkout"}),", ",(0,t.jsx)(n.code,{children:"ChatRequested"}),", ",(0,t.jsx)(n.code,{children:"NavigateTo"}),", ",(0,t.jsx)(n.code,{children:"QueueOpened"}),", ",(0,t.jsx)(n.code,{children:"QueueClosed"}),", ",(0,t.jsx)(n.code,{children:"AgentsOnlineChanged"}),", ",(0,t.jsx)(n.code,{children:"WaitingTimeChanged"}),", ",(0,t.jsx)(n.code,{children:"TrackingEvent"}),", and a catch-all ",(0,t.jsx)(n.code,{children:"Other"}),"."]}),"\n",(0,t.jsx)(n.h2,{id:"native-presentation",children:"Native presentation"}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"BambuserCallOverlay"})," (Compose) owns:"]}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"animateDpAsState"})," for the full-screen \u2194 PiP size transition."]}),"\n",(0,t.jsxs)(n.li,{children:["A drag ",(0,t.jsx)(n.code,{children:"pointerInput { detectDragGestures { } }"})," that is conditionally attached only in PiP so it doesn't compete with WebKit's own gesture recognizers in full-screen mode."]}),"\n",(0,t.jsxs)(n.li,{children:["A ",(0,t.jsx)(n.code,{children:"Modifier.clip(RoundedCornerShape)"})," + shadow applied only in PiP for the mini-player look."]}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["Because the WebView layout, JS bridge, JavaScript state, and Compose overlay all reference the same ",(0,t.jsx)(n.code,{children:"BambuserCallController"})," instance, updating a handler at runtime (before the next ",(0,t.jsx)(n.code,{children:"show()"}),") or reacting to a JS-emitted event from the host app requires no additional plumbing."]})]})}function h(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(c,{...e})}):c(e)}},28453(e,n,i){i.d(n,{R:()=>d,x:()=>o});var r=i(96540);const t={},s=r.createContext(t);function d(e){const n=r.useContext(s);return r.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function o(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:d(e.components),r.createElement(s.Provider,{value:n},e.children)}}}]);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.