Bodygram

Android WebView

Embed the Bodygram Scanner inside an Android WebView and capture scan events through a JavaScript bridge.

This guide covers loading the Bodygram Scanner URL inside an Android WebView, handling the camera permission the scanner needs, and capturing scan lifecycle events from JavaScript via a Kotlin JavascriptInterface.

This guide assumes you already know how to build a Bodygram Scanner URL. If not, start with Use API + Scanner UI.


Prerequisites

  • A working Bodygram Scanner URL (scan token + org ID)
  • An Android project with a WebView you control

Using an iframe inside your WebView?

If you render the scanner via an <iframe> in HTML loaded by your WebView, you also need to load the Bodygram Platform SDK and initialize it with shouldSetupIframe: true. See Embed in an <iframe> in the main scanner guide.


Manifest permissions

Add the following to AndroidManifest.xml:

<uses-permission android:name="android.permission.CAMERA" />
<uses-feature    android:name="android.hardware.camera" />
<uses-permission android:name="android.permission.INTERNET" />

WebView setup

The key requirements:

  • javaScriptEnabled = true — the scanner is a JavaScript app.
  • domStorageEnabled = true — required for the scanner to persist state.
  • mediaPlaybackRequiresUserGesture = falsecamera will not work without this.
  • A WebChromeClient that grants the camera permission through onPermissionRequest.
  • A JavascriptInterface wired up under the name BGScanflowJSWebviewInterface.
  • An onPageStarted hook that injects a small shim exposing the interface to JavaScript.

Here's a complete MainActivity that does all of the above:

package com.example.bsp_integration // ← your package name

import android.Manifest
import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.os.Bundle
import android.util.Log
import android.webkit.JavascriptInterface
import android.webkit.PermissionRequest
import android.webkit.WebChromeClient
import android.webkit.WebResourceRequest
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat

class MainActivity : ComponentActivity() {
    private lateinit var webView: WebView
    private lateinit var cameraPermissionRequest: PermissionRequest

    private val registerCameraPermission =
        registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
            if (granted) {
                cameraPermissionRequest.grant(cameraPermissionRequest.resources)
            }
        }

    fun onRequestPermission(request: PermissionRequest) {
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
            == PackageManager.PERMISSION_GRANTED
        ) {
            request.grant(request.resources)
        } else {
            cameraPermissionRequest = request
            registerCameraPermission.launch(Manifest.permission.CAMERA)
        }
    }

    @SuppressLint("MissingInflatedId")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        webView = findViewById(R.id.webview)
        webView.addJavascriptInterface(
            MyJavaScriptInterface(),
            "BGScanflowJSWebviewInterface"
        )
        webView.settings.javaScriptEnabled = true
        webView.settings.domStorageEnabled = true
        webView.settings.cacheMode = WebSettings.LOAD_NO_CACHE
        webView.settings.mediaPlaybackRequiresUserGesture = false

        webView.webChromeClient = object : WebChromeClient() {
            override fun onPermissionRequest(request: PermissionRequest) {
                onRequestPermission(request)
            }
        }

        webView.webViewClient = object : WebViewClient() {
            override fun shouldOverrideUrlLoading(
                view: WebView?,
                request: WebResourceRequest?
            ) = false

            override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
                super.onPageStarted(view, url, favicon)
                webView.evaluateJavascript(
                    """
                    window.BGScanflowJSWebviewInterface = function(fnName, args) {
                      BGScanflowJSWebviewInterface[fnName] && BGScanflowJSWebviewInterface[fnName](args);
                    }
                    """.trimIndent(),
                    null
                )
            }
        }

        webView.loadUrl("{BODYGRAM_SCANNER_URL}")
    }

    inner class MyJavaScriptInterface {
        @JavascriptInterface
        fun onRequestClose(message: String?) {
            Log.d("Scanner/close", message.orEmpty())
        }

        @JavascriptInterface
        fun onSuccess(message: String?) {
            Log.d("Scanner/success", message.orEmpty())
        }

        @JavascriptInterface
        fun onError(message: String?) {
            Log.d("Scanner/error", message.orEmpty())
        }

        @JavascriptInterface
        fun onStartProcessingScannedImages(message: String?) {
            Log.d("Scanner/processing", message.orEmpty())
        }

        @JavascriptInterface
        fun onModelLoadStatusChange(message: String?) {
            Log.d("Scanner/model", message.orEmpty())
        }
    }
}

Replace {BODYGRAM_SCANNER_URL} with your scanner URL, and update the package name at the top of the file to match your project. Without these the WebView will not load the scanner.


How the JavaScript bridge works

Native Android's addJavascriptInterface exposes the Kotlin class as BGScanflowJSWebviewInterface on the WebView's JavaScript context. But the scanner calls a function, not an object — so in onPageStarted we inject a tiny shim that turns the native object into a callable:

window.BGScanflowJSWebviewInterface = function (fnName, args) {
  if (
    typeof BGScanflowJSWebviewInterface === 'object' &&
    BGScanflowJSWebviewInterface[fnName]
  ) {
    BGScanflowJSWebviewInterface[fnName](args);
  }
};

The scanner then calls window.BGScanflowJSWebviewInterface('onSuccess', '{...}') whenever a scan event occurs, and your Kotlin @JavascriptInterface method receives it as a string.

You must define all five interface methods (onRequestClose, onSuccess, onError, onStartProcessingScannedImages, onModelLoadStatusChange). If a method is missing, the WebView will throw when the scanner tries to call it.


Interface methods

All args values arrive as a string — typically a JSON-encoded object. Parse them with kotlinx.serialization or org.json as needed.

onRequestClose(args)

Fires when the user taps the close (×) button.

args = "{}"

onSuccess(args)

Fires when a scan completes successfully.

{
  "id": "success_scan_id"
}

onError(args)

Fires when a scan fails. args is either a JSON string describing the failed scan or undefined for unexpected errors (network failures, for example):

{
  "entry": {
    "createdAt": 1711714596,
    "customScanId": "customerID123456",
    "error": { "code": "personNotDetected" },
    "id": "some_error_scan_id",
    "input": {
      "photoScan": { /* user input */ }
    },
    "status": "failure"
  }
}

onStartProcessingScannedImages(args)

Fires once the scanner has captured both photos and starts processing them.

{
  "status": "photo-scan-processing",
  "base64_jpeg_front": "<base64 JPEG>",
  "base64_jpeg_side":  "<base64 JPEG>"
}

onModelLoadStatusChange(args)

Fires as the ML models used by the scanner load.

{
  "status": "pending",
  "model": null
}
FieldValues
statuspending | loaded | failed
modelnull | mediapipe | tensorflow | legacy

Further reading

On this page