This page discusses the various methods and best practices for establishing a
native bridge, also known as JavaScript bridge, to facilitate communication
between web content in a [`WebView`](https://developer.android.com/reference/android/webkit/WebView.md.txt) and a host Android application.

This enables web developers to use JavaScript to access native platform
features---such as the camera, file system, or advanced hardware sensors---that
standard web APIs don't normally provide.

## Use cases

A JavaScript bridge implementation enables various integration scenarios where
web content requires deeper access to the Android operating system. The
following are some examples:

- **Platform integration** : Triggering native Android UI components (for example, Biometric prompts, [`BottomSheetDialog`](https://developer.android.com/reference/com/google/android/material/bottomsheet/BottomSheetDialog.md.txt)) from a web page.
- **Performance**: Offloading heavy computational tasks to native Java or Kotlin code.
- **Data persistence**: Accessing local encrypted databases or shared preferences.
- **Large data transfers**: Passing media files or complex data structures between the app and the web renderer.

## Communication mechanisms

Android offers three primary generations of APIs to establish a native bridge.
While they are all still available, they differ significantly in security,
usability, and performance.

### Use `addWebMessageListener` (Recommended)

[`addWebMessageListener`](https://developer.android.com/reference/androidx/webkit/WebViewCompat.md.txt#addWebMessageListener(android.webkit.WebView,java.lang.String,java.util.Set%3Cjava.lang.String%3E,androidx.webkit.WebViewCompat.WebMessageListener)) is the most modern and recommended approach for
communication between the web content and native app code. It combines the ease
of use of the JavaScript interface with the security of the messaging system.

**How it works** : The app adds a listener with a specific name and a set of
allowed origin rules. The WebView then ensures the JavaScript object is
present in the global scope (`window.objectName`) from the moment the page
begins to load.

**Initialization** : To ensure the WebView injects the JavaScript object before
any script runs, you must call `addWebMessageListener` before navigating to the
page (such as calling [`WebViewCompat.navigate`](https://developer.android.com/develop/ui/views/layout/webapps/navigate.md.txt) or [`loadUrl`](https://developer.android.com/reference/android/webkit/WebView.md.txt#loadUrl(java.lang.String))).

**Key features**:

- **Security and trust** : Unlike legacy APIs, this method requires a
  `Set<String>` of `allowedOriginRules` during initialization. This is the
  primary mechanism for establishing trust.

  When you specify a trusted origin, such as `https://example.com`, the
  WebView guarantees that it only exposes the injected JavaScript objects to
  web pages loaded from that exact origin.

  The native listener callback receives a `sourceOrigin` parameter with every
  message. You can use this to verify the exact origin of the sender if your
  bridge supports multiple allowed origins.

  Because the WebView strictly enforces these origin checks at the platform
  level, your app can generally rely upon messages received from a trusted
  `sourceOrigin` as truthful, eliminating the need for rigorous payload
  validation in most standard implementations.
  - WebView matches rules against the scheme (HTTP/HTTPS), host, and port.
  - WebView ignores paths. For example, `https://example.com` allows `https://example.com/login` and `https://example.com/home`.
  - WebView strictly limits wildcards to the start of the host for subdomains. For example, `https://*.example.com` matches `https://foo.example.com` but not `https://example.com`. If you need to match both `https://example.com` and its subdomains, you must add each origin rule separately to the allowlist (for example, `"https://example.com", "https://*.example.com"`). You can't use wildcards for the scheme or in the middle of a domain.

  This restricts the bridge to verified domains, preventing unauthorized
  third-party content or injected iframes from executing native code.
- **Multi-frame support**: Works across all frames that match the origin
  rules.

- **Threading**: The listener callback runs on the application's main (UI)
  thread. If your bridge needs to handle complex data processing, JSON
  parsing, or database lookups, you must offload that work to a background
  thread to prevent freezing the application UI with an "app not responding"
  (ANR) error.

- **Bidirectional** : When the web page sends a message, the app receives a
  [`JavaScriptReplyProxy`](https://developer.android.com/reference/androidx/webkit/JavaScriptReplyProxy.md.txt) that it can use to send messages back to that
  specific frame. You can retain this `replyProxy` object and use it at any
  time to send any number of messages to the page, not just to reply to each
  individual message the page sends. If the originating frame navigates away
  or is destroyed, messages sent using `postMessage()` on the proxy are
  silently ignored.

- **App-side initiation** : Although the web page must always initiate the
  communication channel with the app, the native app can unilaterally prompt
  the web page to begin this process. The native app can communicate to the
  web page with [`addDocumentStartJavaScript()`](https://developer.android.com/reference/androidx/webkit/WebViewCompat.md.txt#addDocumentStartJavaScript(android.webkit.WebView,java.lang.String,java.util.Set%3Cjava.lang.String%3E)) (to evaluate JavaScript
  before the page loads) or [`evaluateJavaScript()`](https://developer.android.com/reference/android/webkit/WebView.md.txt#evaluateJavascript(java.lang.String,android.webkit.ValueCallback%3Cjava.lang.String%3E)) (to evaluate
  JavaScript after the page has loaded).

**Limitation** : This API sends data as either strings or `byte[]` arrays. For
more complicated data structures, such as, JSON objects, you must serialize this
to one of those formats and then deserialize on the other side to reconstruct
the data structure.

**Usage example**:

To understand the full sequence of a bidirectional message exchange, the events
proceed in this order:

1. **Initiation (app)** : The native app registers the listener with `addWebMessageListener` and initiates page navigation (such as with [`WebViewCompat.navigate`](https://developer.android.com/develop/ui/views/layout/webapps/navigate.md.txt) or `loadUrl`).
2. **Message send (web)** : The web page's JavaScript calls `myObject.postMessage(message)` to initiate the communication.
3. **Message receive and reply (app)** : The app receives the message in the listener callback and replies using the provided `replyProxy.postMessage()`.
4. **Reply receive (web)** : The web page receives the asynchronous reply in the `myObject.onmessage()` callback function.

### Kotlin


```kotlin
val myListener = WebViewCompat.WebMessageListener { _, _, _, _, replyProxy ->
    // Handle the message from JS
    replyProxy.postMessage("Acknowledged!")
}

// Check whether the WebView version supports the feature.
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
    val allowedOrigins = setOf("https://www.example.com")
    WebViewCompat.addWebMessageListener(webView, "myObject", allowedOrigins, myListener)
}
```

<br />

### Java

```java
WebMessageListener myListener = (view, message, sourceOrigin, isMainFrame, replyProxy) -> {
    // Handle the message from JS
    replyProxy.postMessage("Acknowledged!");
};

// Check whether the WebView version supports the feature.
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
    Set<String> allowedOrigins = Set.of("https://www.example.com");
    WebViewCompat.addWebMessageListener(webView, "myObject", allowedOrigins, myListener);
}
```

The following JavaScript demonstrates the client-side implementation of
`addWebMessageListener`, allowing the web content to receive messages from the
native app and send its own messages through the `myObject` proxy.

```javascript
myObject.onmessage = function(event) {
    console.log("App says: " + event.data);
};
myObject.postMessage("Hello world!");
```

### Use `postWebMessage` (Alternative)

Android introduced this to provide an asynchronous, messaging-based alternative
similar to the web's `window.postMessage`.

**How it works** : The app uses [`WebViewCompat.postWebMessage`](https://developer.android.com/reference/androidx/webkit/WebViewCompat.md.txt#postWebMessage(android.webkit.WebView,androidx.webkit.WebMessageCompat,android.net.Uri)) to send a payload
to the web page's main frame. To establish a bidirectional communication
channel, you can create a [`WebMessageChannel`](https://developer.android.com/reference/androidx/webkit/WebViewCompat.md.txt#createWebMessageChannel(android.webkit.WebView)) and pass one of its
ports with the message to the web content.

**Characteristics**:

- **Asynchronous** : Like `addWebMessageListener`, this method uses asynchronous messaging, which ensures the web page remains responsive to user interactions while the app processes data in the background.
- **Origin aware** : You can specify a `targetOrigin` to ensure the WebView delivers data only to a trusted website.

**Limitations**:

- **Scope**: This API limits communication to the main frame. It doesn't support directly addressing or sending messages to iframes.
- **URI restrictions** : You cannot use this method for content loaded using `data:` URIs, `file:` URIs, or `loadData()`, unless you specify "\*" as the target origin. Doing this lets any page receive the message.
- **Identity risk**: There is no clear way for the web content to verify the sender's identity. A message that the web page receives could have originated from your native app or another iframe.

Use this method when you need a simple, async channel for string-based data in
earlier Android versions that don't support `addWebMessageListener`.

### Use `addJavascriptInterface` (Legacy)

The oldest method involves injecting a native object instance directly into the
WebView.

**How it works** : You define a Kotlin or Java class, annotate the allowed
methods with [`@JavascriptInterface`](https://developer.android.com/reference/android/webkit/JavascriptInterface.md.txt), and add an instance of the class to the
WebView using [`addJavascriptInterface(Object, String)`](https://developer.android.com/reference/android/webkit/WebView.md.txt#addJavascriptInterface(java.lang.Object,java.lang.String)).

**Characteristics**:

- **Synchronous**: The JavaScript execution environment blocks until the method in your Android code returns.
- **Thread safety**: The system calls methods on a background thread, requiring careful synchronization on the Kotlin or Java side.
- **Security risk** : By default, `addJavascriptInterface` is available to every frame within the WebView, including iframes. It lacks origin-based access control. Due to the asynchronous behavior of WebView, it isn't possible to safely determine the URL of the frame that is calling your interface. You must not rely on methods like [`WebView.getUrl()`](https://developer.android.com/reference/android/webkit/WebView.md.txt#getUrl()) for security verification, as they aren't guaranteed to be accurate and don't indicate which specific frame made the request.

> [!CAUTION]
> **Caution:** We don't recommend using this method for modern applications unless you are targeting very old Android versions.

#### Data type conversions and coercion

When using `addJavascriptInterface`, the Chromium-based Java Bridge converts
data types between the JavaScript runtime and your Android app code.

The following coercion rules apply to method parameters and return values.

##### Parameter type mapping (JavaScript to Java)

When JavaScript passes arguments to an annotated Java or Kotlin method, the
bridge coerces JavaScript values into the corresponding Java parameter types:

| Java parameter type | JavaScript argument value | Coercion behavior |
|---|---|---|
| `byte`, `short`, `int`, `long` | Number (integer) | Values are cast to the target integer type. Out-of-bounds values wrap around according to standard numeric casting rules. |
| `byte`, `short`, `int`, `long` | `NaN` | Coerces to `0`. |
| `byte`, `short`, `int`, `long` | `Infinity` | Coerces to `-1` for `byte` and `short`, or `Integer.MAX_VALUE` and `Long.MAX_VALUE` for `int` and `long`. |
| `float`, `double` | Number | Coerces to the corresponding Java floating-point value. |
| `float`, `double` | `NaN` / `Infinity` | Coerces to `Float.NaN`, `Double.NaN`, `Float.POSITIVE_INFINITY`, or `Double.POSITIVE_INFINITY`. |
| `char` | Number (integer) | Converted to the corresponding Unicode code point. |
| `char` | Non-integer, `NaN`, `Infinity` | Coerces to `\u0000`. |
| `boolean` | `true` / `false` | Coerces to Java `true` or `false`. |
| `boolean` | Number, String, Object | Coerces to `false` (including non-empty strings and non-zero numbers). |
| `String` | String | String value is preserved. |
| `String` | Number, Boolean | Formatted as a string representation (for example, `"42"`, `"true"`, `"false"`). |
| `String` | `null` / `undefined` | `null` coerces to Java `null`; `undefined` coerces to the literal string `"undefined"`. |
| `String` | Object, ArrayBuffer, TypedArray | Coerces to the literal string `"undefined"`. |
| Primitive array (such as `int[]`, `byte[]`, `boolean[]`) or `String[]` | Array (`[...]`) | Converts to a 1D Java array of the target element type. Sparse arrays fill unassigned indexes with default values (`0`, `false`, `null`). |
| Primitive array (such as `int[]`, `byte[]`) | TypedArray (`Int8Array`, `Uint8Array`, `Int32Array`, `Float64Array`) | Elements are coerced into the corresponding Java primitive array. |
| Multi-dimensional array (such as `int[][]`) | Nested array (`[[...]]`) | **Not supported.** Multi-dimensional array parameters evaluate to `null`. |
| `ArrayBuffer`, `DataView` | `ArrayBuffer`, `DataView` | **Not supported as arrays.** ArrayBuffer and DataView instances evaluate to `null`. |
| `Object` or custom class | JavaScript object (`{...}`) | **Not supported.** Arbitrary JavaScript object literals evaluate to `null` in Java. |
| `Object` or custom class | Injected Java object wrapper | **Supported (Round-tripping).** Passes the underlying Java instance to the Java method. Throws a JavaScript exception if the Java type does not match the parameter signature. |
| Boxed types (such as `Integer`, `Double`, `Boolean`) | Number, Boolean | **Not supported.** Boxed primitive types are treated as opaque objects and evaluate to `null`. |
| Any primitive type | `null` / `undefined` | Coerces to default values (`0`, `0.0`, `\u0000`, `false`). |
| `Object`, `String`, array | `null` | Coerces to Java `null`. |

##### Return type mapping (Java to JavaScript)

When an annotated Java or Kotlin method returns a value, the bridge converts it
to a JavaScript type:

| Java return type | JavaScript value | JavaScript `typeof` |
|---|---|---|
| `boolean` | `true` / `false` | `"boolean"` |
| `byte`, `short`, `int`, `long`, `float`, `double` | Number | `"number"` |
| `char` | Number (Unicode code point) | `"number"` |
| `String` (non-null) | String value | `"string"` |
| `String` (`null`) | `undefined` | `"undefined"` |
| `void` | `undefined` | `"undefined"` |
| Java array (such as `int[]`, `String[]`) | `undefined` | `"undefined"`. **Array return values are not supported.** The Java method is not executed, and `undefined` is returned without raising an exception. |
| Java Object / custom type (non-null) | Object wrapper | `"object"`. Creates a JavaScript wrapper around the Java instance. JavaScript code can call any public method on this object that is annotated with `@JavascriptInterface`. |
| Java Object / custom type (`null`) | `null` | `"object"` |
| Boxed primitive (such as `Integer`, `Double`) | Object wrapper | `"object"`. Returned as an opaque Java object wrapper with no accessible `@JavascriptInterface` methods, making the value unusable in JavaScript. |

#### Method and member accessibility

The JavaScript bridge enforces strict member access and visibility rules to
protect against unintended code execution:

- **Fields are not exposed** : Java fields (including `public` and `public
  final` fields) are not accessible from JavaScript and evaluate to `undefined`.
- **Annotation requirement** : Only methods explicitly annotated with `@JavascriptInterface` are exposed to JavaScript.
- **Visibility restrictions** : Methods must be `public`. `private` and `protected` methods are never exposed to JavaScript, even if they carry the `@JavascriptInterface` annotation.
- **Static methods** : Static methods annotated with `@JavascriptInterface` are callable from JavaScript.
- **Inheritance and overriding** : `@JavascriptInterface` annotations are not inherited when a subclass overrides a method. If a subclass overrides an annotated method from a superclass, the subclass must explicitly include the `@JavascriptInterface` annotation on the overridden method to expose it to JavaScript. Non-overridden public methods inherited from a superclass remain accessible if annotated in the superclass.
- **Reflection protection** : Standard Java reflection methods (such as `getClass()`) are blocked and throw a JavaScript exception to prevent remote code execution vulnerabilities.
- **Method overloading**: Overloaded Java methods are supported. The bridge resolves method calls based on the number of passed arguments only, and does not take argument types into account. Calling an overloaded method with an invalid argument count raises a JavaScript exception. If two overloads have the same argument count, one will be chosen arbitrarily.

## Summary of mechanisms

The following table provides a quick comparison of the three primary native
bridge implementation mechanisms:

| Method | `addWebMessageListener` | `postWebMessage` | `addJavascriptInterface` |
|---|---|---|---|
| Implementation | Asynchronous (Listener on main thread) | Asynchronous | Synchronous |
| Security | Highest (Allowlist-based) | High (Origin aware) | Low (No origin checks) |
| Complexity | Moderate | Moderate | Simple |
| Direction | Bidirectional | Bidirectional | Web to app |
| Minimum WebView version | Version 82 (and Jetpack Webkit 1.3.0) | Version 45 (and Jetpack Webkit 1.1.0) | All versions |
| Recommended | Yes | No | No |

## Handle large data transfers

You must manage memory carefully when transferring large payloads, such as
multi-megabyte strings or binary files, to avoid Application Not Responding
(ANR) errors or crashes on 32-bit devices. This section discusses the various
techniques and limitations associated with transferring significant amounts of
data between the host application and web content.

### Transfer binary data with byte arrays

With the [`WebMessageCompat`](https://developer.android.com/reference/androidx/webkit/WebMessageCompat.md.txt) class, you can send `byte[]` arrays directly
instead of serializing binary data into Base64 strings. Since Base64 adds
roughly 33% overhead to the data size, this is significantly more
memory-efficient and faster.

- **Binary advantage**: Transfer binary data like image files or audio between your native app and web content.
- **Limitation**: Even with byte arrays, the system copies data across the inter-process communication (IPC) boundary between the app and the isolated process that WebView uses to render the web content. This still consumes significant memory for very large files.

The following code examples demonstrate how to set up `addWebMessageListener` on
the native app side to receive messages marked with
`WebMessageCompat.TYPE_ARRAY_BUFFER` and optionally reply with binary data by
checking for `WebViewFeature.MESSAGE_ARRAY_BUFFER`.

### Kotlin


```kotlin
fun setupWebView(webView: WebView) {
    if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
        val listener = WebViewCompat.WebMessageListener { view, message, sourceOrigin, isMainFrame, replyProxy ->

            // Check if the received message is an ArrayBuffer
            if (message.type == WebMessageCompat.TYPE_ARRAY_BUFFER) {
                val binaryData: ByteArray = message.arrayBuffer
                // Process your binary data (image, audio, etc.)
                println("Received bytes: ${binaryData.size}")

                // Optional: Send a binary reply back to JavaScript.
                // This example sends a 3-byte array for simplicity.
                if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER)) {
                    val replyBytes = byteArrayOf(0x01, 0x02, 0x03)
                    replyProxy.postMessage(replyBytes)
                }
            }
        }

        // "myBridge" matches the window.myBridge in JavaScript
        WebViewCompat.addWebMessageListener(
            webView,
            "myBridge",
            setOf("https://example.com"), // Security: restrict origins
            listener
        )
    }
}
```

<br />

### Java

```java
public void setupWebView(WebView webView) {
  if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
      WebViewCompat.WebMessageListener listener = (view, message, sourceOrigin, isMainFrame, replyProxy) -> {

          // Check if the received message is an ArrayBuffer
          if (message.getType() == WebMessageCompat.TYPE_ARRAY_BUFFER) {
              byte[] binaryData = message.getArrayBuffer();
              // Process your binary data (image, audio, etc.)
              System.out.println("Received bytes: " + binaryData.length);

              // Optional: Send a binary reply back to JavaScript.
              // This example sends a 3-byte array for simplicity.
              if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER)) {
                  byte[] replyBytes = new byte[]{0x01, 0x02, 0x03};
                  replyProxy.postMessage(replyBytes);
              }
          }
      };

      // "myBridge" matches the window.myBridge in JavaScript
      WebViewCompat.addWebMessageListener(
          webView,
          "myBridge",
          Set.of("https://example.com"), // Security: restrict origins
          listener
      );
  }
}
```

The following JavaScript code demonstrates the client-side implementation of
`addWebMessageListener`, enabling the web content to send and receive binary
data (`ArrayBuffer`) to and from the native app using the `window.myBridge`
proxy injected in the previous example.

```javascript
// Function to send an image or binary buffer to the app
async function sendBinaryToApp() {
    const response = await fetch('image.jpg');
    const buffer = await response.arrayBuffer();

    // Check if the injected bridge object exists
    if (window.myBridge) {
        // You can send the ArrayBuffer directly
        window.myBridge.postMessage(buffer);
    }
}

// Receiving binary data from the app
if (window.myBridge) {
    window.myBridge.onmessage = function(event) {
        if (event.data instanceof ArrayBuffer) {
            console.log('Received binary data from App, length:', event.data.byteLength);
            // Process the binary data (for example, as a Uint8Array)
            const bytes = new Uint8Array(event.data);
            console.log('First byte:', bytes[0]);
        }
    };
}
```

### Efficient large-scale data loading

For very large files (\>10 MB), use the [`shouldInterceptRequest`](https://developer.android.com/reference/android/webkit/WebViewClient.md.txt#shouldInterceptRequest(android.webkit.WebView,android.webkit.WebResourceRequest)) method to
stream data:

1. The web page initiates a `fetch()` call to a custom, placeholder URL. For example, `https://app.local/large-file`.
2. The Android app intercepts this request in [`WebViewClient`](https://developer.android.com/reference/android/webkit/WebViewClient.md.txt).shouldInterceptRequest.
3. The app returns the data as an `InputStream`.

This enables streaming data in chunks rather than loading the entire payload
into memory at once.

The following JavaScript function demonstrates the client-side code for
efficiently loading a large binary file from the native application using a
standard `fetch()` call to a custom, placeholder URL.

```javascript
async function fetchBinaryFromApp() {
    try {
        // This URL doesn't need to exist on the internet
        const response = await fetch('https://app.local/data/large-file.bin');

        if (!response.ok) throw new Error('Network response was not okay');

        // For raw binary data:
        const arrayBuffer = await response.arrayBuffer();
        console.log('Received binary data, size:', arrayBuffer.byteLength);
        // Process buffer (for example, new Uint8Array(arrayBuffer))

        /*
        // OR for an image:
        const blob = await response.blob();
        const imageUrl = URL.createObjectURL(blob);
        document.getElementById('myImage').src = imageUrl;
        */

    } catch (error) {
        console.error('Fetch error:', error);
    }
}
```

The following code examples demonstrate the native app side, using the
`WebViewClient.shouldInterceptRequest` method in both Kotlin and Java, to stream
a large binary file by intercepting a custom placeholder URL requested by the
web content.

### Kotlin


```kotlin
webView.webViewClient = object : WebViewClient() {
    override fun shouldInterceptRequest(
        view: WebView?,
        request: WebResourceRequest?
    ): WebResourceResponse? {
        val url = request?.url ?: return null

        // Check if this is our custom placeholder URL
        if (url.host == "app.local" && url.path == "/data/large-file.bin") {
            try {
                // 1. Get your data as an InputStream
                // (from Assets, Files, or a generated byte stream)
                val inputStream: InputStream = context.assets.open("my_data.pb")

                // 2. Define Response Headers (Crucial for CORS/Fetch)
                val headers = mutableMapOf<String, String>()
                headers["Access-Control-Allow-Origin"] = "*" // Allow fetch from any origin

                // 3. Return the response
                return WebResourceResponse(
                    "application/octet-stream", // MIME type (for example, image/jpeg)
                    "UTF-8", // Encoding
                    200, // Status Code
                    "OK", // Reason Phrase
                    headers, // Custom Headers
                    inputStream // The actual data stream
                )
            } catch (e: Exception) {
                // Handle exception
            }
        }
        return super.shouldInterceptRequest(view, request)
    }
}
```

<br />

### Java

```java
webView.setWebViewClient(new WebViewClient() {
  @Override
  public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
      String urlPath = request.getUrl().getPath();
      String host = request.getUrl().getHost();

      // Check if this is our custom placeholder URL
      if ("app.local".equals(host) && "/data/large-file.bin".equals(urlPath)) {
          try {
              // 1. Get your data as an InputStream
              // (from Assets, Files, or a generated byte stream)
              InputStream inputStream = getContext().getAssets().open("my_data.pb");

              // 2. Define Response Headers (Crucial for CORS/Fetch)
              Map<String, String> headers = new HashMap<>();
              headers.put("Access-Control-Allow-Origin", "*"); // Allow fetch from any origin

              // 3. Return the response
              return new WebResourceResponse(
                  "application/octet-stream", // MIME type (for example, image/jpeg)
                  "UTF-8",                   // Encoding
                  200,                       // Status Code
                  "OK",                      // Reason Phrase
                  headers,                   // Custom Headers
                  inputStream                // The actual data stream
              );
          } catch (Exception e) {
              // Handle exception
          }
      }
      return super.shouldInterceptRequest(view, request);
  }
});
```

## Follow security recommendations

To protect your application and user data, follow these guidelines when
implementing a bridge:

- **Enforce HTTPS**: To ensure that malicious third-party content can't
  invoke your application's native logic, only allow communication with secure
  origins.

- **Rely on origin rules** : The best way to deal with trust is to strictly
  define your `allowedOriginRules` and check the `sourceOrigin` provided in
  the message callback. Avoid using the full wildcard (`*`), which matches all
  origins, as your only origin rule unless absolutely necessary. Using
  wildcards for subdomains (for example, `*.example.com`) remains valid and
  secure for matching multiple subdomains (for example, `foo.example.com`,
  `bar.example.com`).

  **Note**: While origin rules protect against malicious third-party websites
  and hidden iframes, they can't protect against cross-site scripting (XSS)
  vulnerabilities within your own trusted domain. For example, if your web
  page displays user-generated content and is vulnerable to stored XSS, an
  attacker could execute a script acting as your trusted origin. Consider
  applying validation to the message payloads before executing sensitive
  native platform operations.
- **Minimize surface area**: Only expose the specific methods or data that the
  web page requires.

- **Check features at runtime** : Recent bridge APIs, including
  `addWebMessageListener`, are part of the Jetpack Webkit library. So, always
  check for support using [`WebViewFeature.isFeatureSupported()`](https://developer.android.com/reference/androidx/webkit/WebViewFeature.md.txt#isFeatureSupported(java.lang.String)) before
  calling them.