summaryrefslogtreecommitdiffstats
path: root/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtension.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtension.java')
-rw-r--r--mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtension.java2806
1 files changed, 2806 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtension.java b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtension.java
new file mode 100644
index 0000000000..da5573b3c7
--- /dev/null
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtension.java
@@ -0,0 +1,2806 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.geckoview;
+
+import android.annotation.SuppressLint;
+import android.graphics.Color;
+import android.util.Log;
+import androidx.annotation.AnyThread;
+import androidx.annotation.IntDef;
+import androidx.annotation.LongDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.util.BundleEventListener;
+import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.GeckoBundle;
+
+/** Represents a WebExtension that may be used by GeckoView. */
+public class WebExtension {
+ /**
+ * <code>file:</code> or <code>resource:</code> URI that points to the install location of this
+ * WebExtension. When the WebExtension is included with the APK the file can be specified using
+ * the <code>resource://android</code> alias. E.g.
+ *
+ * <pre><code>
+ * resource://android/assets/web_extensions/my_webextension/
+ * </code></pre>
+ *
+ * Will point to folder <code>/assets/web_extensions/my_webextension/</code> in the APK.
+ */
+ public final @NonNull String location;
+
+ /** Unique identifier for this WebExtension */
+ public final @NonNull String id;
+
+ /** {@link Flags} for this WebExtension. */
+ public final @WebExtensionFlags long flags;
+
+ /** Provides information about this {@link WebExtension}. */
+ public final @NonNull MetaData metaData;
+
+ /**
+ * Whether this extension is built-in. Built-in extension can be installed using {@link
+ * WebExtensionController#installBuiltIn}.
+ */
+ public final boolean isBuiltIn;
+
+ /**
+ * Called whenever a delegate is set or unset on this {@link WebExtension} instance. /* package
+ */
+ interface DelegateController {
+ void onMessageDelegate(final String nativeApp, final MessageDelegate delegate);
+
+ void onActionDelegate(final ActionDelegate delegate);
+
+ void onBrowsingDataDelegate(final BrowsingDataDelegate delegate);
+
+ void onTabDelegate(final TabDelegate delegate);
+
+ void onDownloadDelegate(final DownloadDelegate delegate);
+
+ ActionDelegate getActionDelegate();
+
+ BrowsingDataDelegate getBrowsingDataDelegate();
+
+ TabDelegate getTabDelegate();
+
+ DownloadDelegate getDownloadDelegate();
+ }
+
+ /* package */ interface DelegateControllerProvider {
+ @NonNull
+ DelegateController controllerFor(final WebExtension extension);
+ }
+
+ private final DelegateController mDelegateController;
+
+ @Override
+ public String toString() {
+ return "WebExtension {"
+ + "location="
+ + location
+ + ", "
+ + "id="
+ + id
+ + ", "
+ + "flags="
+ + flags
+ + "}";
+ }
+
+ private static final String LOGTAG = "WebExtension";
+
+ // Keep in sync with GeckoViewWebExtension.sys.mjs
+ public static class Flags {
+ /*
+ * Default flags for this WebExtension.
+ */
+ public static final long NONE = 0;
+
+ /**
+ * Set this flag if you want to enable content scripts messaging. To listen to such messages you
+ * can use {@link SessionController#setMessageDelegate}.
+ */
+ public static final long ALLOW_CONTENT_MESSAGING = 1 << 0;
+
+ // Do not instantiate this class.
+ protected Flags() {}
+ }
+
+ @Retention(RetentionPolicy.SOURCE)
+ @LongDef(
+ flag = true,
+ value = {Flags.NONE, Flags.ALLOW_CONTENT_MESSAGING})
+ public @interface WebExtensionFlags {}
+
+ /* package */ WebExtension(final DelegateControllerProvider provider, final GeckoBundle bundle) {
+ location = bundle.getString("locationURI");
+ id = bundle.getString("webExtensionId");
+ flags = bundle.getInt("webExtensionFlags", 0);
+ isBuiltIn = bundle.getBoolean("isBuiltIn", false);
+ if (bundle.containsKey("metaData")) {
+ metaData = new MetaData(bundle.getBundle("metaData"));
+ } else {
+ metaData = null;
+ }
+ mDelegateController = provider.controllerFor(this);
+ }
+
+ /**
+ * Defines the message delegate for a Native App.
+ *
+ * <p>This message delegate will receive messages from the background script for the native app
+ * specified in <code>nativeApp</code>.
+ *
+ * <p>For messages from content scripts, set a session-specific message delegate using {@link
+ * SessionController#setMessageDelegate}.
+ *
+ * <p>See also <a
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging">
+ * WebExtensions/Native_messaging </a>
+ *
+ * @param messageDelegate handles messaging between the WebExtension and the app. To send a
+ * message from the WebExtension use the <code>runtime.sendNativeMessage</code> WebExtension
+ * API: E.g.
+ * <pre><code>
+ * browser.runtime.sendNativeMessage(nativeApp,
+ * {message: "Hello from WebExtension!"});
+ * </code></pre>
+ * For bidirectional communication, use <code>runtime.connectNative</code>. E.g. in a content
+ * script:
+ * <pre><code>
+ * let port = browser.runtime.connectNative(nativeApp);
+ * port.onMessage.addListener(message =&gt; {
+ * console.log("Message received from app");
+ * });
+ * port.postMessage("Ping from WebExtension");
+ * </code></pre>
+ * The code above will trigger a {@link MessageDelegate#onConnect} call that will contain the
+ * corresponding {@link Port} object that can be used to send messages to the WebExtension.
+ * Note: the <code>nativeApp</code> specified in the WebExtension needs to match the <code>
+ * nativeApp</code> parameter of this method.
+ * <p>You can unset the message delegate by setting a <code>null</code> messageDelegate.
+ * @param nativeApp which native app id this message delegate will handle messaging for. Needs to
+ * match the <code>application</code> parameter of <code>runtime.sendNativeMessage</code> and
+ * <code>runtime.connectNative</code>.
+ * @see SessionController#setMessageDelegate
+ */
+ @UiThread
+ public void setMessageDelegate(
+ final @Nullable MessageDelegate messageDelegate, final @NonNull String nativeApp) {
+ mDelegateController.onMessageDelegate(nativeApp, messageDelegate);
+ }
+
+ @Retention(RetentionPolicy.SOURCE)
+ @LongDef(
+ value = {
+ BrowsingDataDelegate.Type.CACHE,
+ BrowsingDataDelegate.Type.COOKIES,
+ BrowsingDataDelegate.Type.DOWNLOADS,
+ BrowsingDataDelegate.Type.FORM_DATA,
+ BrowsingDataDelegate.Type.HISTORY,
+ BrowsingDataDelegate.Type.LOCAL_STORAGE,
+ BrowsingDataDelegate.Type.PASSWORDS
+ },
+ flag = true)
+ public @interface BrowsingDataTypes {}
+
+ /**
+ * This delegate is used to handle calls from the |browsingData| WebExtension API.
+ *
+ * <p>See also: <a
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browsingData">
+ * WebExtensions/API/browsingData </a>
+ */
+ @UiThread
+ public interface BrowsingDataDelegate {
+ /**
+ * This class represents the current default settings for the "Clear Data" functionality in the
+ * browser.
+ *
+ * <p>See also: <a
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browsingData/settings">
+ * WebExtensions/API/browsingData/settings </a>
+ */
+ @UiThread
+ class Settings {
+ /**
+ * Currently selected setting in the browser's "Clear Data" UI for how far back in time to
+ * remove data given in milliseconds since the UNIX epoch.
+ */
+ public final int sinceUnixTimestamp;
+
+ /**
+ * Data types that can be toggled in the browser's "Clear Data" UI. One or more flags from
+ * {@link Type}.
+ */
+ public final @BrowsingDataTypes long toggleableTypes;
+
+ /**
+ * Data types currently selected in the browser's "Clear Data" UI. One or more flags from
+ * {@link Type}.
+ */
+ public final @BrowsingDataTypes long selectedTypes;
+
+ /**
+ * Creates an instance of Settings.
+ *
+ * <p>This class represents the current default settings for the "Clear Data" functionality in
+ * the browser.
+ *
+ * @param since Currently selected setting in the browser's "Clear Data" UI for how far back
+ * in time to remove data given in milliseconds since the UNIX epoch.
+ * @param toggleableTypes Data types that can be toggled in the browser's "Clear Data" UI. One
+ * or more flags from {@link Type}.
+ * @param selectedTypes Data types currently selected in the browser's "Clear Data" UI. One or
+ * more flags from {@link Type}.
+ */
+ @UiThread
+ public Settings(
+ final int since,
+ final @BrowsingDataTypes long toggleableTypes,
+ final @BrowsingDataTypes long selectedTypes) {
+ this.toggleableTypes = toggleableTypes;
+ this.selectedTypes = selectedTypes;
+ this.sinceUnixTimestamp = since;
+ }
+
+ private GeckoBundle fromBrowsingDataType(final @BrowsingDataTypes long types) {
+ final GeckoBundle result = new GeckoBundle(7);
+ result.putBoolean("cache", (types & Type.CACHE) != 0);
+ result.putBoolean("cookies", (types & Type.COOKIES) != 0);
+ result.putBoolean("downloads", (types & Type.DOWNLOADS) != 0);
+ result.putBoolean("formData", (types & Type.FORM_DATA) != 0);
+ result.putBoolean("history", (types & Type.HISTORY) != 0);
+ result.putBoolean("localStorage", (types & Type.LOCAL_STORAGE) != 0);
+ result.putBoolean("passwords", (types & Type.PASSWORDS) != 0);
+ return result;
+ }
+
+ /* package */ GeckoBundle toGeckoBundle() {
+ final GeckoBundle options = new GeckoBundle(1);
+ options.putLong("since", sinceUnixTimestamp);
+
+ final GeckoBundle result = new GeckoBundle(3);
+ result.putBundle("options", options);
+ result.putBundle("dataToRemove", fromBrowsingDataType(selectedTypes));
+ result.putBundle("dataRemovalPermitted", fromBrowsingDataType(toggleableTypes));
+ return result;
+ }
+ }
+
+ /** Types of data that a browser "Clear Data" UI might have access to. */
+ class Type {
+ protected Type() {}
+
+ public static final long CACHE = 1 << 0;
+ public static final long COOKIES = 1 << 1;
+ public static final long DOWNLOADS = 1 << 2;
+ public static final long FORM_DATA = 1 << 3;
+ public static final long HISTORY = 1 << 4;
+ public static final long LOCAL_STORAGE = 1 << 5;
+ public static final long PASSWORDS = 1 << 6;
+ }
+
+ /**
+ * Gets current settings for the browser's "Clear Data" UI.
+ *
+ * @return a {@link GeckoResult} that resolves to an instance of {@link Settings} that
+ * represents the current state for the browser's "Clear Data" UI.
+ * @see Settings
+ */
+ @Nullable
+ default GeckoResult<Settings> onGetSettings() {
+ return null;
+ }
+
+ /**
+ * Clear form data created after the given timestamp.
+ *
+ * @param sinceUnixTimestamp timestamp in seconds since the UNIX Epoch.
+ * @return a {@link GeckoResult} that resolves when data has been cleared.
+ */
+ @Nullable
+ default GeckoResult<Void> onClearFormData(final long sinceUnixTimestamp) {
+ return null;
+ }
+
+ /**
+ * Clear passwords saved after the given timestamp.
+ *
+ * @param sinceUnixTimestamp timestamp in seconds since the UNIX Epoch.
+ * @return a {@link GeckoResult} that resolves when data has been cleared.
+ */
+ @Nullable
+ default GeckoResult<Void> onClearPasswords(final long sinceUnixTimestamp) {
+ return null;
+ }
+
+ /**
+ * Clear history saved after the given timestamp.
+ *
+ * @param sinceUnixTimestamp timestamp in seconds since the UNIX Epoch.
+ * @return a {@link GeckoResult} that resolves when data has been cleared.
+ */
+ @Nullable
+ default GeckoResult<Void> onClearHistory(final long sinceUnixTimestamp) {
+ return null;
+ }
+
+ /**
+ * Clear downloads created after the given timestamp.
+ *
+ * @param sinceUnixTimestamp timestamp in seconds since the UNIX Epoch.
+ * @return a {@link GeckoResult} that resolves when data has been cleared.
+ */
+ @Nullable
+ default GeckoResult<Void> onClearDownloads(final long sinceUnixTimestamp) {
+ return null;
+ }
+ }
+
+ /** Delegates that responds to messages sent from a WebExtension. */
+ @UiThread
+ public interface MessageDelegate {
+ /**
+ * Called whenever the WebExtension sends a message to an app using <code>
+ * runtime.sendNativeMessage</code>.
+ *
+ * @param nativeApp The application identifier of the MessageDelegate that sent this message.
+ * @param message The message that was sent, either a primitive type or a {@link
+ * org.json.JSONObject}.
+ * @param sender The {@link MessageSender} corresponding to the frame that originated the
+ * message.
+ * <p>Note: all messages are to be considered untrusted and should be checked carefully for
+ * validity.
+ * @return A {@link GeckoResult} that resolves with a response to the message.
+ */
+ @Nullable
+ default GeckoResult<Object> onMessage(
+ final @NonNull String nativeApp,
+ final @NonNull Object message,
+ final @NonNull MessageSender sender) {
+ return null;
+ }
+
+ /**
+ * Called whenever the WebExtension connects to an app using <code>runtime.connectNative</code>.
+ *
+ * @param port {@link Port} instance that can be used to send and receive messages from the
+ * WebExtension. Use {@link Port#sender} to verify the origin of this connection request.
+ */
+ @Nullable
+ default void onConnect(final @NonNull Port port) {}
+ }
+
+ /**
+ * Delegate that handles communication from a WebExtension on a specific {@link Port} instance.
+ */
+ @UiThread
+ public interface PortDelegate {
+ /**
+ * Called whenever a message is sent through the corresponding {@link Port} instance.
+ *
+ * @param message The message that was sent, either a primitive type or a {@link
+ * org.json.JSONObject}.
+ * @param port The {@link Port} instance that received this message.
+ */
+ default void onPortMessage(final @NonNull Object message, final @NonNull Port port) {}
+
+ /**
+ * Called whenever the corresponding {@link Port} instance is disconnected or the corresponding
+ * {@link GeckoSession} is destroyed. Any message sent from the port after this call will be
+ * ignored.
+ *
+ * @param port The {@link Port} instance that was disconnected.
+ */
+ @NonNull
+ default void onDisconnect(final @NonNull Port port) {}
+ }
+
+ /**
+ * Port object that can be used for bidirectional communication with a WebExtension.
+ *
+ * <p>See also: <a
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/Port">
+ * WebExtensions/API/runtime/Port </a>.
+ *
+ * @see MessageDelegate#onConnect
+ */
+ @UiThread
+ public static class Port {
+ /* package */ final long id;
+ /* package */ PortDelegate delegate;
+ /* package */ boolean disconnected = false;
+ /* package */ final EventDispatcher mEventDispatcher;
+ /* package */ boolean mListenersRegistered = false;
+
+ /** {@link MessageSender} corresponding to this port. */
+ public @NonNull final MessageSender sender;
+
+ /** The application identifier of the MessageDelegate that opened this port. */
+ public @NonNull final String name;
+
+ /** Override for tests. */
+ protected Port() {
+ this.id = -1;
+ this.delegate = null;
+ this.sender = null;
+ this.name = null;
+ mEventDispatcher = null;
+ }
+
+ /* package */ Port(final String name, final long id, final MessageSender sender) {
+ this.id = id;
+ this.delegate = null;
+ this.sender = sender;
+ this.name = name;
+ mEventDispatcher = EventDispatcher.byName("port:" + id);
+ }
+
+ private BundleEventListener mEventListener =
+ new BundleEventListener() {
+ @Override
+ public void handleMessage(
+ final String event, final GeckoBundle message, final EventCallback callback) {
+ if ("GeckoView:WebExtension:Disconnect".equals(event)) {
+ disconnectFromExtension(callback);
+ } else if ("GeckoView:WebExtension:PortMessage".equals(event)) {
+ portMessage(message, callback);
+ }
+ }
+ };
+
+ private void disconnectFromExtension(final EventCallback callback) {
+ delegate.onDisconnect(this);
+ disconnected();
+ }
+
+ private void portMessage(final GeckoBundle bundle, final EventCallback callback) {
+ final Object content;
+ try {
+ content = bundle.toJSONObject().get("data");
+ } catch (final JSONException ex) {
+ callback.sendError(ex);
+ return;
+ }
+
+ delegate.onPortMessage(content, this);
+ }
+
+ /**
+ * Post a message to the WebExtension connected to this {@link Port} instance.
+ *
+ * @param message {@link JSONObject} that will be sent to the WebExtension.
+ */
+ public void postMessage(final @NonNull JSONObject message) {
+ final GeckoBundle args = new GeckoBundle(1);
+ try {
+ args.putBundle("message", GeckoBundle.fromJSONObject(message));
+ } catch (final JSONException ex) {
+ throw new RuntimeException(ex);
+ }
+
+ mEventDispatcher.dispatch("GeckoView:WebExtension:PortMessageFromApp", args);
+ }
+
+ /** Disconnects this port and notifies the other end. */
+ public void disconnect() {
+ if (this.disconnected) {
+ return;
+ }
+
+ final GeckoBundle args = new GeckoBundle(1);
+ args.putLong("portId", id);
+
+ mEventDispatcher.dispatch("GeckoView:WebExtension:PortDisconnect", args);
+ disconnected();
+ }
+
+ private void disconnected() {
+ unregisterListeners();
+ mEventDispatcher.shutdown();
+ this.disconnected = true;
+ }
+
+ /**
+ * Set a delegate for incoming messages through this {@link Port}.
+ *
+ * @param delegate Delegate that will receive messages sent through this {@link Port}.
+ */
+ public void setDelegate(final @Nullable PortDelegate delegate) {
+ this.delegate = delegate;
+
+ if (delegate != null) {
+ registerListeners();
+ } else {
+ unregisterListeners();
+ }
+ }
+
+ private void unregisterListeners() {
+ if (!mListenersRegistered) {
+ return;
+ }
+
+ mEventDispatcher.unregisterUiThreadListener(
+ mEventListener,
+ "GeckoView:WebExtension:Disconnect",
+ "GeckoView:WebExtension:PortMessage");
+ mListenersRegistered = false;
+ }
+
+ private void registerListeners() {
+ if (mListenersRegistered) {
+ return;
+ }
+
+ mEventDispatcher.registerUiThreadListener(
+ mEventListener,
+ "GeckoView:WebExtension:Disconnect",
+ "GeckoView:WebExtension:PortMessage");
+ mListenersRegistered = true;
+ }
+ }
+
+ /**
+ * This delegate is invoked whenever an extension uses the `tabs` WebExtension API to modify the
+ * state of a tab. See also <a
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs">WebExtensions/API/tabs</a>.
+ */
+ public interface SessionTabDelegate {
+ /**
+ * Called when tabs.remove is invoked, this method decides if WebExtension can close the tab. In
+ * case WebExtension can close the tab, it should close passed GeckoSession and return
+ * GeckoResult.ALLOW or GeckoResult.DENY in case tab cannot be closed.
+ *
+ * <p>See also: <a
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/remove">
+ * WebExtensions/API/tabs/remove</a>
+ *
+ * @param source An instance of {@link WebExtension}
+ * @param session An instance of {@link GeckoSession} to be closed.
+ * @return GeckoResult.ALLOW if the tab will be closed, GeckoResult.DENY otherwise
+ */
+ @UiThread
+ @NonNull
+ default GeckoResult<AllowOrDeny> onCloseTab(
+ @Nullable final WebExtension source, @NonNull final GeckoSession session) {
+ return GeckoResult.deny();
+ }
+
+ /**
+ * Called when tabs.update is invoked. The uri is provided for informational purposes, there's
+ * no need to call <code>loadURI</code> on it. The page will be loaded if this method returns
+ * GeckoResult.ALLOW.
+ *
+ * <p>See also: <a
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/update">
+ * WebExtensions/API/tabs/update</a>
+ *
+ * @param extension The extension that requested to update the tab.
+ * @param session The {@link GeckoSession} instance that needs to be updated.
+ * @param details {@link UpdateTabDetails} instance that describes what needs to be updated for
+ * this tab.
+ * @return <code>GeckoResult.ALLOW</code> if the tab will be updated, <code>GeckoResult.DENY
+ * </code> otherwise.
+ */
+ @UiThread
+ @NonNull
+ default GeckoResult<AllowOrDeny> onUpdateTab(
+ final @NonNull WebExtension extension,
+ final @NonNull GeckoSession session,
+ final @NonNull UpdateTabDetails details) {
+ return GeckoResult.deny();
+ }
+ }
+
+ /**
+ * Provides details about upating a tab with <code>tabs.update</code>.
+ *
+ * <p>Whenever a field is not passed in by the extension that value will be <code>null</code>.
+ *
+ * <p>See also: <a
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/update">
+ * WebExtensions/API/tabs/update </a>.
+ */
+ public static class UpdateTabDetails {
+ /**
+ * Whether the tab should become active. If <code>true</code>, non-active highlighted tabs
+ * should stop being highlighted. If <code>false</code>, does nothing.
+ */
+ @Nullable public final Boolean active;
+
+ /** Whether the tab should be discarded automatically by the app when resources are low. */
+ @Nullable public final Boolean autoDiscardable;
+
+ /** If <code>true</code> and the tab is not highlighted, it should become active by default. */
+ @Nullable public final Boolean highlighted;
+
+ /** Whether the tab should be muted. */
+ @Nullable public final Boolean muted;
+
+ /** Whether the tab should be pinned. */
+ @Nullable public final Boolean pinned;
+
+ /**
+ * The url that the tab will be navigated to. This url is provided just for informational
+ * purposes, there is no need to load the URL manually. The corresponding {@link GeckoSession}
+ * will be navigated to the right URL after returning <code>GeckoResult.ALLOW</code> from {@link
+ * SessionTabDelegate#onUpdateTab}
+ */
+ @Nullable public final String url;
+
+ /** For testing. */
+ protected UpdateTabDetails() {
+ active = null;
+ autoDiscardable = null;
+ highlighted = null;
+ muted = null;
+ pinned = null;
+ url = null;
+ }
+
+ /* package */ UpdateTabDetails(final GeckoBundle bundle) {
+ active = bundle.getBooleanObject("active");
+ autoDiscardable = bundle.getBooleanObject("autoDiscardable");
+ highlighted = bundle.getBooleanObject("highlighted");
+ muted = bundle.getBooleanObject("muted");
+ pinned = bundle.getBooleanObject("pinned");
+ url = bundle.getString("url");
+ }
+ }
+
+ /**
+ * Provides details about creating a tab with <code>tabs.create</code>. See also: <a
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/create">
+ * WebExtensions/API/tabs/create </a>.
+ *
+ * <p>Whenever a field is not passed in by the extension that value will be <code>null</code>.
+ */
+ public static class CreateTabDetails {
+ /**
+ * Whether the tab should become active. If <code>true</code>, non-active highlighted tabs
+ * should stop being highlighted. If <code>false</code>, does nothing.
+ */
+ @Nullable public final Boolean active;
+
+ /**
+ * The CookieStoreId used for the tab. This option is only available if the extension has the
+ * "cookies" permission.
+ */
+ @Nullable public final String cookieStoreId;
+
+ /**
+ * Whether the tab is created and made visible in the tab bar without any content loaded into
+ * memory, a state known as discarded. The tab’s content should be loaded when the tab is
+ * activated.
+ */
+ @Nullable public final Boolean discarded;
+
+ /** The position the tab should take in the window. */
+ @Nullable public final Integer index;
+
+ /** If true, open this tab in Reader Mode. */
+ @Nullable public final Boolean openInReaderMode;
+
+ /** Whether the tab should be pinned. */
+ @Nullable public final Boolean pinned;
+
+ /**
+ * The url that the tab will be navigated to. This url is provided just for informational
+ * purposes, there is no need to load the URL manually. The corresponding {@link GeckoSession}
+ * will be navigated to the right URL after returning <code>GeckoResult.ALLOW</code> from {@link
+ * TabDelegate#onNewTab}
+ */
+ @Nullable public final String url;
+
+ /** For testing. */
+ protected CreateTabDetails() {
+ active = null;
+ cookieStoreId = null;
+ discarded = null;
+ index = null;
+ openInReaderMode = null;
+ pinned = null;
+ url = null;
+ }
+
+ /* package */ CreateTabDetails(final GeckoBundle bundle) {
+ active = bundle.getBooleanObject("active");
+ cookieStoreId = bundle.getString("cookieStoreId");
+ discarded = bundle.getBooleanObject("discarded");
+ index = bundle.getInteger("index");
+ openInReaderMode = bundle.getBooleanObject("openInReaderMode");
+ pinned = bundle.getBooleanObject("pinned");
+ url = bundle.getString("url");
+ }
+ }
+
+ /**
+ * This delegate is invoked whenever an extension uses the `tabs` WebExtension API and the request
+ * is not specific to an existing tab, e.g. when creating a new tab. See also <a
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs">WebExtensions/API/tabs</a>.
+ */
+ public interface TabDelegate {
+ /**
+ * Called when tabs.create is invoked, this method returns a *newly-created* session that
+ * GeckoView will use to load the requested page on. If the returned value is null the page will
+ * not be opened.
+ *
+ * @param source An instance of {@link WebExtension}
+ * @param createDetails Information about this tab.
+ * @return A {@link GeckoResult} which holds the returned GeckoSession. May be null, in which
+ * case the request for a new tab by the extension will fail. The implementation of onNewTab
+ * is responsible for maintaining a reference to the returned object, to prevent it from
+ * being garbage collected.
+ */
+ @UiThread
+ @Nullable
+ default GeckoResult<GeckoSession> onNewTab(
+ @NonNull final WebExtension source, @NonNull final CreateTabDetails createDetails) {
+ return null;
+ }
+
+ /**
+ * Called when runtime.openOptionsPage is invoked with options_ui.open_in_tab = false. In this
+ * case, GeckoView delegates options page handling to the app. With options_ui.open_in_tab =
+ * true, {@link #onNewTab} is called instead.
+ *
+ * @param source An instance of {@link WebExtension}.
+ */
+ @UiThread
+ default void onOpenOptionsPage(@NonNull final WebExtension source) {}
+ }
+
+ /**
+ * Get the tab delegate for this extension.
+ *
+ * <p>See also <a
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs">WebExtensions/API/tabs</a>.
+ *
+ * @return The {@link TabDelegate} instance for this extension.
+ */
+ @UiThread
+ @Nullable
+ public WebExtension.TabDelegate getTabDelegate() {
+ return mDelegateController.getTabDelegate();
+ }
+
+ /**
+ * Set the tab delegate for this extension. This delegate will be invoked whenever this extension
+ * tries to modify the tabs state using the `tabs` WebExtension API.
+ *
+ * <p>See also <a
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs">WebExtensions/API/tabs</a>.
+ *
+ * @param delegate the {@link TabDelegate} instance for this extension.
+ */
+ @UiThread
+ public void setTabDelegate(final @Nullable TabDelegate delegate) {
+ mDelegateController.onTabDelegate(delegate);
+ }
+
+ @UiThread
+ @Nullable
+ public BrowsingDataDelegate getBrowsingDataDelegate() {
+ return mDelegateController.getBrowsingDataDelegate();
+ }
+
+ @UiThread
+ public void setBrowsingDataDelegate(final @Nullable BrowsingDataDelegate delegate) {
+ mDelegateController.onBrowsingDataDelegate(delegate);
+ }
+
+ private static class Sender {
+ public String webExtensionId;
+ public String nativeApp;
+
+ public Sender(final String webExtensionId, final String nativeApp) {
+ this.webExtensionId = webExtensionId;
+ this.nativeApp = nativeApp;
+ }
+
+ @Override
+ public boolean equals(final Object other) {
+ if (!(other instanceof Sender)) {
+ return false;
+ }
+
+ final Sender o = (Sender) other;
+ return webExtensionId.equals(o.webExtensionId) && nativeApp.equals(o.nativeApp);
+ }
+
+ @Override
+ public int hashCode() {
+ return Arrays.hashCode(new Object[] {webExtensionId, nativeApp});
+ }
+ }
+
+ // Public wrapper for Listener
+ public static class SessionController {
+ private final Listener<SessionTabDelegate> mListener;
+
+ /* package */ void setRuntime(final GeckoRuntime runtime) {
+ mListener.runtime = runtime;
+ }
+
+ /* package */ SessionController(final GeckoSession session) {
+ mListener = new Listener<>(session);
+ }
+
+ /**
+ * Defines a message delegate for a Native App.
+ *
+ * <p>If a delegate is already present, this delegate will replace the existing one.
+ *
+ * <p>This message delegate will be responsible for handling messaging between a WebExtension
+ * content script running on the {@link GeckoSession}.
+ *
+ * <p>Note: To receive messages from content scripts, the WebExtension needs to explicitely
+ * allow it in {@link WebExtension#WebExtension} by setting {@link
+ * Flags#ALLOW_CONTENT_MESSAGING}.
+ *
+ * @param webExtension {@link WebExtension} that this delegate receives messages from.
+ * @param delegate {@link MessageDelegate} that will receive messages from this session.
+ * @param nativeApp which native app id this message delegate will handle messaging for.
+ * @see WebExtension#setMessageDelegate
+ */
+ @AnyThread
+ public void setMessageDelegate(
+ final @NonNull WebExtension webExtension,
+ final @Nullable WebExtension.MessageDelegate delegate,
+ final @NonNull String nativeApp) {
+ mListener.setMessageDelegate(webExtension, delegate, nativeApp);
+ }
+
+ /**
+ * Get the message delegate for <code>nativeApp</code>.
+ *
+ * @param extension {@link WebExtension} that this delegate receives messages from.
+ * @param nativeApp identifier for the native app
+ * @return The {@link MessageDelegate} attached to the <code>nativeApp</code>. <code>null</code>
+ * if no delegate is present.
+ */
+ @AnyThread
+ public @Nullable WebExtension.MessageDelegate getMessageDelegate(
+ final @NonNull WebExtension extension, final @NonNull String nativeApp) {
+ return mListener.getMessageDelegate(extension, nativeApp);
+ }
+
+ /**
+ * Set the Action delegate for this session.
+ *
+ * <p>This delegate will receive page and browser action overrides specific to this session. The
+ * default Action will be received by the delegate set by {@link
+ * WebExtension#setActionDelegate}.
+ *
+ * @param extension the {@link WebExtension} object this delegate will receive updates for
+ * @param delegate the {@link ActionDelegate} that will receive updates.
+ * @see WebExtension.Action
+ */
+ @AnyThread
+ public void setActionDelegate(
+ final @NonNull WebExtension extension, final @Nullable ActionDelegate delegate) {
+ mListener.setActionDelegate(extension, delegate);
+ }
+
+ /**
+ * Get the Action delegate for this session.
+ *
+ * @param extension {@link WebExtension} that this delegates receive updates for.
+ * @return {@link ActionDelegate} for this session
+ */
+ @AnyThread
+ @Nullable
+ public ActionDelegate getActionDelegate(final @NonNull WebExtension extension) {
+ return mListener.getActionDelegate(extension);
+ }
+
+ /**
+ * Set the TabDelegate for this session.
+ *
+ * <p>This delegate will receive messages specific for this session coming from the WebExtension
+ * <code>tabs</code> API.
+ *
+ * @param extension the {@link WebExtension} this delegate will receive updates for
+ * @param delegate the {@link TabDelegate} that will receive updates.
+ * @see WebExtension#setTabDelegate
+ */
+ @AnyThread
+ public void setTabDelegate(
+ final @NonNull WebExtension extension, final @Nullable SessionTabDelegate delegate) {
+ mListener.setTabDelegate(extension, delegate);
+ }
+
+ /**
+ * Get the TabDelegate for the given extension.
+ *
+ * @param extension the {@link WebExtension} this delegate refers to.
+ * @return the current {@link SessionTabDelegate} instance
+ */
+ @AnyThread
+ @Nullable
+ public SessionTabDelegate getTabDelegate(final @NonNull WebExtension extension) {
+ return mListener.getTabDelegate(extension);
+ }
+ }
+
+ /* package */ static final class Listener<TabDelegate> implements BundleEventListener {
+ private final HashMap<Sender, MessageDelegate> mMessageDelegates;
+ private final HashMap<String, ActionDelegate> mActionDelegates;
+ private final HashMap<String, BrowsingDataDelegate> mBrowsingDataDelegates;
+ private final HashMap<String, TabDelegate> mTabDelegates;
+ private final HashMap<String, DownloadDelegate> mDownloadDelegates;
+
+ private final GeckoSession mSession;
+ private final EventDispatcher mEventDispatcher;
+
+ private boolean mActionDelegateRegistered = false;
+ private boolean mBrowsingDataDelegateRegistered = false;
+ private boolean mTabDelegateRegistered = false;
+
+ public GeckoRuntime runtime;
+
+ public Listener(final GeckoRuntime runtime) {
+ this(null, runtime);
+ }
+
+ public Listener(final GeckoSession session) {
+ this(session, null);
+
+ // Close tab event is forwarded to the main listener so we need to listen
+ // to it here.
+ mEventDispatcher.registerUiThreadListener(
+ this,
+ "GeckoView:WebExtension:NewTab",
+ "GeckoView:WebExtension:UpdateTab",
+ "GeckoView:WebExtension:CloseTab",
+ "GeckoView:WebExtension:OpenOptionsPage");
+ mTabDelegateRegistered = true;
+ }
+
+ private Listener(final GeckoSession session, final GeckoRuntime runtime) {
+ mMessageDelegates = new HashMap<>();
+ mActionDelegates = new HashMap<>();
+ mBrowsingDataDelegates = new HashMap<>();
+ mTabDelegates = new HashMap<>();
+ mDownloadDelegates = new HashMap<>();
+ mEventDispatcher =
+ session != null ? session.getEventDispatcher() : EventDispatcher.getInstance();
+ mSession = session;
+ this.runtime = runtime;
+
+ // We queue these messages if the delegate has not been attached yet,
+ // so we need to start listening immediately.
+ mEventDispatcher.registerUiThreadListener(
+ this,
+ "GeckoView:WebExtension:Message",
+ "GeckoView:WebExtension:PortMessage",
+ "GeckoView:WebExtension:Connect",
+ "GeckoView:WebExtension:Disconnect",
+ "GeckoView:BrowsingData:GetSettings",
+ "GeckoView:BrowsingData:Clear",
+ "GeckoView:WebExtension:Download");
+ }
+
+ public void unregisterWebExtension(final WebExtension extension) {
+ mMessageDelegates.remove(extension.id);
+ mActionDelegates.remove(extension.id);
+ mBrowsingDataDelegates.remove(extension.id);
+ mTabDelegates.remove(extension.id);
+ mDownloadDelegates.remove(extension.id);
+ }
+
+ public void setTabDelegate(final WebExtension webExtension, final TabDelegate delegate) {
+ if (!mTabDelegateRegistered && delegate != null) {
+ mEventDispatcher.registerUiThreadListener(
+ this,
+ "GeckoView:WebExtension:NewTab",
+ "GeckoView:WebExtension:UpdateTab",
+ "GeckoView:WebExtension:CloseTab",
+ "GeckoView:WebExtension:OpenOptionsPage");
+ mTabDelegateRegistered = true;
+ }
+
+ mTabDelegates.put(webExtension.id, delegate);
+ }
+
+ public TabDelegate getTabDelegate(final WebExtension webExtension) {
+ return mTabDelegates.get(webExtension.id);
+ }
+
+ public void setBrowsingDataDelegate(
+ final WebExtension webExtension, final BrowsingDataDelegate delegate) {
+ mBrowsingDataDelegates.put(webExtension.id, delegate);
+ }
+
+ public BrowsingDataDelegate getBrowsingDataDelegate(final WebExtension webExtension) {
+ return mBrowsingDataDelegates.get(webExtension.id);
+ }
+
+ public void setActionDelegate(
+ final WebExtension webExtension, final WebExtension.ActionDelegate delegate) {
+ if (!mActionDelegateRegistered && delegate != null) {
+ mEventDispatcher.registerUiThreadListener(
+ this,
+ "GeckoView:BrowserAction:Update",
+ "GeckoView:BrowserAction:OpenPopup",
+ "GeckoView:PageAction:Update",
+ "GeckoView:PageAction:OpenPopup");
+ mActionDelegateRegistered = true;
+ }
+
+ mActionDelegates.put(webExtension.id, delegate);
+ }
+
+ public WebExtension.ActionDelegate getActionDelegate(final WebExtension webExtension) {
+ return mActionDelegates.get(webExtension.id);
+ }
+
+ public void setMessageDelegate(
+ final WebExtension webExtension,
+ final WebExtension.MessageDelegate delegate,
+ final String nativeApp) {
+ mMessageDelegates.put(new Sender(webExtension.id, nativeApp), delegate);
+
+ if (runtime != null && delegate != null) {
+ runtime
+ .getWebExtensionController()
+ .releasePendingMessages(webExtension, nativeApp, mSession);
+ }
+ }
+
+ public WebExtension.MessageDelegate getMessageDelegate(
+ final WebExtension webExtension, final String nativeApp) {
+ return mMessageDelegates.get(new Sender(webExtension.id, nativeApp));
+ }
+
+ @Override
+ public void handleMessage(
+ final String event, final GeckoBundle message, final EventCallback callback) {
+ if (runtime == null) {
+ return;
+ }
+
+ runtime.getWebExtensionController().handleMessage(event, message, callback, mSession);
+ }
+
+ public void setDownloadDelegate(
+ final @NonNull WebExtension extension, final @Nullable DownloadDelegate delegate) {
+ mDownloadDelegates.put(extension.id, delegate);
+ }
+
+ public WebExtension.DownloadDelegate getDownloadDelegate(final WebExtension extension) {
+ return mDownloadDelegates.get(extension.id);
+ }
+ }
+
+ /**
+ * Describes the sender of a message from a WebExtension.
+ *
+ * <p>See also: <a
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/MessageSender">
+ * WebExtensions/API/runtime/MessageSender</a>
+ */
+ @UiThread
+ public static class MessageSender {
+ /** {@link WebExtension} that sent this message. */
+ public final @NonNull WebExtension webExtension;
+
+ /**
+ * {@link GeckoSession} that sent this message. <code>null</code> if coming from a background
+ * script.
+ */
+ public final @Nullable GeckoSession session;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({ENV_TYPE_UNKNOWN, ENV_TYPE_EXTENSION, ENV_TYPE_CONTENT_SCRIPT})
+ public @interface EnvType {}
+
+ /* package */ static final int ENV_TYPE_UNKNOWN = 0;
+
+ /** This sender originated inside a privileged extension context like a background script. */
+ public static final int ENV_TYPE_EXTENSION = 1;
+
+ /** This sender originated inside a content script. */
+ public static final int ENV_TYPE_CONTENT_SCRIPT = 2;
+
+ /**
+ * Type of environment that sent this message, either
+ *
+ * <ul>
+ * <li>{@link MessageSender#ENV_TYPE_EXTENSION} if the message was sent from a background page
+ * <li>{@link MessageSender#ENV_TYPE_CONTENT_SCRIPT} if the message was sent from a content
+ * script
+ * </ul>
+ */
+ // TODO: Bug 1534640 do we need ENV_TYPE_EXTENSION_PAGE ?
+ public final @EnvType int environmentType;
+
+ /**
+ * URL of the frame that sent this message.
+ *
+ * <p>Use this value together with {@link MessageSender#isTopLevel} to verify that the message
+ * is coming from the expected page. Only top level frames can be trusted.
+ */
+ public final @NonNull String url;
+
+ /* package */ final boolean isTopLevel;
+
+ /* package */ MessageSender(
+ final @NonNull WebExtension webExtension,
+ final @Nullable GeckoSession session,
+ final @Nullable String url,
+ final @EnvType int environmentType,
+ final boolean isTopLevel) {
+ this.webExtension = webExtension;
+ this.session = session;
+ this.isTopLevel = isTopLevel;
+ this.url = url;
+ this.environmentType = environmentType;
+ }
+
+ /** Override for testing. */
+ protected MessageSender() {
+ this.webExtension = null;
+ this.session = null;
+ this.isTopLevel = false;
+ this.url = null;
+ this.environmentType = ENV_TYPE_UNKNOWN;
+ }
+
+ /**
+ * Whether this MessageSender belongs to a top level frame.
+ *
+ * @return true if the MessageSender was sent from the top level frame, false otherwise.
+ */
+ public boolean isTopLevel() {
+ return this.isTopLevel;
+ }
+ }
+
+ /* package */ static WebExtension fromBundle(
+ final DelegateControllerProvider provider, final GeckoBundle bundle) {
+ if (bundle == null) {
+ return null;
+ }
+ return new WebExtension(provider, bundle.getBundle("extension"));
+ }
+
+ /**
+ * Represents either a Browser Action or a Page Action from the WebExtension API.
+ *
+ * <p>Instances of this class may represent the default <code>Action</code> which applies to all
+ * WebExtension tabs or a tab-specific override. To reconstruct the full <code>Action</code>
+ * object, you can use {@link Action#withDefault}.
+ *
+ * <p>Tab specific overrides can be obtained by registering a delegate using {@link
+ * SessionController#setActionDelegate}, while default values can be obtained by registering a
+ * delegate using {@link #setActionDelegate}. <br>
+ * See also
+ *
+ * <ul>
+ * <li><a target=_blank
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browserAction">
+ * WebExtensions/API/browserAction </a>
+ * <li><a target=_blank
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/pageAction">
+ * WebExtensions/API/pageAction </a>
+ * </ul>
+ */
+ @AnyThread
+ public static class Action {
+ /**
+ * Title of this Action.
+ *
+ * <p>See also: <a target=_blank
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/pageAction/getTitle">
+ * pageAction/getTitle</a>, <a target=_blank
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browserAction/getTitle">
+ * browserAction/getTitle</a>
+ */
+ public final @Nullable String title;
+
+ /**
+ * Icon for this Action.
+ *
+ * <p>See also: <a target=_blank
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/pageAction/setIcon">
+ * pageAction/setIcon</a>, <a target=_blank
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browserAction/setIcon">
+ * browserAction/setIcon</a>
+ */
+ public final @Nullable Image icon;
+
+ /**
+ * Whether this action is enabled and should be visible.
+ *
+ * <p>Note: for page action, this is <code>true</code> when the extension calls <code>
+ * pageAction.show</code> and <code>false</code> when the extension calls <code>pageAction.hide
+ * </code>.
+ *
+ * <p>See also: <a target=_blank
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/pageAction/show">
+ * pageAction/show</a>, <a target=_blank
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browserAction/enabled">
+ * browserAction/enabled</a>
+ */
+ public final @Nullable Boolean enabled;
+
+ /**
+ * Badge text for this action.
+ *
+ * <p>See also: <a target=_blank
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browserAction/getBadgeText">
+ * browserAction/getBadgeText</a>
+ */
+ public final @Nullable String badgeText;
+
+ /**
+ * Background color for the badge for this Action.
+ *
+ * <p>This method will return an Android color int that can be used in {@link
+ * android.widget.TextView#setBackgroundColor(int)} and similar methods.
+ *
+ * <p>See also: <a target=_blank
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browserAction/getBadgeBackgroundColor">
+ * browserAction/getBadgeBackgroundColor</a>
+ */
+ public final @Nullable Integer badgeBackgroundColor;
+
+ /**
+ * Text color for the badge for this Action.
+ *
+ * <p>This method will return an Android color int that can be used in {@link
+ * android.widget.TextView#setTextColor(int)} and similar methods.
+ *
+ * <p>See also: <a target=_blank
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browserAction/getBadgeTextColor">
+ * browserAction/getBadgeTextColor</a>
+ */
+ public final @Nullable Integer badgeTextColor;
+
+ private final WebExtension mExtension;
+
+ /* package */ static final int TYPE_BROWSER_ACTION = 1;
+ /* package */ static final int TYPE_PAGE_ACTION = 2;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({TYPE_BROWSER_ACTION, TYPE_PAGE_ACTION})
+ public @interface ActionType {}
+
+ /* package */ final @ActionType int type;
+
+ /* package */ Action(
+ final @ActionType int type, final GeckoBundle bundle, final WebExtension extension) {
+ mExtension = extension;
+
+ this.type = type;
+
+ title = bundle.getString("title");
+ badgeText = bundle.getString("badgeText");
+ badgeBackgroundColor = colorFromRgbaArray(bundle.getDoubleArray("badgeBackgroundColor"));
+ badgeTextColor = colorFromRgbaArray(bundle.getDoubleArray("badgeTextColor"));
+
+ if (bundle.containsKey("icon")) {
+ icon = Image.fromSizeSrcBundle(bundle.getBundle("icon"));
+ } else {
+ icon = null;
+ }
+
+ if (bundle.getBoolean("patternMatching", false)) {
+ // This action was enabled by pattern matching
+ enabled = true;
+ } else if (bundle.containsKey("enabled")) {
+ enabled = bundle.getBoolean("enabled");
+ } else {
+ enabled = null;
+ }
+ }
+
+ private Integer colorFromRgbaArray(final double[] c) {
+ if (c == null) {
+ return null;
+ }
+
+ return Color.argb((int) c[3], (int) c[0], (int) c[1], (int) c[2]);
+ }
+
+ @Override
+ public String toString() {
+ return "Action {\n"
+ + "\ttitle: "
+ + this.title
+ + ",\n"
+ + "\ticon: "
+ + this.icon
+ + ",\n"
+ + "\tenabled: "
+ + this.enabled
+ + ",\n"
+ + "\tbadgeText: "
+ + this.badgeText
+ + ",\n"
+ + "\tbadgeTextColor: "
+ + this.badgeTextColor
+ + ",\n"
+ + "\tbadgeBackgroundColor: "
+ + this.badgeBackgroundColor
+ + ",\n"
+ + "}";
+ }
+
+ // For testing
+ protected Action() {
+ type = TYPE_BROWSER_ACTION;
+ mExtension = null;
+ title = null;
+ icon = null;
+ enabled = null;
+ badgeText = null;
+ badgeTextColor = null;
+ badgeBackgroundColor = null;
+ }
+
+ /**
+ * Merges values from this Action with the default Action.
+ *
+ * @param defaultValue the default Action as received from {@link
+ * ActionDelegate#onBrowserAction} or {@link ActionDelegate#onPageAction}.
+ * @return an {@link Action} where all <code>null</code> values from this instance are replaced
+ * with values from <code>defaultValue</code>.
+ * @throws IllegalArgumentException if defaultValue is not of the same type, e.g. if this Action
+ * is a Page Action and default value is a Browser Action.
+ */
+ @NonNull
+ public Action withDefault(final @NonNull Action defaultValue) {
+ return new Action(this, defaultValue);
+ }
+
+ /**
+ * @see Action#withDefault
+ */
+ private Action(final Action source, final Action defaultValue) {
+ if (source.type != defaultValue.type) {
+ throw new IllegalArgumentException("defaultValue must be of the same type.");
+ }
+
+ type = source.type;
+ mExtension = source.mExtension;
+
+ title = source.title != null ? source.title : defaultValue.title;
+ icon = source.icon != null ? source.icon : defaultValue.icon;
+ enabled = source.enabled != null ? source.enabled : defaultValue.enabled;
+ badgeText = source.badgeText != null ? source.badgeText : defaultValue.badgeText;
+ badgeTextColor =
+ source.badgeTextColor != null ? source.badgeTextColor : defaultValue.badgeTextColor;
+ badgeBackgroundColor =
+ source.badgeBackgroundColor != null
+ ? source.badgeBackgroundColor
+ : defaultValue.badgeBackgroundColor;
+ }
+
+ /** Notifies the extension that the user has clicked on this Action. */
+ @UiThread
+ public void click() {
+ final GeckoBundle bundle = new GeckoBundle(1);
+ bundle.putString("extensionId", mExtension.id);
+
+ // The click event will return the popup uri if we should open a popup in
+ // response to clicking on the action button.
+ final GeckoResult<String> popupUri;
+ if (type == TYPE_BROWSER_ACTION) {
+ popupUri =
+ EventDispatcher.getInstance().queryString("GeckoView:BrowserAction:Click", bundle);
+ } else if (type == TYPE_PAGE_ACTION) {
+ popupUri = EventDispatcher.getInstance().queryString("GeckoView:PageAction:Click", bundle);
+ } else {
+ throw new IllegalStateException("Unknown Action type");
+ }
+
+ popupUri.accept(
+ uri -> {
+ if (uri == null || uri.isEmpty()) {
+ return;
+ }
+
+ final ActionDelegate delegate = mExtension.mDelegateController.getActionDelegate();
+ if (delegate == null) {
+ return;
+ }
+
+ // The .accept method will be called from the UIThread in this case because
+ // the GeckoResult instance was created on the UIThread
+ @SuppressLint("WrongThread")
+ final GeckoResult<GeckoSession> popup = delegate.onTogglePopup(mExtension, this);
+ openPopup(popup, uri);
+ });
+ }
+
+ /* package */ void openPopup(final GeckoResult<GeckoSession> popup, final String popupUri) {
+ if (popup == null) {
+ return;
+ }
+
+ popup.accept(
+ session -> {
+ if (session == null) {
+ return;
+ }
+
+ session.getSettings().setIsPopup(true);
+ session.loadUri(popupUri);
+ });
+ }
+ }
+
+ /**
+ * Receives updates whenever a Browser action or a Page action has been defined by an extension.
+ *
+ * <p>This delegate will receive the default action when registered with {@link
+ * WebExtension#setActionDelegate}. To receive {@link GeckoSession}-specific overrides you can use
+ * {@link SessionController#setActionDelegate}.
+ */
+ public interface ActionDelegate {
+ /**
+ * Called whenever a browser action is defined or updated.
+ *
+ * <p>This method will be called whenever an extension that defines a browser action is
+ * registered or the properties of the Action are updated.
+ *
+ * <p>See also <a target=_blank
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browserAction">
+ * WebExtensions/API/browserAction </a>, <a target=_blank
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/browser_action">
+ * WebExtensions/manifest.json/browser_action </a>.
+ *
+ * @param extension The extension that defined this browser action.
+ * @param session Either the {@link GeckoSession} corresponding to the tab to which this Action
+ * override applies. <code>null</code> if <code>action</code> is the new default value.
+ * @param action {@link Action} containing the override values for this {@link GeckoSession} or
+ * the default value if <code>session</code> is <code>null</code>.
+ */
+ @UiThread
+ default void onBrowserAction(
+ final @NonNull WebExtension extension,
+ final @Nullable GeckoSession session,
+ final @NonNull Action action) {}
+
+ /**
+ * Called whenever a page action is defined or updated.
+ *
+ * <p>This method will be called whenever an extension that defines a page action is registered
+ * or the properties of the Action are updated.
+ *
+ * <p>See also <a target=_blank
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/pageAction">
+ * WebExtensions/API/pageAction </a>, <a target=_blank
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/page_action">
+ * WebExtensions/manifest.json/page_action </a>.
+ *
+ * @param extension The extension that defined this page action.
+ * @param session Either the {@link GeckoSession} corresponding to the tab to which this Action
+ * override applies. <code>null</code> if <code>action</code> is the new default value.
+ * @param action {@link Action} containing the override values for this {@link GeckoSession} or
+ * the default value if <code>session</code> is <code>null</code>.
+ */
+ @UiThread
+ default void onPageAction(
+ final @NonNull WebExtension extension,
+ final @Nullable GeckoSession session,
+ final @NonNull Action action) {}
+
+ /**
+ * Called whenever the action wants to toggle a popup view.
+ *
+ * @param extension The extension that wants to display a popup
+ * @param action The action where the popup is defined
+ * @return A GeckoSession that will be used to display the pop-up, null if no popup will be
+ * displayed.
+ */
+ @UiThread
+ @Nullable
+ default GeckoResult<GeckoSession> onTogglePopup(
+ final @NonNull WebExtension extension, final @NonNull Action action) {
+ return null;
+ }
+
+ /**
+ * Called whenever the action wants to open a popup view.
+ *
+ * @param extension The extension that wants to display a popup
+ * @param action The action where the popup is defined
+ * @return A GeckoSession that will be used to display the pop-up, null if no popup will be
+ * displayed.
+ */
+ @UiThread
+ @Nullable
+ default GeckoResult<GeckoSession> onOpenPopup(
+ final @NonNull WebExtension extension, final @NonNull Action action) {
+ return null;
+ }
+ }
+
+ /** Extension thrown when an error occurs during extension installation. */
+ public static class InstallException extends Exception {
+ public static class ErrorCodes {
+ /** The download failed due to network problems. */
+ public static final int ERROR_NETWORK_FAILURE = -1;
+
+ /** The downloaded file did not match the provided hash. */
+ public static final int ERROR_INCORRECT_HASH = -2;
+
+ /** The downloaded file seems to be corrupted in some way. */
+ public static final int ERROR_CORRUPT_FILE = -3;
+
+ /** An error occurred trying to write to the filesystem. */
+ public static final int ERROR_FILE_ACCESS = -4;
+
+ /** The extension must be signed and isn't. */
+ public static final int ERROR_SIGNEDSTATE_REQUIRED = -5;
+
+ /** The downloaded extension had a different type than expected. */
+ public static final int ERROR_UNEXPECTED_ADDON_TYPE = -6;
+
+ /** The downloaded extension had a different version than expected */
+ public static final int ERROR_UNEXPECTED_ADDON_VERSION = -9;
+
+ /** The extension did not have the expected ID. */
+ public static final int ERROR_INCORRECT_ID = -7;
+
+ /** The extension did not have the expected ID. */
+ public static final int ERROR_INVALID_DOMAIN = -8;
+
+ /** The extension install was canceled. */
+ public static final int ERROR_USER_CANCELED = -100;
+
+ /** The extension install was postponed until restart. */
+ public static final int ERROR_POSTPONED = -101;
+
+ /** For testing. */
+ protected ErrorCodes() {}
+ }
+
+ /** These states should match gecko's AddonManager.STATE_* constants. */
+ private static class StateCodes {
+ public static final int STATE_POSTPONED = 7;
+ public static final int STATE_CANCELED = 12;
+ }
+
+ /* package */ static Throwable fromQueryException(final Throwable exception) {
+ final EventDispatcher.QueryException queryException =
+ (EventDispatcher.QueryException) exception;
+ final Object response = queryException.data;
+ if (response instanceof GeckoBundle && ((GeckoBundle) response).containsKey("installError")) {
+ final GeckoBundle bundle = (GeckoBundle) response;
+ int errorCode = bundle.getInt("installError");
+ final int installState = bundle.getInt("state");
+ if (errorCode == 0 && installState == StateCodes.STATE_CANCELED) {
+ errorCode = ErrorCodes.ERROR_USER_CANCELED;
+ } else if (errorCode == 0 && installState == StateCodes.STATE_POSTPONED) {
+ errorCode = ErrorCodes.ERROR_POSTPONED;
+ }
+ return new WebExtension.InstallException(errorCode);
+ } else {
+ return new Exception(response.toString());
+ }
+ }
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ value = {
+ ErrorCodes.ERROR_NETWORK_FAILURE,
+ ErrorCodes.ERROR_INCORRECT_HASH,
+ ErrorCodes.ERROR_CORRUPT_FILE,
+ ErrorCodes.ERROR_FILE_ACCESS,
+ ErrorCodes.ERROR_SIGNEDSTATE_REQUIRED,
+ ErrorCodes.ERROR_UNEXPECTED_ADDON_TYPE,
+ ErrorCodes.ERROR_UNEXPECTED_ADDON_VERSION,
+ ErrorCodes.ERROR_INCORRECT_ID,
+ ErrorCodes.ERROR_INVALID_DOMAIN,
+ ErrorCodes.ERROR_USER_CANCELED,
+ ErrorCodes.ERROR_POSTPONED,
+ })
+ public @interface Codes {}
+
+ /** One of {@link ErrorCodes} that provides more information about this exception. */
+ public final @Codes int code;
+
+ /** For testing */
+ protected InstallException() {
+ this.code = ErrorCodes.ERROR_NETWORK_FAILURE;
+ }
+
+ @Override
+ public String toString() {
+ return "InstallException: " + code;
+ }
+
+ /* package */ InstallException(final @Codes int code) {
+ this.code = code;
+ }
+ }
+
+ /**
+ * Set the Action delegate for this WebExtension.
+ *
+ * <p>This delegate will receive updates every time the default Action value changes.
+ *
+ * <p>To listen for {@link GeckoSession}-specific updates, use {@link
+ * SessionController#setActionDelegate}
+ *
+ * @param delegate {@link ActionDelegate} that will receive updates.
+ */
+ @AnyThread
+ public void setActionDelegate(final @Nullable ActionDelegate delegate) {
+ mDelegateController.onActionDelegate(delegate);
+
+ final GeckoBundle bundle = new GeckoBundle(1);
+ bundle.putString("extensionId", id);
+
+ if (delegate != null) {
+ EventDispatcher.getInstance().dispatch("GeckoView:ActionDelegate:Attached", bundle);
+ }
+ }
+
+ /**
+ * Describes the signed status for a {@link WebExtension}.
+ *
+ * <p>See <a href="https://support.mozilla.org/en-US/kb/add-on-signing-in-firefox">Add-on signing
+ * in Firefox. </a>
+ */
+ public static class SignedStateFlags {
+ // Keep in sync with AddonManager.jsm
+ /**
+ * This extension may be signed but by a certificate that doesn't chain to our our trusted
+ * certificate.
+ */
+ public static final int UNKNOWN = -1;
+
+ /** This extension is unsigned. */
+ public static final int MISSING = 0;
+
+ /** This extension has been preliminarily reviewed. */
+ public static final int PRELIMINARY = 1;
+
+ /** This extension has been fully reviewed. */
+ public static final int SIGNED = 2;
+
+ /** This extension is a system add-on. */
+ public static final int SYSTEM = 3;
+
+ /** This extension is signed with a "Mozilla Extensions" certificate. */
+ public static final int PRIVILEGED = 4;
+
+ /* package */ static final int LAST = PRIVILEGED;
+ }
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ SignedStateFlags.UNKNOWN,
+ SignedStateFlags.MISSING,
+ SignedStateFlags.PRELIMINARY,
+ SignedStateFlags.SIGNED,
+ SignedStateFlags.SYSTEM,
+ SignedStateFlags.PRIVILEGED
+ })
+ public @interface SignedState {}
+
+ /**
+ * Describes the blocklist state for a {@link WebExtension}. See <a
+ * href="https://support.mozilla.org/en-US/kb/add-ons-cause-issues-are-on-blocklist">Add-ons that
+ * cause stability or security issues are put on a blocklist </a>.
+ */
+ public static class BlocklistStateFlags {
+ // Keep in sync with nsIBlocklistService.idl
+ /** This extension does not appear in the blocklist. */
+ public static final int NOT_BLOCKED = 0;
+
+ /**
+ * This extension is in the blocklist but the problem is not severe enough to warant forcibly
+ * blocking.
+ */
+ public static final int SOFTBLOCKED = 1;
+
+ /** This extension should be blocked and never used. */
+ public static final int BLOCKED = 2;
+
+ /** This extension is considered outdated, and there is a known update available. */
+ public static final int OUTDATED = 3;
+
+ /** This extension is vulnerable and there is an update. */
+ public static final int VULNERABLE_UPDATE_AVAILABLE = 4;
+
+ /** This extension is vulnerable and there is no update. */
+ public static final int VULNERABLE_NO_UPDATE = 5;
+ }
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ BlocklistStateFlags.NOT_BLOCKED,
+ BlocklistStateFlags.SOFTBLOCKED,
+ BlocklistStateFlags.BLOCKED,
+ BlocklistStateFlags.OUTDATED,
+ BlocklistStateFlags.VULNERABLE_UPDATE_AVAILABLE,
+ BlocklistStateFlags.VULNERABLE_NO_UPDATE
+ })
+ public @interface BlocklistState {}
+
+ public static class DisabledFlags {
+ /** The extension has been disabled by the user */
+ public static final int USER = 1 << 1;
+
+ /**
+ * The extension has been disabled by the blocklist. The details of why this extension was
+ * blocked can be found in {@link MetaData#blocklistState}.
+ */
+ public static final int BLOCKLIST = 1 << 2;
+
+ /**
+ * The extension has been disabled by the application. To enable the extension you can use
+ * {@link WebExtensionController#enable} passing in {@link
+ * WebExtensionController.EnableSource#APP} as <code>source</code>.
+ */
+ public static final int APP = 1 << 3;
+ }
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ flag = true,
+ value = {DisabledFlags.USER, DisabledFlags.BLOCKLIST, DisabledFlags.APP})
+ public @interface EnabledFlags {}
+
+ /** Provides information about a {@link WebExtension}. */
+ public class MetaData {
+ /**
+ * Main {@link Image} branding for this {@link WebExtension}. Can be used when displaying
+ * prompts.
+ */
+ public final @NonNull Image icon;
+
+ /**
+ * API permissions requested or granted to this extension.
+ *
+ * <p>Permission identifiers match entries in the manifest, see <a
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/permissions#API_permissions">
+ * API permissions </a>.
+ */
+ public final @NonNull String[] permissions;
+
+ /**
+ * Host permissions requested or granted to this extension.
+ *
+ * <p>See <a
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/permissions#Host_permissions">
+ * Host permissions </a>.
+ */
+ public final @NonNull String[] origins;
+
+ /**
+ * Branding name for this extension.
+ *
+ * <p>See <a
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/name">
+ * manifest.json/name </a>
+ */
+ public final @Nullable String name;
+
+ /**
+ * Branding description for this extension. This string will be localized using the current
+ * GeckoView language setting.
+ *
+ * <p>See <a
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/description">
+ * manifest.json/description </a>
+ */
+ public final @Nullable String description;
+
+ /**
+ * Version string for this extension.
+ *
+ * <p>See <a
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/version">
+ * manifest.json/version </a>
+ */
+ public final @NonNull String version;
+
+ /**
+ * Creator name as provided in the manifest.
+ *
+ * <p>See <a
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/developer">
+ * manifest.json/developer </a>
+ */
+ public final @Nullable String creatorName;
+
+ /**
+ * Creator url as provided in the manifest.
+ *
+ * <p>See <a
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/developer">
+ * manifest.json/developer </a>
+ */
+ public final @Nullable String creatorUrl;
+
+ /**
+ * Homepage url as provided in the manifest.
+ *
+ * <p>See <a
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/homepage_url">
+ * manifest.json/homepage_url </a>
+ */
+ public final @Nullable String homepageUrl;
+
+ /**
+ * Options page as provided in the manifest.
+ *
+ * <p>See <a
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/options_ui">
+ * manifest.json/options_ui </a>
+ */
+ public final @Nullable String optionsPageUrl;
+
+ /**
+ * Whether the options page should be open in a Tab or not.
+ *
+ * <p>See <a
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/options_ui#Syntax">
+ * manifest.json/options_ui#Syntax </a>
+ */
+ public final boolean openOptionsPageInTab;
+
+ /**
+ * Whether or not this is a recommended extension.
+ *
+ * <p>See <a href="https://blog.mozilla.org/firefox/firefox-recommended-extensions/">Recommended
+ * Extensions program </a>
+ */
+ public final boolean isRecommended;
+
+ /**
+ * Blocklist status for this extension.
+ *
+ * <p>See <a href="https://support.mozilla.org/en-US/kb/add-ons-cause-issues-are-on-blocklist">
+ * Add-ons that cause stability or security issues are put on a blocklist </a>.
+ */
+ public final @BlocklistState int blocklistState;
+
+ /**
+ * Signed status for this extension.
+ *
+ * <p>See <a href="https://support.mozilla.org/en-US/kb/add-on-signing-in-firefox">Add-on
+ * signing in Firefox. </a>.
+ */
+ public final @SignedState int signedState;
+
+ /**
+ * Disabled binary flags for this extension.
+ *
+ * <p>This will be either equal to <code>0</code> if the extension is enabled or will contain
+ * one or more flags from {@link DisabledFlags}.
+ *
+ * <p>e.g. if the extension has been disabled by the user, the value in {@link
+ * DisabledFlags#USER} will be equal to <code>1</code>:
+ *
+ * <pre><code>
+ * boolean isUserDisabled = metaData.disabledFlags
+ * &amp; DisabledFlags.USER &gt; 0;
+ * </code></pre>
+ */
+ public final @EnabledFlags int disabledFlags;
+
+ /**
+ * Root URL for this extension's pages. Can be used to determine if a given URL belongs to this
+ * extension.
+ */
+ public final @NonNull String baseUrl;
+
+ /**
+ * Whether this extension is allowed to run in private browsing or not. To modify this value use
+ * {@link WebExtensionController#setAllowedInPrivateBrowsing}.
+ */
+ public final boolean allowedInPrivateBrowsing;
+
+ /** Whether this extension is enabled or not. */
+ public final boolean enabled;
+
+ /**
+ * Whether this extension is temporary or not. Temporary extensions are not retained and will be
+ * uninstalled when the browser exits.
+ */
+ public final boolean temporary;
+
+ /** Override for testing. */
+ protected MetaData() {
+ icon = null;
+ permissions = null;
+ origins = null;
+ name = null;
+ description = null;
+ version = null;
+ creatorName = null;
+ creatorUrl = null;
+ homepageUrl = null;
+ optionsPageUrl = null;
+ openOptionsPageInTab = false;
+ isRecommended = false;
+ blocklistState = BlocklistStateFlags.NOT_BLOCKED;
+ signedState = SignedStateFlags.UNKNOWN;
+ disabledFlags = 0;
+ enabled = true;
+ temporary = false;
+ baseUrl = null;
+ allowedInPrivateBrowsing = false;
+ }
+
+ /* package */ MetaData(final GeckoBundle bundle) {
+ // We only expose permissions that the embedder should prompt for
+ permissions = bundle.getStringArray("promptPermissions");
+ origins = bundle.getStringArray("origins");
+ description = bundle.getString("description");
+ version = bundle.getString("version");
+ creatorName = bundle.getString("creatorName");
+ creatorUrl = bundle.getString("creatorURL");
+ homepageUrl = bundle.getString("homepageURL");
+ name = bundle.getString("name");
+ optionsPageUrl = bundle.getString("optionsPageURL");
+ openOptionsPageInTab = bundle.getBoolean("openOptionsPageInTab");
+ isRecommended = bundle.getBoolean("isRecommended");
+ blocklistState = bundle.getInt("blocklistState", BlocklistStateFlags.NOT_BLOCKED);
+ enabled = bundle.getBoolean("enabled", false);
+ temporary = bundle.getBoolean("temporary", false);
+ baseUrl = bundle.getString("baseURL");
+ allowedInPrivateBrowsing = bundle.getBoolean("privateBrowsingAllowed", false);
+
+ final int signedState = bundle.getInt("signedState", SignedStateFlags.UNKNOWN);
+ if (signedState <= SignedStateFlags.LAST) {
+ this.signedState = signedState;
+ } else {
+ Log.e(LOGTAG, "Unrecognized signed state: " + signedState);
+ this.signedState = SignedStateFlags.UNKNOWN;
+ }
+
+ int disabledFlags = 0;
+ final String[] disabledFlagsString = bundle.getStringArray("disabledFlags");
+
+ for (final String flag : disabledFlagsString) {
+ if (flag.equals("userDisabled")) {
+ disabledFlags |= DisabledFlags.USER;
+ } else if (flag.equals("blocklistDisabled")) {
+ disabledFlags |= DisabledFlags.BLOCKLIST;
+ } else if (flag.equals("appDisabled")) {
+ disabledFlags |= DisabledFlags.APP;
+ } else {
+ Log.e(LOGTAG, "Unrecognized disabledFlag state: " + flag);
+ }
+ }
+ this.disabledFlags = disabledFlags;
+
+ if (bundle.containsKey("icons")) {
+ icon = Image.fromSizeSrcBundle(bundle.getBundle("icons"));
+ } else {
+ icon = null;
+ }
+ }
+ }
+
+ // TODO: make public bug 1595822
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ flag = true,
+ value = {
+ Context.NONE,
+ Context.BOOKMARK,
+ Context.BROWSER_ACTION,
+ Context.PAGE_ACTION,
+ Context.TAB,
+ Context.TOOLS_MENU
+ })
+ public @interface ContextFlags {}
+
+ /**
+ * Flags to determine which contexts a menu item should be shown in. See <a
+ * href=https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/menus/ContextType>
+ * menus.ContextType</a>.
+ */
+ static class Context {
+ /** Shows the menu item in no contexts. */
+ static final int NONE = 0;
+
+ /**
+ * Shows the menu item when the user context-clicks an item on the bookmarks toolbar, bookmarks
+ * menu, bookmarks sidebar, or Library window.
+ */
+ static final int BOOKMARK = 1 << 1;
+
+ /** Shows the menu item when the user context-clicks the extension's browser action. */
+ static final int BROWSER_ACTION = 1 << 2;
+
+ /** Shows the menu item when the user context-clicks on the extension's page action. */
+ static final int PAGE_ACTION = 1 << 3;
+
+ /** Shows when the user context-clicks on a tab (such as the element on the tab bar.) */
+ static final int TAB = 1 << 4;
+
+ /** Adds the item to the browser's tools menu. */
+ static final int TOOLS_MENU = 1 << 5;
+ }
+
+ // TODO: make public bug 1595822
+
+ /**
+ * Represents an addition to the context menu by an extension.
+ *
+ * <p>In the <a
+ * href=https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/menus>menus</a>
+ * API, all elements added by one extension should be collapsed under one header. This class
+ * represents all of one extension's menu items, as well as the icon that should be used with that
+ * header.
+ */
+ static class Menu {
+ /** List of menu items that belong to this extension. */
+ final @NonNull List<MenuItem> items;
+
+ /** Icon for this extension. */
+ final @Nullable Image icon;
+
+ /** Title for the menu header. */
+ final @Nullable String title;
+
+ /** The extension adding this Menu to the context menu. */
+ final @NonNull WebExtension extension;
+
+ /* package */ Menu(final @NonNull WebExtension extension, final GeckoBundle bundle) {
+ this.extension = extension;
+ title = bundle.getString("title", "");
+ final GeckoBundle[] items = bundle.getBundleArray("items");
+ this.items = new ArrayList<>();
+ if (items != null) {
+ for (final GeckoBundle item : items) {
+ this.items.add(new MenuItem(this.extension, item));
+ }
+ }
+
+ if (bundle.containsKey("icon")) {
+ icon = Image.fromSizeSrcBundle(bundle.getBundle("icon"));
+ } else {
+ icon = null;
+ }
+ }
+
+ /** Notifies the extension that a user has opened the context menu. */
+ void show() {
+ final GeckoBundle bundle = new GeckoBundle(1);
+ bundle.putString("extensionId", extension.id);
+
+ EventDispatcher.getInstance().dispatch("GeckoView:WebExtension:MenuShow", bundle);
+ }
+
+ /** Notifies the extension that a user has hidden the context menu. */
+ void hide() {
+ final GeckoBundle bundle = new GeckoBundle(1);
+ bundle.putString("extensionId", extension.id);
+
+ EventDispatcher.getInstance().dispatch("GeckoView:WebExtension:MenuHide", bundle);
+ }
+ }
+
+ // TODO: make public bug 1595822
+ /**
+ * Represents an item in the menu.
+ *
+ * <p>If there is only one menu item in the list, the embedder should display that item as itself,
+ * not under a header.
+ */
+ static class MenuItem {
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ flag = false,
+ value = {MenuType.NORMAL, MenuType.CHECKBOX, MenuType.RADIO, MenuType.SEPARATOR})
+ public @interface Type {}
+
+ /** A set of constants that represents the display type of this menu item. */
+ static class MenuType {
+ /**
+ * This represents a menu item that just displays a label.
+ *
+ * <p>See <a
+ * href=https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/menus/ItemType>
+ * menus.ItemType.normal</a>
+ */
+ static final int NORMAL = 0;
+
+ /**
+ * This represents a menu item that can be selected and deselected.
+ *
+ * <p>See <a
+ * href=https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/menus/ItemType>
+ * menus.ItemType.checkbox</a>
+ */
+ static final int CHECKBOX = 1;
+
+ /**
+ * This represents a menu item that is one of a group of choices. All menu items for an
+ * extension that are of type radio are part of one radio group.
+ *
+ * <p>See <a
+ * href=https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/menus/ItemType>
+ * menus.ItemType.radio</a>
+ */
+ static final int RADIO = 2;
+
+ /**
+ * This represents a line separating elements.
+ *
+ * <p>See <a
+ * href=https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/menus/ItemType>
+ * menus.ItemType.separator</a>
+ */
+ static final int SEPARATOR = 3;
+ }
+
+ /**
+ * Direct children for this menu item. These should be displayed as a sub-menu.
+ *
+ * <p>See <a
+ * href=https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/menus/create>
+ * createProperties.parentId</a>
+ */
+ final @Nullable List<MenuItem> children;
+
+ /** One of the {@link Type} constants. Determines the type of the action. */
+ final @Type int type;
+
+ /**
+ * The id of this menu item. See <a
+ * href=https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/menus/create>
+ * createProperties.id</a>
+ */
+ final @Nullable String id;
+
+ /** Determines if the menu item should be currently displayed. */
+ final boolean visible;
+
+ /** The title to be displayed for this menu item. */
+ final @Nullable String title;
+
+ /** Whether or not the menu item is initially checked. Defaults to false. */
+ final boolean checked;
+
+ /** Contexts that this menu item should be shown in. */
+ final @ContextFlags int contexts;
+
+ /** Icon for this menu item. */
+ final @Nullable Image icon;
+
+ final WebExtension mExtension;
+
+ /**
+ * Creates a new menu item using a bundle and a reference to the extension that this item
+ * belongs to.
+ *
+ * @param extension WebExtension object.
+ * @param bundle GeckoBundle containing the item information.
+ */
+ /* package */ MenuItem(final WebExtension extension, final GeckoBundle bundle) {
+ title = bundle.getString("title");
+ mExtension = extension;
+ checked = bundle.getBoolean("checked", false);
+ visible = bundle.getBoolean("visible", true);
+ id = bundle.getString("id");
+ contexts = bundle.getInt("contexts");
+ type = bundle.getInt("type");
+ children = new ArrayList<>();
+
+ if (bundle.containsKey("icon")) {
+ icon = Image.fromSizeSrcBundle(bundle.getBundle("icon"));
+ } else {
+ icon = null;
+ }
+ }
+
+ /** Notifies the extension that the user has clicked on this menu item. */
+ void click() {
+ final GeckoBundle bundle = new GeckoBundle(2);
+ bundle.putString("menuId", this.id);
+ bundle.putString("extensionId", mExtension.id);
+
+ EventDispatcher.getInstance().dispatch("GeckoView:WebExtension:MenuClick", bundle);
+ }
+ }
+
+ public interface DownloadDelegate {
+ /**
+ * Method that is called when Web Extension requests a download (when downloads.download() is
+ * called in Web Extension)
+ *
+ * @param source - Web Extension that requested the download
+ * @param request - contains the {@link WebRequest} and additional parameters for the request
+ * @return {@link DownloadInitData} instance
+ */
+ @AnyThread
+ @Nullable
+ default GeckoResult<WebExtension.DownloadInitData> onDownload(
+ @NonNull final WebExtension source, @NonNull final DownloadRequest request) {
+ return null;
+ }
+ }
+
+ /**
+ * Set the download delegate for this extension. This delegate will be invoked whenever this
+ * extension tries to use the `downloads` WebExtension API.
+ *
+ * <p>See also <a
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/downloads">WebExtensions/API/downloads</a>.
+ *
+ * @param delegate the {@link DownloadDelegate} instance for this extension.
+ */
+ @UiThread
+ public void setDownloadDelegate(final @Nullable DownloadDelegate delegate) {
+ mDelegateController.onDownloadDelegate(delegate);
+ }
+
+ /**
+ * Get the download delegate for this extension.
+ *
+ * <p>See also <a
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/downloads">WebExtensions
+ * downloads API</a>.
+ *
+ * @return The {@link DownloadDelegate} instance for this extension.
+ */
+ @UiThread
+ @Nullable
+ public DownloadDelegate getDownloadDelegate() {
+ return mDelegateController.getDownloadDelegate();
+ }
+
+ /**
+ * Represents a download for <a
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/downloads">downloads
+ * API</a> Instantiate using {@link WebExtensionController#createDownload}
+ */
+ public static class Download {
+ /**
+ * Represents a unique identifier for the downloaded item that is persistent across browser
+ * sessions
+ */
+ public final int id;
+
+ /**
+ * For testing.
+ *
+ * @param id - integer id for the download item
+ */
+ protected Download(final int id) {
+ this.id = id;
+ }
+
+ /* package */ void setDelegate(final Delegate delegate) {}
+
+ /**
+ * Updates the download state. This will trigger a call to <a
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/downloads/onChanged">downloads.onChanged</a>
+ * event to the corresponding `DownloadItem` on the extension side.
+ *
+ * @param data - current metadata associated with the download. {@link Download.Info}
+ * implementation instance
+ * @return GeckoResult with nothing or error inside
+ */
+ @Nullable
+ @UiThread
+ public GeckoResult<Void> update(final @NonNull Download.Info data) {
+ final GeckoBundle bundle = new GeckoBundle(12);
+
+ bundle.putInt("downloadItemId", this.id);
+
+ bundle.putString("filename", data.filename());
+ bundle.putString("mime", data.mime());
+ bundle.putString("startTime", String.valueOf(data.startTime()));
+ bundle.putString("endTime", data.endTime() == null ? null : String.valueOf(data.endTime()));
+ bundle.putInt("state", data.state());
+ bundle.putBoolean("canResume", data.canResume());
+ bundle.putBoolean("paused", data.paused());
+ final Integer error = data.error();
+ if (error != null) {
+ bundle.putInt("error", error);
+ }
+ bundle.putLong("totalBytes", data.totalBytes());
+ bundle.putLong("fileSize", data.fileSize());
+ bundle.putBoolean("exists", data.fileExists());
+
+ return EventDispatcher.getInstance()
+ .queryVoid("GeckoView:WebExtension:DownloadChanged", bundle)
+ .map(
+ null,
+ e -> {
+ if (e instanceof EventDispatcher.QueryException) {
+ final EventDispatcher.QueryException queryException =
+ (EventDispatcher.QueryException) e;
+ if (queryException.data instanceof String) {
+ return new IllegalArgumentException((String) queryException.data);
+ }
+ }
+ return e;
+ });
+ }
+
+ /* package */ interface Delegate {
+
+ default GeckoResult<Void> onPause(
+ final WebExtension source, final WebExtension.Download download) {
+ return null;
+ }
+
+ default GeckoResult<Void> onResume(
+ final WebExtension source, final WebExtension.Download download) {
+ return null;
+ }
+
+ default GeckoResult<Void> onCancel(
+ final WebExtension source, final WebExtension.Download download) {
+ return null;
+ }
+
+ default GeckoResult<Void> onErase(
+ final WebExtension source, final WebExtension.Download download) {
+ return null;
+ }
+
+ default GeckoResult<Void> onOpen(
+ final WebExtension source, final WebExtension.Download download) {
+ return null;
+ }
+
+ default GeckoResult<Void> onRemoveFile(
+ final WebExtension source, final WebExtension.Download download) {
+ return null;
+ }
+ }
+
+ /**
+ * Represents a download in progress where the app is currently receiving data from the server.
+ * See also {@link Info#state()}.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({STATE_IN_PROGRESS, STATE_INTERRUPTED, STATE_COMPLETE})
+ public @interface DownloadState {}
+
+ /** Download is in progress. Default state */
+ public static final int STATE_IN_PROGRESS = 0;
+
+ /** An error broke the connection with the server. */
+ public static final int STATE_INTERRUPTED = 1;
+
+ /** The download completed successfully. */
+ public static final int STATE_COMPLETE = 2;
+
+ /**
+ * Represents a possible reason why a download was interrupted. See also {@link Info#error()}.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ INTERRUPT_REASON_NO_INTERRUPT,
+ INTERRUPT_REASON_FILE_FAILED,
+ INTERRUPT_REASON_FILE_ACCESS_DENIED,
+ INTERRUPT_REASON_FILE_NO_SPACE,
+ INTERRUPT_REASON_FILE_NAME_TOO_LONG,
+ INTERRUPT_REASON_FILE_TOO_LARGE,
+ INTERRUPT_REASON_FILE_VIRUS_INFECTED,
+ INTERRUPT_REASON_FILE_TRANSIENT_ERROR,
+ INTERRUPT_REASON_FILE_BLOCKED,
+ INTERRUPT_REASON_FILE_SECURITY_CHECK_FAILED,
+ INTERRUPT_REASON_FILE_TOO_SHORT,
+ INTERRUPT_REASON_NETWORK_FAILED,
+ INTERRUPT_REASON_NETWORK_TIMEOUT,
+ INTERRUPT_REASON_NETWORK_DISCONNECTED,
+ INTERRUPT_REASON_NETWORK_SERVER_DOWN,
+ INTERRUPT_REASON_NETWORK_INVALID_REQUEST,
+ INTERRUPT_REASON_SERVER_FAILED,
+ INTERRUPT_REASON_SERVER_NO_RANGE,
+ INTERRUPT_REASON_SERVER_BAD_CONTENT,
+ INTERRUPT_REASON_SERVER_UNAUTHORIZED,
+ INTERRUPT_REASON_SERVER_CERT_PROBLEM,
+ INTERRUPT_REASON_SERVER_FORBIDDEN,
+ INTERRUPT_REASON_USER_CANCELED,
+ INTERRUPT_REASON_USER_SHUTDOWN,
+ INTERRUPT_REASON_CRASH
+ })
+ public @interface DownloadInterruptReason {}
+
+ // File-related errors
+ public static final int INTERRUPT_REASON_NO_INTERRUPT = 0;
+ public static final int INTERRUPT_REASON_FILE_FAILED = 1;
+ public static final int INTERRUPT_REASON_FILE_ACCESS_DENIED = 2;
+ public static final int INTERRUPT_REASON_FILE_NO_SPACE = 3;
+ public static final int INTERRUPT_REASON_FILE_NAME_TOO_LONG = 4;
+ public static final int INTERRUPT_REASON_FILE_TOO_LARGE = 5;
+ public static final int INTERRUPT_REASON_FILE_VIRUS_INFECTED = 6;
+ public static final int INTERRUPT_REASON_FILE_TRANSIENT_ERROR = 7;
+ public static final int INTERRUPT_REASON_FILE_BLOCKED = 8;
+ public static final int INTERRUPT_REASON_FILE_SECURITY_CHECK_FAILED = 9;
+ public static final int INTERRUPT_REASON_FILE_TOO_SHORT = 10;
+ // Network-related errors
+ public static final int INTERRUPT_REASON_NETWORK_FAILED = 11;
+ public static final int INTERRUPT_REASON_NETWORK_TIMEOUT = 12;
+ public static final int INTERRUPT_REASON_NETWORK_DISCONNECTED = 13;
+ public static final int INTERRUPT_REASON_NETWORK_SERVER_DOWN = 14;
+ public static final int INTERRUPT_REASON_NETWORK_INVALID_REQUEST = 15;
+ // Server-related errors
+ public static final int INTERRUPT_REASON_SERVER_FAILED = 16;
+ public static final int INTERRUPT_REASON_SERVER_NO_RANGE = 17;
+ public static final int INTERRUPT_REASON_SERVER_BAD_CONTENT = 18;
+ public static final int INTERRUPT_REASON_SERVER_UNAUTHORIZED = 19;
+ public static final int INTERRUPT_REASON_SERVER_CERT_PROBLEM = 20;
+ public static final int INTERRUPT_REASON_SERVER_FORBIDDEN = 21;
+ // User-related errors
+ public static final int INTERRUPT_REASON_USER_CANCELED = 22;
+ public static final int INTERRUPT_REASON_USER_SHUTDOWN = 23;
+ // Miscellaneous
+ public static final int INTERRUPT_REASON_CRASH = 24;
+
+ /**
+ * Interface for communicating the state of downloads to Web Extensions. See also <a
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/downloads/DownloadItem">WebExtensions/API/downloads/DownloadItem</a>
+ */
+ public interface Info {
+
+ /**
+ * @return A number representing the number of bytes received so far from the host during the
+ * download This does not take file compression into consideration
+ */
+ @UiThread
+ default long bytesReceived() {
+ return 0;
+ }
+
+ /**
+ * @return boolean indicating whether a currently-interrupted (e.g. paused) download can be
+ * resumed from the point where it was interrupted
+ */
+ @UiThread
+ default boolean canResume() {
+ return false;
+ }
+
+ /**
+ * @return A number representing the time when this download ended. This is null if the
+ * download has not yet finished.
+ */
+ @Nullable
+ @UiThread
+ default Long endTime() {
+ return null;
+ }
+
+ /**
+ * @return One of <a
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/downloads/InterruptReason">Interrupt
+ * Reason</a> constants denoting the error reason.
+ */
+ @Nullable
+ @UiThread
+ default @DownloadInterruptReason Integer error() {
+ return null;
+ }
+
+ /**
+ * @return the estimated number of milliseconds between the UNIX epoch and when this download
+ * is estimated to be completed. This is null if it is not known.
+ */
+ @Nullable
+ @UiThread
+ default Long estimatedEndTime() {
+ return null;
+ }
+
+ /**
+ * @return boolean indicating whether a downloaded file still exists
+ */
+ @UiThread
+ default boolean fileExists() {
+ return false;
+ }
+
+ /**
+ * @return the filename.
+ */
+ @NonNull
+ @UiThread
+ default String filename() {
+ return "";
+ }
+
+ /**
+ * @return the total number of bytes in the whole file, after decompression. A value of -1
+ * means that the total file size is unknown.
+ */
+ @UiThread
+ default long fileSize() {
+ return -1;
+ }
+
+ /**
+ * @return the downloaded file's MIME type
+ */
+ @NonNull
+ @UiThread
+ default String mime() {
+ return "";
+ }
+
+ /**
+ * @return boolean indicating whether the download is paused i.e. if the download has stopped
+ * reading data from the host but has kept the connection open
+ */
+ @UiThread
+ default boolean paused() {
+ return false;
+ }
+
+ /**
+ * @return String representing the downloaded file's referrer
+ */
+ @NonNull
+ @UiThread
+ default String referrer() {
+ return "";
+ }
+
+ /**
+ * @return the number of milliseconds between the UNIX epoch and when this download began
+ */
+ @UiThread
+ default long startTime() {
+ return -1;
+ }
+
+ /**
+ * @return a new state; one of the state constants to indicate whether the download is in
+ * progress, interrupted or complete
+ */
+ @UiThread
+ default @DownloadState int state() {
+ return STATE_IN_PROGRESS;
+ }
+
+ /**
+ * @return total number of bytes in the file being downloaded. This does not take file
+ * compression into consideration. A value of -1 here means that the total number of bytes
+ * is unknown
+ */
+ @UiThread
+ default long totalBytes() {
+ return -1;
+ }
+ }
+
+ @NonNull
+ /* package */ static GeckoBundle downloadInfoToBundle(final @NonNull Info data) {
+ final GeckoBundle dataBundle = new GeckoBundle();
+
+ dataBundle.putLong("bytesReceived", data.bytesReceived());
+ dataBundle.putBoolean("canResume", data.canResume());
+ dataBundle.putBoolean("exists", data.fileExists());
+ dataBundle.putString("filename", data.filename());
+ dataBundle.putLong("fileSize", data.fileSize());
+ dataBundle.putString("mime", data.mime());
+ dataBundle.putBoolean("paused", data.paused());
+ dataBundle.putString("referrer", data.referrer());
+ dataBundle.putString("startTime", String.valueOf(data.startTime()));
+ dataBundle.putInt("state", data.state());
+ dataBundle.putLong("totalBytes", data.totalBytes());
+
+ final Long endTime = data.endTime();
+ if (endTime != null) {
+ dataBundle.putString("endTime", endTime.toString());
+ }
+ final Integer error = data.error();
+ if (error != null) {
+ dataBundle.putInt("error", error);
+ }
+ final Long estimatedEndTime = data.estimatedEndTime();
+ if (estimatedEndTime != null) {
+ dataBundle.putString("estimatedEndTime", estimatedEndTime.toString());
+ }
+
+ return dataBundle;
+ }
+ }
+
+ /** Represents Web Extension API specific download request */
+ public static class DownloadRequest {
+ /** Regular GeckoView {@link WebRequest} object */
+ public final @NonNull WebRequest request;
+
+ /** Optional fetch flags for {@link GeckoWebExecutor} */
+ public final @GeckoWebExecutor.FetchFlags int downloadFlags;
+
+ /** A file path relative to the default downloads directory */
+ public final @Nullable String filename;
+
+ /**
+ * The action you want taken if there is a filename conflict, as defined <a
+ * href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/downloads/FilenameConflictAction">here</a>
+ */
+ public final @ConflictActionFlags int conflictActionFlag;
+
+ /**
+ * Specifies whether to provide a file chooser dialog to allow the user to select a filename
+ * (true), or not (false)
+ */
+ public final boolean saveAs;
+
+ /**
+ * Flag that enables downloads to continue even if they encounter HTTP errors. When false, the
+ * download is canceled when it encounters an HTTP error. When true, the download continues when
+ * an HTTP error is encountered and the HTTP server error is not reported. However, if the
+ * download fails due to file-related, network-related, user-related, or other error, that error
+ * is reported.
+ */
+ public final boolean allowHttpErrors;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ flag = true,
+ value = {CONFLICT_ACTION_UNIQUIFY, CONFLICT_ACTION_OVERWRITE, CONFLICT_ACTION_PROMPT})
+ public @interface ConflictActionFlags {}
+
+ /** The app should modify the filename to make it unique */
+ public static final int CONFLICT_ACTION_UNIQUIFY = 0;
+
+ /** The app should overwrite the old file with the newly-downloaded file */
+ public static final int CONFLICT_ACTION_OVERWRITE = 1;
+
+ /** The app should prompt the user, asking them to choose whether to uniquify or overwrite */
+ public static final int CONFLICT_ACTION_PROMPT = 1 << 1;
+
+ protected DownloadRequest(final DownloadRequest.Builder builder) {
+ this.request = builder.mRequest;
+ this.downloadFlags = builder.mDownloadFlags;
+ this.filename = builder.mFilename;
+ this.conflictActionFlag = builder.mConflictActionFlag;
+ this.saveAs = builder.mSaveAs;
+ this.allowHttpErrors = builder.mAllowHttpErrors;
+ }
+
+ /**
+ * Convenience method to convert a GeckoBundle to a DownloadRequest.
+ *
+ * @param optionsBundle - in the shape of the options object browser.downloads.download()
+ * accepts
+ * @return request - a DownloadRequest instance
+ */
+ /* package */ static DownloadRequest fromBundle(final GeckoBundle optionsBundle) {
+ final String uri = optionsBundle.getString("url");
+
+ final WebRequest.Builder mainRequestBuilder = new WebRequest.Builder(uri);
+
+ final String method = optionsBundle.getString("method");
+ if (method != null) {
+ mainRequestBuilder.method(method);
+
+ if (method.equals("POST")) {
+ final String body = optionsBundle.getString("body");
+ mainRequestBuilder.body(body);
+ }
+ }
+
+ final GeckoBundle[] headers = optionsBundle.getBundleArray("headers");
+ if (headers != null) {
+ for (final GeckoBundle header : headers) {
+ String value = header.getString("value");
+ if (value == null) {
+ value = header.getString("binaryValue");
+ }
+ mainRequestBuilder.addHeader(header.getString("name"), value);
+ }
+ }
+
+ final WebRequest mainRequest = mainRequestBuilder.build();
+
+ int downloadFlags = GeckoWebExecutor.FETCH_FLAGS_NONE;
+ final boolean incognito = optionsBundle.getBoolean("incognito");
+ if (incognito) {
+ downloadFlags |= GeckoWebExecutor.FETCH_FLAGS_PRIVATE;
+ }
+
+ final boolean allowHttpErrors = optionsBundle.getBoolean("allowHttpErrors");
+
+ int conflictActionFlags = CONFLICT_ACTION_UNIQUIFY;
+ final String conflictActionString = optionsBundle.getString("conflictAction");
+ if (conflictActionString != null) {
+ switch (conflictActionString.toLowerCase(Locale.ROOT)) {
+ case "overwrite":
+ conflictActionFlags |= WebExtension.DownloadRequest.CONFLICT_ACTION_OVERWRITE;
+ break;
+ case "prompt":
+ conflictActionFlags |= WebExtension.DownloadRequest.CONFLICT_ACTION_PROMPT;
+ break;
+ }
+ }
+
+ final boolean saveAs = optionsBundle.getBoolean("saveAs");
+
+ final WebExtension.DownloadRequest request =
+ new WebExtension.DownloadRequest.Builder(mainRequest)
+ .filename(optionsBundle.getString("filename"))
+ .downloadFlags(downloadFlags)
+ .conflictAction(conflictActionFlags)
+ .saveAs(saveAs)
+ .allowHttpErrors(allowHttpErrors)
+ .build();
+
+ return request;
+ }
+
+ /* package */ static class Builder {
+ private final WebRequest mRequest;
+ private @GeckoWebExecutor.FetchFlags int mDownloadFlags = 0;
+ private String mFilename = null;
+ private @ConflictActionFlags int mConflictActionFlag = CONFLICT_ACTION_UNIQUIFY;
+ private boolean mSaveAs = false;
+ private boolean mAllowHttpErrors = false;
+
+ /* package */ Builder(final WebRequest request) {
+ this.mRequest = request;
+ }
+
+ /* package */ Builder downloadFlags(final @GeckoWebExecutor.FetchFlags int flags) {
+ this.mDownloadFlags = flags;
+ return this;
+ }
+
+ /* package */ Builder filename(final String filename) {
+ this.mFilename = filename;
+ return this;
+ }
+
+ /* package */ Builder conflictAction(final @ConflictActionFlags int conflictActionFlag) {
+ this.mConflictActionFlag = conflictActionFlag;
+ return this;
+ }
+
+ /* package */ Builder saveAs(final boolean saveAs) {
+ this.mSaveAs = saveAs;
+ return this;
+ }
+
+ /* package */ Builder allowHttpErrors(final boolean allowHttpErrors) {
+ this.mAllowHttpErrors = allowHttpErrors;
+ return this;
+ }
+
+ /* package */ DownloadRequest build() {
+ return new DownloadRequest(this);
+ }
+ }
+ }
+
+ /** Represents initial information on a download provided to Web Extension */
+ public static class DownloadInitData {
+ @NonNull public final WebExtension.Download download;
+ @NonNull public final Download.Info initData;
+
+ public DownloadInitData(final Download download, final Download.Info initData) {
+ this.download = download;
+ this.initData = initData;
+ }
+ }
+}