Truy cập vào các API gốc bằng cầu nối JavaScript

Trang này thảo luận về nhiều phương pháp và các phương pháp hay nhất để thiết lập một cầu nối gốc (còn gọi là cầu nối JavaScript) nhằm tạo điều kiện giao tiếp giữa nội dung web trong WebView và một ứng dụng Android lưu trữ.

Điều này cho phép nhà phát triển web sử dụng JavaScript để truy cập vào các tính năng của nền tảng gốc (chẳng hạn như camera, hệ thống tệp hoặc các cảm biến phần cứng nâng cao) mà các API web tiêu chuẩn thường không cung cấp.

Trường hợp sử dụng

Việc triển khai cầu nối JavaScript cho phép nhiều trường hợp tích hợp trong đó nội dung trên web yêu cầu quyền truy cập sâu hơn vào hệ điều hành Android. Sau đây là một số ví dụ:

  • Tích hợp nền tảng: Kích hoạt các thành phần giao diện người dùng Android gốc (ví dụ: Lời nhắc sinh trắc học, BottomSheetDialog) từ một trang web.
  • Hiệu suất: Chuyển các tác vụ tính toán nặng sang mã Java hoặc Kotlin gốc.
  • Tính duy trì dữ liệu: Truy cập vào cơ sở dữ liệu đã mã hoá cục bộ hoặc các lựa chọn ưu tiên dùng chung.
  • Truyền dữ liệu lớn: Truyền tệp đa phương tiện hoặc cấu trúc dữ liệu phức tạp giữa ứng dụng và trình kết xuất web.

Cơ chế giao tiếp

Android cung cấp 3 thế hệ API chính để thiết lập một cầu nối gốc. Mặc dù tất cả các phương thức này vẫn có sẵn, nhưng chúng khác nhau đáng kể về tính bảo mật, khả năng sử dụng và hiệu suất.

Sử dụng addWebMessageListener (Đề xuất)

addWebMessageListener là phương pháp hiện đại và được đề xuất nhất để giao tiếp giữa nội dung web và mã ứng dụng gốc. Thư viện này kết hợp tính dễ sử dụng của giao diện JavaScript với tính bảo mật của hệ thống nhắn tin.

Cách hoạt động: Ứng dụng sẽ thêm một trình nghe có tên cụ thể và một bộ quy tắc nguồn gốc được phép. Sau đó, WebView đảm bảo rằng đối tượng JavaScript có trong phạm vi toàn cục (window.objectName) kể từ thời điểm trang bắt đầu tải.

Khởi chạy: Để đảm bảo WebView chèn đối tượng JavaScript trước khi bất kỳ tập lệnh nào chạy, bạn phải gọi addWebMessageListener trước khi gọi loadUrl().

Các tính năng chính:

  • Bảo mật và độ tin cậy: Không giống như các API cũ, phương thức này yêu cầu Set<String> của allowedOriginRules trong quá trình khởi tạo. Đây là cơ chế chính để thiết lập lòng tin.

    Khi bạn chỉ định một nguồn đáng tin cậy, chẳng hạn như https://example.com, WebView đảm bảo rằng chỉ hiển thị các đối tượng JavaScript được chèn cho các trang web được tải từ chính nguồn đó.

    Lệnh gọi lại của trình nghe gốc sẽ nhận được một tham số sourceOrigin cho mỗi thông báo. Bạn có thể dùng thuộc tính này để xác minh chính xác nguồn gốc của người gửi nếu cầu nối của bạn hỗ trợ nhiều nguồn gốc được phép.

    Vì WebView thực thi nghiêm ngặt các quy trình kiểm tra nguồn này ở cấp nền tảng, nên ứng dụng của bạn thường có thể dựa vào các thông báo nhận được từ một sourceOrigin đáng tin cậy là đúng sự thật, giúp loại bỏ nhu cầu xác thực tải trọng nghiêm ngặt trong hầu hết các quy trình triển khai tiêu chuẩn.

    • WebView so khớp các quy tắc với lược đồ (HTTP/HTTPS), máy chủ lưu trữ và cổng.
    • WebView bỏ qua các đường dẫn. Ví dụ: https://example.com cho phép https://example.com/loginhttps://example.com/home.
    • WebView giới hạn nghiêm ngặt ký tự đại diện ở đầu máy chủ cho các miền con. Ví dụ: https://*.example.com khớp với https://foo.example.com nhưng không khớp với https://example.com. Nếu cần so khớp cả https://example.com và các miền con của miền này, bạn phải thêm từng quy tắc nguồn riêng biệt vào danh sách cho phép (ví dụ: "https://example.com", "https://*.example.com"). Bạn không thể sử dụng ký tự đại diện cho lược đồ hoặc ở giữa một miền.

    Điều này hạn chế cầu chỉ hoạt động trên các miền đã xác minh, ngăn nội dung của bên thứ ba trái phép hoặc các iframe được chèn thực thi mã gốc.

  • Hỗ trợ nhiều khung hình: Hoạt động trên tất cả các khung hình khớp với các quy tắc về nguồn gốc.

  • Phân luồng: Lệnh gọi lại của trình nghe chạy trên luồng chính (giao diện người dùng) của ứng dụng. Nếu cầu của bạn cần xử lý dữ liệu phức tạp, phân tích cú pháp JSON hoặc tra cứu cơ sở dữ liệu, thì bạn phải giảm tải công việc đó sang một luồng trong nền để ngăn chặn tình trạng giao diện người dùng ứng dụng bị treo do lỗi "ứng dụng không phản hồi" (ANR).

  • Hai chiều: Khi trang web gửi một thông báo, ứng dụng sẽ nhận được một JavaScriptReplyProxy mà ứng dụng có thể dùng để gửi thông báo trở lại khung hình cụ thể đó. Bạn có thể giữ lại đối tượng replyProxy này và sử dụng bất cứ lúc nào để gửi bao nhiêu tin nhắn tuỳ ý đến trang, chứ không chỉ để trả lời từng tin nhắn riêng lẻ mà trang gửi. Nếu khung ban đầu chuyển hướng hoặc bị huỷ, thì các thông báo được gửi bằng postMessage() trên proxy sẽ bị bỏ qua một cách âm thầm.

  • Bắt đầu từ phía ứng dụng: Mặc dù trang web luôn phải bắt đầu kênh giao tiếp với ứng dụng, nhưng ứng dụng gốc có thể đơn phương nhắc trang web bắt đầu quy trình này. Ứng dụng gốc có thể giao tiếp với trang web bằng addDocumentStartJavaScript() (để đánh giá JavaScript trước khi trang tải) hoặc evaluateJavaScript() (để đánh giá JavaScript sau khi trang đã tải).

Giới hạn: API này gửi dữ liệu dưới dạng chuỗi hoặc mảng byte[]. Đối với các cấu trúc dữ liệu phức tạp hơn, chẳng hạn như đối tượng JSON, bạn phải chuyển đổi tuần tự cấu trúc này thành một trong các định dạng đó, rồi giải tuần tự ở phía bên kia để tạo lại cấu trúc dữ liệu.

Ví dụ về cách sử dụng:

Để hiểu toàn bộ trình tự trao đổi thông báo hai chiều, các sự kiện sẽ diễn ra theo thứ tự sau:

  1. Khởi tạo (ứng dụng): Ứng dụng gốc đăng ký trình nghe bằng addWebMessageListener và tải trang web bằng loadUrl().
  2. Gửi tin nhắn (web): JavaScript của trang web gọi myObject.postMessage(message) để bắt đầu giao tiếp.
  3. Nhận và trả lời tin nhắn (ứng dụng): Ứng dụng nhận tin nhắn trong lệnh gọi lại của trình nghe và trả lời bằng cách sử dụng replyProxy.postMessage() được cung cấp.
  4. Nhận phản hồi (web): Trang web nhận được phản hồi không đồng bộ trong hàm 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);
}

JavaScript sau đây minh hoạ việc triển khai phía máy khách của addWebMessageListener, cho phép nội dung web nhận tin nhắn từ ứng dụng gốc và gửi tin nhắn của riêng mình thông qua proxy myObject.

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

Sử dụng postWebMessage (Tuỳ chọn)

Android đã giới thiệu tính năng này để cung cấp một giải pháp thay thế dựa trên việc nhắn tin và không đồng bộ, tương tự như window.postMessage của web.

Cách hoạt động: Ứng dụng này sử dụng WebViewCompat.postWebMessage để gửi tải trọng đến khung chính của trang web. Để thiết lập kênh giao tiếp hai chiều, bạn có thể tạo một WebMessageChannel và truyền một trong các cổng của kênh này cùng với thông báo đến nội dung web.

Đặc điểm:

  • Không đồng bộ: Tương tự như addWebMessageListener, phương thức này sử dụng tính năng truyền thông báo không đồng bộ, đảm bảo trang web vẫn phản hồi các hoạt động tương tác của người dùng trong khi ứng dụng xử lý dữ liệu ở chế độ nền.
  • Nhận biết nguồn gốc: Bạn có thể chỉ định một targetOrigin để đảm bảo WebView chỉ gửi dữ liệu đến một trang web đáng tin cậy.

Các điểm hạn chế:

  • Phạm vi: API này giới hạn hoạt động giao tiếp trong khung chính. Nó không hỗ trợ việc giải quyết trực tiếp hoặc gửi tin nhắn đến iframe.
  • Các hạn chế về URI: Bạn không thể sử dụng phương thức này cho nội dung được tải bằng URI data:, URI file: hoặc loadData(), trừ phi bạn chỉ định "*" làm nguồn gốc đích. Khi đó, mọi trang đều có thể nhận được thông báo.
  • Rủi ro về danh tính: Nội dung trên web không có cách nào rõ ràng để xác minh danh tính của người gửi. Thông báo mà trang web nhận được có thể bắt nguồn từ ứng dụng gốc của bạn hoặc một iframe khác.

Hãy sử dụng phương thức này khi bạn cần một kênh đơn giản, không đồng bộ cho dữ liệu dựa trên chuỗi trong các phiên bản Android cũ không hỗ trợ addWebMessageListener.

Sử dụng addJavascriptInterface (Cũ)

Phương thức cũ nhất liên quan đến việc chèn trực tiếp một thực thể đối tượng gốc vào WebView.

Cách hoạt động: Bạn xác định một lớp Kotlin hoặc Java, chú thích các phương thức được phép bằng @JavascriptInterface và thêm một thực thể của lớp vào WebView bằng addJavascriptInterface(Object, String).

Đặc điểm:

  • Đồng bộ: Môi trường thực thi JavaScript sẽ chặn cho đến khi phương thức trong mã Android của bạn trả về.
  • Độ an toàn của luồng: Hệ thống gọi các phương thức trên một luồng nền, yêu cầu đồng bộ hoá cẩn thận ở phía Kotlin hoặc Java.
  • Rủi ro bảo mật: Theo mặc định, addJavascriptInterface có sẵn cho mọi khung trong WebView, kể cả iframe. Thiếu tính năng kiểm soát quyền truy cập dựa trên nguồn gốc. Do hành vi không đồng bộ của WebView, bạn không thể xác định một cách an toàn URL của khung đang gọi giao diện của bạn. Bạn không được dựa vào các phương thức như WebView.getUrl() để xác minh tính bảo mật, vì các phương thức này không đảm bảo tính chính xác và không cho biết khung hình cụ thể nào đã đưa ra yêu cầu.

Tóm tắt các cơ chế

Bảng sau đây cung cấp thông tin so sánh nhanh về 3 cơ chế triển khai cầu nối gốc chính:

Phương thức addWebMessageListener postWebMessage addJavascriptInterface
Triển khai Không đồng bộ (Trình nghe trên luồng chính) Không đồng bộ Đồng bộ
Bảo mật Cao nhất (Dựa trên danh sách cho phép) Cao (Nhận biết nguồn) Thấp (Không kiểm tra nguồn gốc)
Độ phức tạp Trung bình Trung bình Đơn giản
Hướng Hai chiều Hai chiều Web đến ứng dụng
Phiên bản WebView tối thiểu Phiên bản 82 (và Jetpack Webkit 1.3.0) Phiên bản 45 (và Jetpack Webkit 1.1.0) Tất cả phiên bản
Đề xuất Không Không

Xử lý các hoạt động chuyển dữ liệu lớn

Bạn phải quản lý bộ nhớ một cách cẩn thận khi chuyển các tải trọng lớn, chẳng hạn như chuỗi nhiều megabyte hoặc tệp nhị phân, để tránh lỗi Ứng dụng không phản hồi (ANR) hoặc sự cố trên các thiết bị 32 bit. Phần này thảo luận về nhiều kỹ thuật và hạn chế liên quan đến việc chuyển một lượng lớn dữ liệu giữa ứng dụng lưu trữ và nội dung web.

Truyền dữ liệu nhị phân bằng mảng byte

Với lớp WebMessageCompat, bạn có thể gửi trực tiếp các mảng byte[] thay vì chuyển đổi dữ liệu nhị phân thành chuỗi Base64. Vì Base64 làm tăng khoảng 33% chi phí chung cho kích thước dữ liệu, nên cách này tiết kiệm bộ nhớ và nhanh hơn đáng kể.

  • Lợi thế của dữ liệu nhị phân: Truyền dữ liệu nhị phân như tệp hình ảnh hoặc âm thanh giữa ứng dụng gốc và nội dung web của bạn.
  • Hạn chế: Ngay cả với mảng byte, hệ thống vẫn sao chép dữ liệu trên ranh giới giao tiếp giữa các quy trình (IPC) giữa ứng dụng và quy trình riêng biệt mà WebView dùng để kết xuất nội dung web. Thao tác này vẫn tiêu tốn đáng kể bộ nhớ đối với các tệp rất lớn.

Các ví dụ về mã sau đây minh hoạ cách thiết lập addWebMessageListener ở phía ứng dụng gốc để nhận các thông báo được đánh dấu bằng WebMessageCompat.TYPE_ARRAY_BUFFER và tuỳ ý trả lời bằng dữ liệu nhị phân bằng cách kiểm tra 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
      );
  }
}

Đoạn mã JavaScript sau đây minh hoạ việc triển khai addWebMessageListener phía máy khách, cho phép nội dung trên web gửi và nhận dữ liệu nhị phân (ArrayBuffer) đến và từ ứng dụng gốc bằng cách sử dụng proxy window.myBridge được chèn trong ví dụ trước.

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

Tải dữ liệu quy mô lớn một cách hiệu quả

Đối với các tệp có kích thước rất lớn (>10 MB), hãy sử dụng phương thức shouldInterceptRequest để truyền trực tuyến dữ liệu:

  1. Trang web này sẽ bắt đầu một lệnh gọi fetch() đến một URL tuỳ chỉnh, là phần giữ chỗ. Ví dụ: https://app.local/large-file.
  2. Ứng dụng Android chặn yêu cầu này trong WebViewClient.shouldInterceptRequest.
  3. Ứng dụng trả về dữ liệu dưới dạng InputStream.

Điều này cho phép truyền trực tuyến dữ liệu theo các khối thay vì tải toàn bộ tải trọng vào bộ nhớ cùng một lúc.

Hàm JavaScript sau đây minh hoạ mã phía máy khách để tải hiệu quả một tệp nhị phân lớn từ ứng dụng gốc bằng cách sử dụng lệnh gọi fetch() tiêu chuẩn đến một URL tuỳ chỉnh, phần giữ chỗ.

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

Các ví dụ về mã sau đây minh hoạ phía ứng dụng gốc, sử dụng phương thức WebViewClient.shouldInterceptRequest bằng cả Kotlin và Java, để truyền trực tuyến một tệp nhị phân lớn bằng cách chặn một URL trình giữ chỗ tuỳ chỉnh do nội dung trên web yêu cầu.

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

Làm theo các đề xuất bảo mật

Để bảo vệ ứng dụng và dữ liệu người dùng, hãy làm theo các nguyên tắc sau khi triển khai cầu nối:

  • Thực thi HTTPS: Để đảm bảo rằng nội dung của bên thứ ba độc hại không thể gọi logic gốc của ứng dụng, hãy chỉ cho phép giao tiếp với các nguồn gốc bảo mật.

  • Dựa vào các quy tắc về nguồn gốc: Cách tốt nhất để xử lý vấn đề về độ tin cậy là xác định nghiêm ngặt allowedOriginRules và kiểm tra sourceOrigin được cung cấp trong lệnh gọi lại thông báo. Tránh sử dụng ký tự đại diện đầy đủ (*) (phù hợp với tất cả các nguồn) làm quy tắc nguồn duy nhất, trừ phi thực sự cần thiết. Việc sử dụng ký tự đại diện cho miền con (ví dụ: *.example.com) vẫn hợp lệ và an toàn để so khớp nhiều miền con (ví dụ: foo.example.com, bar.example.com).

    Lưu ý: Mặc dù các quy tắc nguồn gốc bảo vệ khỏi các trang web độc hại của bên thứ ba và iframe ẩn, nhưng chúng không thể bảo vệ khỏi các lỗ hổng tập lệnh trên nhiều trang web (XSS) trong miền đáng tin cậy của riêng bạn. Ví dụ: nếu trang web của bạn hiển thị nội dung do người dùng tạo và dễ bị tấn công XSS được lưu trữ, thì kẻ tấn công có thể thực thi một tập lệnh đóng vai trò là nguồn gốc đáng tin cậy của bạn. Hãy cân nhắc việc áp dụng quy trình xác thực cho tải trọng thông báo trước khi thực thi các thao tác nhạy cảm trên nền tảng gốc.

  • Giảm thiểu diện tích bề mặt: Chỉ hiển thị các phương thức hoặc dữ liệu cụ thể mà trang web yêu cầu.

  • Kiểm tra các tính năng trong thời gian chạy: Các API cầu nối gần đây, bao gồm cả addWebMessageListener, là một phần của thư viện Jetpack Webkit. Vì vậy, hãy luôn kiểm tra xem có hỗ trợ bằng WebViewFeature.isFeatureSupported() hay không trước khi gọi.