دسترسی به API های بومی با پل جاوا اسکریپت

این صفحه به بررسی روش‌ها و بهترین شیوه‌های مختلف برای ایجاد یک پل بومی، که با نام پل جاوا اسکریپت نیز شناخته می‌شود، می‌پردازد تا ارتباط بین محتوای وب در یک WebView و یک برنامه اندروید میزبان را تسهیل کند.

این امر توسعه‌دهندگان وب را قادر می‌سازد تا از جاوا اسکریپت برای دسترسی به ویژگی‌های بومی پلتفرم - مانند دوربین، سیستم فایل یا حسگرهای سخت‌افزاری پیشرفته - که APIهای استاندارد وب معمولاً ارائه نمی‌دهند، استفاده کنند.

موارد استفاده

پیاده‌سازی پل جاوااسکریپت، سناریوهای مختلف ادغام را که در آن‌ها محتوای وب نیاز به دسترسی عمیق‌تر به سیستم عامل اندروید دارد، امکان‌پذیر می‌کند. در زیر چند نمونه آورده شده است:

  • یکپارچه‌سازی پلتفرم : فعال‌سازی اجزای رابط کاربری بومی اندروید (برای مثال، Biometric prompts، BottomSheetDialog ) از یک صفحه وب.
  • عملکرد : واگذاری وظایف محاسباتی سنگین به کد بومی جاوا یا کاتلین.
  • ماندگاری داده‌ها : دسترسی به پایگاه‌های داده رمزگذاری شده محلی یا تنظیمات برگزیده مشترک.
  • انتقال داده‌های حجیم : انتقال فایل‌های رسانه‌ای یا ساختارهای داده پیچیده بین برنامه و رندرکننده وب.

مکانیسم‌های ارتباطی

اندروید سه نسل اصلی از APIها را برای ایجاد یک پل بومی ارائه می‌دهد. اگرچه همه آنها هنوز در دسترس هستند، اما از نظر امنیت، قابلیت استفاده و عملکرد تفاوت‌های چشمگیری دارند.

استفاده از addWebMessageListener (توصیه می‌شود)

addWebMessageListener مدرن‌ترین و توصیه‌شده‌ترین رویکرد برای ارتباط بین محتوای وب و کد برنامه بومی است. این رویکرد، سهولت استفاده از رابط جاوا اسکریپت را با امنیت سیستم پیام‌رسانی ترکیب می‌کند.

نحوه کار : برنامه یک شنونده با نام خاص و مجموعه‌ای از قوانین مبدا مجاز اضافه می‌کند. سپس WebView از لحظه شروع بارگذاری صفحه، اطمینان حاصل می‌کند که شیء جاوا اسکریپت در محدوده سراسری ( window.objectName ) وجود دارد.

مقداردهی اولیه : برای اطمینان از اینکه WebView قبل از اجرای هر اسکریپتی، شیء جاوا اسکریپت را تزریق می‌کند، باید قبل از پیمایش به صفحه (مانند فراخوانی WebViewCompat.navigate یا loadUrladdWebMessageListener فراخوانی کنید.

ویژگی‌های کلیدی :

  • امنیت و اعتماد : برخلاف APIهای قدیمی، این روش در طول مقداردهی اولیه به Set<String> از allowedOriginRules نیاز دارد. این مکانیسم اصلی برای ایجاد اعتماد است.

    وقتی یک منبع معتبر مانند https://example.com را مشخص می‌کنید، WebView تضمین می‌کند که اشیاء جاوا اسکریپت تزریق شده را فقط در صفحات وب بارگذاری شده از همان منبع دقیق نمایش دهد.

    تابع فراخوانی شنونده نیتیو (native listener) با هر پیام، یک پارامتر sourceOrigin دریافت می‌کند. اگر بریج شما از چندین مبدا مجاز پشتیبانی می‌کند، می‌توانید از این برای تأیید مبدا دقیق فرستنده استفاده کنید.

    از آنجا که وب‌ویو این بررسی‌های مبدا را در سطح پلتفرم به شدت اجرا می‌کند، برنامه شما می‌تواند به طور کلی به پیام‌های دریافتی از یک sourceOrigin معتبر به عنوان پیام‌های معتبر تکیه کند و نیاز به اعتبارسنجی دقیق بار داده را در اکثر پیاده‌سازی‌های استاندارد از بین ببرد.

    • وب ویو قوانین را با طرح (HTTP/HTTPS)، میزبان و پورت مطابقت می‌دهد.
    • وب ویو مسیرها را نادیده می‌گیرد. برای مثال، https://example.com https://example.com/login و https://example.com/home اجازه می‌دهد.
    • وب‌ویو به شدت استفاده از wildcardها را برای زیردامنه‌ها به ابتدای میزبان محدود می‌کند. برای مثال، https://*.example.com با https://foo.example.com مطابقت دارد اما با https://example.com مطابقت ندارد. اگر نیاز دارید که هم https://example.com و هم زیردامنه‌های آن را مطابقت دهید، باید هر قانون مبدا را جداگانه به لیست مجاز اضافه کنید (برای مثال، "https://example.com", "https://*.example.com" ). شما نمی‌توانید از wildcardها برای طرح یا در وسط یک دامنه استفاده کنید.

    این کار، پل را به دامنه‌های تأیید شده محدود می‌کند و از اجرای کد بومی توسط محتوای غیرمجاز شخص ثالث یا iframeهای تزریق‌شده جلوگیری می‌کند.

  • پشتیبانی از چند فریم : در تمام فریم‌هایی که با قوانین مبدا مطابقت دارند، کار می‌کند.

  • Threading : فراخوانی شنونده (listener callback) روی thread اصلی (UI) برنامه اجرا می‌شود. اگر bridge شما نیاز به پردازش داده‌های پیچیده، تجزیه JSON یا جستجو در پایگاه داده دارد، باید این کار را به یک thread پس‌زمینه منتقل کنید تا از هنگ کردن UI برنامه با خطای "app not responding" (ANR) جلوگیری شود.

  • دوطرفه : وقتی صفحه وب پیامی ارسال می‌کند، برنامه یک JavaScriptReplyProxy دریافت می‌کند که می‌تواند از آن برای ارسال پیام‌ها به آن فریم خاص استفاده کند. شما می‌توانید این شیء replyProxy را نگه دارید و در هر زمانی از آن برای ارسال هر تعداد پیام به صفحه استفاده کنید، نه فقط برای پاسخ به هر پیام جداگانه‌ای که صفحه ارسال می‌کند. اگر فریم مبدا حرکت نکند یا از بین برود، پیام‌های ارسالی با استفاده از postMessage() روی پروکسی بی‌صدا نادیده گرفته می‌شوند.

  • شروع به کار در سمت برنامه : اگرچه صفحه وب همیشه باید کانال ارتباطی با برنامه را آغاز کند، برنامه بومی می‌تواند به طور یکجانبه صفحه وب را برای شروع این فرآیند ترغیب کند. برنامه بومی می‌تواند با استفاده از addDocumentStartJavaScript() (برای ارزیابی جاوا اسکریپت قبل از بارگذاری صفحه) یا evaluateJavaScript() (برای ارزیابی جاوا اسکریپت پس از بارگذاری صفحه) با صفحه وب ارتباط برقرار کند.

محدودیت : این API داده‌ها را به صورت رشته یا آرایه‌های byte[] ارسال می‌کند. برای ساختارهای داده پیچیده‌تر، مانند اشیاء JSON، باید این را به یکی از این قالب‌ها سریالایز کنید و سپس در طرف دیگر deserialize کنید تا ساختار داده بازسازی شود.

مثال کاربرد :

برای درک کامل توالی تبادل پیام دو طرفه، رویدادها به این ترتیب ادامه می‌یابند:

  1. شروع (برنامه) : برنامه بومی شنونده را با addWebMessageListener ثبت می‌کند و پیمایش صفحه را آغاز می‌کند (مانند WebViewCompat.navigate یا loadUrl ).
  2. ارسال پیام (وب) : جاوا اسکریپت صفحه وب، myObject.postMessage(message) را برای شروع ارتباط فراخوانی می‌کند.
  3. دریافت و پاسخ پیام (برنامه) : برنامه پیام را در callback شنونده دریافت می‌کند و با استفاده از replyProxy.postMessage() ارائه شده پاسخ می‌دهد.
  4. دریافت پاسخ (وب) : صفحه وب پاسخ ناهمزمان را در تابع فراخوانی myObject.onmessage() دریافت می‌کند.

کاتلین

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

جاوا

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

کد جاوا اسکریپت زیر پیاده‌سازی سمت کلاینت addWebMessageListener را نشان می‌دهد که به محتوای وب اجازه می‌دهد پیام‌ها را از برنامه بومی دریافت کند و پیام‌های خود را از طریق پروکسی myObject ارسال کند.

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

استفاده از postWebMessage (جایگزین)

اندروید این را معرفی کرد تا یک جایگزین مبتنی بر پیام‌رسانی ناهمزمان، مشابه window.postMessage وب، ارائه دهد.

نحوه کار : این برنامه از WebViewCompat.postWebMessage برای ارسال یک payload به فریم اصلی صفحه وب استفاده می‌کند. برای ایجاد یک کانال ارتباطی دو طرفه، می‌توانید یک WebMessageChannel ایجاد کنید و یکی از پورت‌های آن را به همراه پیام به محتوای وب منتقل کنید.

ویژگی‌ها :

  • ناهمگام (Asynchronous ): مانند addWebMessageListener ، این متد از پیام‌رسانی ناهمگام استفاده می‌کند که تضمین می‌کند صفحه وب در حالی که برنامه داده‌ها را در پس‌زمینه پردازش می‌کند، به تعاملات کاربر پاسخگو باقی می‌ماند.
  • آگاه از مبدا : می‌توانید یک targetOrigin مشخص کنید تا مطمئن شوید که WebView داده‌ها را فقط به یک وب‌سایت معتبر ارسال می‌کند.

محدودیت‌ها :

  • محدوده : این API ارتباط را به فریم اصلی محدود می‌کند. از آدرس‌دهی مستقیم یا ارسال پیام به iframeها پشتیبانی نمی‌کند.
  • محدودیت‌های URI : شما نمی‌توانید از این روش برای محتوایی که با استفاده data: URIs، file: URIs یا loadData() بارگذاری می‌شود استفاده کنید، مگر اینکه "*" را به عنوان مبدا هدف مشخص کنید. انجام این کار به هر صفحه‌ای اجازه می‌دهد پیام را دریافت کند.
  • خطر هویت : هیچ راه مشخصی برای تأیید هویت فرستنده توسط محتوای وب وجود ندارد. پیامی که صفحه وب دریافت می‌کند می‌تواند از برنامه اصلی شما یا یک iframe دیگر باشد.

از این متد زمانی استفاده کنید که به یک کانال ساده و غیرهمزمان برای داده‌های مبتنی بر رشته در نسخه‌های قدیمی‌تر اندروید که addWebMessageListener پشتیبانی نمی‌کنند، نیاز دارید.

استفاده از addJavascriptInterface (قدیمی)

قدیمی‌ترین روش شامل تزریق مستقیم یک نمونه شیء بومی به WebView است.

نحوه کار : شما یک کلاس Kotlin یا Java تعریف می‌کنید، متدهای مجاز را با @JavascriptInterface حاشیه‌نویسی می‌کنید و با استفاده از addJavascriptInterface(Object, String) یک نمونه از کلاس را به WebView اضافه می‌کنید.

ویژگی‌ها :

  • همگام (Synchronous ): محیط اجرای جاوا اسکریپت تا زمانی که متد موجود در کد اندروید شما خروجی دهد، مسدود می‌شود.
  • ایمنی نخ : سیستم متدها را روی یک نخ پس‌زمینه فراخوانی می‌کند که نیاز به همگام‌سازی دقیق در سمت کاتلین یا جاوا دارد.
  • ریسک امنیتی : به طور پیش‌فرض، addJavascriptInterface برای هر فریم درون WebView، از جمله iframeها، در دسترس است. فاقد کنترل دسترسی مبتنی بر مبدا است. به دلیل رفتار ناهمزمان WebView، تعیین ایمن URL فریمی که رابط شما را فراخوانی می‌کند، امکان‌پذیر نیست. شما نباید برای تأیید امنیت به روش‌هایی مانند WebView.getUrl() تکیه کنید، زیرا تضمینی برای دقت آنها وجود ندارد و مشخص نمی‌کنند که کدام فریم خاص درخواست را انجام داده است.

تبدیل نوع داده‌ها و اجبار

هنگام استفاده از addJavascriptInterface ، پل جاوا مبتنی بر Chromium انواع داده‌ها را بین زمان اجرای جاوا اسکریپت و کد برنامه اندروید شما تبدیل می‌کند.

قوانین اجباری زیر برای پارامترهای متد و مقادیر برگشتی اعمال می‌شوند.

نگاشت نوع پارامتر (از جاوا اسکریپت به جاوا)

وقتی جاوا اسکریپت آرگومان‌ها را به یک متد حاشیه‌نویسی شده جاوا یا کاتلین ارسال می‌کند، پل، مقادیر جاوا اسکریپت را به انواع پارامترهای جاوای مربوطه تبدیل می‌کند:

نوع پارامتر جاوا مقدار آرگومان جاوا اسکریپت رفتار اجباری
byte ، short ، int ، long عدد (عدد صحیح) مقادیر به نوع عدد صحیح هدف تبدیل می‌شوند. مقادیر خارج از محدوده طبق قوانین استاندارد تبدیل عددی، به اطراف منتقل می‌شوند.
byte ، short ، int ، long NaN به 0 وادار می‌کند.
byte ، short ، int ، long Infinity برای byte و short به -1 و برای int و long به Integer.MAX_VALUE و Long.MAX_VALUE اجبار می‌کند.
float ، double شماره به مقدار ممیز شناور متناظر جاوا تبدیل می‌کند.
float ، double NaN / Infinity تابع ()Coerces را به Float.NaN ، Double.NaN ، Float.POSITIVE_INFINITY یا Double.POSITIVE_INFINITY وادار می‌کند.
char عدد (عدد صحیح) به کد پوینت یونیکد مربوطه تبدیل شد.
char غیر صحیح، NaN ، Infinity وادار به \u0000 می‌کند.
boolean true / false جاوا را مجبور به true یا false می‌کند.
boolean عدد، رشته، شیء اجباراً مقدار را false برمی‌گرداند (شامل رشته‌های غیر تهی و اعداد غیر صفر).
String رشته مقدار رشته حفظ می‌شود.
String عدد، بولی به صورت رشته‌ای قالب‌بندی شده است (برای مثال، "42" ، "true" ، "false" ).
String null / undefined null به جاوا null و undefined به رشته‌ی تحت‌اللفظی "undefined" اجبار می‌کند.
String شیء، ArrayBuffer، TypedArray رشته‌ی تحت‌اللفظی "undefined" را اجبار می‌کند.
آرایه اولیه (مانند int[] ، byte[] ، boolean[] ) یا String[] آرایه ( [...] ) به یک آرایه تک بعدی جاوا از نوع عنصر هدف تبدیل می‌شود. آرایه‌های پراکنده، اندیس‌های اختصاص داده نشده را با مقادیر پیش‌فرض ( 0 ، false ، null ) پر می‌کنند.
آرایه اولیه (مانند int[] ، byte[] ) آرایه نوع‌بندی‌شده ( Int8Array ، Uint8Array ، Int32Array ، Float64Array ) عناصر به اجبار در آرایه اولیه جاوا مربوطه قرار می‌گیرند.
آرایه چند بعدی (مانند int[][] ) آرایه تو در تو ( [[...]] ) پشتیبانی نمی‌شود. پارامترهای آرایه چندبعدی مقدار null را ارزیابی می‌کنند.
ArrayBuffer ، DataView ArrayBuffer ، DataView به عنوان آرایه پشتیبانی نمی‌شود. نمونه‌های ArrayBuffer و DataView مقدار null را ارزیابی می‌کنند.
Object یا کلاس سفارشی شیء جاوا اسکریپت ( {...} ) پشتیبانی نمی‌شود. مقادیر دلخواه شیء جاوا اسکریپت در جاوا به null ارزیابی می‌شوند.
Object یا کلاس سفارشی پوشش دهنده شیء جاوا تزریق شده پشتیبانی شده (رفت و برگشت). نمونه جاوای زیرین را به متد جاوا ارسال می‌کند. اگر نوع جاوا با امضای پارامتر مطابقت نداشته باشد، یک استثنای جاوا اسکریپت ایجاد می‌کند.
انواع جعبه‌ای (مانند Integer ، Double ، Boolean ) عدد، بولی پشتیبانی نمی‌شود. انواع اولیه‌ی جعبه‌ای به عنوان اشیاء مات در نظر گرفته می‌شوند و مقدار null را دریافت می‌کنند.
هر نوع اولیه null / undefined مقادیر پیش‌فرض ( 0 ، 0.0 ، \u0000 ، false ) را اجبار می‌کند.
Object ، String ، آرایه null جاوا را به null وادار می‌کند.
نگاشت نوع بازگشتی (جاوا به جاوا اسکریپت)

وقتی یک متد جاوا یا کاتلینِ حاشیه‌نویسی‌شده مقداری را برمی‌گرداند، پل آن را به یک نوع جاوااسکریپت تبدیل می‌کند:

نوع بازگشتی جاوا مقدار جاوا اسکریپت typeof جاوا اسکریپت
boolean true / false "boolean"
byte ، short ، int ، long ، float ، double شماره "number"
char شماره (کد نقطه یونیکد) "number"
String (غیر تهی) مقدار رشته‌ای "string"
String ( null ) undefined "undefined"
void undefined "undefined"
آرایه جاوا (مانند int[] ، String[] ) undefined "undefined" . مقادیر بازگشتی آرایه پشتیبانی نمی‌شوند. متد جاوا اجرا نمی‌شود و undefined بدون ایجاد استثنا بازگردانده می‌شود.
شیء جاوا / نوع سفارشی (غیر تهی) پوشش دهنده شیء "object" . یک پوشش جاوااسکریپت در اطراف نمونه جاوا ایجاد می‌کند. کد جاوااسکریپت می‌تواند هر متد عمومی را روی این شیء که با @JavascriptInterface حاشیه‌نویسی شده است، فراخوانی کند.
شیء جاوا / نوع سفارشی ( null ) null "object"
انواع اولیه جعبه‌ای (مانند Integer ، Double ) پوشش دهنده شیء "object" . به صورت یک پوشش شیء جاوای غیرشفاف بدون هیچ متد قابل دسترسی @JavascriptInterface برگردانده می‌شود و مقدار را در جاوا اسکریپت غیرقابل استفاده می‌کند.

دسترسی‌پذیری متد و عضو

پل جاوا اسکریپت، قوانین سختگیرانه‌ای برای دسترسی اعضا و قابلیت مشاهده اعمال می‌کند تا از اجرای ناخواسته کد جلوگیری کند:

  • فیلدها نمایش داده نمی‌شوند : فیلدهای جاوا (از جمله فیلدهای public و public final ) از طریق جاوا اسکریپت قابل دسترسی نیستند و به صورت undefined ارزیابی می‌شوند.
  • الزام حاشیه‌نویسی : فقط متدهایی که صریحاً با @JavascriptInterface حاشیه‌نویسی شده‌اند، در معرض جاوا اسکریپت قرار می‌گیرند.
  • محدودیت‌های دید : متدها باید public باشند. متدهای private و protected هرگز در معرض جاوااسکریپت قرار نمی‌گیرند، حتی اگر حاشیه‌نویسی @JavascriptInterface را داشته باشند.
  • متدهای استاتیک : متدهای استاتیک که با @JavascriptInterface حاشیه‌نویسی شده‌اند، از جاوا اسکریپت قابل فراخوانی هستند.
  • وراثت و بازنویسی : حاشیه‌نویسی‌های @JavascriptInterface زمانی که یک زیرکلاس یک متد را بازنویسی می‌کند، به ارث برده نمی‌شوند. اگر یک زیرکلاس یک متد حاشیه‌نویسی‌شده از یک ابرکلاس را بازنویسی کند، زیرکلاس باید صریحاً حاشیه‌نویسی @JavascriptInterface را در متد بازنویسی‌شده قرار دهد تا آن را در معرض جاوااسکریپت قرار دهد. متدهای عمومی بازنویسی‌نشده که از یک ابرکلاس به ارث رسیده‌اند، در صورت حاشیه‌نویسی در ابرکلاس، قابل دسترسی باقی می‌مانند.
  • محافظت در برابر بازتاب : متدهای استاندارد بازتاب جاوا (مانند getClass() ) مسدود شده و یک استثنای جاوا اسکریپت ایجاد می‌کنند تا از آسیب‌پذیری‌های اجرای کد از راه دور جلوگیری شود.
  • سربارگذاری متد : متدهای جاوای سربارگذاری‌شده پشتیبانی می‌شوند. پل، فراخوانی‌های متد را فقط بر اساس تعداد آرگومان‌های ارسالی حل می‌کند و انواع آرگومان‌ها را در نظر نمی‌گیرد. فراخوانی یک متد سربارگذاری‌شده با تعداد آرگومان نامعتبر، یک خطای جاوا اسکریپت ایجاد می‌کند. اگر دو سربارگذاری تعداد آرگومان یکسانی داشته باشند، یکی به طور دلخواه انتخاب می‌شود.

خلاصه مکانیسم‌ها

جدول زیر مقایسه‌ای سریع از سه مکانیسم اصلی پیاده‌سازی پل بومی ارائه می‌دهد:

روش addWebMessageListener postWebMessage addJavascriptInterface
پیاده‌سازی ناهمزمان (شنونده روی نخ اصلی) ناهمزمان همزمان
امنیت بالاترین (مبتنی بر لیست مجاز) بالا (آگاه از مبدا) کم (بدون بررسی مبدا)
پیچیدگی متوسط متوسط ساده
جهت دو جهته دو جهته وب به اپلیکیشن
حداقل نسخه وب ویو نسخه ۸۲ (و Jetpack Webkit 1.3.0) نسخه ۴۵ (و Jetpack Webkit 1.1.0) همه نسخه‌ها
توصیه شده بله خیر خیر

انتقال داده‌های بزرگ را مدیریت کنید

هنگام انتقال بارهای داده بزرگ، مانند رشته‌های چند مگابایتی یا فایل‌های باینری، باید حافظه را با دقت مدیریت کنید تا از خطاهای عدم پاسخگویی برنامه (ANR) یا خرابی در دستگاه‌های 32 بیتی جلوگیری شود. این بخش به بررسی تکنیک‌ها و محدودیت‌های مختلف مرتبط با انتقال حجم قابل توجهی از داده‌ها بین برنامه میزبان و محتوای وب می‌پردازد.

انتقال داده‌های دودویی با آرایه‌های بایتی

با کلاس WebMessageCompat ، می‌توانید آرایه‌های byte[] را مستقیماً به جای سریال‌سازی داده‌های دودویی به رشته‌های Base64 ارسال کنید. از آنجایی که Base64 تقریباً 33٪ سربار به اندازه داده‌ها اضافه می‌کند، این روش به طور قابل توجهی از نظر حافظه کارآمدتر و سریع‌تر است.

  • مزیت دودویی : انتقال داده‌های دودویی مانند فایل‌های تصویری یا صوتی بین برنامه بومی و محتوای وب شما.
  • محدودیت : حتی با آرایه‌های بایتی، سیستم داده‌ها را در مرز ارتباط بین فرآیندی (IPC) بین برنامه و فرآیند ایزوله‌ای که WebView برای رندر محتوای وب استفاده می‌کند، کپی می‌کند. این روش هنوز هم برای فایل‌های بسیار بزرگ، حافظه قابل توجهی را مصرف می‌کند.

مثال‌های کد زیر نحوه تنظیم addWebMessageListener در سمت برنامه بومی نشان می‌دهند تا پیام‌های علامت‌گذاری شده با WebMessageCompat.TYPE_ARRAY_BUFFER را دریافت کرده و به صورت اختیاری با بررسی WebViewFeature.MESSAGE_ARRAY_BUFFER با داده‌های دودویی پاسخ دهد.

کاتلین

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

جاوا

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

کد جاوا اسکریپت زیر پیاده‌سازی addWebMessageListener در سمت کلاینت را نشان می‌دهد که به محتوای وب امکان ارسال و دریافت داده‌های باینری ( ArrayBuffer ) به و از برنامه بومی را با استفاده از پروکسی window.myBridge که در مثال قبلی تزریق شده است، می‌دهد.

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

بارگذاری کارآمد داده‌ها در مقیاس بزرگ

برای فایل‌های بسیار بزرگ (بیش از 10 مگابایت)، از متد shouldInterceptRequest برای استریم داده‌ها استفاده کنید:

  1. صفحه وب، فراخوانی تابع fetch() را به یک آدرس اینترنتی (URL) سفارشی و جاگذاری‌شده آغاز می‌کند. برای مثال، https://app.local/large-file .
  2. برنامه اندروید این درخواست را در WebViewClient .shouldInterceptRequest رهگیری می‌کند.
  3. برنامه داده‌ها را به عنوان یک InputStream برمی‌گرداند.

این امر امکان پخش داده‌ها را به صورت تکه‌ای فراهم می‌کند، به جای اینکه کل بار داده به طور همزمان در حافظه بارگذاری شود.

تابع جاوا اسکریپت زیر، کد سمت کلاینت را برای بارگذاری کارآمد یک فایل باینری بزرگ از برنامه بومی با استفاده از فراخوانی استاندارد fetch() به یک URL سفارشی و placeholder نشان می‌دهد.

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

مثال‌های کد زیر، سمت برنامه‌ی بومی را نشان می‌دهند که با استفاده از متد WebViewClient.shouldInterceptRequest در کاتلین و جاوا، یک فایل باینری بزرگ را با رهگیری یک URL سفارشی درخواست شده توسط محتوای وب، استریم می‌کند.

کاتلین

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

جاوا

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

رعایت توصیه‌های امنیتی

برای محافظت از برنامه و داده‌های کاربر، هنگام پیاده‌سازی یک پل، این دستورالعمل‌ها را دنبال کنید:

  • اجرای HTTPS : برای اطمینان از اینکه محتوای مخرب شخص ثالث نمی‌تواند منطق بومی برنامه شما را فراخوانی کند، فقط ارتباط با منابع امن را مجاز کنید.

  • به قوانین مبدا تکیه کنید : بهترین راه برای مقابله با اعتماد، تعریف دقیق allowedOriginRules و بررسی sourceOrigin ارائه شده در callback پیام است. از استفاده از wildcard کامل ( * ) که با همه originها مطابقت دارد، به عنوان تنها قانون origin خود، مگر در موارد ضروری، خودداری کنید. استفاده از wildcardها برای زیر دامنه‌ها (به عنوان مثال، *.example.com ) برای تطبیق چندین زیر دامنه (به عنوان مثال، foo.example.com ، bar.example.com ) معتبر و ایمن باقی می‌ماند.

    توجه : اگرچه قوانین مبدا در برابر وب‌سایت‌های شخص ثالث مخرب و iframe های پنهان محافظت می‌کنند، اما نمی‌توانند در برابر آسیب‌پذیری‌های اسکریپت‌نویسی بین سایتی (XSS) در دامنه مورد اعتماد شما محافظت کنند. به عنوان مثال، اگر صفحه وب شما محتوای تولید شده توسط کاربر را نمایش می‌دهد و در برابر XSS ذخیره شده آسیب‌پذیر است، یک مهاجم می‌تواند اسکریپتی را اجرا کند که به عنوان مبدا مورد اعتماد شما عمل می‌کند. قبل از اجرای عملیات حساس پلتفرم بومی، اعتبارسنجی را برای بارهای داده پیام در نظر بگیرید.

  • به حداقل رساندن مساحت سطح : فقط روش‌ها یا داده‌های خاصی را که صفحه وب نیاز دارد، نمایش دهید.

  • بررسی ویژگی‌ها در زمان اجرا : APIهای پل اخیر، از جمله addWebMessageListener ، بخشی از کتابخانه Jetpack Webkit هستند. بنابراین، همیشه قبل از فراخوانی آنها، با استفاده از WebViewFeature.isFeatureSupported() پشتیبانی آنها را بررسی کنید.

،

این صفحه به بررسی روش‌ها و بهترین شیوه‌های مختلف برای ایجاد یک پل بومی، که با نام پل جاوا اسکریپت نیز شناخته می‌شود، می‌پردازد تا ارتباط بین محتوای وب در یک WebView و یک برنامه اندروید میزبان را تسهیل کند.

این امر توسعه‌دهندگان وب را قادر می‌سازد تا از جاوا اسکریپت برای دسترسی به ویژگی‌های بومی پلتفرم - مانند دوربین، سیستم فایل یا حسگرهای سخت‌افزاری پیشرفته - که APIهای استاندارد وب معمولاً ارائه نمی‌دهند، استفاده کنند.

موارد استفاده

پیاده‌سازی پل جاوااسکریپت، سناریوهای مختلف ادغام را که در آن‌ها محتوای وب نیاز به دسترسی عمیق‌تر به سیستم عامل اندروید دارد، امکان‌پذیر می‌کند. در زیر چند نمونه آورده شده است:

  • یکپارچه‌سازی پلتفرم : فعال‌سازی اجزای رابط کاربری بومی اندروید (برای مثال، Biometric prompts، BottomSheetDialog ) از یک صفحه وب.
  • عملکرد : واگذاری وظایف محاسباتی سنگین به کد بومی جاوا یا کاتلین.
  • ماندگاری داده‌ها : دسترسی به پایگاه‌های داده رمزگذاری شده محلی یا تنظیمات برگزیده مشترک.
  • انتقال داده‌های حجیم : انتقال فایل‌های رسانه‌ای یا ساختارهای داده پیچیده بین برنامه و رندرکننده وب.

مکانیسم‌های ارتباطی

اندروید سه نسل اصلی از APIها را برای ایجاد یک پل بومی ارائه می‌دهد. اگرچه همه آنها هنوز در دسترس هستند، اما از نظر امنیت، قابلیت استفاده و عملکرد تفاوت‌های چشمگیری دارند.

استفاده از addWebMessageListener (توصیه می‌شود)

addWebMessageListener مدرن‌ترین و توصیه‌شده‌ترین رویکرد برای ارتباط بین محتوای وب و کد برنامه بومی است. این رویکرد، سهولت استفاده از رابط جاوا اسکریپت را با امنیت سیستم پیام‌رسانی ترکیب می‌کند.

نحوه کار : برنامه یک شنونده با نام خاص و مجموعه‌ای از قوانین مبدا مجاز اضافه می‌کند. سپس WebView از لحظه شروع بارگذاری صفحه، اطمینان حاصل می‌کند که شیء جاوا اسکریپت در محدوده سراسری ( window.objectName ) وجود دارد.

مقداردهی اولیه : برای اطمینان از اینکه WebView قبل از اجرای هر اسکریپتی، شیء جاوا اسکریپت را تزریق می‌کند، باید قبل از پیمایش به صفحه (مانند فراخوانی WebViewCompat.navigate یا loadUrladdWebMessageListener فراخوانی کنید.

ویژگی‌های کلیدی :

  • امنیت و اعتماد : برخلاف APIهای قدیمی، این روش در طول مقداردهی اولیه به Set<String> از allowedOriginRules نیاز دارد. این مکانیسم اصلی برای ایجاد اعتماد است.

    وقتی یک منبع معتبر مانند https://example.com را مشخص می‌کنید، WebView تضمین می‌کند که اشیاء جاوا اسکریپت تزریق شده را فقط در صفحات وب بارگذاری شده از همان منبع دقیق نمایش دهد.

    تابع فراخوانی شنونده نیتیو (native listener) با هر پیام، یک پارامتر sourceOrigin دریافت می‌کند. اگر بریج شما از چندین مبدا مجاز پشتیبانی می‌کند، می‌توانید از این برای تأیید مبدا دقیق فرستنده استفاده کنید.

    از آنجا که وب‌ویو این بررسی‌های مبدا را در سطح پلتفرم به شدت اجرا می‌کند، برنامه شما می‌تواند به طور کلی به پیام‌های دریافتی از یک sourceOrigin معتبر به عنوان پیام‌های معتبر تکیه کند و نیاز به اعتبارسنجی دقیق بار داده را در اکثر پیاده‌سازی‌های استاندارد از بین ببرد.

    • وب ویو قوانین را با طرح (HTTP/HTTPS)، میزبان و پورت مطابقت می‌دهد.
    • وب ویو مسیرها را نادیده می‌گیرد. برای مثال، https://example.com https://example.com/login و https://example.com/home اجازه می‌دهد.
    • وب‌ویو به شدت استفاده از wildcardها را برای زیردامنه‌ها به ابتدای میزبان محدود می‌کند. برای مثال، https://*.example.com با https://foo.example.com مطابقت دارد اما با https://example.com مطابقت ندارد. اگر نیاز دارید که هم https://example.com و هم زیردامنه‌های آن را مطابقت دهید، باید هر قانون مبدا را جداگانه به لیست مجاز اضافه کنید (برای مثال، "https://example.com", "https://*.example.com" ). شما نمی‌توانید از wildcardها برای طرح یا در وسط یک دامنه استفاده کنید.

    این کار، پل را به دامنه‌های تأیید شده محدود می‌کند و از اجرای کد بومی توسط محتوای غیرمجاز شخص ثالث یا iframeهای تزریق‌شده جلوگیری می‌کند.

  • پشتیبانی از چند فریم : در تمام فریم‌هایی که با قوانین مبدا مطابقت دارند، کار می‌کند.

  • Threading : فراخوانی شنونده (listener callback) روی thread اصلی (UI) برنامه اجرا می‌شود. اگر bridge شما نیاز به پردازش داده‌های پیچیده، تجزیه JSON یا جستجو در پایگاه داده دارد، باید این کار را به یک thread پس‌زمینه منتقل کنید تا از هنگ کردن UI برنامه با خطای "app not responding" (ANR) جلوگیری شود.

  • دوطرفه : وقتی صفحه وب پیامی ارسال می‌کند، برنامه یک JavaScriptReplyProxy دریافت می‌کند که می‌تواند از آن برای ارسال پیام‌ها به آن فریم خاص استفاده کند. شما می‌توانید این شیء replyProxy را نگه دارید و در هر زمانی از آن برای ارسال هر تعداد پیام به صفحه استفاده کنید، نه فقط برای پاسخ به هر پیام جداگانه‌ای که صفحه ارسال می‌کند. اگر فریم مبدا حرکت نکند یا از بین برود، پیام‌های ارسالی با استفاده از postMessage() روی پروکسی بی‌صدا نادیده گرفته می‌شوند.

  • شروع به کار در سمت برنامه : اگرچه صفحه وب همیشه باید کانال ارتباطی با برنامه را آغاز کند، برنامه بومی می‌تواند به طور یکجانبه صفحه وب را برای شروع این فرآیند ترغیب کند. برنامه بومی می‌تواند با استفاده از addDocumentStartJavaScript() (برای ارزیابی جاوا اسکریپت قبل از بارگذاری صفحه) یا evaluateJavaScript() (برای ارزیابی جاوا اسکریپت پس از بارگذاری صفحه) با صفحه وب ارتباط برقرار کند.

محدودیت : این API داده‌ها را به صورت رشته یا آرایه‌های byte[] ارسال می‌کند. برای ساختارهای داده پیچیده‌تر، مانند اشیاء JSON، باید این را به یکی از این قالب‌ها سریالایز کنید و سپس در طرف دیگر deserialize کنید تا ساختار داده بازسازی شود.

مثال کاربرد :

برای درک کامل توالی تبادل پیام دو طرفه، رویدادها به این ترتیب ادامه می‌یابند:

  1. شروع (برنامه) : برنامه بومی شنونده را با addWebMessageListener ثبت می‌کند و پیمایش صفحه را آغاز می‌کند (مانند WebViewCompat.navigate یا loadUrl ).
  2. ارسال پیام (وب) : جاوا اسکریپت صفحه وب، myObject.postMessage(message) را برای شروع ارتباط فراخوانی می‌کند.
  3. دریافت و پاسخ پیام (برنامه) : برنامه پیام را در callback شنونده دریافت می‌کند و با استفاده از replyProxy.postMessage() ارائه شده پاسخ می‌دهد.
  4. دریافت پاسخ (وب) : صفحه وب پاسخ ناهمزمان را در تابع فراخوانی myObject.onmessage() دریافت می‌کند.

کاتلین

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

جاوا

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

کد جاوا اسکریپت زیر پیاده‌سازی سمت کلاینت addWebMessageListener را نشان می‌دهد که به محتوای وب اجازه می‌دهد پیام‌ها را از برنامه بومی دریافت کند و پیام‌های خود را از طریق پروکسی myObject ارسال کند.

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

استفاده از postWebMessage (جایگزین)

اندروید این را معرفی کرد تا یک جایگزین مبتنی بر پیام‌رسانی ناهمزمان، مشابه window.postMessage وب، ارائه دهد.

نحوه کار : این برنامه از WebViewCompat.postWebMessage برای ارسال یک payload به فریم اصلی صفحه وب استفاده می‌کند. برای ایجاد یک کانال ارتباطی دو طرفه، می‌توانید یک WebMessageChannel ایجاد کنید و یکی از پورت‌های آن را به همراه پیام به محتوای وب منتقل کنید.

ویژگی‌ها :

  • ناهمگام (Asynchronous ): مانند addWebMessageListener ، این متد از پیام‌رسانی ناهمگام استفاده می‌کند که تضمین می‌کند صفحه وب در حالی که برنامه داده‌ها را در پس‌زمینه پردازش می‌کند، به تعاملات کاربر پاسخگو باقی می‌ماند.
  • آگاه از مبدا : می‌توانید یک targetOrigin مشخص کنید تا مطمئن شوید که WebView داده‌ها را فقط به یک وب‌سایت معتبر ارسال می‌کند.

محدودیت‌ها :

  • محدوده : این API ارتباط را به فریم اصلی محدود می‌کند. از آدرس‌دهی مستقیم یا ارسال پیام به iframeها پشتیبانی نمی‌کند.
  • محدودیت‌های URI : شما نمی‌توانید از این روش برای محتوایی که با استفاده data: URIs، file: URIs یا loadData() بارگذاری می‌شود استفاده کنید، مگر اینکه "*" را به عنوان مبدا هدف مشخص کنید. انجام این کار به هر صفحه‌ای اجازه می‌دهد پیام را دریافت کند.
  • خطر هویت : هیچ راه مشخصی برای تأیید هویت فرستنده توسط محتوای وب وجود ندارد. پیامی که صفحه وب دریافت می‌کند می‌تواند از برنامه اصلی شما یا یک iframe دیگر باشد.

از این متد زمانی استفاده کنید که به یک کانال ساده و غیرهمزمان برای داده‌های مبتنی بر رشته در نسخه‌های قدیمی‌تر اندروید که addWebMessageListener پشتیبانی نمی‌کنند، نیاز دارید.

استفاده از addJavascriptInterface (قدیمی)

قدیمی‌ترین روش شامل تزریق مستقیم یک نمونه شیء بومی به WebView است.

نحوه کار : شما یک کلاس Kotlin یا Java تعریف می‌کنید، متدهای مجاز را با @JavascriptInterface حاشیه‌نویسی می‌کنید و با استفاده از addJavascriptInterface(Object, String) یک نمونه از کلاس را به WebView اضافه می‌کنید.

ویژگی‌ها :

  • همگام (Synchronous ): محیط اجرای جاوا اسکریپت تا زمانی که متد موجود در کد اندروید شما خروجی دهد، مسدود می‌شود.
  • ایمنی نخ : سیستم متدها را روی یک نخ پس‌زمینه فراخوانی می‌کند که نیاز به همگام‌سازی دقیق در سمت کاتلین یا جاوا دارد.
  • ریسک امنیتی : به طور پیش‌فرض، addJavascriptInterface برای هر فریم درون WebView، از جمله iframeها، در دسترس است. فاقد کنترل دسترسی مبتنی بر مبدا است. به دلیل رفتار ناهمزمان WebView، تعیین ایمن URL فریمی که رابط شما را فراخوانی می‌کند، امکان‌پذیر نیست. شما نباید برای تأیید امنیت به روش‌هایی مانند WebView.getUrl() تکیه کنید، زیرا تضمینی برای دقت آنها وجود ندارد و مشخص نمی‌کنند که کدام فریم خاص درخواست را انجام داده است.

تبدیل نوع داده‌ها و اجبار

هنگام استفاده از addJavascriptInterface ، پل جاوا مبتنی بر Chromium انواع داده‌ها را بین زمان اجرای جاوا اسکریپت و کد برنامه اندروید شما تبدیل می‌کند.

قوانین اجباری زیر برای پارامترهای متد و مقادیر برگشتی اعمال می‌شوند.

نگاشت نوع پارامتر (از جاوا اسکریپت به جاوا)

وقتی جاوا اسکریپت آرگومان‌ها را به یک متد حاشیه‌نویسی شده جاوا یا کاتلین ارسال می‌کند، پل، مقادیر جاوا اسکریپت را به انواع پارامترهای جاوای مربوطه تبدیل می‌کند:

نوع پارامتر جاوا مقدار آرگومان جاوا اسکریپت رفتار اجباری
byte ، short ، int ، long عدد (عدد صحیح) مقادیر به نوع عدد صحیح هدف تبدیل می‌شوند. مقادیر خارج از محدوده طبق قوانین استاندارد تبدیل عددی، به اطراف منتقل می‌شوند.
byte ، short ، int ، long NaN به 0 وادار می‌کند.
byte ، short ، int ، long Infinity برای byte و short به -1 و برای int و long به Integer.MAX_VALUE و Long.MAX_VALUE اجبار می‌کند.
float ، double شماره به مقدار ممیز شناور متناظر جاوا تبدیل می‌کند.
float ، double NaN / Infinity تابع ()Coerces را به Float.NaN ، Double.NaN ، Float.POSITIVE_INFINITY یا Double.POSITIVE_INFINITY وادار می‌کند.
char عدد (عدد صحیح) به کد پوینت یونیکد مربوطه تبدیل شد.
char غیر صحیح، NaN ، Infinity وادار به \u0000 می‌کند.
boolean true / false جاوا را مجبور به true یا false می‌کند.
boolean عدد، رشته، شیء اجباراً مقدار را false برمی‌گرداند (شامل رشته‌های غیر تهی و اعداد غیر صفر).
String رشته مقدار رشته حفظ می‌شود.
String عدد، بولی به صورت رشته‌ای قالب‌بندی شده است (برای مثال، "42" ، "true" ، "false" ).
String null / undefined null به جاوا null و undefined به رشته‌ی تحت‌اللفظی "undefined" اجبار می‌کند.
String شیء، ArrayBuffer، TypedArray رشته‌ی تحت‌اللفظی "undefined" را اجبار می‌کند.
آرایه اولیه (مانند int[] ، byte[] ، boolean[] ) یا String[] آرایه ( [...] ) به یک آرایه تک بعدی جاوا از نوع عنصر هدف تبدیل می‌شود. آرایه‌های پراکنده، اندیس‌های اختصاص داده نشده را با مقادیر پیش‌فرض ( 0 ، false ، null ) پر می‌کنند.
آرایه اولیه (مانند int[] ، byte[] ) آرایه نوع‌بندی‌شده ( Int8Array ، Uint8Array ، Int32Array ، Float64Array ) عناصر به اجبار در آرایه اولیه جاوا مربوطه قرار می‌گیرند.
آرایه چند بعدی (مانند int[][] ) آرایه تو در تو ( [[...]] ) پشتیبانی نمی‌شود. پارامترهای آرایه چندبعدی مقدار null را ارزیابی می‌کنند.
ArrayBuffer ، DataView ArrayBuffer ، DataView به عنوان آرایه پشتیبانی نمی‌شود. نمونه‌های ArrayBuffer و DataView مقدار null را ارزیابی می‌کنند.
Object یا کلاس سفارشی شیء جاوا اسکریپت ( {...} ) پشتیبانی نمی‌شود. مقادیر دلخواه شیء جاوا اسکریپت در جاوا به null ارزیابی می‌شوند.
Object یا کلاس سفارشی پوشش دهنده شیء جاوا تزریق شده پشتیبانی شده (رفت و برگشت). نمونه جاوای زیرین را به متد جاوا ارسال می‌کند. اگر نوع جاوا با امضای پارامتر مطابقت نداشته باشد، یک استثنای جاوا اسکریپت ایجاد می‌کند.
انواع جعبه‌ای (مانند Integer ، Double ، Boolean ) عدد، بولی پشتیبانی نمی‌شود. انواع اولیه‌ی جعبه‌ای به عنوان اشیاء مات در نظر گرفته می‌شوند و مقدار null را دریافت می‌کنند.
هر نوع اولیه null / undefined مقادیر پیش‌فرض ( 0 ، 0.0 ، \u0000 ، false ) را اجبار می‌کند.
Object ، String ، آرایه null جاوا را به null وادار می‌کند.
نگاشت نوع بازگشتی (جاوا به جاوا اسکریپت)

وقتی یک متد جاوا یا کاتلینِ حاشیه‌نویسی‌شده مقداری را برمی‌گرداند، پل آن را به یک نوع جاوااسکریپت تبدیل می‌کند:

نوع بازگشتی جاوا مقدار جاوا اسکریپت typeof جاوا اسکریپت
boolean true / false "boolean"
byte ، short ، int ، long ، float ، double شماره "number"
char شماره (کد نقطه یونیکد) "number"
String (غیر تهی) مقدار رشته‌ای "string"
String ( null ) undefined "undefined"
void undefined "undefined"
آرایه جاوا (مانند int[] ، String[] ) undefined "undefined" . مقادیر بازگشتی آرایه پشتیبانی نمی‌شوند. متد جاوا اجرا نمی‌شود و undefined بدون ایجاد استثنا بازگردانده می‌شود.
شیء جاوا / نوع سفارشی (غیر تهی) پوشش دهنده شیء "object" . یک پوشش جاوااسکریپت در اطراف نمونه جاوا ایجاد می‌کند. کد جاوااسکریپت می‌تواند هر متد عمومی را روی این شیء که با @JavascriptInterface حاشیه‌نویسی شده است، فراخوانی کند.
شیء جاوا / نوع سفارشی ( null ) null "object"
انواع اولیه جعبه‌ای (مانند Integer ، Double ) پوشش دهنده شیء "object" . به صورت یک پوشش شیء جاوای غیرشفاف بدون هیچ متد قابل دسترسی @JavascriptInterface برگردانده می‌شود و مقدار را در جاوا اسکریپت غیرقابل استفاده می‌کند.

دسترسی‌پذیری متد و عضو

پل جاوا اسکریپت، قوانین سختگیرانه‌ای برای دسترسی اعضا و قابلیت مشاهده اعمال می‌کند تا از اجرای ناخواسته کد جلوگیری کند:

  • فیلدها نمایش داده نمی‌شوند : فیلدهای جاوا (از جمله فیلدهای public و public final ) از طریق جاوا اسکریپت قابل دسترسی نیستند و به صورت undefined ارزیابی می‌شوند.
  • الزام حاشیه‌نویسی : فقط متدهایی که صریحاً با @JavascriptInterface حاشیه‌نویسی شده‌اند، در معرض جاوا اسکریپت قرار می‌گیرند.
  • Visibility restrictions : Methods must be public . private and protected methods are never exposed to JavaScript, even if they carry the @JavascriptInterface annotation.
  • Static methods : Static methods annotated with @JavascriptInterface are callable from JavaScript.
  • Inheritance and overriding : @JavascriptInterface annotations are not inherited when a subclass overrides a method. If a subclass overrides an annotated method from a superclass, the subclass must explicitly include the @JavascriptInterface annotation on the overridden method to expose it to JavaScript. Non-overridden public methods inherited from a superclass remain accessible if annotated in the superclass.
  • Reflection protection : Standard Java reflection methods (such as getClass() ) are blocked and throw a JavaScript exception to prevent remote code execution vulnerabilities.
  • Method overloading : Overloaded Java methods are supported. The bridge resolves method calls based on the number of passed arguments only, and does not take argument types into account. Calling an overloaded method with an invalid argument count raises a JavaScript exception. If two overloads have the same argument count, one will be chosen arbitrarily.

Summary of mechanisms

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

روش addWebMessageListener postWebMessage addJavascriptInterface
پیاده‌سازی Asynchronous (Listener on main thread) ناهمزمان همزمان
امنیت Highest (Allowlist-based) High (Origin aware) Low (No origin checks)
Complexity متوسط متوسط ساده
جهت دو جهته دو جهته Web to app
Minimum WebView version Version 82 (and Jetpack Webkit 1.3.0) Version 45 (and Jetpack Webkit 1.1.0) All versions
توصیه شده بله خیر خیر

Handle large data transfers

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

Transfer binary data with byte arrays

With the WebMessageCompat class, you can send byte[] arrays directly instead of serializing binary data into Base64 strings. Since Base64 adds roughly 33% overhead to the data size, this is significantly more memory-efficient and faster.

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

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

کاتلین

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

جاوا

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

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

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

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

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

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

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

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

Efficient large-scale data loading

For very large files (>10 MB), use the shouldInterceptRequest method to stream data:

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

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

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

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

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

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

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

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

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

کاتلین

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

جاوا

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

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

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

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

Follow security recommendations

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

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

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

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

  • Minimize surface area : Only expose the specific methods or data that the web page requires.

  • Check features at runtime : Recent bridge APIs, including addWebMessageListener , are part of the Jetpack Webkit library. So, always check for support using WebViewFeature.isFeatureSupported() before calling them.

،

This page discusses the various methods and best practices for establishing a native bridge, also known as JavaScript bridge, to facilitate communication between web content in a WebView and a host Android application.

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

موارد استفاده

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

  • Platform integration : Triggering native Android UI components (for example, Biometric prompts, BottomSheetDialog ) from a web page.
  • Performance : Offloading heavy computational tasks to native Java or Kotlin code.
  • Data persistence : Accessing local encrypted databases or shared preferences.
  • Large data transfers : Passing media files or complex data structures between the app and the web renderer.

Communication mechanisms

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

Use addWebMessageListener (Recommended)

addWebMessageListener is the most modern and recommended approach for communication between the web content and native app code. It combines the ease of use of the JavaScript interface with the security of the messaging system.

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

Initialization : To ensure the WebView injects the JavaScript object before any script runs, you must call addWebMessageListener before navigating to the page (such as calling WebViewCompat.navigate or loadUrl ).

Key features :

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

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

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

    Because the WebView strictly enforces these origin checks at the platform level, your app can generally rely upon messages received from a trusted sourceOrigin as truthful, eliminating the need for rigorous payload validation in most standard implementations.

    • WebView matches rules against the scheme (HTTP/HTTPS), host, and port.
    • WebView ignores paths. For example, https://example.com allows https://example.com/login and https://example.com/home .
    • WebView strictly limits wildcards to the start of the host for subdomains. For example, https://*.example.com matches https://foo.example.com but not https://example.com . If you need to match both https://example.com and its subdomains, you must add each origin rule separately to the allowlist (for example, "https://example.com", "https://*.example.com" ). You can't use wildcards for the scheme or in the middle of a domain.

    This restricts the bridge to verified domains, preventing unauthorized third-party content or injected iframes from executing native code.

  • Multi-frame support : Works across all frames that match the origin rules.

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

  • Bidirectional : When the web page sends a message, the app receives a JavaScriptReplyProxy that it can use to send messages back to that specific frame. You can retain this replyProxy object and use it at any time to send any number of messages to the page, not just to reply to each individual message the page sends. If the originating frame navigates away or is destroyed, messages sent using postMessage() on the proxy are silently ignored.

  • App-side initiation : Although the web page must always initiate the communication channel with the app, the native app can unilaterally prompt the web page to begin this process. The native app can communicate to the web page with addDocumentStartJavaScript() (to evaluate JavaScript before the page loads) or evaluateJavaScript() (to evaluate JavaScript after the page has loaded).

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

Usage example :

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

  1. Initiation (app) : The native app registers the listener with addWebMessageListener and initiates page navigation (such as with WebViewCompat.navigate or loadUrl ).
  2. Message send (web) : The web page's JavaScript calls myObject.postMessage(message) to initiate the communication.
  3. Message receive and reply (app) : The app receives the message in the listener callback and replies using the provided replyProxy.postMessage() .
  4. Reply receive (web) : The web page receives the asynchronous reply in the myObject.onmessage() callback function.

کاتلین

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

جاوا

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

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

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

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

Use postWebMessage (Alternative)

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

How it works : The app uses WebViewCompat.postWebMessage to send a payload to the web page's main frame. To establish a bidirectional communication channel, you can create a WebMessageChannel and pass one of its ports with the message to the web content.

Characteristics :

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

Limitations :

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

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

Use addJavascriptInterface (Legacy)

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

How it works : You define a Kotlin or Java class, annotate the allowed methods with @JavascriptInterface , and add an instance of the class to the WebView using addJavascriptInterface(Object, String) .

Characteristics :

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

Data type conversions and coercion

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

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

Parameter type mapping (JavaScript to Java)

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

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

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

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

Method and member accessibility

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

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

Summary of mechanisms

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

روش addWebMessageListener postWebMessage addJavascriptInterface
پیاده‌سازی Asynchronous (Listener on main thread) ناهمزمان همزمان
امنیت Highest (Allowlist-based) High (Origin aware) Low (No origin checks)
Complexity متوسط متوسط ساده
جهت دو جهته دو جهته Web to app
Minimum WebView version Version 82 (and Jetpack Webkit 1.3.0) Version 45 (and Jetpack Webkit 1.1.0) All versions
توصیه شده بله خیر خیر

Handle large data transfers

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

Transfer binary data with byte arrays

With the WebMessageCompat class, you can send byte[] arrays directly instead of serializing binary data into Base64 strings. Since Base64 adds roughly 33% overhead to the data size, this is significantly more memory-efficient and faster.

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

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

کاتلین

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

جاوا

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

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

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

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

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

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

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

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

Efficient large-scale data loading

For very large files (>10 MB), use the shouldInterceptRequest method to stream data:

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

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

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

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

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

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

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

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

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

کاتلین

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

جاوا

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

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

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

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

Follow security recommendations

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

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

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

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

  • Minimize surface area : Only expose the specific methods or data that the web page requires.

  • Check features at runtime : Recent bridge APIs, including addWebMessageListener , are part of the Jetpack Webkit library. So, always check for support using WebViewFeature.isFeatureSupported() before calling them.

،

This page discusses the various methods and best practices for establishing a native bridge, also known as JavaScript bridge, to facilitate communication between web content in a WebView and a host Android application.

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

موارد استفاده

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

  • Platform integration : Triggering native Android UI components (for example, Biometric prompts, BottomSheetDialog ) from a web page.
  • Performance : Offloading heavy computational tasks to native Java or Kotlin code.
  • Data persistence : Accessing local encrypted databases or shared preferences.
  • Large data transfers : Passing media files or complex data structures between the app and the web renderer.

Communication mechanisms

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

Use addWebMessageListener (Recommended)

addWebMessageListener is the most modern and recommended approach for communication between the web content and native app code. It combines the ease of use of the JavaScript interface with the security of the messaging system.

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

Initialization : To ensure the WebView injects the JavaScript object before any script runs, you must call addWebMessageListener before navigating to the page (such as calling WebViewCompat.navigate or loadUrl ).

Key features :

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

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

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

    Because the WebView strictly enforces these origin checks at the platform level, your app can generally rely upon messages received from a trusted sourceOrigin as truthful, eliminating the need for rigorous payload validation in most standard implementations.

    • WebView matches rules against the scheme (HTTP/HTTPS), host, and port.
    • WebView ignores paths. For example, https://example.com allows https://example.com/login and https://example.com/home .
    • WebView strictly limits wildcards to the start of the host for subdomains. For example, https://*.example.com matches https://foo.example.com but not https://example.com . If you need to match both https://example.com and its subdomains, you must add each origin rule separately to the allowlist (for example, "https://example.com", "https://*.example.com" ). You can't use wildcards for the scheme or in the middle of a domain.

    This restricts the bridge to verified domains, preventing unauthorized third-party content or injected iframes from executing native code.

  • Multi-frame support : Works across all frames that match the origin rules.

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

  • Bidirectional : When the web page sends a message, the app receives a JavaScriptReplyProxy that it can use to send messages back to that specific frame. You can retain this replyProxy object and use it at any time to send any number of messages to the page, not just to reply to each individual message the page sends. If the originating frame navigates away or is destroyed, messages sent using postMessage() on the proxy are silently ignored.

  • App-side initiation : Although the web page must always initiate the communication channel with the app, the native app can unilaterally prompt the web page to begin this process. The native app can communicate to the web page with addDocumentStartJavaScript() (to evaluate JavaScript before the page loads) or evaluateJavaScript() (to evaluate JavaScript after the page has loaded).

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

Usage example :

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

  1. Initiation (app) : The native app registers the listener with addWebMessageListener and initiates page navigation (such as with WebViewCompat.navigate or loadUrl ).
  2. Message send (web) : The web page's JavaScript calls myObject.postMessage(message) to initiate the communication.
  3. Message receive and reply (app) : The app receives the message in the listener callback and replies using the provided replyProxy.postMessage() .
  4. Reply receive (web) : The web page receives the asynchronous reply in the myObject.onmessage() callback function.

کاتلین

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

جاوا

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

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

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

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

Use postWebMessage (Alternative)

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

How it works : The app uses WebViewCompat.postWebMessage to send a payload to the web page's main frame. To establish a bidirectional communication channel, you can create a WebMessageChannel and pass one of its ports with the message to the web content.

Characteristics :

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

Limitations :

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

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

Use addJavascriptInterface (Legacy)

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

How it works : You define a Kotlin or Java class, annotate the allowed methods with @JavascriptInterface , and add an instance of the class to the WebView using addJavascriptInterface(Object, String) .

Characteristics :

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

Data type conversions and coercion

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

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

Parameter type mapping (JavaScript to Java)

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

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

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

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

Method and member accessibility

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

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

Summary of mechanisms

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

روش addWebMessageListener postWebMessage addJavascriptInterface
پیاده‌سازی Asynchronous (Listener on main thread) ناهمزمان همزمان
امنیت Highest (Allowlist-based) High (Origin aware) Low (No origin checks)
Complexity متوسط متوسط ساده
جهت دو جهته دو جهته Web to app
Minimum WebView version Version 82 (and Jetpack Webkit 1.3.0) Version 45 (and Jetpack Webkit 1.1.0) All versions
توصیه شده بله خیر خیر

Handle large data transfers

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

Transfer binary data with byte arrays

With the WebMessageCompat class, you can send byte[] arrays directly instead of serializing binary data into Base64 strings. Since Base64 adds roughly 33% overhead to the data size, this is significantly more memory-efficient and faster.

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

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

کاتلین

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

جاوا

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

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

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

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

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

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

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

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

Efficient large-scale data loading

For very large files (>10 MB), use the shouldInterceptRequest method to stream data:

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

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

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

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

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

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

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

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

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

کاتلین

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

جاوا

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

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

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

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

Follow security recommendations

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

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

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

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

  • Minimize surface area : Only expose the specific methods or data that the web page requires.

  • Check features at runtime : Recent bridge APIs, including addWebMessageListener , are part of the Jetpack Webkit library. So, always check for support using WebViewFeature.isFeatureSupported() before calling them.