Accéder aux API natives avec le pont JavaScript

Cette page aborde les différentes méthodes et bonnes pratiques pour établir un pont natif, également appelé pont JavaScript, afin de faciliter la communication entre le contenu Web d'une WebView et une application hôte Android.

Cela permet aux développeurs Web d'utiliser JavaScript pour accéder aux fonctionnalités de la plate-forme native (comme l'appareil photo, le système de fichiers ou les capteurs matériels avancés) que les API Web standards ne fournissent normalement pas.

Cas d'utilisation

Une implémentation de pont JavaScript permet différents scénarios d'intégration dans lesquels le contenu Web nécessite un accès plus approfondi au système d'exploitation Android. Voici quelques exemples :

  • Intégration de la plate-forme : déclenchement de composants d'UI Android natifs (par exemple, invites biométriques, BottomSheetDialog) à partir d'une page Web.
  • Performances : décharger les tâches de calcul intensives sur du code natif Java ou Kotlin.
  • Persistance des données : accès aux bases de données chiffrées locales ou aux préférences partagées.
  • Transferts de données volumineux : transfert de fichiers multimédias ou de structures de données complexes entre l'application et le moteur de rendu Web.

Mécanismes de communication

Android propose trois générations principales d'API pour établir un pont natif. Bien qu'elles soient toutes encore disponibles, elles diffèrent considérablement en termes de sécurité, d'usabilité et de performances.

Utiliser addWebMessageListener (recommandé)

addWebMessageListener est l'approche la plus moderne et recommandée pour la communication entre le contenu Web et le code de l'application native. Il combine la facilité d'utilisation de l'interface JavaScript avec la sécurité du système de messagerie.

Fonctionnement : l'application ajoute un écouteur avec un nom spécifique et un ensemble de règles d'origine autorisées. WebView s'assure ensuite que l'objet JavaScript est présent dans le champ d'application global (window.objectName) à partir du moment où la page commence à se charger.

Initialisation : pour vous assurer que WebView injecte l'objet JavaScript avant l'exécution de tout script, vous devez appeler addWebMessageListener avant d'appeler loadUrl().

Principales fonctionnalités :

  • Sécurité et confiance : contrairement aux anciennes API, cette méthode nécessite un Set<String> de allowedOriginRules lors de l'initialisation. Il s'agit du principal mécanisme permettant d'établir la confiance.

    Lorsque vous spécifiez une origine de confiance, telle que https://example.com, la WebView garantit qu'elle n'expose les objets JavaScript injectés qu'aux pages Web chargées à partir de cette origine exacte.

    Le rappel de l'écouteur natif reçoit un paramètre sourceOrigin à chaque message. Vous pouvez l'utiliser pour vérifier l'origine exacte de l'expéditeur si votre pont accepte plusieurs origines autorisées.

    Étant donné que WebView applique strictement ces vérifications d'origine au niveau de la plate-forme, votre application peut généralement se fier aux messages reçus d'un sourceOrigin de confiance, ce qui élimine la nécessité d'une validation rigoureuse de la charge utile dans la plupart des implémentations standards.

    • WebView compare les règles au schéma (HTTP/HTTPS), à l'hôte et au port.
    • WebView ignore les chemins. Par exemple, https://example.com autorise https://example.com/login et https://example.com/home.
    • WebView limite strictement les caractères génériques au début de l'hôte pour les sous-domaines. Par exemple, https://*.example.com correspond à https://foo.example.com, mais pas à https://example.com. Si vous devez faire correspondre https://example.com et ses sous-domaines, vous devez ajouter chaque règle d'origine séparément à la liste d'autorisation (par exemple, "https://example.com", "https://*.example.com"). Vous ne pouvez pas utiliser de caractères génériques pour le schéma ni au milieu d'un domaine.

    Cela limite le pont aux domaines validés, ce qui empêche l'exécution de code natif par du contenu tiers non autorisé ou des iFrames injectés.

  • Compatibilité multiframe : fonctionne sur tous les frames correspondant aux règles d'origine.

  • Threading : le rappel de l'écouteur s'exécute sur le thread principal (UI) de l'application. Si votre pont doit gérer un traitement de données complexe, l'analyse JSON ou des recherches dans une base de données, vous devez décharger ce travail sur un thread d'arrière-plan pour éviter de bloquer l'UI de l'application avec une erreur "L'application ne répond pas" (ANR).

  • Bidirectionnel : lorsque la page Web envoie un message, l'application reçoit un JavaScriptReplyProxy qu'elle peut utiliser pour renvoyer des messages à ce frame spécifique. Vous pouvez conserver cet objet replyProxy et l'utiliser à tout moment pour envoyer un nombre illimité de messages à la page, et pas seulement pour répondre à chaque message individuel qu'elle envoie. Si le frame d'origine quitte la page ou est détruit, les messages envoyés à l'aide de postMessage() sur le proxy sont ignorés silencieusement.

  • Initiation côté application : bien que la page Web doive toujours initier le canal de communication avec l'application, l'application native peut unilatéralement inviter la page Web à commencer ce processus. L'application native peut communiquer avec la page Web à l'aide de addDocumentStartJavaScript() (pour évaluer JavaScript avant le chargement de la page) ou de evaluateJavaScript() (pour évaluer JavaScript après le chargement de la page).

Limitation : Cette API envoie des données sous forme de chaînes ou de tableaux byte[]. Pour les structures de données plus complexes, telles que les objets JSON, vous devez les sérialiser dans l'un de ces formats, puis les désérialiser de l'autre côté pour reconstruire la structure de données.

Exemple d'utilisation :

Pour comprendre la séquence complète d'un échange de messages bidirectionnel, les événements se déroulent dans l'ordre suivant :

  1. Déclenchement (application) : l'application native enregistre l'écouteur avec addWebMessageListener et charge la page Web avec loadUrl().
  2. Envoi de message (Web) : les appels JavaScript de la page Web myObject.postMessage(message) pour lancer la communication.
  3. Réception et réponse aux messages (application) : l'application reçoit le message dans le rappel d'écouteur et y répond à l'aide de replyProxy.postMessage().
  4. Réception de la réponse (Web) : la page Web reçoit la réponse asynchrone dans la fonction de rappel 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);
}

Le code JavaScript suivant illustre l'implémentation côté client de addWebMessageListener, ce qui permet au contenu Web de recevoir des messages de l'application native et d'envoyer ses propres messages via le proxy myObject.

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

Utiliser postWebMessage (alternative)

Android a introduit cela pour fournir une alternative asynchrone basée sur la messagerie, semblable à window.postMessage sur le Web.

Fonctionnement : l'application utilise WebViewCompat.postWebMessage pour envoyer une charge utile au frame principal de la page Web. Pour établir un canal de communication bidirectionnel, vous pouvez créer un WebMessageChannel et transmettre l'un de ses ports avec le message au contenu Web.

Caractéristiques :

  • Asynchrone : comme addWebMessageListener, cette méthode utilise la messagerie asynchrone, qui garantit que la page Web reste réactive aux interactions des utilisateurs pendant que l'application traite les données en arrière-plan.
  • Connaissance de l'origine : vous pouvez spécifier un targetOrigin pour vous assurer que WebView ne fournit des données qu'à un site Web de confiance.

Limites :

  • Champ d'application : cette API limite la communication au frame principal. Il n'est pas possible d'adresser directement des messages aux iFrames ni de leur en envoyer.
  • Restrictions concernant les URI : vous ne pouvez pas utiliser cette méthode pour le contenu chargé à l'aide des URI data:, file: ou loadData(), sauf si vous spécifiez "*" comme origine cible. Toute page peut alors recevoir le message.
  • Risque lié à l'identité : le contenu Web ne permet pas de vérifier clairement l'identité de l'expéditeur. Un message reçu par la page Web peut provenir de votre application native ou d'un autre iFrame.

Utilisez cette méthode lorsque vous avez besoin d'un canal asynchrone simple pour les données basées sur des chaînes dans les anciennes versions d'Android qui ne sont pas compatibles avec addWebMessageListener.

Utiliser addJavascriptInterface (ancienne version)

La méthode la plus ancienne consiste à injecter une instance d'objet natif directement dans la WebView.

Fonctionnement : vous définissez une classe Kotlin ou Java, annotez les méthodes autorisées avec @JavascriptInterface et ajoutez une instance de la classe à la WebView à l'aide de addJavascriptInterface(Object, String).

Caractéristiques :

  • Synchrone : l'environnement d'exécution JavaScript est bloqué jusqu'à ce que la méthode de votre code Android renvoie une valeur.
  • Sécurité des threads : le système appelle des méthodes sur un thread d'arrière-plan, ce qui nécessite une synchronisation minutieuse côté Kotlin ou Java.
  • Risque de sécurité : par défaut, addJavascriptInterface est disponible pour chaque frame de la WebView, y compris les iFrames. Il ne dispose pas de contrôle des accès basé sur l'origine. En raison du comportement asynchrone de WebView, il n'est pas possible de déterminer de manière fiable l'URL du frame qui appelle votre interface. Vous ne devez pas vous fier à des méthodes telles que WebView.getUrl() pour la validation de la sécurité, car leur précision n'est pas garantie et elles n'indiquent pas quel frame spécifique a envoyé la requête.

Résumé des mécanismes

Le tableau suivant fournit une comparaison rapide des trois principaux mécanismes d'implémentation du pont natif :

Méthode addWebMessageListener postWebMessage addJavascriptInterface
Implémentation Asynchrone (écouteur sur le thread principal) Asynchrone Synchrone
Sécurité Le plus élevé (basé sur une liste d'autorisation) Élevée (tenant compte de l'origine) Faible (aucune vérification de l'origine)
Complexité Modéré Modéré Simple
Direction Bidirectionnel Bidirectionnel Web-to-app
Version minimale de WebView Version 82 (et Jetpack Webkit 1.3.0) Version 45 (et Jetpack Webkit 1.1.0) Toutes les versions
Recommandé Oui Non Non

Gérer les transferts de données volumineux

Vous devez gérer la mémoire avec soin lorsque vous transférez de grandes charges utiles, telles que des chaînes de plusieurs mégaoctets ou des fichiers binaires, pour éviter les erreurs "Application ne répond pas" (ANR) ou les plantages sur les appareils 32 bits. Cette section aborde les différentes techniques et limites associées au transfert de grandes quantités de données entre l'application hôte et le contenu Web.

Transférer des données binaires avec des tableaux d'octets

Avec la classe WebMessageCompat, vous pouvez envoyer directement des tableaux byte[] au lieu de sérialiser les données binaires en chaînes Base64. Comme Base64 ajoute environ 33 % de surcharge à la taille des données, cette méthode est beaucoup plus efficace en termes de mémoire et plus rapide.

  • Avantage binaire : transférez des données binaires telles que des fichiers image ou audio entre votre application native et votre contenu Web.
  • Limitation : Même avec les tableaux d'octets, le système copie les données au-delà de la limite de communication interprocessus (IPC) entre l'application et le processus isolé que WebView utilise pour afficher le contenu Web. Cela consomme toujours beaucoup de mémoire pour les fichiers très volumineux.

Les exemples de code suivants montrent comment configurer addWebMessageListener côté application native pour recevoir les messages marqués avec WebMessageCompat.TYPE_ARRAY_BUFFER et, éventuellement, répondre avec des données binaires en recherchant 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
      );
  }
}

Le code JavaScript suivant illustre l'implémentation côté client de addWebMessageListener, permettant au contenu Web d'envoyer et de recevoir des données binaires (ArrayBuffer) vers et depuis l'application native à l'aide du proxy window.myBridge injecté dans l'exemple précédent.

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

Chargement efficace de données à grande échelle

Pour les fichiers très volumineux (> 10 Mo), utilisez la méthode shouldInterceptRequest pour diffuser les données :

  1. La page Web lance un appel fetch() vers une URL personnalisée et réservée. Par exemple, https://app.local/large-file.
  2. L'application Android intercepte cette requête dans WebViewClient.shouldInterceptRequest.
  3. L'application renvoie les données sous la forme d'un InputStream.

Cela permet de diffuser les données par blocs plutôt que de charger l'intégralité de la charge utile en mémoire en une seule fois.

La fonction JavaScript suivante illustre le code côté client permettant de charger efficacement un fichier binaire volumineux à partir de l'application native à l'aide d'un appel fetch() standard à une URL personnalisée et réservée.

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

Les exemples de code suivants montrent le côté application native, en utilisant la méthode WebViewClient.shouldInterceptRequest en Kotlin et en Java, pour diffuser un fichier binaire volumineux en interceptant une URL d'espace réservé personnalisée demandée par le contenu 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);
  }
});

Suivre les recommandations de sécurité

Pour protéger votre application et les données utilisateur, suivez ces consignes lorsque vous implémentez un pont :

  • Appliquer HTTPS : pour vous assurer qu'un contenu tiers malveillant ne peut pas appeler la logique native de votre application, n'autorisez que la communication avec des origines sécurisées.

  • S'appuyer sur les règles d'origine : la meilleure façon de gérer la confiance est de définir strictement votre allowedOriginRules et de vérifier le sourceOrigin fourni dans le rappel de message. Évitez d'utiliser le caractère générique complet (*), qui correspond à toutes les origines, comme seule règle d'origine, sauf en cas d'absolue nécessité. L'utilisation de caractères génériques pour les sous-domaines (par exemple, *.example.com) reste valide et sécurisée pour faire correspondre plusieurs sous-domaines (par exemple, foo.example.com, bar.example.com).

    Remarque : Bien que les règles d'origine protègent contre les sites Web tiers malveillants et les iFrames cachés, elles ne peuvent pas protéger contre les failles de script intersites (XSS) au sein de votre propre domaine de confiance. Par exemple, si votre page Web affiche du contenu généré par les utilisateurs et est vulnérable à XSS stocké, un pirate informatique peut exécuter un script agissant en tant qu'origine de confiance. Envisagez d'appliquer une validation aux charges utiles des messages avant d'exécuter des opérations sensibles sur la plate-forme native.

  • Minimisez la surface d'attaque : n'exposez que les méthodes ou les données spécifiques dont la page Web a besoin.

  • Vérifier les fonctionnalités lors de l'exécution : les API Bridge récentes, y compris addWebMessageListener, font partie de la bibliothèque Jetpack Webkit. Vérifiez donc toujours si l'assistance est disponible à l'aide de WebViewFeature.isFeatureSupported() avant de les appeler.