Accede a las APIs nativas con el puente de JavaScript

En esta página, se analizan los diversos métodos y prácticas recomendadas para establecer un puente nativo, también conocido como puente de JavaScript, que facilite la comunicación entre el contenido web en un WebView y una aplicación para Android host.

Esto permite que los desarrolladores web usen JavaScript para acceder a funciones nativas de la plataforma, como la cámara, el sistema de archivos o los sensores de hardware avanzados, que las APIs web estándar no suelen proporcionar.

Casos de uso

Una implementación de puente de JavaScript permite varias situaciones de integración en las que el contenido web requiere un acceso más profundo al sistema operativo Android. Estos son algunos ejemplos:

  • Integración en la plataforma: Activación de componentes de IU nativos de Android (por ejemplo, mensajes biométricos, BottomSheetDialog) desde una página web
  • Rendimiento: Descarga tareas de procesamiento pesadas en código nativo de Java o Kotlin.
  • Persistencia de datos: Acceder a bases de datos locales encriptadas o preferencias compartidas
  • Transferencias de datos grandes: Pasar archivos multimedia o estructuras de datos complejas entre la app y el renderizador web

Mecanismos de comunicación

Android ofrece tres generaciones principales de APIs para establecer un puente nativo. Si bien todos siguen disponibles, difieren significativamente en seguridad, usabilidad y rendimiento.

Usar addWebMessageListener (recomendado)

addWebMessageListener es el enfoque más moderno y recomendado para la comunicación entre el contenido web y el código de la aplicación nativa. Combina la facilidad de uso de la interfaz de JavaScript con la seguridad del sistema de mensajería.

Cómo funciona: La app agrega un objeto de escucha con un nombre específico y un conjunto de reglas de origen permitidas. Luego, WebView se asegura de que el objeto JavaScript esté presente en el alcance global (window.objectName) desde el momento en que comienza a cargarse la página.

Inicialización: Para asegurarte de que WebView inserte el objeto JavaScript antes de que se ejecute cualquier secuencia de comandos, debes llamar a addWebMessageListener antes de navegar a la página (por ejemplo, llamar a WebViewCompat.navigate o loadUrl).

Funciones principales:

  • Seguridad y confianza: A diferencia de las APIs heredadas, este método requiere un Set<String> de allowedOriginRules durante la inicialización. Este es el mecanismo principal para establecer la confianza.

    Cuando especificas un origen de confianza, como https://example.com, WebView garantiza que solo expone los objetos de JavaScript insertados a las páginas web cargadas desde ese origen exacto.

    La devolución de llamada del objeto de escucha nativo recibe un parámetro sourceOrigin con cada mensaje. Puedes usarlo para verificar el origen exacto del remitente si tu puente admite varios orígenes permitidos.

    Dado que WebView aplica estrictamente estas verificaciones de origen a nivel de la plataforma, tu app puede confiar en general en los mensajes recibidos de un sourceOrigin de confianza como veraces, lo que elimina la necesidad de una validación rigurosa de la carga útil en la mayoría de las implementaciones estándar.

    • WebView compara las reglas con el esquema (HTTP/HTTPS), el host y el puerto.
    • WebView ignora las rutas de acceso. Por ejemplo, https://example.com permite https://example.com/login y https://example.com/home.
    • WebView limita estrictamente los comodines al inicio del host para los subdominios. Por ejemplo, https://*.example.com coincide con https://foo.example.com, pero no con https://example.com. Si necesitas hacer coincidir tanto https://example.com como sus subdominios, debes agregar cada regla de origen por separado a la lista de entidades permitidas (por ejemplo, "https://example.com", "https://*.example.com"). No puedes usar comodines para el esquema ni en el medio de un dominio.

    Esto restringe el puente a dominios verificados, lo que evita que se ejecute código nativo desde contenido de terceros no autorizado o iframes insertados.

  • Compatibilidad con varios marcos: Funciona en todos los marcos que coinciden con las reglas de origen.

  • Subprocesos: La devolución de llamada del objeto de escucha se ejecuta en el subproceso principal (de IU) de la aplicación. Si tu puente necesita controlar el procesamiento de datos complejos, el análisis de JSON o las búsquedas en bases de datos, debes descargar ese trabajo en un subproceso en segundo plano para evitar que la IU de la aplicación se bloquee con un error de "app no responde" (ANR).

  • Bidireccional: Cuando la página web envía un mensaje, la app recibe un objeto JavaScriptReplyProxy que puede usar para enviar mensajes a ese marco específico. Puedes conservar este objeto replyProxy y usarlo en cualquier momento para enviar cualquier cantidad de mensajes a la página, no solo para responder a cada mensaje individual que envíe la página. Si el frame de origen se desplaza o se destruye, los mensajes enviados con postMessage() en el proxy se ignoran de forma silenciosa.

  • Inicio del lado de la app: Si bien la página web siempre debe iniciar el canal de comunicación con la app, la aplicación nativa puede solicitar unilateralmente a la página web que comience este proceso. La aplicación nativa puede comunicarse con la página web con addDocumentStartJavaScript() (para evaluar JavaScript antes de la carga de la página) o evaluateJavaScript() (para evaluar JavaScript después de que se haya cargado la página).

Limitación: Esta API envía datos como cadenas o arreglos de byte[]. Para estructuras de datos más complejas, como objetos JSON, debes serializarlo en uno de esos formatos y, luego, deserializarlo en el otro extremo para reconstruir la estructura de datos.

Ejemplo de uso:

Para comprender la secuencia completa de un intercambio de mensajes bidireccional, los eventos se suceden en este orden:

  1. Inicio (aplicación): La aplicación nativa registra el objeto de escucha con addWebMessageListener y, luego, inicia la navegación de páginas (por ejemplo, con WebViewCompat.navigate o loadUrl).
  2. Envío de mensajes (Web): El JavaScript de la página web llama a myObject.postMessage(message) para iniciar la comunicación.
  3. Recepción y respuesta de mensajes (app): La app recibe el mensaje en la devolución de llamada del objeto de escucha y responde con el objeto replyProxy.postMessage() proporcionado.
  4. Recepción de respuesta (web): La página web recibe la respuesta asíncrona en la función de devolución de llamada myObject.onmessage().

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)
}

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);
}

El siguiente código JavaScript muestra la implementación del lado del cliente de addWebMessageListener, lo que permite que el contenido web reciba mensajes de la aplicación nativa y envíe sus propios mensajes a través del proxy myObject.

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

Usa postWebMessage (alternativa)

Android introdujo esto para proporcionar una alternativa asíncrona basada en mensajes similar a window.postMessage de la Web.

Cómo funciona: La app usa WebViewCompat.postWebMessage para enviar una carga útil al marco principal de la página web. Para establecer un canal de comunicación bidireccional, puedes crear un WebMessageChannel y pasar uno de sus puertos con el mensaje al contenido web.

Características:

  • Asíncrono: Al igual que addWebMessageListener, este método usa mensajería asíncrona, lo que garantiza que la página web siga respondiendo a las interacciones del usuario mientras la app procesa datos en segundo plano.
  • Conocimiento del origen: Puedes especificar un targetOrigin para asegurarte de que WebView entregue datos solo a un sitio web de confianza.

Limitaciones:

  • Alcance: Esta API limita la comunicación al marco principal. No admite el envío de mensajes ni la dirección directa a los elementos iframe.
  • Restricciones de URI: No puedes usar este método para el contenido cargado con URIs data:, URIs file: o loadData(), a menos que especifiques "*" como el origen de destino. Si lo haces, cualquier página podrá recibir el mensaje.
  • Riesgo de identidad: No hay una forma clara de que el contenido web verifique la identidad del remitente. Un mensaje que recibe la página web podría haberse originado en tu aplicación nativa o en otro iframe.

Usa este método cuando necesites un canal asíncrono simple para datos basados en cadenas en versiones anteriores de Android que no admiten addWebMessageListener.

Usa addJavascriptInterface (heredado)

El método más antiguo consiste en insertar una instancia de objeto nativo directamente en WebView.

Cómo funciona: Defines una clase de Kotlin o Java, anotas los métodos permitidos con @JavascriptInterface y agregas una instancia de la clase al WebView con addJavascriptInterface(Object, String).

Características:

  • Síncrono: El entorno de ejecución de JavaScript se bloquea hasta que el método en tu código de Android devuelve un valor.
  • Seguridad de subprocesos: El sistema llama a métodos en un subproceso en segundo plano, lo que requiere una sincronización cuidadosa en el lado de Kotlin o Java.
  • Riesgo de seguridad: De forma predeterminada, addJavascriptInterface está disponible para cada frame dentro de WebView, incluidos los iframes. No tiene control de acceso basado en el origen. Debido al comportamiento asíncrono de WebView, no es posible determinar de forma segura la URL del marco que llama a tu interfaz. No debes depender de métodos como WebView.getUrl() para la verificación de seguridad, ya que no se garantiza que sean precisos y no indican qué fotograma específico realizó la solicitud.

Conversiones y coerciones de tipos de datos

Cuando se usa addJavascriptInterface, el puente de Java basado en Chromium convierte los tipos de datos entre el tiempo de ejecución de JavaScript y el código de tu app para Android.

Las siguientes reglas de coerción se aplican a los parámetros y los valores de devolución de los métodos.

Asignación de tipos de parámetros (de JavaScript a Java)

Cuando JavaScript pasa argumentos a un método anotado de Java o Kotlin, el puente convierte los valores de JavaScript en los tipos de parámetros de Java correspondientes:

Tipo de parámetro de Java Valor del argumento de JavaScript Comportamiento de coerción
byte, short, int, long Número (entero) Los valores se convierten al tipo de número entero objetivo. Los valores fuera de los límites se ajustan según las reglas estándar de conversión numérica.
byte, short, int, long NaN Se fuerza a 0.
byte, short, int, long Infinity Se convierte en -1 para byte y short, o bien en Integer.MAX_VALUE y Long.MAX_VALUE para int y long.
float, double Número Se fuerza a la conversión al valor de punto flotante de Java correspondiente.
float, double NaN/Infinity Se fuerza a Float.NaN, Double.NaN, Float.POSITIVE_INFINITY o Double.POSITIVE_INFINITY.
char Número (entero) Se convierte al punto de código Unicode correspondiente.
char No entero, NaN, Infinity Se fuerza a \u0000.
boolean true/false Se convierte en true o false de Java.
boolean Número, cadena, objeto Se fuerza a false (incluidas las cadenas no vacías y los números distintos de cero).
String String Se conserva el valor de la cadena.
String Número, booleano Se da formato como una representación de cadena (por ejemplo, "42", "true", "false").
String null/undefined null se convierte en null de Java; undefined se convierte en la cadena literal "undefined".
String Object, ArrayBuffer, TypedArray Se convierte a la cadena literal "undefined".
Es un array primitivo (como int[], byte[], boolean[]) o String[]. Array ([...]) Convierte a un array de Java de 1 dimensión del tipo de elemento de destino. Los arrays dispersos completan los índices no asignados con valores predeterminados (0, false, null).
Es un array primitivo (como int[] o byte[]). TypedArray (Int8Array, Uint8Array, Int32Array, Float64Array) Los elementos se coercionan en el array primitivo de Java correspondiente.
Es un array multidimensional (por ejemplo, int[][]). Matriz anidada ([[...]]) No compatible. Los parámetros de array multidimensional se evalúan como null.
ArrayBuffer, DataView ArrayBuffer, DataView No admitido como arrays. Las instancias de ArrayBuffer y DataView se evalúan como null.
Object o clase personalizada Objeto de JavaScript ({...}) No compatible. Los literales de objetos JavaScript arbitrarios se evalúan como null en Java.
Object o clase personalizada Wrapper de objeto Java insertado Se admite el viaje de ida y vuelta. Pasa la instancia de Java subyacente al método de Java. Arroja una excepción de JavaScript si el tipo de Java no coincide con la firma del parámetro.
Tipos de datos entre corchetes (como Integer, Double, Boolean) Número, booleano No compatible. Los tipos primitivos empaquetados se tratan como objetos opacos y se evalúan como null.
Cualquier tipo primitivo null/undefined Se fuerza a los valores predeterminados (0, 0.0, \u0000, false).
Object, String, array null Se convierte en null de Java.
Asignación de tipos de devolución (de Java a JavaScript)

Cuando un método anotado de Java o Kotlin devuelve un valor, el puente lo convierte en un tipo de JavaScript:

Tipo de devolución de Java Valor de JavaScript typeof de JavaScript
boolean true/false "boolean"
byte, short, int, long, float y double Número "number"
char Número (punto de código Unicode) "number"
String (no nulo) Valor de cadena "string"
String (null) undefined "undefined"
void undefined "undefined"
Matriz de Java (como int[], String[]) undefined "undefined". No se admiten los valores de retorno de array. No se ejecuta el método de Java y se devuelve undefined sin generar una excepción.
Objeto Java o tipo personalizado (no nulo) Wrapper de objeto "object": Crea un wrapper de JavaScript alrededor de la instancia de Java. El código JavaScript puede llamar a cualquier método público de este objeto que esté anotado con @JavascriptInterface.
Objeto Java o tipo personalizado (null) null "object"
Primitivo empaquetado (como Integer o Double) Wrapper de objeto "object": Se devuelve como un wrapper opaco de objeto Java sin métodos @JavascriptInterface accesibles, lo que hace que el valor sea inutilizable en JavaScript.

Accesibilidad de métodos y miembros

El puente de JavaScript aplica reglas estrictas de acceso y visibilidad de miembros para proteger contra la ejecución de código no intencional:

  • Los campos no están expuestos: No se puede acceder a los campos de Java (incluidos los campos public y public final) desde JavaScript, y se evalúan como undefined.
  • Requisito de anotación: Solo los métodos anotados de forma explícita con @JavascriptInterface se exponen a JavaScript.
  • Restricciones de visibilidad: Los métodos deben ser public. Los métodos private y protected nunca se exponen a JavaScript, incluso si llevan la anotación @JavascriptInterface.
  • Métodos estáticos: Los métodos estáticos anotados con @JavascriptInterface se pueden llamar desde JavaScript.
  • Herencia y anulación: Las anotaciones @JavascriptInterface no se heredan cuando una subclase anula un método. Si una subclase anula un método anotado de una superclase, la subclase debe incluir de forma explícita la anotación @JavascriptInterface en el método anulado para exponerlo a JavaScript. Los métodos públicos no anulados heredados de una superclase siguen siendo accesibles si se anotan en la superclase.
  • Protección contra la reflexión: Se bloquean los métodos de reflexión de Java estándar (como getClass()) y se arroja una excepción de JavaScript para evitar vulnerabilidades de ejecución de código remoto.
  • Sobrecarga de métodos: Se admiten métodos de Java sobrecargados. El puente resuelve las llamadas a métodos solo en función de la cantidad de argumentos pasados y no tiene en cuenta los tipos de argumentos. Llamar a un método sobrecargado con un recuento de argumentos no válido genera una excepción de JavaScript. Si dos sobrecargas tienen la misma cantidad de argumentos, se elegirá una de forma arbitraria.

Resumen de los mecanismos

En la siguiente tabla, se proporciona una comparación rápida de los tres mecanismos principales de implementación del puente nativo:

Método addWebMessageListener postWebMessage addJavascriptInterface
Implementación Asíncrono (Listener en el subproceso principal) Asíncrono Síncrona
Seguridad Más alta (basada en la lista de entidades permitidas) Alto (tiene en cuenta el origen) Baja (sin verificaciones de origen)
Complejidad Moderado Moderado Simple
Dirección Bidireccional Bidireccional De la Web a la app
Versión mínima de WebView Versión 82 (y Jetpack Webkit 1.3.0) Versión 45 (y Jetpack Webkit 1.1.0) Todas las versiones
Recomendado No No

Controlar transferencias de datos grandes

Debes administrar la memoria con cuidado cuando transfieras cargas útiles grandes, como cadenas de varios megabytes o archivos binarios, para evitar errores de Aplicación no responde (ANR) o fallas en dispositivos de 32 bits. En esta sección, se analizan las diversas técnicas y limitaciones asociadas con la transferencia de cantidades significativas de datos entre la aplicación host y el contenido web.

Cómo transferir datos binarios con arrays de bytes

Con la clase WebMessageCompat, puedes enviar arrays de byte[] directamente en lugar de serializar datos binarios en cadenas de Base64. Dado que Base64 agrega aproximadamente un 33% de sobrecarga al tamaño de los datos, este método es significativamente más eficiente en cuanto a la memoria y más rápido.

  • Ventaja binaria: Transfiere datos binarios, como archivos de imagen o audio, entre tu app nativa y el contenido web.
  • Limitación: Incluso con los arrays de bytes, el sistema copia datos a través del límite de comunicación entre procesos (IPC) entre la app y el proceso aislado que WebView usa para renderizar el contenido web. Esto aún consume una cantidad significativa de memoria para archivos muy grandes.

En los siguientes ejemplos de código, se muestra cómo configurar addWebMessageListener en el lado de la app nativa para recibir mensajes marcados con WebMessageCompat.TYPE_ARRAY_BUFFER y, de manera opcional, responder con datos binarios verificando WebViewFeature.MESSAGE_ARRAY_BUFFER.

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
      )
  }
}

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
      );
  }
}

El siguiente código JavaScript muestra la implementación del lado del cliente de addWebMessageListener, lo que permite que el contenido web envíe y reciba datos binarios (ArrayBuffer) hacia y desde la aplicación nativa con el proxy window.myBridge insertado en el ejemplo anterior.

// 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]);
        }
    };
}

Carga eficiente de datos a gran escala

Para archivos muy grandes (más de 10 MB), usa el método shouldInterceptRequest para transmitir datos:

  1. La página web inicia una llamada fetch() a una URL personalizada de marcador de posición. Por ejemplo, https://app.local/large-file.
  2. La app para Android intercepta esta solicitud en WebViewClient.shouldInterceptRequest.
  3. La app devuelve los datos como un InputStream.

Esto permite transmitir datos en fragmentos en lugar de cargar toda la carga útil en la memoria de una sola vez.

La siguiente función de JavaScript muestra el código del cliente para cargar de manera eficiente un archivo binario grande desde la aplicación nativa con una llamada fetch() estándar a una URL personalizada de marcador de posición.

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);
    }
}

En los siguientes ejemplos de código, se muestra el lado de la aplicación nativa, con el método WebViewClient.shouldInterceptRequest en Kotlin y Java, para transmitir un archivo binario grande interceptando una URL de marcador de posición personalizada solicitada por el contenido web.

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)
  }
}

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);
  }
});

Sigue las recomendaciones de seguridad

Para proteger tu aplicación y los datos del usuario, sigue estos lineamientos cuando implementes un puente:

  • Aplicar HTTPS: Para garantizar que el contenido de terceros malicioso no pueda invocar la lógica nativa de tu aplicación, solo permite la comunicación con orígenes seguros.

  • Confía en las reglas de origen: La mejor manera de abordar la confianza es definir estrictamente tu allowedOriginRules y verificar el sourceOrigin proporcionado en la devolución de llamada del mensaje. Evita usar el comodín completo (*), que coincide con todos los orígenes, como tu única regla de origen, a menos que sea absolutamente necesario. El uso de comodines para subdominios (por ejemplo, *.example.com) sigue siendo válido y seguro para que coincidan varios subdominios (por ejemplo, foo.example.com, bar.example.com).

    Nota: Si bien las reglas de origen protegen contra sitios web maliciosos de terceros y elementos iframe ocultos, no pueden proteger contra vulnerabilidades de secuencias de comandos entre sitios (XSS) dentro de tu propio dominio de confianza. Por ejemplo, si tu página web muestra contenido generado por usuarios y es vulnerable a XSS almacenado, un atacante podría ejecutar una secuencia de comandos que actúe como tu origen de confianza. Considera aplicar la validación a las cargas útiles de los mensajes antes de ejecutar operaciones sensibles de la plataforma nativa.

  • Minimiza la superficie: Solo expón los métodos o datos específicos que requiere la página web.

  • Verifica las funciones en el tiempo de ejecución: Las APIs de puente recientes, incluida addWebMessageListener, forman parte de la biblioteca de Jetpack Webkit. Por lo tanto, siempre verifica si hay asistencia disponible con WebViewFeature.isFeatureSupported() antes de llamarlos.