diff options
Diffstat (limited to 'mobile/android/docs/geckoview/consumer')
5 files changed, 954 insertions, 0 deletions
diff --git a/mobile/android/docs/geckoview/consumer/automation.rst b/mobile/android/docs/geckoview/consumer/automation.rst new file mode 100644 index 0000000000..2fd596e51b --- /dev/null +++ b/mobile/android/docs/geckoview/consumer/automation.rst @@ -0,0 +1,124 @@ +.. -*- Mode: rst; fill-column: 80; -*- + +Configuring GeckoView for Automation +#################################### +How to set environment variables, Gecko arguments, and Gecko preferences for automation and debugging. + +.. contents:: :local: + +Configuring GeckoView +===================================== + +GeckoView and the underlying Gecko engine have many, many options, switches, and toggles "under the hood". Automation (and to a lesser extent, debugging) can require configuring the Gecko engine to allow (or disallow) specific actions or features. + +Some such actions and features are controlled by the `GeckoRuntimeSettings <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/GeckoRuntimeSettings.html>`_ instance you configure in your consuming project. For example, remote debugging web content via the Firefox Developer Tools is configured by `GeckoRuntimeSettings.Builder#remoteDebuggingEnabled <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/GeckoRuntimeSettings.Builder.html#remoteDebuggingEnabled(boolean)>`_ + +Not all actions and features have GeckoView API interfaces. Generally, actions and features that do not have GeckoView API interfaces are not intended for broad usage. Configuration for these types of things is controlled by: + +- environment variables in GeckoView's runtime environment +- command line arguments to the Gecko process +- internal Gecko preferences + +Automation-specific configuration is generally in this category. + +Running GeckoView with environment variables +------------------------------------------------ + +After a successful ``./mach build``, ``./mach run --setenv`` can be used to run GeckoView with +the given environment variables. + +For example, to enable extended logging for ``JSComponentLoader``, run ``./mach +run --setenv MOZ_LOG=JSComponentLoader:5``. + +Reading configuration from a file +------------------------------------------------ + +When GeckoView is embedded into a debugabble application (i.e., when your manifest includes ``android:debuggable="true"``), by default GeckoView reads configuration from a file named ``/data/local/tmp/$PACKAGE-geckoview-config.yaml``. For example, if your Android package name is ``com.yourcompany.yourapp``, GeckoView will read configuration from:: + + /data/local/tmp/com.yourcompany.yourapp-geckoview-config.yaml + + +Configuration file format +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The configuration file format is `YAML <https://yaml.org>`_. The following keys are recognized: + +- ``env`` is a map from string environment variable name to string value to set in GeckoView's runtime environment +- ``args`` is a list of string command line arguments to pass to the Gecko process +- ``prefs`` is a map from string Gecko preference name to boolean, string, or integer value to set in the Gecko profile + +.. code-block:: yaml + + # Contents of /data/local/tmp/com.yourcompany.yourapp-geckoview-config.yaml + + env: + MOZ_LOG: nsHttp:5 + + args: + - --marionette + - --profile + - "/path/to/gecko-profile" + + prefs: + foo.bar.boolean: true + foo.bar.string: "string" + foo.bar.int: 500 + + +Verifying configuration from a file +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When configuration from a file is read, GeckoView logs to ``adb logcat``, like: :: + + GeckoRuntime I Adding debug configuration from: /data/local/tmp/org.mozilla.geckoview_example-geckoview-config.yaml + GeckoDebugConfig D Adding environment variables from debug config: {MOZ_LOG=nsHttp:5} + GeckoDebugConfig D Adding arguments from debug config: [--marionette] + GeckoDebugConfig D Adding prefs from debug config: {foo.bar.baz=true} + + +When a configuration file is found but cannot be parsed, an error is logged and the file is ignored entirely. When a configuration file is not found, nothing is logged. + +Controlling configuration from a file +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +By default, GeckoView provides a secure web rendering engine. Custom configuration can compromise security in many ways: by storing sensitive data in insecure locations on the device, by trusting websites with incorrect security configurations, by not validating HTTP Public Key Pinning configurations; the list goes on. + +**You should only allow such configuration if your end-user opts-in to the configuration!** + +GeckoView will always read configuration from a file if the consuming Android package is set as the current Android "debug app" (see ``set-debug-app`` and ``clear-debug-app`` in the `adb documentation <https://developer.android.com/studio/command-line/adb>`_). An Android package can be set as the "debug app" without regard to the ``android:debuggable`` flag. There can only be one "debug app" set at a time. To disable the "debug app" check, `disable reading configuration from a file entirely <#disabling-reading-configuration-from-a-file-entirely>`_. Setting an Android package as the "debug app" requires privileged shell access to the device (generally via ``adb shell am ...``, which is only possible on devices which have ADB debugging enabled) and therefore it is safe to act on the "debug app" flag. + +To enable reading configuration from a file: :: + + adb shell am set-debug-app --persistent com.yourcompany.yourapp + + +To disable reading configuration from a file: :: + + adb shell am clear-debug-app + +Enabling reading configuration from a file unconditionally +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Some applications (for example, web browsers) may want to allow configuration for automation unconditionally, i.e., even when the application is not debuggable, like release builds that have ``android:debuggable="false"``. In such cases, you can use `GeckoRuntimeSettings.Builder#configFilePath`_ to force GeckoView to read configuration from the given file path, like: + +.. code-block:: java + + new GeckoRuntimeSettings.Builder() + .configFilePath("/your/app/specific/location") + .build(); + +Disabling reading configuration from a file entirely +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To force GeckoView to never read configuration from a file, even when the embedding application is debuggable, invoke `GeckoRuntimeSettings.Builder#configFilePath`_ with an empty path, like: + +.. code-block:: java + + new GeckoRuntimeSettings.Builder() + .configFilePath("") + .build(); + +The empty path is recognized and no file I/O is performed. + + +.. _GeckoRuntimeSettings.Builder#configFilePath: https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/GeckoRuntimeSettings.Builder.html#configFilePath(java.lang.String) diff --git a/mobile/android/docs/geckoview/consumer/geckoview-quick-start.rst b/mobile/android/docs/geckoview/consumer/geckoview-quick-start.rst new file mode 100644 index 0000000000..6a529a0dfa --- /dev/null +++ b/mobile/android/docs/geckoview/consumer/geckoview-quick-start.rst @@ -0,0 +1,117 @@ +.. -*- Mode: rst; fill-column: 80; -*- + +Getting Started with GeckoView +###################################### + +How to use GeckoView in your Android app. + +*Building a browser? Check out* `Android Components <https://mozilla-mobile.github.io/firefox-android/>`_, *our collection of ready-to-use support libraries!* + +The following article is a brief guide to embedding GeckoView in an app. For a more in depth tutorial on getting started with GeckoView please read the article we have published on `raywenderlich.com <https://www.raywenderlich.com/1381698-android-tutorial-for-geckoview-getting-started>`_. + +.. contents:: :local: + +Configure Gradle +================= + +You need to add or edit four stanzas inside your module's ``build.gradle`` file. + +**1. Set the GeckoView version** + +*Like Firefox, GeckoView has three release channels: Stable, Beta, and Nightly. Browse the* `Maven Repository <https://maven.mozilla.org/?prefix=maven2/org/mozilla/geckoview/>`_ *to see currently available builds.* + +.. code-block:: groovy + + ext { + geckoviewChannel = <channel> + geckoviewVersion = <version> + } + + +**2. Add Mozilla's Maven repository** + +.. code-block:: groovy + + repositories { + maven { + url "https://maven.mozilla.org/maven2/" + } + } + + +**3. Java 17 required support** + +As GeckoView uses some Java 17 APIs, it requires these compatibility flags: + +.. code-block:: groovy + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + +**4. Add GeckoView Implementations** + +.. code-block:: groovy + + dependencies { + // ... + implementation "org.mozilla.geckoview:geckoview-${geckoviewChannel}:${geckoviewVersion}" + } + +Add GeckoView to a Layout +========================== + +Inside a layout ``.xml`` file, add the following: + +.. code-block:: xml + + <org.mozilla.geckoview.GeckoView + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/geckoview" + android:layout_width="fill_parent" + android:layout_height="fill_parent" /> + +Initialize GeckoView in an Activity +==================================== + +**1. Import the GeckoView classes inside an Activity:** + +.. code-block:: java + + import org.mozilla.geckoview.GeckoRuntime; + import org.mozilla.geckoview.GeckoSession; + import org.mozilla.geckoview.GeckoView; + + +**2. Create a ``static`` member variable to store the ``GeckoRuntime`` instance.** + +.. code-block:: java + + private static GeckoRuntime sRuntime; + +**3. In that activity's** ``onCreate`` **function, add the following:** + +.. code-block:: java + + GeckoView view = findViewById(R.id.geckoview); + GeckoSession session = new GeckoSession(); + + // Workaround for Bug 1758212 + session.setContentDelegate(new GeckoSession.ContentDelegate() {}); + + if (sRuntime == null) { + // GeckoRuntime can only be initialized once per process + sRuntime = GeckoRuntime.create(this); + } + + session.open(sRuntime); + view.setSession(session); + session.loadUri("about:buildconfig"); // Or any other URL... + +You're done! +============== + +Your application should now load and display a webpage inside of GeckoView. + +To learn more about GeckoView's capabilities, review GeckoView's `JavaDoc <https://mozilla.github.io/geckoview/javadoc/mozilla-central/>`_ or the `reference application <https://searchfox.org/mozilla-central/source/mobile/android/geckoview_example>`_. diff --git a/mobile/android/docs/geckoview/consumer/index.rst b/mobile/android/docs/geckoview/consumer/index.rst new file mode 100644 index 0000000000..793ead2386 --- /dev/null +++ b/mobile/android/docs/geckoview/consumer/index.rst @@ -0,0 +1,23 @@ +.. -*- Mode: rst; fill-column: 80; -*- + +=============== +Using GeckoView +=============== + +.. toctree:: + :maxdepth: 1 + :glob: + :hidden: + + * + +- `GeckoView Quick Start Guide <geckoview-quick-start.html>`__: Get + GeckoView up and running inside your application. +- `Interacting with Web Content <web-extensions.html>`__: Writing Web + Extensions, running content scripts and interacting with Javascript + running in a web page. +- `Working with Site Permissions <permissions.html>`__: Handling and + responding to requests from websites for permissions, such as + geolocation, storage, media etc. +- `Configuring GeckoView for Automation <automation.html>`__: Get GeckoView + set up on your automation system. diff --git a/mobile/android/docs/geckoview/consumer/permissions.rst b/mobile/android/docs/geckoview/consumer/permissions.rst new file mode 100644 index 0000000000..5dbab9b6b6 --- /dev/null +++ b/mobile/android/docs/geckoview/consumer/permissions.rst @@ -0,0 +1,287 @@ +.. -*- Mode: rst; fill-column: 80; -*- + +============================= +Working with Site Permissions +============================= + +When a website wants to access certain services on a user’s device, it +will send out a permissions request. This document will explain how to +use GeckoView to receive those requests, and respond to them by granting +or denying those permissions. + +.. contents:: :local: + +The Permission Delegate +----------------------- + +The way an app interacts with site permissions in GeckoView is through +the +`PermissionDelegate <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/GeckoSession.PermissionDelegate.html>`_. +There are three broad categories of permission that the +``PermissionDelegate`` handles, Android Permissions, Content Permissions +and Media Permissions. All site permissions handled by GeckoView fall +into one of these three categories. + +To get notified about permission requests, you need to implement the +``PermissionDelegate`` interface: + +.. code:: java + + private class ExamplePermissionDelegate implements GeckoSession.PermissionDelegate { + @Override + public void onAndroidPermissionsRequest(final GeckoSession session, + final String[] permissions, + final Callback callback) { } + + @Override + public void onContentPermissionRequest(final GeckoSession session, + final String uri, + final int type, final Callback callback) { } + + @Override + public void onMediaPermissionRequest(final GeckoSession session, + final String uri, + final MediaSource[] video, + final MediaSource[] audio, + final MediaCallback callback) { } + } + +You will then need to register the delegate with your +`GeckoSession <https://mozilla.github.io/geckoview/https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/GeckoSession.html>`_ +instance. + +.. code:: java + + public class GeckoViewActivity extends AppCompatActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + ... + + final ExamplePermissionDelegate permission = new ExamplePermissionDelegate(); + session.setPermissionDelegate(permission); + + ... + } + } + +Android Permissions +~~~~~~~~~~~~~~~~~~~ + +Android permissions are requested whenever a site wants access to a +device’s navigation or input capabilities. + +The user will often need to grant these Android permissions to the app +alongside granting the Content or Media site permissions. + +When you receive an +`onAndroidPermissionsRequest <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/GeckoSession.PermissionDelegate.html#onAndroidPermissionsRequest(org.mozilla.geckoview.GeckoSession,java.lang.String[],org.mozilla.geckoview.GeckoSession.PermissionDelegate.Callback)>`_ +call, you will also receive the ``GeckoSession`` the request was sent +from, an array containing the permissions that are being requested, and +a +`Callback`_ +to respond to the request. It is then up to the app to request those +permissions from the device, which can be done using +`requestPermissions <https://developer.android.com/reference/android/app/Activity#requestPermissions(java.lang.String%5B%5D,%2520int)>`_. + +Possible ``permissions`` values are: +`ACCESS_COARSE_LOCATION <https://developer.android.com/reference/android/Manifest.permission.html#ACCESS_COARSE_LOCATION>`_, +`ACCESS_FINE_LOCATION <https://developer.android.com/reference/android/Manifest.permission.html#ACCESS_FINE_LOCATION>`_, +`CAMERA <https://developer.android.com/reference/android/Manifest.permission.html#CAMERA>`_ +or +`RECORD_AUDIO <https://developer.android.com/reference/android/Manifest.permission.html#RECORD_AUDIO>`_. + +.. code:: java + + private class ExamplePermissionDelegate implements GeckoSession.PermissionDelegate { + private Callback mCallback; + + public void onRequestPermissionsResult(final String[] permissions, + final int[] grantResults) { + if (mCallback == null) { return; } + + final Callback cb = mCallback; + mCallback = null; + for (final int result : grantResults) { + if (result != PackageManager.PERMISSION_GRANTED) { + // At least one permission was not granted. + cb.reject(); + return; + } + } + cb.grant(); + } + + @Override + public void onAndroidPermissionsRequest(final GeckoSession session, + final String[] permissions, + final Callback callback) { + mCallback = callback; + requestPermissions(permissions, androidPermissionRequestCode); + } + } + + public class GeckoViewActivity extends AppCompatActivity { + @Override + public void onRequestPermissionsResult(final int requestCode, + final String[] permissions, + final int[] grantResults) { + if (requestCode == REQUEST_PERMISSIONS || + requestCode == REQUEST_WRITE_EXTERNAL_STORAGE) { + final ExamplePermissionDelegate permission = (ExamplePermissionDelegate) + getCurrentSession().getPermissionDelegate(); + permission.onRequestPermissionsResult(permissions, grantResults); + } else { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + } + } + +Content Permissions +~~~~~~~~~~~~~~~~~~~ + +Content permissions are requested whenever a site wants access to +content that is stored on the device. The content permissions that can +be requested through GeckoView are: +`Geolocation <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/GeckoSession.PermissionDelegate.html#PERMISSION_GEOLOCATION>`_, +`Site Notifications <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/GeckoSession.PermissionDelegate.html#PERMISSION_DESKTOP_NOTIFICATION>`_, +`Persistent Storage <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/GeckoSession.PermissionDelegate.html#PERMISSION_PERSISTENT_STORAGE>`_, +`XR <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/GeckoSession.PermissionDelegate.html#PERMISSION_XR>`_, +`Autoplay Inaudible <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/GeckoSession.PermissionDelegate.html#PERMISSION_AUTOPLAY_INAUDIBLE>`_, +`Autoplay Audible <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/GeckoSession.PermissionDelegate.html#PERMISSION_AUTOPLAY_AUDIBLE>`_, +and +`DRM Media access <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/GeckoSession.PermissionDelegate.html#PERMISSION_MEDIA_KEY_SYSTEM_ACCESS>`_. +Additionally, `tracking protection exceptions <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/GeckoSession.PermissionDelegate.html#PERMISSION_TRACKING>`_ +are treated as a type of content permission. + +When you receive an +`onContentPermissionRequest <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/GeckoSession.PermissionDelegate.html#onContentPermissionRequest(org.mozilla.geckoview.GeckoSession,org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission)>`_ +call, you will also receive the ``GeckoSession`` the request was sent +from, and all relevant information about the permission being requested +stored in a `ContentPermission <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/GeckoSession.PermissionDelegate.ContentPermission.html>`_. +It is then up to the app to present UI to the user asking for the +permissions, and to notify GeckoView of the response via the returned +``GeckoResult``. + +Once a permission has been set in this fashion, GeckoView will persist it +across sessions until it is cleared or modified. When a page is loaded, +the active permissions associated with it (both allowed and denied) will +be reported in `onLocationChange <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/GeckoSession.NavigationDelegate.html#onLocationChange(org.mozilla.geckoview.GeckoSession,java.lang.String,java.util.List)>`_ +as a list of ``ContentPermission`` objects; additionally, one may check all stored +content permissions by calling `getAllPermissions <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/StorageController.html#getAllPermissions()>`_ +and the content permissions associated with a given URI by calling +`getPermissions <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/StorageController.html#getPermissions(java.lang.String,java.lang.String)>`_. +In order to modify an existing permission, you will need the associated +``ContentPermission`` (which can be retrieved from any of the above methods); +then, call `setPermission <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/StorageController.html#setPermission(org.mozilla.geckoview.GeckoSession.PermissionDelegate.ContentPermission,int)>`_ +with the desired new value, or `VALUE_PROMPT <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/GeckoSession.PermissionDelegate.ContentPermission.html#VALUE_PROMPT>`_ +if you wish to unset the permission and let the site request it again in the future. + +Media Permissions +~~~~~~~~~~~~~~~~~ + +Media permissions are requested whenever a site wants access to play or +record media from the device’s camera and microphone. + +When you receive an +`onMediaPermissionRequest <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/GeckoSession.PermissionDelegate.html#onMediaPermissionRequest(org.mozilla.geckoview.GeckoSession,java.lang.String,org.mozilla.geckoview.GeckoSession.PermissionDelegate.MediaSource[],org.mozilla.geckoview.GeckoSession.PermissionDelegate.MediaSource[],org.mozilla.geckoview.GeckoSession.PermissionDelegate.MediaCallback)>`_ +call, you will also receive the ``GeckoSession`` the request was sent +from, the URI of the site that requested the permission, as a String, +the list of video devices available, if requesting video, the list of +audio devices available, if requesting audio, and a +`MediaCallback <https://searchfox.org/mozilla-central/source/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java#686>`_ +to respond to the request. + +It is up to the app to present UI to the user asking for the +permissions, and to notify GeckoView of the response via the +``MediaCallback``. + +*Please note, media permissions will still be requested if the +associated device permissions have been denied if there are video or +audio sources in that category that can still be accessed when listed. +It is the responsibility of consumers to ensure that media permission +requests are not displayed in this case.* + +.. code:: java + + private class ExamplePermissionDelegate implements GeckoSession.PermissionDelegate { + @Override + public void onMediaPermissionRequest(final GeckoSession session, + final String uri, + final MediaSource[] video, + final MediaSource[] audio, + final MediaCallback callback) { + // Reject permission if Android permission has been previously denied. + if ((audio != null + && ContextCompat.checkSelfPermission(GeckoViewActivity.this, + Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) + || (video != null + && ContextCompat.checkSelfPermission(GeckoViewActivity.this, + Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED)) { + callback.reject(); + return; + } + + final String host = Uri.parse(uri).getAuthority(); + final String title; + if (audio == null) { + title = getString(R.string.request_video, host); + } else if (video == null) { + title = getString(R.string.request_audio, host); + } else { + title = getString(R.string.request_media, host); + } + + // Get the media device name from the `MediaDevice` + String[] videoNames = normalizeMediaName(video); + String[] audioNames = normalizeMediaName(audio); + + final AlertDialog.Builder builder = new AlertDialog.Builder(activity); + + // Create drop down boxes to allow users to select which device to grant permission to + final LinearLayout container = addStandardLayout(builder, title, null); + final Spinner videoSpinner; + if (video != null) { + videoSpinner = addMediaSpinner(builder.getContext(), container, video, videoNames); // create spinner and add to alert UI + } else { + videoSpinner = null; + } + + final Spinner audioSpinner; + if (audio != null) { + audioSpinner = addMediaSpinner(builder.getContext(), container, audio, audioNames); // create spinner and add to alert UI + } else { + audioSpinner = null; + } + + builder.setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + @Override + public void onClick(final DialogInterface dialog, final int which) { + // gather selected media devices and grant access + final MediaSource video = (videoSpinner != null) + ? (MediaSource) videoSpinner.getSelectedItem() : null; + final MediaSource audio = (audioSpinner != null) + ? (MediaSource) audioSpinner.getSelectedItem() : null; + callback.grant(video, audio); + } + }); + + final AlertDialog dialog = builder.create(); + dialog.setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(final DialogInterface dialog) { + callback.reject(); + } + }); + dialog.show(); + } + } + +To see the ``PermissionsDelegate`` in action, you can find the full +example implementation in the `GeckoView example +app <https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/GeckoSession.PermissionDelegate.MediaCallback.html>`_. + +.. _Callback: https://mozilla.github.io/geckoview/javadoc/mozilla-central/org/mozilla/geckoview/GeckoSession.PermissionDelegate.Callback.html diff --git a/mobile/android/docs/geckoview/consumer/web-extensions.rst b/mobile/android/docs/geckoview/consumer/web-extensions.rst new file mode 100644 index 0000000000..ef4e75348f --- /dev/null +++ b/mobile/android/docs/geckoview/consumer/web-extensions.rst @@ -0,0 +1,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 |