summaryrefslogtreecommitdiffstats
path: root/mobile/android/docs/geckoview/consumer/web-extensions.rst
blob: ef4e75348f6ee78821b29cef500467203cd2c582 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
.. -*- Mode: rst; fill-column: 80; -*-

============================
Interacting with Web content
============================

Interacting with Web content and WebExtensions
==============================================

GeckoView allows embedder applications to register and run
`WebExtensions <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions>`_
in a GeckoView instance. Extensions are the preferred way to interact
with Web content.

.. contents:: :local:

Running extensions in GeckoView
-------------------------------

Extensions bundled with applications can be provided in a folder in the
``/assets`` section of the APK. Like ordinary extensions, every
extension bundled with GeckoView requires a
`manifest.json <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json>`_
file.

To locate files bundled with the APK, GeckoView provides a shorthand
``resource://android/`` that points to the root of the APK.

E.g. ``resource://android/assets/messaging/`` will point to the
``/assets/messaging/`` folder present in the APK.

Note: Every installed extension will need an
`id <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/browser_specific_settings>`_
and
`version <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/version>`_
specified in the ``manifest.json`` file.

To install a bundled extension in GeckoView, simply call
`WebExtensionController.installBuiltIn <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/WebExtensionController.html#installBuiltIn(java.lang.String)>`_.

.. code:: java

   runtime.getWebExtensionController()
     .installBuiltIn("resource://android/assets/messaging/")

Note that the lifetime of the extension is not tied with the lifetime of
the
`GeckoRuntime <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/GeckoRuntime.html>`_
instance. The extension persists even when your app is restarted.
Installing at every start up is fine, but it could be slow. To avoid
installing multiple times you can use ``WebExtensionRuntime.ensureBuiltIn``,
which will only install if the extension is not installed yet.

.. code:: java

   runtime.getWebExtensionController()
     .ensureBuiltIn("resource://android/assets/messaging/", "messaging@example.com")
     .accept(
           extension -> Log.i("MessageDelegate", "Extension installed: " + extension),
           e -> Log.e("MessageDelegate", "Error registering WebExtension", e)
     );

Communicating with Web Content
------------------------------

GeckoView allows bidirectional communication with Web pages through
extensions.

When using GeckoView, `native
messaging <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging#Exchanging_messages>`_
can be used for communicating to and from the browser.

- `runtime.sendNativeMessage <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/sendNativeMessage>`_
  for one-off messages.
- `runtime.connectNative <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/connectNative>`_
  for connection-based messaging.

Note: these APIs are only available when the ``geckoViewAddons``
`permission <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/permissions>`_
is present in the ``manifest.json`` file of the extension.

One-off messages
~~~~~~~~~~~~~~~~

The easiest way to send messages from a `content
script <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Content_scripts>`_
or a `background
script <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Anatomy_of_a_WebExtension#Background_scripts>`_
is using
`runtime.sendNativeMessage <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/sendNativeMessage>`_.

Note: Ordinarily, native extensions would use a `native
manifest <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging#App_manifest>`_
to define what native app identifier to use. For GeckoView this is *not*
needed, the ``nativeApp`` parameter in ``setMessageDelegate`` will be
use to determine what native app string is used.

Messaging Example
~~~~~~~~~~~~~~~~~

To receive messages from the background script, call
`setMessageDelegate <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/WebExtension.html#setMessageDelegate(org.mozilla.geckoview.WebExtension.MessageDelegate,java.lang.String)>`_
on the
`WebExtension <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/WebExtension.html>`_
object.

.. code:: java

   extension.setMessageDelegate(messageDelegate, "browser");

`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)>`_
allows the app to receive messages from content scripts.

.. code:: java

   session.getWebExtensionController()
       .setMessageDelegate(extension, messageDelegate, "browser");

Note: the ``"browser"`` parameter in the code above determines what
native app id the extension can use to send native messages.

Note: extension can only send messages from content scripts if
explicitly authorized by the app by adding
``nativeMessagingFromContent`` in the manifest.json file, e.g.

.. code:: json

     "permissions": [
       "nativeMessaging",
       "nativeMessagingFromContent",
       "geckoViewAddons"
     ]

Example
~~~~~~~

Let’s set up an activity that registers an extension located in the
``/assets/messaging/`` folder of the APK. This activity will set up a
`MessageDelegate <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/WebExtension.MessageDelegate.html>`_
that will be used to communicate with Web Content.

You can find the full example here:
`MessagingExample <https://searchfox.org/mozilla-central/source/mobile/android/examples/messaging_example>`_.

Activity.java
^^^^^^^^^^^^^

.. code:: java

   WebExtension.MessageDelegate messageDelegate = new WebExtension.MessageDelegate() {
       @Nullable
       public GeckoResult<Object> onMessage(final @NonNull String nativeApp,
                                            final @NonNull Object message,
                                            final @NonNull WebExtension.MessageSender sender) {
           // The sender object contains information about the session that
           // originated this message and can be used to validate that the message
           // has been sent from the expected location.

           // Be careful when handling the type of message as it depends on what
           // type of object was sent from the WebExtension script.
           if (message instanceof JSONObject) {
               // Do something with message
           }
           return null;
       }
   };

   // Let's make sure the extension is installed
   runtime.getWebExtensionController()
           .ensureBuiltIn(EXTENSION_LOCATION, "messaging@example.com").accept(
               // Set delegate that will receive messages coming from this extension.
               extension -> session.getWebExtensionController()
                       .setMessageDelegate(extension, messageDelegate, "browser"),
               // Something bad happened, let's log an error
               e -> Log.e("MessageDelegate", "Error registering extension", e)
           );


Now add the ``geckoViewAddons``, ``nativeMessaging`` and
``nativeMessagingFromContent`` permissions to your ``manifest.json``
file.

/assets/messaging/manifest.json
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. code:: json

   {
     "manifest_version": 2,
     "name": "messaging",
     "version": "1.0",
     "description": "Example messaging web extension.",
     "browser_specific_settings": {
       "gecko": {
         "id": "messaging@example.com"
       }
     },
     "content_scripts": [
       {
         "matches": ["*://*.twitter.com/*"],
         "js": ["messaging.js"]
       }
     ],
     "permissions": [
       "nativeMessaging",
       "nativeMessagingFromContent",
       "geckoViewAddons"
     ]
   }

And finally, write a content script that will send a message to the app
when a certain event occurs. For example, you could send a message
whenever a `WPA
manifest <https://developer.mozilla.org/en-US/docs/Web/Manifest>`_ is
found on the page. Note that our ``nativeApp`` identifier used for
``sendNativeMessage`` is the same as the one used in the
``setMessageDelegate`` call in `Activity.java <#activityjava>`_.

/assets/messaging/messaging.js
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. code:: JavaScript

   let manifest = document.querySelector("head > link[rel=manifest]");
   if (manifest) {
        fetch(manifest.href)
           .then(response => response.json())
           .then(json => {
                let message = {type: "WPAManifest", manifest: json};
                browser.runtime.sendNativeMessage("browser", message);
           });
   }

You can handle this message in the ``onMessage`` method in the
``messageDelegate`` `above <#activityjava>`_.

.. code:: java

   @Nullable
   public GeckoResult<Object> onMessage(final @NonNull String nativeApp,
                                        final @NonNull Object message,
                                        final @NonNull WebExtension.MessageSender sender) {
       if (message instanceof JSONObject) {
           JSONObject json = (JSONObject) message;
           try {
               if (json.has("type") && "WPAManifest".equals(json.getString("type"))) {
                   JSONObject manifest = json.getJSONObject("manifest");
                   Log.d("MessageDelegate", "Found WPA manifest: " + manifest);
               }
           } catch (JSONException ex) {
               Log.e("MessageDelegate", "Invalid manifest", ex);
           }
       }
       return null;
   }

Note that, in the case of content scripts, ``sender.session`` will be a
reference to the ``GeckoSession`` instance from which the message
originated. For background scripts, ``sender.session`` will always be
``null``.

Also note that the type of ``message`` will depend on what was sent from
the extension.

The type of ``message`` will be ``JSONObject`` when the extension sends
a javascript object, but could also be a primitive type if the extension
sends one, e.g. for

.. code:: javascript

   runtime.browser.sendNativeMessage("browser", "Hello World!");

the type of ``message`` will be ``java.util.String``.

Connection-based messaging
--------------------------

For more complex scenarios or for when you want to send messages *from*
the app to the extension,
`runtime.connectNative <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/connectNative>`_
is the appropriate API to use.

``connectNative`` returns a
`runtime.Port <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/Port>`_
that can be used to send messages to the app. On the app side,
implementing
`MessageDelegate#onConnect <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/WebExtension.MessageDelegate.html#onConnect(org.mozilla.geckoview.WebExtension.Port)>`_
will allow you to receive a
`Port <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/WebExtension.Port.html>`_
object that can be used to receive and send messages to the extension.

The following example can be found
`here <https://searchfox.org/mozilla-central/source/mobile/android/examples/port_messaging_example>`_.

For this example, the extension side will do the following:

- open a port on the background script using ``connectNative``
- listen on the port and log to console every message received
- send a message immediately after opening the port.

/assets/messaging/background.js
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. code:: JavaScript

   // Establish connection with app
   let port = browser.runtime.connectNative("browser");
   port.onMessage.addListener(response => {
       // Let's just echo the message back
       port.postMessage(`Received: ${JSON.stringify(response)}`);
   });
   port.postMessage("Hello from WebExtension!");

On the app side, following the `above <#activityjava>`_ example,
``onConnect`` will be storing the ``Port`` object in a member variable
and then using it when needed.

.. code:: java

   private WebExtension.Port mPort;

   @Override
   protected void onCreate(Bundle savedInstanceState) {
       // ... initialize GeckoView

       // This delegate will handle all communications from and to a specific Port
       // object
       WebExtension.PortDelegate portDelegate = new WebExtension.PortDelegate() {
           public WebExtension.Port port = null;

           public void onPortMessage(final @NonNull Object message,
                                     final @NonNull WebExtension.Port port) {
               // This method will be called every time a message is sent from the
               // extension through this port. For now, let's just log a
               // message.
               Log.d("PortDelegate", "Received message from WebExtension: "
                       + message);
           }

           public void onDisconnect(final @NonNull WebExtension.Port port) {
               // After this method is called, this port is not usable anymore.
               if (port == mPort) {
                   mPort = null;
               }
           }
       };

       // This delegate will handle requests to open a port coming from the
       // extension
       WebExtension.MessageDelegate messageDelegate = new WebExtension.MessageDelegate() {
           @Nullable
           public void onConnect(final @NonNull WebExtension.Port port) {
               // Let's store the Port object in a member variable so it can be
               // used later to exchange messages with the WebExtension.
               mPort = port;

               // Registering the delegate will allow us to receive messages sent
               // through this port.
               mPort.setDelegate(portDelegate);
           }
       };

       runtime.getWebExtensionController()
           .ensureBuiltIn("resource://android/assets/messaging/", "messaging@example.com")
           .accept(
               // Register message delegate for background script
               extension -> extension.setMessageDelegate(messageDelegate, "browser"),
               e -> Log.e("MessageDelegate", "Error registering WebExtension", e)
           );

       // ... other
   }

For example, let’s send a message to the extension every time the user
long presses on a key on the virtual keyboard, e.g. on the back button.

.. code:: java

   @Override
   public boolean onKeyLongPress(int keyCode, KeyEvent event) {
       if (mPort == null) {
           // No extension registered yet, let's ignore this message
           return false;
       }

       JSONObject message = new JSONObject();
       try {
           message.put("keyCode", keyCode);
           message.put("event", KeyEvent.keyCodeToString(event.getKeyCode()));
       } catch (JSONException ex) {
           throw new RuntimeException(ex);
       }

       mPort.postMessage(message);
       return true;
   }

This allows bidirectional communication between the app and the
extension.

.. _GeckoRuntime: https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/GeckoRuntime.html
.. _runtime.sendNativeMessage: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/sendNativeMessage
.. _WebExtension: https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/WebExtension.html