Accedere alle API native con il bridge JavaScript

Questa pagina illustra i vari metodi e le best practice per stabilire un bridge nativo, noto anche come bridge JavaScript, per facilitare la comunicazione tra i contenuti web in un WebView e un'app per Android host.

In questo modo, gli sviluppatori web possono utilizzare JavaScript per accedere alle funzionalità della piattaforma nativa, come la fotocamera, il file system o i sensori hardware avanzati, che le API web standard normalmente non forniscono.

Casi d'uso

L'implementazione di un bridge JavaScript consente vari scenari di integrazione in cui i contenuti web richiedono un accesso più approfondito al sistema operativo Android. Di seguito sono riportati alcuni esempi:

  • Integrazione della piattaforma: attivazione di componenti UI nativi di Android (ad esempio, prompt biometrici, BottomSheetDialog) da una pagina web.
  • Prestazioni: delega di attività di calcolo pesanti a codice Java o Kotlin nativo.
  • Persistenza dei dati: accesso a database criptati locali o preferenze condivise.
  • Trasferimenti di grandi quantità di dati: passaggio di file multimediali o strutture di dati complesse tra l'app e il renderer web.

Meccanismi di comunicazione

Android offre tre generazioni principali di API per stabilire un bridge nativo. Sebbene siano ancora tutti disponibili, differiscono in modo significativo per sicurezza, usabilità e prestazioni.

Utilizza addWebMessageListener (consigliato)

addWebMessageListener è l'approccio più moderno e consigliato per la comunicazione tra i contenuti web e il codice dell'app nativa. Combina la facilità d'uso dell'interfaccia JavaScript con la sicurezza del sistema di messaggistica.

Come funziona: l'app aggiunge un listener con un nome specifico e un insieme di regole di origine consentite. Il componente WebView garantisce quindi che l'oggetto JavaScript sia presente nell'ambito globale (window.objectName) dal momento in cui la pagina inizia a caricarsi.

Inizializzazione: per assicurarti che WebView inserisca l'oggetto JavaScript prima dell'esecuzione di qualsiasi script, devi chiamare addWebMessageListener prima di passare alla pagina (ad esempio chiamando WebViewCompat.navigate o loadUrl).

Funzionalità principali:

  • Sicurezza e affidabilità: a differenza delle API legacy, questo metodo richiede un Set<String> di allowedOriginRules durante l'inizializzazione. Questo è il meccanismo principale per stabilire la fiducia.

    Quando specifichi un'origine attendibile, ad esempio https://example.com, il componente WebView garantisce che esponga gli oggetti JavaScript inseriti solo alle pagine web caricate da quell'origine esatta.

    Il callback del listener nativo riceve un parametro sourceOrigin con ogni messaggio. Puoi utilizzarlo per verificare l'origine esatta del mittente se il tuo bridge supporta più origini consentite.

    Poiché WebView applica rigorosamente questi controlli dell'origine a livello di piattaforma, la tua app può generalmente fare affidamento sui messaggi ricevuti da un sourceOrigin attendibile come veritieri, eliminando la necessità di una rigorosa convalida del payload nella maggior parte delle implementazioni standard.

    • WebView confronta le regole con lo schema (HTTP/HTTPS), l'host e la porta.
    • WebView ignora i percorsi. Ad esempio, https://example.com consente https://example.com/login e https://example.com/home.
    • WebView limita rigorosamente i caratteri jolly all'inizio dell'host per i sottodomini. Ad esempio, https://*.example.com corrisponde a https://foo.example.com, ma non a https://example.com. Se devi corrispondere sia a https://example.com sia ai relativi sottodomini, devi aggiungere ogni regola di origine separatamente all'elenco consentito (ad esempio, "https://example.com", "https://*.example.com"). Non puoi utilizzare caratteri jolly per lo schema o al centro di un dominio.

    In questo modo, il bridge è limitato ai domini verificati, impedendo l'esecuzione di codice nativo da parte di contenuti di terze parti non autorizzati o iframe inseriti.

  • Supporto multi-frame: funziona su tutti i frame che corrispondono alle regole di origine.

  • Threading: il callback del listener viene eseguito sul thread principale (UI) dell'applicazione. Se il bridge deve gestire l'elaborazione di dati complessi, l'analisi JSON o le ricerche nel database, devi trasferire il lavoro su un thread in background per evitare il blocco dell'interfaccia utente dell'applicazione con un errore "L'app non risponde" (ANR).

  • Bidirezionale: quando la pagina web invia un messaggio, l'app riceve un JavaScriptReplyProxy che può utilizzare per inviare messaggi a quel frame specifico. Puoi conservare questo oggetto replyProxy e utilizzarlo in qualsiasi momento per inviare un numero qualsiasi di messaggi alla pagina, non solo per rispondere a ogni singolo messaggio inviato dalla pagina. Se il frame di origine esce dalla navigazione o viene eliminato, i messaggi inviati utilizzando postMessage() sul proxy vengono ignorati automaticamente.

  • Avvio lato app: sebbene la pagina web debba sempre avviare il canale di comunicazione con l'app, l'app nativa può richiedere unilateralmente alla pagina web di iniziare questa procedura. L'app nativa può comunicare con la pagina web con addDocumentStartJavaScript() (per valutare JavaScript prima del caricamento della pagina) o evaluateJavaScript() (per valutare JavaScript dopo il caricamento della pagina).

Limitazione: questa API invia i dati come stringhe o array byte[]. Per strutture di dati più complesse, ad esempio oggetti JSON, devi serializzarle in uno di questi formati e poi deserializzarle dall'altra parte per ricostruire la struttura dei dati.

Esempio di utilizzo:

Per comprendere la sequenza completa di uno scambio di messaggi bidirezionale, gli eventi si susseguono in questo ordine:

  1. Avvio (app): l'app nativa registra il listener con addWebMessageListener e avvia la navigazione della pagina (ad esempio con WebViewCompat.navigate o loadUrl).
  2. Invio di messaggi (web): le chiamate JavaScript della pagina web myObject.postMessage(message) per avviare la comunicazione.
  3. Ricezione e risposta ai messaggi (app): l'app riceve il messaggio nel callback del listener e risponde utilizzando replyProxy.postMessage() fornito.
  4. Ricezione risposta (web): la pagina web riceve la risposta asincrona nella funzione di callback 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);
}

Il seguente codice JavaScript mostra l'implementazione lato client di addWebMessageListener, consentendo ai contenuti web di ricevere messaggi dall'app nativa e di inviare i propri messaggi tramite il proxy myObject.

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

Usa postWebMessage (alternativa)

Android ha introdotto questa funzionalità per fornire un'alternativa asincrona basata sulla messaggistica simile a window.postMessage del web.

Come funziona: l'app utilizza WebViewCompat.postWebMessage per inviare un payload al frame principale della pagina web. Per stabilire un canale di comunicazione bidirezionale, puoi creare un WebMessageChannel e passare una delle sue porte con il messaggio ai contenuti web.

Caratteristiche:

  • Asincrono: come addWebMessageListener, questo metodo utilizza la messaggistica asincrona, che garantisce che la pagina web rimanga reattiva alle interazioni degli utenti mentre l'app elabora i dati in background.
  • Riconoscimento dell'origine: puoi specificare un targetOrigin per assicurarti che WebView fornisca i dati solo a un sito web attendibile.

Limitazioni:

  • Ambito: questa API limita la comunicazione al frame principale. Non supporta l'indirizzamento diretto o l'invio di messaggi agli iframe.
  • Limitazioni URI: non puoi utilizzare questo metodo per i contenuti caricati utilizzando URI data:, URI file: o loadData(), a meno che tu non specifichi "*" come origine di destinazione. In questo modo, qualsiasi pagina può ricevere il messaggio.
  • Rischio di identità: non esiste un modo chiaro per i contenuti web di verificare l'identità del mittente. Un messaggio ricevuto dalla pagina web potrebbe provenire dalla tua app nativa o da un altro iframe.

Utilizza questo metodo quando hai bisogno di un canale asincrono semplice per i dati basati su stringhe nelle versioni precedenti di Android che non supportano addWebMessageListener.

Usa addJavascriptInterface (legacy)

Il metodo più vecchio prevede l'inserimento di un'istanza di oggetto nativa direttamente in WebView.

Come funziona: definisci una classe Kotlin o Java, annota i metodi consentiti con @JavascriptInterface e aggiungi un'istanza della classe a WebView utilizzando addJavascriptInterface(Object, String).

Caratteristiche:

  • Sincrono: l'ambiente di esecuzione JavaScript viene bloccato finché il metodo nel codice Android non restituisce un valore.
  • Thread safety: il sistema chiama i metodi su un thread in background, richiedendo una sincronizzazione accurata sul lato Kotlin o Java.
  • Rischio per la sicurezza: per impostazione predefinita, addJavascriptInterface è disponibile per ogni frame all'interno di WebView, inclusi gli iframe. Manca il controllo dell'accesso basato sull'origine. A causa del comportamento asincrono di WebView, non è possibile determinare in modo sicuro l'URL del frame che chiama la tua interfaccia. Non devi fare affidamento su metodi come WebView.getUrl() per la verifica della sicurezza, in quanto non è garantita la loro accuratezza e non indicano quale frame specifico ha effettuato la richiesta.

Conversioni e forzatura dei tipi di dati

Quando utilizzi addJavascriptInterface, il bridge Java basato su Chromium converte i tipi di dati tra il runtime JavaScript e il codice dell'app per Android.

Ai parametri del metodo e ai valori restituiti si applicano le seguenti regole di coercizione.

Mappatura dei tipi di parametri (da JavaScript a Java)

Quando JavaScript passa argomenti a un metodo Java o Kotlin annotato, il bridge converte i valori JavaScript nei tipi di parametri Java corrispondenti:

Tipo di parametro Java Valore dell'argomento JavaScript Comportamento coercitivo
byte, short, int, long Numero (intero) I valori vengono convertiti nel tipo di numero intero di destinazione. I valori fuori dai limiti vengono riportati all'interno in base alle regole standard di conversione numerica.
byte, short, int, long NaN Forza a 0.
byte, short, int, long Infinity Forza la conversione in -1 per byte e short oppure in Integer.MAX_VALUE e Long.MAX_VALUE per int e long.
float, double Numero Forza la conversione al valore in virgola mobile Java corrispondente.
float, double NaN/Infinity Forza la conversione in Float.NaN, Double.NaN, Float.POSITIVE_INFINITY o Double.POSITIVE_INFINITY.
char Numero (intero) Convertito nel punto di codice Unicode corrispondente.
char Non intero, NaN, Infinity Forza a \u0000.
boolean true/false Forza la conversione in Java true o false.
boolean Numero, stringa, oggetto Forza il valore a false (incluse stringhe non vuote e numeri diversi da zero).
String Stringa Il valore della stringa viene conservato.
String Numero, booleano Formattato come rappresentazione di stringa (ad esempio, "42", "true", "false").
String null/undefined null viene forzato a Java null; undefined viene forzato alla stringa letterale "undefined".
String Object, ArrayBuffer, TypedArray Forza la conversione nella stringa letterale "undefined".
Array primitivo (ad esempio int[], byte[], boolean[]) o String[] Array ([...]) Converte in un array Java 1D del tipo di elemento di destinazione. Gli array sparsi riempiono gli indici non assegnati con valori predefiniti (0, false, null).
Array primitivo (ad esempio int[], byte[]) TypedArray (Int8Array, Uint8Array, Int32Array, Float64Array) Gli elementi vengono forzati nell'array primitivo Java corrispondente.
Array multidimensionale (ad esempio int[][]) Array nidificato ([[...]]) Non supportato. I parametri di array multidimensionali restituiscono null.
ArrayBuffer, DataView ArrayBuffer, DataView Non supportato come array. Le istanze ArrayBuffer e DataView vengono valutate come null.
Object o classe personalizzata Oggetto JavaScript ({...}) Non supportato. I valori letterali degli oggetti JavaScript arbitrari vengono valutati come null in Java.
Object o classe personalizzata Wrapper dell'oggetto Java inserito Supportato (conversione bidirezionale). Passa l'istanza Java sottostante al metodo Java. Genera un'eccezione JavaScript se il tipo Java non corrisponde alla firma del parametro.
Tipi di scatole (ad esempio Integer, Double, Boolean) Numero, booleano Non supportato. I tipi primitivi in scatola vengono trattati come oggetti opachi e restituiscono null.
Qualsiasi tipo primitivo null/undefined Forza i valori predefiniti (0, 0.0, \u0000, false).
Object, String, array null Esegue la conversione in Java null.
Mappatura dei tipi restituiti (da Java a JavaScript)

Quando un metodo Java o Kotlin annotato restituisce un valore, il bridge lo converte in un tipo JavaScript:

Tipo restituito Java Valore JavaScript JavaScript typeof
boolean true/false "boolean"
byte, short, int, long, float, double Numero "number"
char Numero (punto di codice Unicode) "number"
String (non nullo) Valore stringa "string"
String (null) undefined "undefined"
void undefined "undefined"
Array Java (ad esempio int[], String[]) undefined "undefined". I valori restituiti di array non sono supportati. Il metodo Java non viene eseguito e undefined viene restituito senza generare un'eccezione.
Oggetto Java / tipo personalizzato (non nullo) Wrapper oggetto "object". Crea un wrapper JavaScript intorno all'istanza Java. Il codice JavaScript può chiamare qualsiasi metodo pubblico di questo oggetto annotato con @JavascriptInterface.
Oggetto Java / tipo personalizzato (null) null "object"
Primitiva boxed (ad esempio Integer, Double) Wrapper oggetto "object". Restituito come wrapper di oggetti Java opaco senza metodi @JavascriptInterface accessibili, il che rende il valore inutilizzabile in JavaScript.

Accessibilità di metodi e membri

Il bridge JavaScript applica regole rigorose di accesso e visibilità dei membri per proteggere dall'esecuzione di codice non intenzionale:

  • I campi non sono esposti: i campi Java (inclusi i campi public e public final) non sono accessibili da JavaScript e vengono valutati come undefined.
  • Requisito di annotazione: solo i metodi annotati esplicitamente con @JavascriptInterface sono esposti a JavaScript.
  • Limitazioni di visibilità: i metodi devono essere public. I metodi private e protected non vengono mai esposti a JavaScript, anche se contengono l'annotazione @JavascriptInterface.
  • Metodi statici: i metodi statici annotati con @JavascriptInterface sono richiamabili da JavaScript.
  • Ereditarietà e override: le annotazioni @JavascriptInterface non vengono ereditate quando una sottoclasse esegue l'override di un metodo. Se una sottoclasse esegue l'override di un metodo annotato di una superclasse, deve includere esplicitamente l'annotazione @JavascriptInterface nel metodo sottoposto a override per esporlo a JavaScript. I metodi pubblici non sottoposti a override ereditati da una superclasse rimangono accessibili se annotati nella superclasse.
  • Protezione della reflection: i metodi di reflection Java standard (ad esempio getClass()) vengono bloccati e generano un'eccezione JavaScript per impedire vulnerabilità di esecuzione di codice da remoto.
  • Overloading dei metodi: sono supportati i metodi Java sovraccarichi. Il bridge risolve le chiamate di metodi in base al numero di argomenti passati e non tiene conto dei tipi di argomenti. La chiamata di un metodo sovraccarico con un numero di argomenti non valido genera un'eccezione JavaScript. Se due sovraccarichi hanno lo stesso numero di argomenti, ne verrà scelto uno in modo arbitrario.

Riepilogo dei meccanismi

La seguente tabella fornisce un rapido confronto dei tre meccanismi di implementazione del bridge nativo principali:

Metodo addWebMessageListener postWebMessage addJavascriptInterface
Implementazione Asincrono (listener sul thread principale) Asincrona Sincrona
Sicurezza Più alta (in base alla lista consentita) Elevata (con riconoscimento dell'origine) Bassa (nessun controllo dell'origine)
complessità Moderato Moderato Semplice
Direzione Bidirezionale Bidirezionale Web to app
Versione minima di WebView Versione 82 (e Jetpack Webkit 1.3.0) Versione 45 (e Jetpack Webkit 1.1.0) Tutte le versioni
Consigliato No No

Gestire trasferimenti di dati di grandi dimensioni

Devi gestire attentamente la memoria quando trasferisci payload di grandi dimensioni, ad esempio stringhe di più megabyte o file binari, per evitare errori "L'applicazione non risponde" (ANR) o arresti anomali sui dispositivi a 32 bit. Questa sezione descrive le varie tecniche e limitazioni associate al trasferimento di grandi quantità di dati tra l'applicazione host e i contenuti web.

Trasferire dati binari con array di byte

Con la classe WebMessageCompat, puoi inviare direttamente array byte[] anziché serializzare i dati binari in stringhe Base64. Poiché Base64 aggiunge un sovraccarico di circa il 33% alle dimensioni dei dati, questo metodo è molto più efficiente in termini di memoria e più veloce.

  • Vantaggio binario: trasferisci dati binari come file immagine o audio tra la tua app nativa e i contenuti web.
  • Limitazione: anche con gli array di byte, il sistema copia i dati oltre il limite di comunicazione interprocesso (IPC) tra l'app e il processo isolato che WebView utilizza per il rendering dei contenuti web. Ciò comporta comunque un consumo di memoria significativo per i file di grandi dimensioni.

I seguenti esempi di codice mostrano come configurare addWebMessageListener sul lato dell'app nativa per ricevere messaggi contrassegnati con WebMessageCompat.TYPE_ARRAY_BUFFER e, facoltativamente, rispondere con dati binari controllando 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
      );
  }
}

Il seguente codice JavaScript mostra l'implementazione lato client di addWebMessageListener, consentendo ai contenuti web di inviare e ricevere dati binari (ArrayBuffer) da e verso l'app nativa utilizzando il proxy window.myBridge inserito nell'esempio precedente.

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

Caricamento efficiente di dati su larga scala

Per file molto grandi (> 10 MB), utilizza il metodo shouldInterceptRequest per trasmettere i dati in streaming:

  1. La pagina web avvia una chiamata fetch() a un URL personalizzato segnaposto. Ad esempio, https://app.local/large-file.
  2. L'app per Android intercetta questa richiesta in WebViewClient.shouldInterceptRequest.
  3. L'app restituisce i dati come InputStream.

Ciò consente di trasmettere i dati in blocchi anziché caricare l'intero payload in memoria contemporaneamente.

La seguente funzione JavaScript mostra il codice lato client per caricare in modo efficiente un file binario di grandi dimensioni dall'applicazione nativa utilizzando una chiamata fetch() standard a un URL segnaposto personalizzato.

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

I seguenti esempi di codice mostrano il lato dell'app nativa, utilizzando il metodo WebViewClient.shouldInterceptRequest sia in Kotlin che in Java, per trasmettere in streaming un file binario di grandi dimensioni intercettando un URL segnaposto personalizzato richiesto dai contenuti 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);
  }
});

Seguire i consigli per la sicurezza

Per proteggere l'applicazione e i dati degli utenti, segui queste linee guida quando implementi un bridge:

  • Applica HTTPS: per assicurarti che i contenuti di terze parti dannosi non possano richiamare la logica nativa della tua applicazione, consenti solo la comunicazione con origini sicure.

  • Affidati alle regole di origine: il modo migliore per gestire l'attendibilità è definire rigorosamente il tuo allowedOriginRules e controllare il sourceOrigin fornito nel callback del messaggio. Evita di utilizzare il carattere jolly completo (*), che corrisponde a tutte le origini, come unica regola di origine, a meno che non sia assolutamente necessario. L'utilizzo di caratteri jolly per i sottodomini (ad esempio, *.example.com) rimane valido e sicuro per la corrispondenza con più sottodomini (ad esempio, foo.example.com, bar.example.com).

    Nota: mentre le regole di origine proteggono da siti web di terze parti dannosi e da iframe nascosti, non possono proteggere dalle vulnerabilità cross-site scripting (XSS) all'interno del tuo dominio attendibile. Ad esempio, se la tua pagina web mostra contenuti generati dagli utenti ed è vulnerabile a XSS memorizzato, un malintenzionato potrebbe eseguire uno script che funge da origine attendibile. Valuta la possibilità di applicare la convalida ai payload dei messaggi prima di eseguire operazioni sensibili della piattaforma nativa.

  • Ridurre al minimo la superficie di attacco: esponi solo i metodi o i dati specifici richiesti dalla pagina web.

  • Controlla le funzionalità in fase di runtime: le API bridge recenti, tra cui addWebMessageListener, fanno parte della libreria Jetpack Webkit. Pertanto, controlla sempre se è disponibile l'assistenza tramite WebViewFeature.isFeatureSupported() prima di chiamare.