גישה לממשקי API מקוריים באמצעות JavaScript bridge

בדף הזה נסביר על השיטות השונות ועל השיטות המומלצות להקמת גשר מקורי, שנקרא גם גשר JavaScript, כדי לאפשר תקשורת בין תוכן מהאינטרנט ב-WebView לבין אפליקציה ל-Android מארחת.

כך מפתחי אתרים יכולים להשתמש ב-JavaScript כדי לגשת לתכונות של פלטפורמה מקומית – כמו המצלמה, מערכת הקבצים או חיישני חומרה מתקדמים – שבדרך כלל לא מסופקות על ידי ממשקי API רגילים לאינטרנט.

תרחישים לדוגמה

הטמעה של גשר JavaScript מאפשרת תרחישי שילוב שונים שבהם תוכן אינטרנט דורש גישה מעמיקה יותר למערכת ההפעלה Android. הנה כמה דוגמאות:

  • שילוב פלטפורמות: הפעלה של רכיבי ממשק משתמש מקוריים של Android (לדוגמה, הנחיות ביומטריות, BottomSheetDialog) מדף אינטרנט.
  • ביצועים: העברת משימות חישוביות כבדות לקוד Java או Kotlin מקורי.
  • התמדת נתונים: גישה למסדי נתונים מוצפנים מקומיים או להעדפות משותפות.
  • העברות נתונים גדולות: העברת קובצי מדיה או מבני נתונים מורכבים בין האפליקציה לבין רכיב העיבוד של הדף.

מנגנוני תקשורת

‫Android מציעה שלושה דורות עיקריים של ממשקי API ליצירת גשר מקורי. הן עדיין זמינות, אבל יש ביניהן הבדלים משמעותיים באבטחה, בנוחות השימוש ובביצועים.

שימוש ב-addWebMessageListener (מומלץ)

addWebMessageListener היא הגישה המודרנית והמומלצת ביותר לתקשורת בין תוכן האינטרנט לבין קוד אפליקציית הנייטיב. הוא משלב את קלות השימוש בממשק JavaScript עם האבטחה של מערכת ההודעות.

איך זה עובד: האפליקציה מוסיפה מאזין עם שם ספציפי וקבוצה של כללי מקור מותרים. לאחר מכן, רכיב ה-WebView מוודא שאובייקט ה-JavaScript נמצא בהיקף הגלובלי (window.objectName) מרגע שהדף מתחיל להיטען.

הפעלה: כדי לוודא שרכיב ה-WebView מזריק את אובייקט ה-JavaScript לפני הפעלת סקריפט כלשהו, צריך לקרוא ל-addWebMessageListener לפני המעבר לדף (למשל, קריאה ל-WebViewCompat.navigate או ל-loadUrl).

התכונות העיקריות:

  • אבטחה ואמון: בניגוד לממשקי API מדור קודם, השיטה הזו מחייבת Set<String> של allowedOriginRules במהלך האתחול. זהו המנגנון העיקרי ליצירת אמון.

    כשמציינים מקור מהימן, כמו https://example.com, רכיב ה-WebView מבטיח שהוא יחשוף את אובייקטי ה-JavaScript המוזרקים רק לדפי אינטרנט שנטענו מהמקור הזה בדיוק.

    הקריאה החוזרת של מאזין מקורי מקבלת פרמטר sourceOrigin עם כל הודעה. אפשר להשתמש בזה כדי לאמת את המקור המדויק של השולח אם הגשר תומך בכמה מקורות מותרים.

    מכיוון ש-WebView אוכף את הבדיקות האלה של המקור ברמת הפלטפורמה, האפליקציה יכולה בדרך כלל להסתמך על הודעות שמתקבלות ממקור מהימן sourceOrigin כהודעות אמיתיות, ולכן ברוב ההטמעות הרגילות אין צורך באימות קפדני של המטען הייעודי.

    • ‫WebView מתאים כללים לסכמה (HTTP/HTTPS), למארח ולפורט.
    • ‫WebView מתעלם מנתיבים. לדוגמה, https://example.com מאפשרת https://example.com/login ו-https://example.com/home.
    • ב-WebView, השימוש בתווים כלליים מוגבל לתחילת המארח עבור תת-דומיינים. לדוגמה, https://*.example.com תואם ל-https://foo.example.com אבל לא ל-https://example.com. אם אתם צריכים להתאים גם את https://example.com וגם את תתי-הדומיינים שלו, אתם צריכים להוסיף כל כלל מקור בנפרד לרשימת ההיתרים (לדוגמה, "https://example.com", "https://*.example.com"). אי אפשר להשתמש בתווים כלליים בסכימה או באמצע של דומיין.

    ההגדרה הזו מגבילה את הגשר לדומיינים מאומתים, ומונעת מתוכן של צד שלישי לא מורשה או מ-iframe מוחדר להריץ קוד Native.

  • תמיכה במספר מסגרות: פועל בכל המסגרות שתואמות לכללי המקור.

  • שרשור: הקריאה החוזרת (callback) של מאזין פועלת בשרשור הראשי (UI) של האפליקציה. אם הגשר צריך לטפל בעיבוד נתונים מורכב, בניתוח של JSON או בחיפושים במסד נתונים, צריך להעביר את העבודה הזו לשרשור ברקע כדי למנוע את קפיאת ממשק המשתמש של האפליקציה עם השגיאה 'האפליקציה לא מגיבה' (ANR).

  • דו-כיוונית: כשדף האינטרנט שולח הודעה, האפליקציה מקבלת JavaScriptReplyProxy שאפשר להשתמש בה כדי לשלוח הודעות חזרה למסגרת הספציפית הזו. אפשר לשמור את אובייקט replyProxy הזה ולהשתמש בו בכל שלב כדי לשלוח מספר כלשהו של הודעות לדף, ולא רק כדי להשיב לכל הודעה שהדף שולח. אם המסגרת המקורית עוברת למקום אחר או נהרסת, המערכת מתעלמת בשקט מהודעות שנשלחות באמצעות postMessage() בשרת ה-proxy.

  • הפעלה בצד האפליקציה: למרות שדף האינטרנט תמיד צריך להפעיל את ערוץ התקשורת עם האפליקציה, אפליקציית הנייטיב יכולה להציג לדף האינטרנט באופן חד-צדדי הנחיה להתחיל את התהליך הזה. אפליקציית הנייטיב יכולה לתקשר עם דף האינטרנט באמצעות addDocumentStartJavaScript() (כדי להעריך JavaScript לפני שהדף נטען) או evaluateJavaScript() (כדי להעריך JavaScript אחרי שהדף נטען).

הגבלה: ה-API הזה שולח נתונים כמחרוזות או כמערכים של byte[]. במבני נתונים מורכבים יותר, כמו אובייקטים של JSON, צריך לבצע סריאליזציה לאחד מהפורמטים האלה ואז לבצע דה-סריאליזציה בצד השני כדי לשחזר את מבנה הנתונים.

דוגמה לשימוש:

כדי להבין את הרצף המלא של חילופי הודעות דו-כיווניים, האירועים מתרחשים בסדר הבא:

  1. הפעלה (אפליקציה): אפליקציית הנייטיב רושמת את מאזין האירועים ב-addWebMessageListener ומפעילה את הניווט בדף (למשל באמצעות WebViewCompat.navigate או loadUrl).
  2. שליחת הודעה (אינטרנט): קריאות JavaScript בדף האינטרנט myObject.postMessage(message) כדי להתחיל את התקשורת.
  3. קבלת הודעות ושליחת תשובות (אפליקציה): האפליקציה מקבלת את ההודעה ב-callback של listener ועונה באמצעות replyProxy.postMessage() שסופק.
  4. קבלת תשובה (אינטרנט): דף האינטרנט מקבל את התשובה האסינכרונית בפונקציית הקריאה החוזרת myObject.onmessage().

Kotlin

val myListener = WebViewCompat.WebMessageListener { _, _, _, _, replyProxy ->
    // Handle the message from JS
    replyProxy.postMessage("Acknowledged!")
}

// Check whether the WebView version supports the feature.
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
    val allowedOrigins = setOf("https://www.example.com")
    WebViewCompat.addWebMessageListener(webView, "myObject", allowedOrigins, myListener)
}

Java

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

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

קוד ה-JavaScript הבא מדגים את ההטמעה בצד הלקוח של addWebMessageListener, שמאפשרת לתוכן האינטרנט לקבל הודעות מהאפליקציית נייטיב ולשלוח הודעות משלו דרך ה-proxy של myObject.

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

שימוש ב-postWebMessage (חלופה)

התכונה הזו הושקה ב-Android כדי לספק חלופה אסינכרונית שמבוססת על העברת הודעות, בדומה ל-window.postMessage באינטרנט.

איך זה עובד: האפליקציה משתמשת ב-WebViewCompat.postWebMessage כדי לשלוח מטען ייעודי (payload) למסגרת הראשית של דף האינטרנט. כדי ליצור ערוץ תקשורת דו-כיווני, אפשר ליצור WebMessageChannel ולהעביר את אחד מהיציאות שלו עם ההודעה לתוכן האינטרנט.

מאפיינים:

  • אסינכרוני: כמו addWebMessageListener, השיטה הזו משתמשת בהעברת הודעות אסינכרונית, שמבטיחה שדף האינטרנט ימשיך להגיב לאינטראקציות של המשתמשים בזמן שהאפליקציה מעבדת נתונים ברקע.
  • מודע למקור: אפשר לציין targetOrigin כדי לוודא שרכיב ה-WebView יעביר נתונים רק לאתר מהימן.

מגבלות:

  • היקף: ה-API הזה מגביל את התקשורת למסגרת הראשית. הוא לא תומך בפנייה ישירה או בשליחת הודעות ל-iframe.
  • הגבלות על URI: אי אפשר להשתמש בשיטה הזו לתוכן שנטען באמצעות מזהי URI מסוג data:,‏ file: או loadData(), אלא אם מציינים '*' כמקור היעד. כך כל דף יוכל לקבל את ההודעה.
  • סיכון לזהות: אין דרך ברורה לאמת את זהות השולח בתוכן האינטרנט. יכול להיות שהודעה שמתקבלת בדף אינטרנט הגיעה מאפליקציית נייטיב או מ-iframe אחר.

משתמשים בשיטה הזו כשצריך ערוץ פשוט ואסינכרוני לנתונים מבוססי-מחרוזת בגרסאות קודמות של Android שלא תומכות ב-addWebMessageListener.

שימוש ב-addJavascriptInterface (גרסה קודמת)

השיטה הכי ישנה כוללת הזרקה של מופע אובייקט מקורי ישירות ל-WebView.

איך זה עובד: מגדירים מחלקה ב-Kotlin או ב-Java, מוסיפים את ההערה @JavascriptInterface לשיטות המותרות ומוסיפים מופע של המחלקה ל-WebView באמצעות addJavascriptInterface(Object, String).

מאפיינים:

  • סינכרוני: סביבת ההפעלה של JavaScript נחסמת עד שהשיטה בקוד Android מחזירה ערך.
  • בטיחות ב-Thread: המערכת מפעילה שיטות ב-Thread ברקע, ולכן נדרש סנכרון זהיר בצד של Kotlin או Java.
  • סיכון אבטחה: כברירת מחדל, addJavascriptInterface זמין לכל מסגרת ב-WebView, כולל iframe. אין בו בקרת גישה שמבוססת על מקור. בגלל ההתנהגות האסינכרונית של WebView, אי אפשר לקבוע באופן בטוח את כתובת ה-URL של המסגרת שמפעילה את הממשק. אסור להסתמך על שיטות כמו WebView.getUrl() לאימות אבטחה, כי אין ערובה לכך שהן מדויקות, והן לא מציינות איזה פריים ספציפי של iframe שלח את הבקשה.

המרות והכפייה של סוגי נתונים

כשמשתמשים ב-addJavascriptInterface, גשר Java שמבוסס על Chromium ממיר סוגי נתונים בין זמן הריצה של JavaScript לבין קוד האפליקציה ל-Android.

כללי ההמרה הבאים חלים על פרמטרים של שיטות ועל ערכים מוחזרים.

מיפוי סוגי פרמטרים (JavaScript ל-Java)

כש-JavaScript מעביר ארגומנטים לשיטת Java או Kotlin עם הערות, ה-bridge מבצע המרה של ערכי JavaScript לסוגי הפרמטרים התואמים ב-Java:

סוג הפרמטר ב-Java ערך ארגומנט של JavaScript התנהגות כפייה
byte, short, int, long מספר (מספר שלם) הערכים מומרים לסוג המספר השלם של היעד. ערכים מחוץ לטווח עוברים המרה בהתאם לכללי ההמרה המספריים הרגילים.
byte, short, int, long NaN ההמרה מתבצעת ל-0.
byte, short, int, long Infinity ההמרה מתבצעת ל--1 עבור byte ו-short, או ל-Integer.MAX_VALUE ו-Long.MAX_VALUE עבור int ו-long.
float, double מספר הפונקציה מבצעת המרה לערך הנקודה הצפה (floating-point) המתאים ב-Java.
float, double NaN / Infinity ההמרה מתבצעת ל-Float.NaN, ל-Double.NaN, ל-Float.POSITIVE_INFINITY או ל-Double.POSITIVE_INFINITY.
char מספר (מספר שלם) הומר למיקום התו (code point) המתאים ב-Unicode.
char מספר לא שלם, NaN, Infinity ההמרה מתבצעת ל-\u0000.
boolean true / false ההמרה מתבצעת ל-Java true או false.
boolean מספר, מחרוזת, אובייקט הפונקציה מבצעת המרה ל-false (כולל מחרוזות לא ריקות ומספרים שונים מאפס).
String מחרוזת ערך המחרוזת נשמר.
String מספר, ערך בוליאני הפורמט הוא ייצוג מחרוזת (לדוגמה, "42", ‏ "true", ‏ "false").
String null / undefined null עובר המרה ל-Java null;‏ undefined עובר המרה למחרוזת המילולית "undefined".
String אובייקט, ArrayBuffer, ‏ TypedArray הפונקציה מבצעת המרה למחרוזת המילולית "undefined".
מערך פרימיטיבי (כמו int[], ‏ byte[], ‏ boolean[]) או String[] מערך ([...]) הפונקציה ממירה למערך Java חד-ממדי של סוג רכיב היעד. מערכים דלילים ממלאים אינדקסים לא מוקצים בערכי ברירת מחדל (0, ‏ false, ‏ null).
מערך פרימיטיבי (כמו int[], byte[]) TypedArray (Int8Array, ‏ Uint8Array, ‏ Int32Array, ‏ Float64Array) הרכיבים נכפים למערך הפרימיטיבי המתאים של Java.
מערך רב-ממדי (כמו int[][]) מערך מקונן ([[...]]) לא נתמך. הערך של פרמטרים של מערך רב-ממדי הוא null.
ArrayBuffer, DataView ArrayBuffer, DataView לא אפשרי כמערכים. הערך של מופעים של ArrayBuffer ו-DataView הוא null.
Object או סוג מותאם אישית אובייקט JavaScript‏ ({...}) לא נתמך. אובייקטים מילוליים שרירותיים של JavaScript מקבלים את הערך null ב-Java.
Object או סוג מותאם אישית ‫wrapper של אובייקט Java שהוחדר נתמך (המרת קובץ הלוך ושוב). הפונקציה מעבירה את מופע ה-Java הבסיסי לשיטת ה-Java. הפונקציה יוצרת חריג ב-JavaScript אם סוג ה-Java לא תואם לחתימת הפרמטר.
סוגים באריזה (כמו Integer, ‏ Double, ‏ Boolean) מספר, ערך בוליאני לא נתמך. סוגי נתונים פרימיטיביים שמופיעים בתוך תיבות נחשבים לאובייקטים אטומים, והערך שלהם הוא null.
כל סוג פרימיטיבי null / undefined הערכים משתנים לערכי ברירת מחדל (0, ‏ 0.0, ‏ \u0000, ‏ false).
Object, String, מערך null ההמרה מתבצעת ל-Java null.
מיפוי סוגי החזרה (Java ל-JavaScript)

כששיטה עם הערה ב-Java או ב-Kotlin מחזירה ערך, הגשר ממיר אותו לסוג JavaScript:

סוג ההחזרה של Java ערך JavaScript ‫JavaScript typeof
boolean true / false "boolean"
byte, short, int, long, float, double מספר "number"
char מספר (מיקום תו (code point) של Unicode) "number"
String (non-null) ערך מחרוזת "string"
String (null) undefined "undefined"
void undefined "undefined"
מערך Java (לדוגמה, int[],‏ String[]) undefined "undefined". אין תמיכה בערכי החזרה של מערך. השיטה של Java לא מופעלת, ומוחזרת undefined בלי להעלות חריגה.
אובייקט Java / סוג בהתאמה אישית (לא null) רכיב wrapper של אובייקט "object". יוצר עטיפת JavaScript סביב מופע Java. קוד JavaScript יכול לקרוא לכל שיטה ציבורית באובייקט הזה שמסומנת ב-@JavascriptInterface.
אובייקט Java / סוג מותאם אישית (null) null "object"
פרימיטיב באריזה (כמו Integer, Double) רכיב wrapper של אובייקט "object". הערך מוחזר כעטיפה של אובייקט Java אטום ללא שיטות @JavascriptInterface נגישות, ולכן אי אפשר להשתמש בו ב-JavaScript.

שיטת הגישה לחברים

גשר ה-JavaScript אוכף כללי גישה וחשיפה מחמירים כדי להגן מפני הרצת קוד לא מכוונת:

  • השדות לא נחשפים: אי אפשר לגשת לשדות Java (כולל השדות public ו-public final) מ-JavaScript, והערך שלהם הוא undefined.
  • דרישה להערה: רק שיטות שמוערות באופן מפורש באמצעות @JavascriptInterface נחשפות ל-JavaScript.
  • הגבלות על החשיפה: השיטות חייבות להיות public. השיטות private ו-protected אף פעם לא נחשפות ל-JavaScript, גם אם הן כוללות את ההערה @JavascriptInterface.
  • שיטות סטטיות: שיטות סטטיות שמסומנות ב-@JavascriptInterface ניתנות להפעלה מ-JavaScript.
  • הורשה ודריסה: @JavascriptInterface הערות לא עוברות בירושה כשמחלקת משנה דורסת שיטה. אם מחלקה משנית מבטלת שיטה עם הערה ממחלקת-על, המחלקה המשנית חייבת לכלול באופן מפורש את ההערה @JavascriptInterface בשיטה שבוטלה כדי לחשוף אותה ל-JavaScript. שיטות ציבוריות שלא בוטלו שמועברות ממחלקת-על נשארות נגישות אם הן מסומנות בהערה במחלקת-העל.
  • הגנה מפני השתקפות: שיטות השתקפות רגילות של Java (כמו getClass()) נחסמות ומוצג חריג של JavaScript כדי למנוע פגיעות בביצוע קוד מרחוק.
  • העמסת שיטות: יש תמיכה בשיטות Java שעברו העמסה. הגשר פותר קריאות לשיטות על סמך מספר הארגומנטים שהועברו בלבד, ולא מתחשב בסוגי הארגומנטים. הפעלת שיטה עם עומס יתר עם מספר לא תקין של ארגומנטים מעלה חריגה ב-JavaScript. אם לשתי פונקציות עמוסות מדי יש אותו מספר ארגומנטים, אחת מהן תיבחר באופן שרירותי.

סיכום המנגנונים

בטבלה הבאה מופיעה השוואה מהירה בין שלושת המנגנונים העיקריים להטמעה של גשרים מקוריים:

שיטה addWebMessageListener postWebMessage addJavascriptInterface
הטמעה אסינכרוני (Listener ב-thread הראשי) אסינכרוני סינכרוני
אבטחה הגבוהה ביותר (על בסיס רשימת היתרים) גבוהה (מודעת למקור) נמוכה (ללא בדיקות מקור)
מורכבות בינוני בינוני פשוט
כיוון דו-כיווני דו-כיווני צירוף משתמשים לאפליקציה דרך האתר
גרסת WebView מינימלית גרסה 82 (ו-Jetpack Webkit 1.3.0) גרסה 45 (ו-Jetpack Webkit 1.1.0) כל הגירסאות
מומלץ כן לא לא

טיפול בהעברות נתונים גדולות

כשמעבירים נתונים גדולים, כמו מחרוזות של כמה מגה-בייט או קבצים בינאריים, צריך לנהל את הזיכרון בקפידה כדי למנוע שגיאות מסוג ANR (האפליקציה לא מגיבה) או קריסות במכשירים עם 32 ביט. בקטע הזה נדון בטכניקות השונות ובמגבלות שקשורות להעברת כמויות גדולות של נתונים בין אפליקציית המארח לבין תוכן האינטרנט.

העברת נתונים בינאריים באמצעות מערכים של בייטים

בעזרת המחלקה WebMessageCompat, אפשר לשלוח מערכים מסוג byte[] ישירות במקום לבצע סריאליזציה של נתונים בינאריים למחרוזות Base64. הקידוד ב-Base64 מוסיף תקורת נתונים של כ-33%, ולכן השיטה הזו חסכונית יותר בזיכרון ומהירה יותר.

  • יתרון בינארי: העברת נתונים בינאריים כמו קובצי תמונות או אודיו בין אפליקציית הנייטיב לבין תוכן מהאינטרנט.
  • מגבלה: גם עם מערכי בייטים, המערכת מעתיקה נתונים מעבר לגבול של תקשורת בין תהליכים (IPC) בין האפליקציה לבין התהליך המבודד שבו WebView משתמש כדי לעבד את תוכן האינטרנט. הפעולה הזו עדיין צורכת כמות משמעותית של זיכרון עבור קבצים גדולים מאוד.

בדוגמאות הקוד הבאות אפשר לראות איך מגדירים את addWebMessageListener בצד של אפליקציית נייטיב כדי לקבל הודעות שמסומנות ב-WebMessageCompat.TYPE_ARRAY_BUFFER, ואיך אפשר לענות באמצעות נתונים בינאריים על ידי בדיקה של WebViewFeature.MESSAGE_ARRAY_BUFFER.

Kotlin

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

          // Check if the received message is an ArrayBuffer
          if (message.type == WebMessageCompat.TYPE_ARRAY_BUFFER) {
              val binaryData: ByteArray = message.arrayBuffer
              // Process your binary data (image, audio, etc.)
              println("Received bytes: ${binaryData.size}")

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

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

Java

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

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

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

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

קוד ה-JavaScript הבא מדגים את ההטמעה בצד הלקוח של addWebMessageListener, שמאפשרת לתוכן האינטרנט לשלוח ולקבל נתונים בינאריים (ArrayBuffer) אל אפליקציית הנייטיב וממנה באמצעות ה-proxy של 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]);
        }
    };
}

טעינת נתונים יעילה בקנה מידה גדול

לקבצים גדולים מאוד (מעל 10MB), משתמשים בשיטה shouldInterceptRequest כדי להזרים נתונים:

  1. דף האינטרנט יוזם קריאה לכתובת URL מותאמת אישית של placeholder‏ fetch(). לדוגמה, https://app.local/large-file.
  2. אפליקציית Android מיירטת את הבקשה הזו ב-WebViewClient.shouldInterceptRequest.
  3. האפליקציה מחזירה את הנתונים כ-InputStream.

האפשרות הזו מאפשרת להזרים נתונים במקטעים במקום לטעון את כל מטען הנתונים לזיכרון בבת אחת.

פונקציית JavaScript הבאה מדגימה את הקוד בצד הלקוח לטעינה יעילה של קובץ בינארי גדול מאפליקציית נייטיב באמצעות קריאה רגילה fetch() לכתובת 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);
    }
}

בדוגמאות הקוד הבאות מוצג הצד של אפליקציית הנייטיב, באמצעות השיטה WebViewClient.shouldInterceptRequest ב-Kotlin וב-Java, להזרמת קובץ בינארי גדול על ידי יירוט של כתובת URL של פלייסהולדר מותאם אישית שנדרשה על ידי תוכן מהאינטרנט.

Kotlin

webView.webViewClient = object : WebViewClient() {
  override fun shouldInterceptRequest(
      view: WebView?,
      request: WebResourceRequest?
  ): WebResourceResponse? {
      val url = request?.url ?: return null

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

              // 2. Define Response Headers (Crucial for CORS/Fetch)
              val headers = mutableMapOf<String, String>()
              headers["Access-Control-Allow-Origin"] = "*" // Allow fetch from any origin

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

Java

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

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

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

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

פעולה בהתאם להמלצות האבטחה

כדי להגן על האפליקציה ועל נתוני המשתמשים, חשוב לפעול לפי ההנחיות האלה כשמטמיעים גשר:

  • החלת HTTPS: כדי לוודא שתוכן זדוני של צד שלישי לא יוכל להפעיל את הלוגיקה המקורית של האפליקציה, צריך לאפשר תקשורת רק עם מקורות מאובטחים.

  • להסתמך על כללים לגבי מקור: הדרך הטובה ביותר להתמודד עם בעיות שקשורות למהימנות היא להגדיר בקפדנות את allowedOriginRules ולבדוק את sourceOrigin שמופיע בקריאה החוזרת של ההודעה. מומלץ להימנע משימוש בתו כללי מלא (*), שתואם לכל המקורות, ככלל המקור היחיד, אלא אם זה ממש הכרחי. השימוש בתווים כלליים לחיפוש בתתי-דומיינים (לדוגמה, *.example.com) עדיין תקף ומאפשר התאמה מאובטחת למספר תתי-דומיינים (לדוגמה, foo.example.com,‏ bar.example.com).

    הערה: כללים לגבי מקורות מגנים מפני אתרים זדוניים של צד שלישי ומפני iframe מוסתרים, אבל הם לא יכולים להגן מפני פרצות אבטחה מסוג XSS (cross-site scripting) בדומיין המהימן שלכם. לדוגמה, אם דף אינטרנט מציג תוכן שנוצר על ידי משתמשים והוא פגיע ל-XSS מאוחסן, תוקף יכול להריץ סקריפט שפועל כמקור מהימן. מומלץ להחיל אימות על מטען הייעודי (payload) של ההודעות לפני שמבצעים פעולות רגישות בפלטפורמה המקורית.

  • צמצום שטח הפנים: חשיפה רק של השיטות או הנתונים הספציפיים שדרושים לדף האינטרנט.

  • בדיקת תכונות בזמן ריצה: ממשקי Bridge API חדשים, כולל addWebMessageListener, הם חלק מספריית Jetpack Webkit. לכן, תמיד כדאי לבדוק אם יש תמיכה באמצעות WebViewFeature.isFeatureSupported() לפני שמתקשרים אליהם.