web-extensions.rst (15964B)
1 .. -*- Mode: rst; fill-column: 80; -*- 2 3 ============================ 4 Interacting with Web content 5 ============================ 6 7 Interacting with Web content and WebExtensions 8 ============================================== 9 10 GeckoView allows embedder applications to register and run 11 `WebExtensions <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions>`_ 12 in a GeckoView instance. Extensions are the preferred way to interact 13 with Web content. 14 15 .. contents:: :local: 16 17 Running extensions in GeckoView 18 ------------------------------- 19 20 Extensions bundled with applications can be provided in a folder in the 21 ``/assets`` section of the APK. Like ordinary extensions, every 22 extension bundled with GeckoView requires a 23 `manifest.json <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json>`_ 24 file. 25 26 To locate files bundled with the APK, GeckoView provides a shorthand 27 ``resource://android/`` that points to the root of the APK. 28 29 E.g. ``resource://android/assets/messaging/`` will point to the 30 ``/assets/messaging/`` folder present in the APK. 31 32 Note: Every installed extension will need an 33 `id <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/browser_specific_settings>`_ 34 and 35 `version <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/version>`_ 36 specified in the ``manifest.json`` file. 37 38 To install a bundled extension in GeckoView, simply call 39 `WebExtensionController.installBuiltIn <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/WebExtensionController.html#installBuiltIn(java.lang.String)>`_. 40 41 .. code:: java 42 43 runtime.getWebExtensionController() 44 .installBuiltIn("resource://android/assets/messaging/") 45 46 Note that the lifetime of the extension is not tied with the lifetime of 47 the 48 `GeckoRuntime <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/GeckoRuntime.html>`_ 49 instance. The extension persists even when your app is restarted. 50 Installing at every start up is fine, but it could be slow. To avoid 51 installing multiple times you can use ``WebExtensionRuntime.ensureBuiltIn``, 52 which will only install if the extension is not installed yet. 53 54 .. code:: java 55 56 runtime.getWebExtensionController() 57 .ensureBuiltIn("resource://android/assets/messaging/", "messaging@example.com") 58 .accept( 59 extension -> Log.i("MessageDelegate", "Extension installed: " + extension), 60 e -> Log.e("MessageDelegate", "Error registering WebExtension", e) 61 ); 62 63 Communicating with Web Content 64 ------------------------------ 65 66 GeckoView allows bidirectional communication with Web pages through 67 extensions. 68 69 When using GeckoView, `native 70 messaging <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging#Exchanging_messages>`_ 71 can be used for communicating to and from the browser. 72 73 - `runtime.sendNativeMessage <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/sendNativeMessage>`_ 74 for one-off messages. 75 - `runtime.connectNative <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/connectNative>`_ 76 for connection-based messaging. 77 78 Note: these APIs are only available when the ``geckoViewAddons`` 79 `permission <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/permissions>`_ 80 is present in the ``manifest.json`` file of the extension. 81 82 One-off messages 83 ~~~~~~~~~~~~~~~~ 84 85 The easiest way to send messages from a `content 86 script <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Content_scripts>`_ 87 or a `background 88 script <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Anatomy_of_a_WebExtension#Background_scripts>`_ 89 is using 90 `runtime.sendNativeMessage <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/sendNativeMessage>`_. 91 92 Note: Ordinarily, native extensions would use a `native 93 manifest <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging#App_manifest>`_ 94 to define what native app identifier to use. For GeckoView this is *not* 95 needed, the ``nativeApp`` parameter in ``setMessageDelegate`` will be 96 use to determine what native app string is used. 97 98 Messaging Example 99 ~~~~~~~~~~~~~~~~~ 100 101 To receive messages from the background script, call 102 `setMessageDelegate <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/WebExtension.html#setMessageDelegate(org.mozilla.geckoview.WebExtension.MessageDelegate,java.lang.String)>`_ 103 on the 104 `WebExtension <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/WebExtension.html>`_ 105 object. 106 107 .. code:: java 108 109 extension.setMessageDelegate(messageDelegate, "browser"); 110 111 `SessionController.setMessageDelegate <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/WebExtension.SessionController.html#setMessageDelegate(org.mozilla.geckoview.WebExtension,org.mozilla.geckoview.WebExtension.MessageDelegate,java.lang.String)>`_ 112 allows the app to receive messages from content scripts. 113 114 .. code:: java 115 116 session.getWebExtensionController() 117 .setMessageDelegate(extension, messageDelegate, "browser"); 118 119 Note: the ``"browser"`` parameter in the code above determines what 120 native app id the extension can use to send native messages. 121 122 Note: extension can only send messages from content scripts if 123 explicitly authorized by the app by adding 124 ``nativeMessagingFromContent`` in the manifest.json file, e.g. 125 126 .. code:: json 127 128 "permissions": [ 129 "nativeMessaging", 130 "nativeMessagingFromContent", 131 "geckoViewAddons" 132 ] 133 134 Example 135 ~~~~~~~ 136 137 Let’s set up an activity that registers an extension located in the 138 ``/assets/messaging/`` folder of the APK. This activity will set up a 139 `MessageDelegate <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/WebExtension.MessageDelegate.html>`_ 140 that will be used to communicate with Web Content. 141 142 You can find the full example here: 143 `MessagingExample <https://searchfox.org/mozilla-central/source/mobile/android/examples/messaging_example>`_. 144 145 Activity.java 146 ^^^^^^^^^^^^^ 147 148 .. code:: java 149 150 WebExtension.MessageDelegate messageDelegate = new WebExtension.MessageDelegate() { 151 @Nullable 152 public GeckoResult<Object> onMessage(final @NonNull String nativeApp, 153 final @NonNull Object message, 154 final @NonNull WebExtension.MessageSender sender) { 155 // The sender object contains information about the session that 156 // originated this message and can be used to validate that the message 157 // has been sent from the expected location. 158 159 // Be careful when handling the type of message as it depends on what 160 // type of object was sent from the WebExtension script. 161 if (message instanceof JSONObject) { 162 // Do something with message 163 } 164 return null; 165 } 166 }; 167 168 // Let's make sure the extension is installed 169 runtime.getWebExtensionController() 170 .ensureBuiltIn(EXTENSION_LOCATION, "messaging@example.com").accept( 171 // Set delegate that will receive messages coming from this extension. 172 extension -> session.getWebExtensionController() 173 .setMessageDelegate(extension, messageDelegate, "browser"), 174 // Something bad happened, let's log an error 175 e -> Log.e("MessageDelegate", "Error registering extension", e) 176 ); 177 178 179 Now add the ``geckoViewAddons``, ``nativeMessaging`` and 180 ``nativeMessagingFromContent`` permissions to your ``manifest.json`` 181 file. 182 183 /assets/messaging/manifest.json 184 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 185 186 .. code:: json 187 188 { 189 "manifest_version": 2, 190 "name": "messaging", 191 "version": "1.0", 192 "description": "Example messaging web extension.", 193 "browser_specific_settings": { 194 "gecko": { 195 "id": "messaging@example.com" 196 } 197 }, 198 "content_scripts": [ 199 { 200 "matches": ["*://*.twitter.com/*"], 201 "js": ["messaging.js"] 202 } 203 ], 204 "permissions": [ 205 "nativeMessaging", 206 "nativeMessagingFromContent", 207 "geckoViewAddons" 208 ] 209 } 210 211 And finally, write a content script that will send a message to the app 212 when a certain event occurs. For example, you could send a message 213 whenever a `WPA 214 manifest <https://developer.mozilla.org/en-US/docs/Web/Manifest>`_ is 215 found on the page. Note that our ``nativeApp`` identifier used for 216 ``sendNativeMessage`` is the same as the one used in the 217 ``setMessageDelegate`` call in `Activity.java <#activityjava>`_. 218 219 /assets/messaging/messaging.js 220 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 221 222 .. code:: JavaScript 223 224 let manifest = document.querySelector("head > link[rel=manifest]"); 225 if (manifest) { 226 fetch(manifest.href) 227 .then(response => response.json()) 228 .then(json => { 229 let message = {type: "WPAManifest", manifest: json}; 230 browser.runtime.sendNativeMessage("browser", message); 231 }); 232 } 233 234 You can handle this message in the ``onMessage`` method in the 235 ``messageDelegate`` `above <#activityjava>`_. 236 237 .. code:: java 238 239 @Nullable 240 public GeckoResult<Object> onMessage(final @NonNull String nativeApp, 241 final @NonNull Object message, 242 final @NonNull WebExtension.MessageSender sender) { 243 if (message instanceof JSONObject) { 244 JSONObject json = (JSONObject) message; 245 try { 246 if (json.has("type") && "WPAManifest".equals(json.getString("type"))) { 247 JSONObject manifest = json.getJSONObject("manifest"); 248 Log.d("MessageDelegate", "Found WPA manifest: " + manifest); 249 } 250 } catch (JSONException ex) { 251 Log.e("MessageDelegate", "Invalid manifest", ex); 252 } 253 } 254 return null; 255 } 256 257 Note that, in the case of content scripts, ``sender.session`` will be a 258 reference to the ``GeckoSession`` instance from which the message 259 originated. For background scripts, ``sender.session`` will always be 260 ``null``. 261 262 Also note that the type of ``message`` will depend on what was sent from 263 the extension. 264 265 The type of ``message`` will be ``JSONObject`` when the extension sends 266 a javascript object, but could also be a primitive type if the extension 267 sends one, e.g. for 268 269 .. code:: javascript 270 271 runtime.browser.sendNativeMessage("browser", "Hello World!"); 272 273 the type of ``message`` will be ``java.util.String``. 274 275 Connection-based messaging 276 -------------------------- 277 278 For more complex scenarios or for when you want to send messages *from* 279 the app to the extension, 280 `runtime.connectNative <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/connectNative>`_ 281 is the appropriate API to use. 282 283 ``connectNative`` returns a 284 `runtime.Port <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/Port>`_ 285 that can be used to send messages to the app. On the app side, 286 implementing 287 `MessageDelegate#onConnect <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/WebExtension.MessageDelegate.html#onConnect(org.mozilla.geckoview.WebExtension.Port)>`_ 288 will allow you to receive a 289 `Port <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/WebExtension.Port.html>`_ 290 object that can be used to receive and send messages to the extension. 291 292 The following example can be found 293 `here <https://searchfox.org/mozilla-central/source/mobile/android/examples/port_messaging_example>`_. 294 295 For this example, the extension side will do the following: 296 297 - open a port on the background script using ``connectNative`` 298 - listen on the port and log to console every message received 299 - send a message immediately after opening the port. 300 301 /assets/messaging/background.js 302 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 303 304 .. code:: JavaScript 305 306 // Establish connection with app 307 let port = browser.runtime.connectNative("browser"); 308 port.onMessage.addListener(response => { 309 // Let's just echo the message back 310 port.postMessage(`Received: ${JSON.stringify(response)}`); 311 }); 312 port.postMessage("Hello from WebExtension!"); 313 314 On the app side, following the `above <#activityjava>`_ example, 315 ``onConnect`` will be storing the ``Port`` object in a member variable 316 and then using it when needed. 317 318 .. code:: java 319 320 private WebExtension.Port mPort; 321 322 @Override 323 protected void onCreate(Bundle savedInstanceState) { 324 // ... initialize GeckoView 325 326 // This delegate will handle all communications from and to a specific Port 327 // object 328 WebExtension.PortDelegate portDelegate = new WebExtension.PortDelegate() { 329 public WebExtension.Port port = null; 330 331 public void onPortMessage(final @NonNull Object message, 332 final @NonNull WebExtension.Port port) { 333 // This method will be called every time a message is sent from the 334 // extension through this port. For now, let's just log a 335 // message. 336 Log.d("PortDelegate", "Received message from WebExtension: " 337 + message); 338 } 339 340 public void onDisconnect(final @NonNull WebExtension.Port port) { 341 // After this method is called, this port is not usable anymore. 342 if (port == mPort) { 343 mPort = null; 344 } 345 } 346 }; 347 348 // This delegate will handle requests to open a port coming from the 349 // extension 350 WebExtension.MessageDelegate messageDelegate = new WebExtension.MessageDelegate() { 351 @Nullable 352 public void onConnect(final @NonNull WebExtension.Port port) { 353 // Let's store the Port object in a member variable so it can be 354 // used later to exchange messages with the WebExtension. 355 mPort = port; 356 357 // Registering the delegate will allow us to receive messages sent 358 // through this port. 359 mPort.setDelegate(portDelegate); 360 } 361 }; 362 363 runtime.getWebExtensionController() 364 .ensureBuiltIn("resource://android/assets/messaging/", "messaging@example.com") 365 .accept( 366 // Register message delegate for background script 367 extension -> extension.setMessageDelegate(messageDelegate, "browser"), 368 e -> Log.e("MessageDelegate", "Error registering WebExtension", e) 369 ); 370 371 // ... other 372 } 373 374 For example, let’s send a message to the extension every time the user 375 long presses on a key on the virtual keyboard, e.g. on the back button. 376 377 .. code:: java 378 379 @Override 380 public boolean onKeyLongPress(int keyCode, KeyEvent event) { 381 if (mPort == null) { 382 // No extension registered yet, let's ignore this message 383 return false; 384 } 385 386 JSONObject message = new JSONObject(); 387 try { 388 message.put("keyCode", keyCode); 389 message.put("event", KeyEvent.keyCodeToString(event.getKeyCode())); 390 } catch (JSONException ex) { 391 throw new RuntimeException(ex); 392 } 393 394 mPort.postMessage(message); 395 return true; 396 } 397 398 This allows bidirectional communication between the app and the 399 extension. 400 401 .. _GeckoRuntime: https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/GeckoRuntime.html 402 .. _runtime.sendNativeMessage: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/sendNativeMessage 403 .. _WebExtension: https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/WebExtension.html