tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

WebExtensionController.java (72867B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 package org.mozilla.geckoview;
      6 
      7 import android.annotation.SuppressLint;
      8 import android.util.Log;
      9 import android.util.SparseArray;
     10 import androidx.annotation.AnyThread;
     11 import androidx.annotation.IntDef;
     12 import androidx.annotation.NonNull;
     13 import androidx.annotation.Nullable;
     14 import androidx.annotation.StringDef;
     15 import androidx.annotation.UiThread;
     16 import java.lang.annotation.Retention;
     17 import java.lang.annotation.RetentionPolicy;
     18 import java.util.ArrayList;
     19 import java.util.Arrays;
     20 import java.util.HashMap;
     21 import java.util.List;
     22 import java.util.Map;
     23 import java.util.Objects;
     24 import java.util.UUID;
     25 import org.json.JSONException;
     26 import org.mozilla.gecko.EventDispatcher;
     27 import org.mozilla.gecko.MultiMap;
     28 import org.mozilla.gecko.util.BundleEventListener;
     29 import org.mozilla.gecko.util.EventCallback;
     30 import org.mozilla.gecko.util.GeckoBundle;
     31 import org.mozilla.geckoview.WebExtension.InstallException;
     32 import org.mozilla.geckoview.WebExtension.InvalidMetaDataException;
     33 
     34 /**
     35 * Controller for managing WebExtensions within a GeckoRuntime instance. Provides functionality for
     36 * installing, uninstalling, enabling, disabling, and managing delegates for WebExtensions.
     37 */
     38 public class WebExtensionController {
     39  private static final String LOGTAG = "WebExtension";
     40 
     41  private AddonManagerDelegate mAddonManagerDelegate;
     42  private ExtensionProcessDelegate mExtensionProcessDelegate;
     43  private DebuggerDelegate mDebuggerDelegate;
     44  private PromptDelegate mPromptDelegate;
     45  private final WebExtension.Listener<WebExtension.TabDelegate> mListener;
     46 
     47  // Map [ (extensionId, nativeApp, session) -> message ]
     48  private final MultiMap<MessageRecipient, Message> mPendingMessages;
     49  private final MultiMap<String, Message> mPendingNewTab;
     50  private final MultiMap<String, Message> mPendingBrowsingData;
     51  private final MultiMap<String, Message> mPendingDownload;
     52 
     53  private final SparseArray<WebExtension.Download> mDownloads;
     54 
     55  private static class Message {
     56    final GeckoBundle bundle;
     57    final EventCallback callback;
     58    final String event;
     59    final GeckoSession session;
     60 
     61    public Message(
     62        final String event,
     63        final GeckoBundle bundle,
     64        final EventCallback callback,
     65        final GeckoSession session) {
     66      this.bundle = bundle;
     67      this.callback = callback;
     68      this.event = event;
     69      this.session = session;
     70    }
     71  }
     72 
     73  private static class ExtensionStore {
     74    private final Map<String, WebExtension> mData = new HashMap<>();
     75    private Observer mObserver;
     76 
     77    interface Observer {
     78      /**
     79       * * This event is fired every time a new extension object is created by the store.
     80       *
     81       * @param extension the newly-created extension object
     82       */
     83      WebExtension onNewExtension(final GeckoBundle extension);
     84    }
     85 
     86    public GeckoResult<WebExtension> get(final String id) {
     87      final WebExtension extension = mData.get(id);
     88      if (extension != null) {
     89        return GeckoResult.fromValue(extension);
     90      }
     91 
     92      final GeckoBundle bundle = new GeckoBundle(1);
     93      bundle.putString("extensionId", id);
     94 
     95      return EventDispatcher.getInstance()
     96          .queryBundle("GeckoView:WebExtension:Get", bundle)
     97          .map(
     98              extensionBundle -> {
     99                final WebExtension ext = mObserver.onNewExtension(extensionBundle);
    100                mData.put(ext.id, ext);
    101                return ext;
    102              });
    103    }
    104 
    105    public void setObserver(final Observer observer) {
    106      mObserver = observer;
    107    }
    108 
    109    public void remove(final String id) {
    110      mData.remove(id);
    111    }
    112 
    113    /**
    114     * Add this extension to the store and update it's current value if it's already present.
    115     *
    116     * @param id the {@link WebExtension} id.
    117     * @param extension the {@link WebExtension} to add to the store.
    118     */
    119    public void update(final String id, final WebExtension extension) {
    120      mData.put(id, extension);
    121    }
    122  }
    123 
    124  private ExtensionStore mExtensions = new ExtensionStore();
    125 
    126  private Internals mInternals = new Internals();
    127 
    128  // Avoids exposing listeners to the API
    129  private class Internals implements BundleEventListener, ExtensionStore.Observer {
    130    @Override
    131    // BundleEventListener
    132    public void handleMessage(
    133        final String event, final GeckoBundle message, final EventCallback callback) {
    134      WebExtensionController.this.handleMessage(event, message, callback, null);
    135    }
    136 
    137    @Override
    138    public WebExtension onNewExtension(final GeckoBundle bundle) {
    139      return WebExtension.fromBundle(mDelegateControllerProvider, bundle);
    140    }
    141  }
    142 
    143  /* package */ void releasePendingMessages(
    144      final WebExtension extension, final String nativeApp, final GeckoSession session) {
    145    Log.i(
    146        LOGTAG,
    147        "releasePendingMessages:"
    148            + " extension="
    149            + extension.id
    150            + " nativeApp="
    151            + nativeApp
    152            + " session="
    153            + session);
    154    final List<Message> messages =
    155        mPendingMessages.remove(new MessageRecipient(nativeApp, extension.id, session));
    156    if (messages == null) {
    157      return;
    158    }
    159 
    160    for (final Message message : messages) {
    161      WebExtensionController.this.handleMessage(
    162          message.event, message.bundle, message.callback, message.session);
    163    }
    164  }
    165 
    166  private class DelegateController implements WebExtension.DelegateController {
    167    private final WebExtension mExtension;
    168 
    169    public DelegateController(final WebExtension extension) {
    170      mExtension = extension;
    171    }
    172 
    173    @Override
    174    public void onMessageDelegate(
    175        final String nativeApp, final WebExtension.MessageDelegate delegate) {
    176      mListener.setMessageDelegate(mExtension, delegate, nativeApp);
    177    }
    178 
    179    @Override
    180    public void onActionDelegate(final WebExtension.ActionDelegate delegate) {
    181      mListener.setActionDelegate(mExtension, delegate);
    182    }
    183 
    184    @Override
    185    public WebExtension.ActionDelegate getActionDelegate() {
    186      return mListener.getActionDelegate(mExtension);
    187    }
    188 
    189    @Override
    190    public void onBrowsingDataDelegate(final WebExtension.BrowsingDataDelegate delegate) {
    191      mListener.setBrowsingDataDelegate(mExtension, delegate);
    192 
    193      for (final Message message : mPendingBrowsingData.get(mExtension.id)) {
    194        WebExtensionController.this.handleMessage(
    195            message.event, message.bundle, message.callback, message.session);
    196      }
    197 
    198      mPendingBrowsingData.remove(mExtension.id);
    199    }
    200 
    201    @Override
    202    public WebExtension.BrowsingDataDelegate getBrowsingDataDelegate() {
    203      return mListener.getBrowsingDataDelegate(mExtension);
    204    }
    205 
    206    @Override
    207    public void onTabDelegate(final WebExtension.TabDelegate delegate) {
    208      mListener.setTabDelegate(mExtension, delegate);
    209 
    210      for (final Message message : mPendingNewTab.get(mExtension.id)) {
    211        WebExtensionController.this.handleMessage(
    212            message.event, message.bundle, message.callback, message.session);
    213      }
    214 
    215      mPendingNewTab.remove(mExtension.id);
    216    }
    217 
    218    @Override
    219    public WebExtension.TabDelegate getTabDelegate() {
    220      return mListener.getTabDelegate(mExtension);
    221    }
    222 
    223    @Override
    224    public void onDownloadDelegate(final WebExtension.DownloadDelegate delegate) {
    225      mListener.setDownloadDelegate(mExtension, delegate);
    226 
    227      for (final Message message : mPendingDownload.get(mExtension.id)) {
    228        WebExtensionController.this.handleMessage(
    229            message.event, message.bundle, message.callback, message.session);
    230      }
    231 
    232      mPendingDownload.remove(mExtension.id);
    233    }
    234 
    235    @Override
    236    public WebExtension.DownloadDelegate getDownloadDelegate() {
    237      return mListener.getDownloadDelegate(mExtension);
    238    }
    239  }
    240 
    241  final WebExtension.DelegateControllerProvider mDelegateControllerProvider =
    242      new WebExtension.DelegateControllerProvider() {
    243        @Override
    244        public WebExtension.DelegateController controllerFor(final WebExtension extension) {
    245          return new DelegateController(extension);
    246        }
    247      };
    248 
    249  /**
    250   * This delegate will be called whenever an extension is about to be installed or it needs new
    251   * permissions, e.g during an update or because it called <code>permissions.request</code>
    252   */
    253  @UiThread
    254  public interface PromptDelegate {
    255 
    256    /**
    257     * Called whenever a new extension is being installed. This is intended as an opportunity for
    258     * the app to prompt the user for the permissions required by this extension.
    259     *
    260     * @param extension The {@link WebExtension} that is about to be installed. You can use {@link
    261     *     WebExtension#metaData} to gather information about this extension when building the user
    262     *     prompt dialog.
    263     * @param permissions The list of permissions that are granted during installation.
    264     * @param origins The list of origins that are granted during installation.
    265     * @param dataCollectionPermissions The list of data collection permissions that are requested
    266     *     during installation.
    267     * @return A {@link GeckoResult} that completes with a {@link
    268     *     WebExtension.PermissionPromptResponse} containing all the details from the user response.
    269     */
    270    @Nullable
    271    default GeckoResult<WebExtension.PermissionPromptResponse> onInstallPromptRequest(
    272        @NonNull final WebExtension extension,
    273        @NonNull final String[] permissions,
    274        @NonNull final String[] origins,
    275        @NonNull final String[] dataCollectionPermissions) {
    276      return null;
    277    }
    278 
    279    /**
    280     * Called whenever an updated extension has new permissions. This is intended as an opportunity
    281     * for the app to prompt the user for the new permissions required by this extension.
    282     *
    283     * @param extension The {@link WebExtension} being updated.
    284     * @param newPermissions The new permissions that are needed.
    285     * @param newOrigins The new origins that are needed.
    286     * @param newDataCollectionPermissions The new data collection permissions that are needed.
    287     * @return A {@link GeckoResult} that completes to either {@link AllowOrDeny#ALLOW ALLOW} if
    288     *     this extension should be update or {@link AllowOrDeny#DENY DENY} if this extension should
    289     *     not be update. A null value will be interpreted as {@link AllowOrDeny#DENY DENY}.
    290     */
    291    @Nullable
    292    default GeckoResult<AllowOrDeny> onUpdatePrompt(
    293        @NonNull final WebExtension extension,
    294        @NonNull final String[] newPermissions,
    295        @NonNull final String[] newOrigins,
    296        @NonNull final String[] newDataCollectionPermissions) {
    297      return null;
    298    }
    299 
    300    /**
    301     * Called whenever permissions are requested. This is intended as an opportunity for the app to
    302     * prompt the user for the permissions required by this extension at runtime.
    303     *
    304     * @param extension The {@link WebExtension} that is about to be installed. You can use {@link
    305     *     WebExtension#metaData} to gather information about this extension when building the user
    306     *     prompt dialog.
    307     * @param permissions The permissions that are requested.
    308     * @param origins The requested host permissions.
    309     * @param dataCollectionPermissions The requested data collection permissions.
    310     * @return A {@link GeckoResult} that completes to either {@link AllowOrDeny#ALLOW ALLOW} if the
    311     *     request should be approved or {@link AllowOrDeny#DENY DENY} if the request should be
    312     *     denied. A null value will be interpreted as {@link AllowOrDeny#DENY DENY}.
    313     */
    314    @Nullable
    315    default GeckoResult<AllowOrDeny> onOptionalPrompt(
    316        final @NonNull WebExtension extension,
    317        final @NonNull String[] permissions,
    318        final @NonNull String[] origins,
    319        final @NonNull String[] dataCollectionPermissions) {
    320      return null;
    321    }
    322  }
    323 
    324  /**
    325   * Delegate for receiving debugger-related WebExtension events. Used to notify about changes to
    326   * extensions during development with tools like web-ext.
    327   */
    328  public interface DebuggerDelegate {
    329    /**
    330     * Called whenever the list of installed extensions has been modified using the debugger with
    331     * tools like web-ext.
    332     *
    333     * <p>This is intended as an opportunity to refresh the list of installed extensions using
    334     * {@link WebExtensionController#list} and to set delegates on the new {@link WebExtension}
    335     * objects, e.g. using {@link WebExtension#setActionDelegate} and {@link
    336     * WebExtension#setMessageDelegate}.
    337     *
    338     * @see <a
    339     *     href="https://extensionworkshop.com/documentation/develop/getting-started-with-web-ext">
    340     *     Getting started with web-ext</a>
    341     */
    342    @UiThread
    343    default void onExtensionListUpdated() {}
    344  }
    345 
    346  /** This delegate will be called whenever the state of an extension has changed. */
    347  public interface AddonManagerDelegate {
    348    /**
    349     * Called whenever an extension is being disabled.
    350     *
    351     * @param extension The {@link WebExtension} that is being disabled.
    352     */
    353    @UiThread
    354    default void onDisabling(@NonNull WebExtension extension) {}
    355 
    356    /**
    357     * Called whenever optional permissions of an extension have changed.
    358     *
    359     * @param extension The {@link WebExtension} that has optional permissions changed.
    360     */
    361    @UiThread
    362    default void onOptionalPermissionsChanged(@NonNull WebExtension extension) {}
    363 
    364    /**
    365     * Called whenever an extension has been disabled.
    366     *
    367     * @param extension The {@link WebExtension} that is being disabled.
    368     */
    369    @UiThread
    370    default void onDisabled(final @NonNull WebExtension extension) {}
    371 
    372    /**
    373     * Called whenever an extension is being enabled.
    374     *
    375     * @param extension The {@link WebExtension} that is being enabled.
    376     */
    377    @UiThread
    378    default void onEnabling(final @NonNull WebExtension extension) {}
    379 
    380    /**
    381     * Called whenever an extension has been enabled.
    382     *
    383     * @param extension The {@link WebExtension} that is being enabled.
    384     */
    385    @UiThread
    386    default void onEnabled(final @NonNull WebExtension extension) {}
    387 
    388    /**
    389     * Called whenever an extension is being uninstalled.
    390     *
    391     * @param extension The {@link WebExtension} that is being uninstalled.
    392     */
    393    @UiThread
    394    default void onUninstalling(final @NonNull WebExtension extension) {}
    395 
    396    /**
    397     * Called whenever an extension has been uninstalled.
    398     *
    399     * @param extension The {@link WebExtension} that is being uninstalled.
    400     */
    401    @UiThread
    402    default void onUninstalled(final @NonNull WebExtension extension) {}
    403 
    404    /**
    405     * Called whenever an extension is being installed.
    406     *
    407     * @param extension The {@link WebExtension} that is being installed.
    408     */
    409    @UiThread
    410    default void onInstalling(final @NonNull WebExtension extension) {}
    411 
    412    /**
    413     * Called whenever an extension has been installed.
    414     *
    415     * @param extension The {@link WebExtension} that is being installed.
    416     */
    417    @UiThread
    418    default void onInstalled(final @NonNull WebExtension extension) {}
    419 
    420    /**
    421     * Called whenever an error happened when installing a WebExtension.
    422     *
    423     * @param extension {@link WebExtension} which failed to be installed.
    424     * @param installException {@link InstallException} indicates which type of error happened.
    425     */
    426    @UiThread
    427    default void onInstallationFailed(
    428        final @Nullable WebExtension extension, final @NonNull InstallException installException) {}
    429 
    430    /**
    431     * Called whenever an extension startup has been completed (and relative urls to assets packaged
    432     * with the extension can be resolved into a full moz-extension url, e.g. optionsPageUrl is
    433     * going to be empty until the extension has reached this callback).
    434     *
    435     * @param extension The {@link WebExtension} that has been fully started.
    436     */
    437    @UiThread
    438    default void onReady(final @NonNull WebExtension extension) {}
    439  }
    440 
    441  /** This delegate is used to notify of extension process state changes. */
    442  public interface ExtensionProcessDelegate {
    443    /** Called when extension process spawning has been disabled. */
    444    @UiThread
    445    default void onDisabledProcessSpawning() {}
    446  }
    447 
    448  /**
    449   * @return the current {@link PromptDelegate} instance.
    450   * @see PromptDelegate
    451   */
    452  @UiThread
    453  @Nullable
    454  public PromptDelegate getPromptDelegate() {
    455    return mPromptDelegate;
    456  }
    457 
    458  /**
    459   * Set the {@link PromptDelegate} for this instance. This delegate will be used to be notified
    460   * whenever an extension is being installed or needs new permissions.
    461   *
    462   * @param delegate the delegate instance.
    463   * @see PromptDelegate
    464   */
    465  @UiThread
    466  public void setPromptDelegate(final @Nullable PromptDelegate delegate) {
    467    if (delegate == null && mPromptDelegate != null) {
    468      EventDispatcher.getInstance()
    469          .unregisterUiThreadListener(
    470              mInternals,
    471              "GeckoView:WebExtension:InstallPrompt",
    472              "GeckoView:WebExtension:UpdatePrompt",
    473              "GeckoView:WebExtension:OptionalPrompt");
    474    } else if (delegate != null && mPromptDelegate == null) {
    475      EventDispatcher.getInstance()
    476          .registerUiThreadListener(
    477              mInternals,
    478              "GeckoView:WebExtension:InstallPrompt",
    479              "GeckoView:WebExtension:UpdatePrompt",
    480              "GeckoView:WebExtension:OptionalPrompt");
    481    }
    482 
    483    mPromptDelegate = delegate;
    484  }
    485 
    486  /**
    487   * Set the {@link DebuggerDelegate} for this instance. This delegate will receive updates about
    488   * extension changes using developer tools.
    489   *
    490   * @param delegate the Delegate instance
    491   */
    492  @UiThread
    493  public void setDebuggerDelegate(final @NonNull DebuggerDelegate delegate) {
    494    if (delegate == null && mDebuggerDelegate != null) {
    495      EventDispatcher.getInstance()
    496          .unregisterUiThreadListener(mInternals, "GeckoView:WebExtension:DebuggerListUpdated");
    497    } else if (delegate != null && mDebuggerDelegate == null) {
    498      EventDispatcher.getInstance()
    499          .registerUiThreadListener(mInternals, "GeckoView:WebExtension:DebuggerListUpdated");
    500    }
    501 
    502    mDebuggerDelegate = delegate;
    503  }
    504 
    505  /**
    506   * Set the {@link AddonManagerDelegate} for this instance. This delegate will be used to be
    507   * notified whenever the state of an extension has changed.
    508   *
    509   * @param delegate the delegate instance
    510   * @see AddonManagerDelegate
    511   */
    512  @UiThread
    513  public void setAddonManagerDelegate(final @Nullable AddonManagerDelegate delegate) {
    514    if (delegate == null && mAddonManagerDelegate != null) {
    515      EventDispatcher.getInstance()
    516          .unregisterUiThreadListener(
    517              mInternals,
    518              "GeckoView:WebExtension:OnOptionalPermissionsChanged",
    519              "GeckoView:WebExtension:OnDisabling",
    520              "GeckoView:WebExtension:OnDisabled",
    521              "GeckoView:WebExtension:OnEnabling",
    522              "GeckoView:WebExtension:OnEnabled",
    523              "GeckoView:WebExtension:OnUninstalling",
    524              "GeckoView:WebExtension:OnUninstalled",
    525              "GeckoView:WebExtension:OnInstalling",
    526              "GeckoView:WebExtension:OnInstallationFailed",
    527              "GeckoView:WebExtension:OnInstalled",
    528              "GeckoView:WebExtension:OnReady");
    529    } else if (delegate != null && mAddonManagerDelegate == null) {
    530      EventDispatcher.getInstance()
    531          .registerUiThreadListener(
    532              mInternals,
    533              "GeckoView:WebExtension:OnOptionalPermissionsChanged",
    534              "GeckoView:WebExtension:OnDisabling",
    535              "GeckoView:WebExtension:OnDisabled",
    536              "GeckoView:WebExtension:OnEnabling",
    537              "GeckoView:WebExtension:OnEnabled",
    538              "GeckoView:WebExtension:OnUninstalling",
    539              "GeckoView:WebExtension:OnUninstalled",
    540              "GeckoView:WebExtension:OnInstalling",
    541              "GeckoView:WebExtension:OnInstallationFailed",
    542              "GeckoView:WebExtension:OnInstalled",
    543              "GeckoView:WebExtension:OnReady");
    544    }
    545 
    546    mAddonManagerDelegate = delegate;
    547  }
    548 
    549  /**
    550   * Set the {@link ExtensionProcessDelegate} for this instance. This delegate will be used to
    551   * notify when the state of the extension process has changed.
    552   *
    553   * @param delegate the extension process delegate
    554   * @see ExtensionProcessDelegate
    555   */
    556  @UiThread
    557  public void setExtensionProcessDelegate(final @Nullable ExtensionProcessDelegate delegate) {
    558    if (delegate == null && mExtensionProcessDelegate != null) {
    559      EventDispatcher.getInstance()
    560          .unregisterUiThreadListener(
    561              mInternals, "GeckoView:WebExtension:OnDisabledProcessSpawning");
    562    } else if (delegate != null && mExtensionProcessDelegate == null) {
    563      EventDispatcher.getInstance()
    564          .registerUiThreadListener(mInternals, "GeckoView:WebExtension:OnDisabledProcessSpawning");
    565    }
    566 
    567    mExtensionProcessDelegate = delegate;
    568  }
    569 
    570  /**
    571   * Enable extension process spawning.
    572   *
    573   * <p>Extension process spawning can be disabled when the extension process has been killed or
    574   * crashed beyond the threshold set for Gecko. This method can be called to reset the threshold
    575   * count and allow the spawning again. If the threshold is reached again, {@link
    576   * ExtensionProcessDelegate#onDisabledProcessSpawning()} will still be called.
    577   *
    578   * @see ExtensionProcessDelegate#onDisabledProcessSpawning()
    579   */
    580  @AnyThread
    581  public void enableExtensionProcessSpawning() {
    582    EventDispatcher.getInstance().dispatch("GeckoView:WebExtension:EnableProcessSpawning", null);
    583  }
    584 
    585  /**
    586   * Disable extension process spawning.
    587   *
    588   * <p>Extension process spawning can be re-enabled with {@link
    589   * WebExtensionController#enableExtensionProcessSpawning()}. This method does the opposite and
    590   * stops the extension process. This method can be called when we no longer want to run extensions
    591   * for the rest of the session.
    592   *
    593   * @see ExtensionProcessDelegate#onDisabledProcessSpawning()
    594   */
    595  @AnyThread
    596  public void disableExtensionProcessSpawning() {
    597    EventDispatcher.getInstance().dispatch("GeckoView:WebExtension:DisableProcessSpawning", null);
    598  }
    599 
    600  private static class InstallCanceller implements GeckoResult.CancellationDelegate {
    601    public final String installId;
    602 
    603    public InstallCanceller() {
    604      installId = UUID.randomUUID().toString();
    605    }
    606 
    607    @Override
    608    public GeckoResult<Boolean> cancel() {
    609      final GeckoBundle bundle = new GeckoBundle(1);
    610      bundle.putString("installId", installId);
    611 
    612      return EventDispatcher.getInstance()
    613          .queryBundle("GeckoView:WebExtension:CancelInstall", bundle)
    614          .map(response -> response.getBoolean("cancelled"));
    615    }
    616  }
    617 
    618  /**
    619   * Install an extension.
    620   *
    621   * <p>An installed extension will persist and will be available even when restarting the {@link
    622   * GeckoRuntime}.
    623   *
    624   * <p>Installed extensions through this method need to be signed by Mozilla, see <a
    625   * href="https://extensionworkshop.com/documentation/publish/signing-and-distribution-overview/#distributing-your-addon">
    626   * Distributing your add-on </a>.
    627   *
    628   * <p>When calling this method, the GeckoView library will download the extension, validate its
    629   * manifest and signature, and give you an opportunity to verify its permissions through {@link
    630   * PromptDelegate#onInstallPromptRequest}, you can use this method to prompt the user if
    631   * appropriate.
    632   *
    633   * @param uri URI to the extension's <code>.xpi</code> package. This can be a remote <code>https:
    634   *     </code> URI or a local <code>file:</code> or <code>resource:</code> URI. Note: the app
    635   *     needs the appropriate permissions for local URIs.
    636   * @param installationMethod The method used by the embedder to install the {@link WebExtension}.
    637   * @return A {@link GeckoResult} that will complete when the installation process finishes. For
    638   *     successful installations, the GeckoResult will return the {@link WebExtension} object that
    639   *     you can use to set delegates and retrieve information about the WebExtension using {@link
    640   *     WebExtension#metaData}.
    641   *     <p>If an error occurs during the installation process, the GeckoResult will complete
    642   *     exceptionally with a {@link WebExtension.InstallException InstallException} that will
    643   *     contain the relevant error code in {@link WebExtension.InstallException#code
    644   *     InstallException#code}.
    645   * @see PromptDelegate#onInstallPromptRequest(WebExtension, String[], String[], String[])
    646   * @see WebExtension.InstallException.ErrorCodes
    647   * @see WebExtension#metaData
    648   */
    649  @NonNull
    650  @AnyThread
    651  public GeckoResult<WebExtension> install(
    652      final @NonNull String uri, final @Nullable @InstallationMethod String installationMethod) {
    653    final InstallCanceller canceller = new InstallCanceller();
    654    final GeckoBundle bundle = new GeckoBundle(3);
    655    bundle.putString("locationUri", uri);
    656    bundle.putString("installId", canceller.installId);
    657    bundle.putString("installMethod", installationMethod);
    658 
    659    final GeckoResult<WebExtension> result =
    660        EventDispatcher.getInstance()
    661            .queryBundle("GeckoView:WebExtension:Install", bundle)
    662            .map(
    663                ext -> WebExtension.fromBundle(mDelegateControllerProvider, ext),
    664                WebExtension.InstallException::fromQueryException)
    665            .map(this::registerWebExtension);
    666    result.setCancellationDelegate(canceller);
    667    return result;
    668  }
    669 
    670  /**
    671   * Install an extension.
    672   *
    673   * <p>An installed extension will persist and will be available even when restarting the {@link
    674   * GeckoRuntime}.
    675   *
    676   * <p>Installed extensions through this method need to be signed by Mozilla, see <a
    677   * href="https://extensionworkshop.com/documentation/publish/signing-and-distribution-overview/#distributing-your-addon">
    678   * Distributing your add-on </a>.
    679   *
    680   * <p>When calling this method, the GeckoView library will download the extension, validate its
    681   * manifest and signature, and give you an opportunity to verify its permissions through {@link
    682   * PromptDelegate#installPromptRequest(GeckoBundle, EventCallback)}, you can use this method to
    683   * prompt the user if appropriate. If you are looking to provide an {@link InstallationMethod},
    684   * please use {@link WebExtensionController#install(String, String)}
    685   *
    686   * @param uri URI to the extension's <code>.xpi</code> package. This can be a remote <code>https:
    687   *     </code> URI or a local <code>file:</code> or <code>resource:</code> URI. Note: the app
    688   *     needs the appropriate permissions for local URIs.
    689   * @return A {@link GeckoResult} that will complete when the installation process finishes. For
    690   *     successful installations, the GeckoResult will return the {@link WebExtension} object that
    691   *     you can use to set delegates and retrieve information about the WebExtension using {@link
    692   *     WebExtension#metaData}.
    693   *     <p>If an error occurs during the installation process, the GeckoResult will complete
    694   *     exceptionally with a {@link WebExtension.InstallException InstallException} that will
    695   *     contain the relevant error code in {@link WebExtension.InstallException#code
    696   *     InstallException#code}.
    697   * @see PromptDelegate#installPromptRequest
    698   * @see WebExtension.InstallException.ErrorCodes
    699   * @see WebExtension#metaData
    700   */
    701  @NonNull
    702  @AnyThread
    703  public GeckoResult<WebExtension> install(final @NonNull String uri) {
    704    return install(uri, null);
    705  }
    706 
    707  /** The method used by the embedder to install the {@link WebExtension}. */
    708  @Retention(RetentionPolicy.SOURCE)
    709  @StringDef({
    710    INSTALLATION_METHOD_MANAGER,
    711    INSTALLATION_METHOD_FROM_FILE,
    712    INSTALLATION_METHOD_ONBOARDING
    713  })
    714  public @interface InstallationMethod {};
    715 
    716  /** Indicates the {@link WebExtension} was installed using from the embedder's add-ons manager. */
    717  public static final String INSTALLATION_METHOD_MANAGER = "manager";
    718 
    719  /** Indicates the {@link WebExtension} was installed from a file. */
    720  public static final String INSTALLATION_METHOD_FROM_FILE = "install-from-file";
    721 
    722  /** Indicates the {@link WebExtension} was installed from the embedder's onboarding feature. */
    723  public static final String INSTALLATION_METHOD_ONBOARDING = "onboarding";
    724 
    725  /**
    726   * Set whether an extension should be allowed to run in private browsing or not.
    727   *
    728   * @param extension the {@link WebExtension} instance to modify.
    729   * @param allowed true if this extension should be allowed to run in private browsing pages, false
    730   *     otherwise.
    731   * @return the updated {@link WebExtension} instance.
    732   */
    733  @NonNull
    734  @AnyThread
    735  public GeckoResult<WebExtension> setAllowedInPrivateBrowsing(
    736      final @NonNull WebExtension extension, final boolean allowed) {
    737    final GeckoBundle bundle = new GeckoBundle(2);
    738    bundle.putString("extensionId", extension.id);
    739    bundle.putBoolean("allowed", allowed);
    740 
    741    return EventDispatcher.getInstance()
    742        .queryBundle("GeckoView:WebExtension:SetPBAllowed", bundle)
    743        .map(ext -> WebExtension.fromBundle(mDelegateControllerProvider, ext))
    744        .map(this::registerWebExtension);
    745  }
    746 
    747  /**
    748   * Add the provided permissions to the {@link WebExtension} with the given id.
    749   *
    750   * @param extensionId the id of {@link WebExtension} instance to modify.
    751   * @param permissions the permissions to add, pass an empty array to not update.
    752   * @param origins the origins to add, pass an empty array to not update.
    753   * @param dataCollectionPermissions the data collection permissions to add, pass an empty array to
    754   *     not update.
    755   * @return the updated {@link WebExtension} instance.
    756   */
    757  @NonNull
    758  @AnyThread
    759  public GeckoResult<WebExtension> addOptionalPermissions(
    760      final @NonNull String extensionId,
    761      @NonNull final String[] permissions,
    762      @NonNull final String[] origins,
    763      @NonNull final String[] dataCollectionPermissions) {
    764    final GeckoBundle bundle = new GeckoBundle(4);
    765    bundle.putString("extensionId", extensionId);
    766    bundle.putStringArray("permissions", permissions);
    767    bundle.putStringArray("origins", origins);
    768    bundle.putStringArray("dataCollectionPermissions", dataCollectionPermissions);
    769 
    770    return EventDispatcher.getInstance()
    771        .queryBundle("GeckoView:WebExtension:AddOptionalPermissions", bundle)
    772        .map(ext -> WebExtension.fromBundle(mDelegateControllerProvider, ext))
    773        .map(this::registerWebExtension);
    774  }
    775 
    776  /**
    777   * Remove the provided permissions from the {@link WebExtension} with the given id.
    778   *
    779   * @param extensionId the id of {@link WebExtension} instance to modify.
    780   * @param permissions the permissions to remove, pass an empty array to not update.
    781   * @param origins the origins to remove, pass an empty array to not update.
    782   * @param dataCollectionPermissions the data collection permissions to remove, pass an array to
    783   *     not update.
    784   * @return the updated {@link WebExtension} instance.
    785   */
    786  @NonNull
    787  @AnyThread
    788  public GeckoResult<WebExtension> removeOptionalPermissions(
    789      final @NonNull String extensionId,
    790      @NonNull final String[] permissions,
    791      @NonNull final String[] origins,
    792      @NonNull final String[] dataCollectionPermissions) {
    793    final GeckoBundle bundle = new GeckoBundle(4);
    794    bundle.putString("extensionId", extensionId);
    795    bundle.putStringArray("permissions", permissions);
    796    bundle.putStringArray("origins", origins);
    797    bundle.putStringArray("dataCollectionPermissions", dataCollectionPermissions);
    798 
    799    return EventDispatcher.getInstance()
    800        .queryBundle("GeckoView:WebExtension:RemoveOptionalPermissions", bundle)
    801        .map(ext -> WebExtension.fromBundle(mDelegateControllerProvider, ext))
    802        .map(this::registerWebExtension);
    803  }
    804 
    805  /**
    806   * Install a built-in extension.
    807   *
    808   * <p>Built-in extensions have access to native messaging, don't need to be signed and are
    809   * installed from a folder in the APK instead of a .xpi bundle.
    810   *
    811   * <p>Example:
    812   *
    813   * <p><code>
    814   *    controller.installBuiltIn("resource://android/assets/example/");
    815   * </code> Will install the built-in extension located at <code>/assets/example/</code> in the
    816   * app's APK.
    817   *
    818   * @param uri Folder where the extension is located. To ensure this folder is inside the APK, only
    819   *     <code>resource://android</code> URIs are allowed.
    820   * @see WebExtension.MessageDelegate
    821   * @return A {@link GeckoResult} that completes with the extension once it's installed.
    822   */
    823  @NonNull
    824  @AnyThread
    825  public GeckoResult<WebExtension> installBuiltIn(final @NonNull String uri) {
    826    final GeckoBundle bundle = new GeckoBundle(1);
    827    bundle.putString("locationUri", uri);
    828 
    829    return EventDispatcher.getInstance()
    830        .queryBundle("GeckoView:WebExtension:InstallBuiltIn", bundle)
    831        .map(
    832            ext -> WebExtension.fromBundle(mDelegateControllerProvider, ext),
    833            WebExtension.InstallException::fromQueryException)
    834        .map(this::registerWebExtension);
    835  }
    836 
    837  /**
    838   * Ensure that a built-in extension is installed.
    839   *
    840   * <p>Similar to {@link #installBuiltIn}, except the extension is not re-installed if it's already
    841   * present and it has the same version.
    842   *
    843   * <p>Example:
    844   *
    845   * <p><code>
    846   *    controller.ensureBuiltIn("resource://android/assets/example/", "example@example.com");
    847   * </code> Will install the built-in extension located at <code>/assets/example/</code> in the
    848   * app's APK.
    849   *
    850   * @param uri Folder where the extension is located. To ensure this folder is inside the APK, only
    851   *     <code>resource://android</code> URIs are allowed.
    852   * @param id Extension ID as present in the manifest.json file.
    853   * @see WebExtension.MessageDelegate
    854   * @return A {@link GeckoResult} that completes with the extension once it's installed.
    855   */
    856  @NonNull
    857  @AnyThread
    858  public GeckoResult<WebExtension> ensureBuiltIn(
    859      final @NonNull String uri, final @Nullable String id) {
    860    final GeckoBundle bundle = new GeckoBundle(2);
    861    bundle.putString("locationUri", uri);
    862    bundle.putString("webExtensionId", id);
    863 
    864    return EventDispatcher.getInstance()
    865        .queryBundle("GeckoView:WebExtension:EnsureBuiltIn", bundle)
    866        .map(
    867            ext -> WebExtension.fromBundle(mDelegateControllerProvider, ext),
    868            WebExtension.InstallException::fromQueryException)
    869        .map(this::registerWebExtension);
    870  }
    871 
    872  /**
    873   * Uninstall an extension.
    874   *
    875   * <p>Uninstalling an extension will remove it from the current {@link GeckoRuntime} instance,
    876   * delete all its data and trigger a request to close all extension pages currently open.
    877   *
    878   * @param extension The {@link WebExtension} to be uninstalled.
    879   * @return A {@link GeckoResult} that will complete when the uninstall process is completed.
    880   */
    881  @NonNull
    882  @AnyThread
    883  public GeckoResult<Void> uninstall(final @NonNull WebExtension extension) {
    884    final GeckoBundle bundle = new GeckoBundle(1);
    885    bundle.putString("webExtensionId", extension.id);
    886 
    887    return EventDispatcher.getInstance()
    888        .queryBundle("GeckoView:WebExtension:Uninstall", bundle)
    889        .accept(result -> unregisterWebExtension(extension));
    890  }
    891 
    892  /** Defines the possible sources that can enable or disable a WebExtension. */
    893  @Retention(RetentionPolicy.SOURCE)
    894  @IntDef({EnableSource.USER, EnableSource.APP})
    895  public @interface EnableSources {}
    896 
    897  /**
    898   * Contains the possible values for the <code>source</code> parameter in {@link #enable} and
    899   * {@link #disable}.
    900   */
    901  public static class EnableSource {
    902    /** Action has been requested by the user. */
    903    public static final int USER = 1;
    904 
    905    /**
    906     * Action requested by the app itself, e.g. to disable an extension that is not supported in
    907     * this version of the app.
    908     */
    909    public static final int APP = 2;
    910 
    911    static String toString(final @EnableSources int flag) {
    912      if (flag == USER) {
    913        return "user";
    914      } else if (flag == APP) {
    915        return "app";
    916      } else {
    917        throw new IllegalArgumentException("Value provided in flags is not valid.");
    918      }
    919    }
    920  }
    921 
    922  /**
    923   * Enable an extension that has been disabled. If the extension is already enabled, this method
    924   * has no effect.
    925   *
    926   * @param extension The {@link WebExtension} to be enabled.
    927   * @param source The agent that initiated this action, e.g. if the action has been initiated by
    928   *     the user,use {@link EnableSource#USER}.
    929   * @return the new {@link WebExtension} instance, updated to reflect the enablement.
    930   */
    931  @AnyThread
    932  @NonNull
    933  public GeckoResult<WebExtension> enable(
    934      final @NonNull WebExtension extension, final @EnableSources int source) {
    935    final GeckoBundle bundle = new GeckoBundle(2);
    936    bundle.putString("webExtensionId", extension.id);
    937    bundle.putString("source", EnableSource.toString(source));
    938 
    939    return EventDispatcher.getInstance()
    940        .queryBundle("GeckoView:WebExtension:Enable", bundle)
    941        .map(ext -> WebExtension.fromBundle(mDelegateControllerProvider, ext))
    942        .map(this::registerWebExtension);
    943  }
    944 
    945  /**
    946   * Disable an extension that is enabled. If the extension is already disabled, this method has no
    947   * effect.
    948   *
    949   * @param extension The {@link WebExtension} to be disabled.
    950   * @param source The agent that initiated this action, e.g. if the action has been initiated by
    951   *     the user, use {@link EnableSource#USER}.
    952   * @return the new {@link WebExtension} instance, updated to reflect the disablement.
    953   */
    954  @AnyThread
    955  @NonNull
    956  public GeckoResult<WebExtension> disable(
    957      final @NonNull WebExtension extension, final @EnableSources int source) {
    958    final GeckoBundle bundle = new GeckoBundle(2);
    959    bundle.putString("webExtensionId", extension.id);
    960    bundle.putString("source", EnableSource.toString(source));
    961 
    962    return EventDispatcher.getInstance()
    963        .queryBundle("GeckoView:WebExtension:Disable", bundle)
    964        .map(ext -> WebExtension.fromBundle(mDelegateControllerProvider, ext))
    965        .map(this::registerWebExtension);
    966  }
    967 
    968  private List<WebExtension> listFromBundle(final GeckoBundle response) {
    969    final GeckoBundle[] bundles = response.getBundleArray("extensions");
    970    final List<WebExtension> list = new ArrayList<>(bundles.length);
    971 
    972    for (final GeckoBundle bundle : bundles) {
    973      final WebExtension extension = new WebExtension(mDelegateControllerProvider, bundle);
    974      list.add(registerWebExtension(extension));
    975    }
    976 
    977    return list;
    978  }
    979 
    980  /**
    981   * List installed extensions for this {@link GeckoRuntime}.
    982   *
    983   * <p>The returned list can be used to set delegates on the {@link WebExtension} objects using
    984   * {@link WebExtension#setActionDelegate}, {@link WebExtension#setMessageDelegate}.
    985   *
    986   * @return a {@link GeckoResult} that will resolve when the list of extensions is available.
    987   */
    988  @AnyThread
    989  @NonNull
    990  public GeckoResult<List<WebExtension>> list() {
    991    return EventDispatcher.getInstance()
    992        .queryBundle("GeckoView:WebExtension:List")
    993        .map(this::listFromBundle);
    994  }
    995 
    996  /**
    997   * Update a web extension.
    998   *
    999   * <p>When checking for an update, GeckoView will download the update manifest that is defined by
   1000   * the web extension's manifest property <a
   1001   * href="https://extensionworkshop.com/documentation/manage/updating-your-extension/">browser_specific_settings.gecko.update_url</a>.
   1002   * If an update is found it will be downloaded and installed. If the extension needs any new
   1003   * permissions the {@link PromptDelegate#updatePrompt} will be triggered.
   1004   *
   1005   * <p>More information about the update manifest format is available <a
   1006   * href="https://extensionworkshop.com/documentation/manage/updating-your-extension/#manifest-structure">here</a>.
   1007   *
   1008   * @param extension The extension to update.
   1009   * @return A {@link GeckoResult} that will complete when the update process finishes. If an update
   1010   *     is found and installed successfully, the GeckoResult will return the updated {@link
   1011   *     WebExtension}. If no update is available, null will be returned. If the updated extension
   1012   *     requires new permissions, the {@link PromptDelegate#installPromptRequest} will be called.
   1013   * @see PromptDelegate#updatePrompt
   1014   */
   1015  @AnyThread
   1016  @NonNull
   1017  public GeckoResult<WebExtension> update(final @NonNull WebExtension extension) {
   1018    final GeckoBundle bundle = new GeckoBundle(1);
   1019    bundle.putString("webExtensionId", extension.id);
   1020 
   1021    return EventDispatcher.getInstance()
   1022        .queryBundle("GeckoView:WebExtension:Update", bundle)
   1023        .map(
   1024            ext -> WebExtension.fromBundle(mDelegateControllerProvider, ext),
   1025            WebExtension.InstallException::fromQueryException)
   1026        .map(this::registerWebExtension);
   1027  }
   1028 
   1029  /* package */ WebExtensionController(final GeckoRuntime runtime) {
   1030    mListener = new WebExtension.Listener<>(runtime);
   1031    mPendingMessages = new MultiMap<>();
   1032    mPendingNewTab = new MultiMap<>();
   1033    mPendingBrowsingData = new MultiMap<>();
   1034    mPendingDownload = new MultiMap<>();
   1035    mExtensions.setObserver(mInternals);
   1036    mDownloads = new SparseArray<>();
   1037  }
   1038 
   1039  /* package */ WebExtension registerWebExtension(final WebExtension webExtension) {
   1040    if (webExtension != null) {
   1041      mExtensions.update(webExtension.id, webExtension);
   1042    }
   1043    return webExtension;
   1044  }
   1045 
   1046  /* package */ void handleMessage(
   1047      final String event,
   1048      final GeckoBundle bundle,
   1049      final EventCallback callback,
   1050      final GeckoSession session) {
   1051    final Message message = new Message(event, bundle, callback, session);
   1052 
   1053    Log.d(LOGTAG, "handleMessage " + event);
   1054 
   1055    try {
   1056      if ("GeckoView:WebExtension:InstallPrompt".equals(event)) {
   1057        installPromptRequest(bundle, callback);
   1058        return;
   1059      } else if ("GeckoView:WebExtension:UpdatePrompt".equals(event)) {
   1060        updatePrompt(bundle, callback);
   1061        return;
   1062      } else if ("GeckoView:WebExtension:DebuggerListUpdated".equals(event)) {
   1063        if (mDebuggerDelegate != null) {
   1064          mDebuggerDelegate.onExtensionListUpdated();
   1065        }
   1066        return;
   1067      } else if ("GeckoView:WebExtension:OnDisabling".equals(event)) {
   1068        onDisabling(bundle);
   1069        return;
   1070      } else if ("GeckoView:WebExtension:OnOptionalPermissionsChanged".equals(event)) {
   1071        onOptionalPermissionsChanged(bundle);
   1072        return;
   1073      } else if ("GeckoView:WebExtension:OnDisabled".equals(event)) {
   1074        onDisabled(bundle);
   1075        return;
   1076      } else if ("GeckoView:WebExtension:OnEnabling".equals(event)) {
   1077        onEnabling(bundle);
   1078        return;
   1079      } else if ("GeckoView:WebExtension:OnEnabled".equals(event)) {
   1080        onEnabled(bundle);
   1081        return;
   1082      } else if ("GeckoView:WebExtension:OnUninstalling".equals(event)) {
   1083        onUninstalling(bundle);
   1084        return;
   1085      } else if ("GeckoView:WebExtension:OnUninstalled".equals(event)) {
   1086        onUninstalled(bundle);
   1087        return;
   1088      } else if ("GeckoView:WebExtension:OnInstalling".equals(event)) {
   1089        onInstalling(bundle);
   1090        return;
   1091      } else if ("GeckoView:WebExtension:OnInstalled".equals(event)) {
   1092        onInstalled(bundle);
   1093        return;
   1094      } else if ("GeckoView:WebExtension:OnDisabledProcessSpawning".equals(event)) {
   1095        onDisabledProcessSpawning();
   1096        return;
   1097      } else if ("GeckoView:WebExtension:OnInstallationFailed".equals(event)) {
   1098        onInstallationFailed(bundle);
   1099        return;
   1100      } else if ("GeckoView:WebExtension:OnReady".equals(event)) {
   1101        onReady(bundle);
   1102        return;
   1103      }
   1104    } catch (final InvalidMetaDataException e) {
   1105      Log.e(LOGTAG, "Unexpected invalid bundle data on event " + event, e);
   1106      if (callback != null) {
   1107        callback.sendError("Unexpected invalid WebExtensions metaData on event " + event);
   1108      }
   1109      return;
   1110    }
   1111 
   1112    extensionFromBundle(bundle)
   1113        .accept(
   1114            extension -> {
   1115              if ("GeckoView:WebExtension:NewTab".equals(event)) {
   1116                newTab(message, extension);
   1117                return;
   1118              } else if ("GeckoView:WebExtension:UpdateTab".equals(event)) {
   1119                updateTab(message, extension);
   1120                return;
   1121              } else if ("GeckoView:WebExtension:CloseTab".equals(event)) {
   1122                closeTab(message, extension);
   1123                return;
   1124              } else if ("GeckoView:BrowserAction:Update".equals(event)) {
   1125                actionUpdate(message, extension, WebExtension.Action.TYPE_BROWSER_ACTION);
   1126                return;
   1127              } else if ("GeckoView:PageAction:Update".equals(event)) {
   1128                actionUpdate(message, extension, WebExtension.Action.TYPE_PAGE_ACTION);
   1129                return;
   1130              } else if ("GeckoView:BrowserAction:OpenPopup".equals(event)) {
   1131                openPopup(message, extension, WebExtension.Action.TYPE_BROWSER_ACTION);
   1132                return;
   1133              } else if ("GeckoView:PageAction:OpenPopup".equals(event)) {
   1134                openPopup(message, extension, WebExtension.Action.TYPE_PAGE_ACTION);
   1135                return;
   1136              } else if ("GeckoView:WebExtension:OpenOptionsPage".equals(event)) {
   1137                openOptionsPage(message, extension);
   1138                return;
   1139              } else if ("GeckoView:BrowsingData:GetSettings".equals(event)) {
   1140                getSettings(message, extension);
   1141                return;
   1142              } else if ("GeckoView:BrowsingData:Clear".equals(event)) {
   1143                browsingDataClear(message, extension);
   1144                return;
   1145              } else if ("GeckoView:WebExtension:Download".equals(event)) {
   1146                download(message, extension);
   1147                return;
   1148              } else if ("GeckoView:WebExtension:OptionalPrompt".equals(event)) {
   1149                optionalPrompt(message, extension);
   1150                return;
   1151              }
   1152 
   1153              // GeckoView:WebExtension:Connect and GeckoView:WebExtension:Message
   1154              // are handled below.
   1155              final String nativeApp = bundle.getString("nativeApp");
   1156              if (nativeApp == null) {
   1157                if (BuildConfig.DEBUG_BUILD) {
   1158                  throw new RuntimeException("Missing required nativeApp message parameter.");
   1159                }
   1160                callback.sendError("Missing nativeApp parameter.");
   1161                return;
   1162              }
   1163 
   1164              final GeckoBundle senderBundle = bundle.getBundle("sender");
   1165              final WebExtension.MessageSender sender =
   1166                  fromBundle(extension, senderBundle, session);
   1167              if (sender == null) {
   1168                if (callback != null) {
   1169                  if (BuildConfig.DEBUG_BUILD) {
   1170                    try {
   1171                      Log.e(
   1172                          LOGTAG, "Could not find recipient for message: " + bundle.toJSONObject());
   1173                    } catch (final JSONException ex) {
   1174                    }
   1175                  }
   1176                  callback.sendError("Could not find recipient for " + bundle.getBundle("sender"));
   1177                }
   1178                return;
   1179              }
   1180 
   1181              if ("GeckoView:WebExtension:Connect".equals(event)) {
   1182                connect(nativeApp, bundle.getLong("portId", -1), message, sender);
   1183              } else if ("GeckoView:WebExtension:Message".equals(event)) {
   1184                message(nativeApp, message, sender);
   1185              }
   1186            });
   1187  }
   1188 
   1189  private boolean isBundledExtension(final String extensionId) {
   1190    return "{73a6fe31-595d-460b-a920-fcc0f8843232}".equals(extensionId);
   1191  }
   1192 
   1193  private boolean promptBypass(final WebExtension extension, final EventCallback callback) {
   1194    // allow bundled extensions, e.g. NoScript, to be installed with no prompt
   1195    if (isBundledExtension(extension.id)) {
   1196      callback.resolveTo(
   1197        GeckoResult.allow().map(
   1198          allowOrDeny -> {
   1199            final GeckoBundle response = new GeckoBundle(1);
   1200            response.putBoolean("allow", true);
   1201            return response;
   1202          }
   1203        )
   1204      );
   1205      return true;
   1206    }
   1207    return false;
   1208  }
   1209 
   1210  private void installPromptRequest(final GeckoBundle message, final EventCallback callback) {
   1211    final GeckoBundle extensionBundle = message.getBundle("extension");
   1212    if (extensionBundle == null
   1213        || !extensionBundle.containsKey("webExtensionId")
   1214        || !extensionBundle.containsKey("locationURI")) {
   1215      if (BuildConfig.DEBUG_BUILD) {
   1216        throw new RuntimeException("Missing webExtensionId or locationURI");
   1217      }
   1218 
   1219      Log.e(LOGTAG, "Missing webExtensionId or locationURI");
   1220      return;
   1221    }
   1222 
   1223    final WebExtension extension = new WebExtension(mDelegateControllerProvider, extensionBundle);
   1224 
   1225    if (promptBypass(extension, callback)) {
   1226      return;
   1227    }
   1228 
   1229    if (mPromptDelegate == null) {
   1230      Log.e(
   1231          LOGTAG, "Tried to install extension " + extension.id + " but no delegate is registered");
   1232      return;
   1233    }
   1234 
   1235    final GeckoResult<WebExtension.PermissionPromptResponse> promptResponse =
   1236        mPromptDelegate.onInstallPromptRequest(
   1237            extension,
   1238            message.getStringArray("permissions"),
   1239            message.getStringArray("origins"),
   1240            message.getStringArray("dataCollectionPermissions"));
   1241 
   1242    if (promptResponse == null) {
   1243      return;
   1244    }
   1245 
   1246    callback.resolveTo(
   1247        promptResponse.map(
   1248            userResponse -> {
   1249              final GeckoBundle response = new GeckoBundle(3);
   1250              response.putBoolean("allow", userResponse.isPermissionsGranted);
   1251              response.putBoolean("privateBrowsingAllowed", userResponse.isPrivateModeGranted);
   1252              response.putBoolean(
   1253                  "isTechnicalAndInteractionDataGranted",
   1254                  userResponse.isTechnicalAndInteractionDataGranted);
   1255              return response;
   1256            }));
   1257  }
   1258 
   1259  private void updatePrompt(final GeckoBundle message, final EventCallback callback) {
   1260    final WebExtension extension =
   1261        new WebExtension(mDelegateControllerProvider, message.getBundle("extension"));
   1262    final String[] newPermissions = message.getStringArray("newPermissions");
   1263    final String[] newOrigins = message.getStringArray("newOrigins");
   1264    final String[] newDataCollectionPermissions =
   1265        message.getStringArray("newDataCollectionPermissions");
   1266 
   1267    if (promptBypass(extension, callback)) {
   1268      return;
   1269    }
   1270 
   1271    if (mPromptDelegate == null) {
   1272      Log.e(LOGTAG, "Tried to update extension " + extension.id + " but no delegate is registered");
   1273      return;
   1274    }
   1275 
   1276    final GeckoResult<AllowOrDeny> promptResponse =
   1277        mPromptDelegate.onUpdatePrompt(
   1278            extension, newPermissions, newOrigins, newDataCollectionPermissions);
   1279 
   1280    if (promptResponse == null) {
   1281      return;
   1282    }
   1283 
   1284    callback.resolveTo(
   1285        promptResponse.map(
   1286            allowOrDeny -> {
   1287              final GeckoBundle response = new GeckoBundle(1);
   1288              response.putBoolean("allow", AllowOrDeny.ALLOW.equals(allowOrDeny));
   1289              return response;
   1290            }));
   1291  }
   1292 
   1293  private void optionalPrompt(final Message message, final WebExtension extension) {
   1294    if (mPromptDelegate == null) {
   1295      Log.e(
   1296          LOGTAG,
   1297          "Tried to request optional permissions for extension "
   1298              + extension.id
   1299              + " but no delegate is registered");
   1300      return;
   1301    }
   1302 
   1303    final String[] permissions =
   1304        message.bundle.getBundle("permissions").getStringArray("permissions");
   1305    final String[] origins = message.bundle.getBundle("permissions").getStringArray("origins");
   1306    final String[] dataCollectionPermissions =
   1307        message.bundle.getBundle("permissions").getStringArray("data_collection");
   1308 
   1309    final GeckoResult<AllowOrDeny> promptResponse =
   1310        mPromptDelegate.onOptionalPrompt(
   1311            extension, permissions, origins, dataCollectionPermissions);
   1312 
   1313    if (promptResponse == null) {
   1314      return;
   1315    }
   1316 
   1317    message.callback.resolveTo(
   1318        promptResponse.map(
   1319            allowOrDeny -> {
   1320              final GeckoBundle response = new GeckoBundle(1);
   1321              response.putBoolean("allow", AllowOrDeny.ALLOW.equals(allowOrDeny));
   1322              return response;
   1323            }));
   1324  }
   1325 
   1326  private void onInstallationFailed(final GeckoBundle bundle) {
   1327    if (mAddonManagerDelegate == null) {
   1328      Log.e(LOGTAG, "no AddonManager delegate registered");
   1329      return;
   1330    }
   1331 
   1332    final int errorCode = bundle.getInt("error");
   1333    final GeckoBundle extensionBundle = bundle.getBundle("extension");
   1334    WebExtension extension = null;
   1335    final String extensionId = bundle.getString("addonId");
   1336    final String extensionName = bundle.getString("addonName");
   1337    final String extensionVersion = bundle.getString("addonVersion");
   1338 
   1339    if (extensionBundle != null) {
   1340      extension = new WebExtension(mDelegateControllerProvider, extensionBundle);
   1341    }
   1342    mAddonManagerDelegate.onInstallationFailed(
   1343        extension, new InstallException(errorCode, extensionId, extensionName, extensionVersion));
   1344  }
   1345 
   1346  private void onDisabling(final GeckoBundle bundle) {
   1347    if (mAddonManagerDelegate == null) {
   1348      Log.e(LOGTAG, "no AddonManager delegate registered");
   1349      return;
   1350    }
   1351 
   1352    final GeckoBundle extensionBundle = bundle.getBundle("extension");
   1353    final WebExtension extension = new WebExtension(mDelegateControllerProvider, extensionBundle);
   1354    mAddonManagerDelegate.onDisabling(extension);
   1355  }
   1356 
   1357  private void onOptionalPermissionsChanged(final GeckoBundle bundle) {
   1358    if (mAddonManagerDelegate == null) {
   1359      Log.e(LOGTAG, "no AddonManager delegate registered");
   1360      return;
   1361    }
   1362 
   1363    final GeckoBundle extensionBundle = bundle.getBundle("extension");
   1364    final WebExtension extension = new WebExtension(mDelegateControllerProvider, extensionBundle);
   1365    mAddonManagerDelegate.onOptionalPermissionsChanged(extension);
   1366  }
   1367 
   1368  private void onDisabled(final GeckoBundle bundle) {
   1369    if (mAddonManagerDelegate == null) {
   1370      Log.e(LOGTAG, "no AddonManager delegate registered");
   1371      return;
   1372    }
   1373 
   1374    final GeckoBundle extensionBundle = bundle.getBundle("extension");
   1375    final WebExtension extension = new WebExtension(mDelegateControllerProvider, extensionBundle);
   1376    mAddonManagerDelegate.onDisabled(extension);
   1377  }
   1378 
   1379  private void onEnabling(final GeckoBundle bundle) {
   1380    if (mAddonManagerDelegate == null) {
   1381      Log.e(LOGTAG, "no AddonManager delegate registered");
   1382      return;
   1383    }
   1384 
   1385    final GeckoBundle extensionBundle = bundle.getBundle("extension");
   1386    final WebExtension extension = new WebExtension(mDelegateControllerProvider, extensionBundle);
   1387    mAddonManagerDelegate.onEnabling(extension);
   1388  }
   1389 
   1390  private void onEnabled(final GeckoBundle bundle) {
   1391    if (mAddonManagerDelegate == null) {
   1392      Log.e(LOGTAG, "no AddonManager delegate registered");
   1393      return;
   1394    }
   1395 
   1396    final GeckoBundle extensionBundle = bundle.getBundle("extension");
   1397    final WebExtension extension = new WebExtension(mDelegateControllerProvider, extensionBundle);
   1398    mAddonManagerDelegate.onEnabled(extension);
   1399  }
   1400 
   1401  private void onUninstalling(final GeckoBundle bundle) {
   1402    if (mAddonManagerDelegate == null) {
   1403      Log.e(LOGTAG, "no AddonManager delegate registered");
   1404      return;
   1405    }
   1406 
   1407    final GeckoBundle extensionBundle = bundle.getBundle("extension");
   1408    final WebExtension extension = new WebExtension(mDelegateControllerProvider, extensionBundle);
   1409    mAddonManagerDelegate.onUninstalling(extension);
   1410  }
   1411 
   1412  private void onUninstalled(final GeckoBundle bundle) {
   1413    if (mAddonManagerDelegate == null) {
   1414      Log.e(LOGTAG, "no AddonManager delegate registered");
   1415      return;
   1416    }
   1417 
   1418    final GeckoBundle extensionBundle = bundle.getBundle("extension");
   1419    final WebExtension extension = new WebExtension(mDelegateControllerProvider, extensionBundle);
   1420    mAddonManagerDelegate.onUninstalled(extension);
   1421  }
   1422 
   1423  private void onInstalling(final GeckoBundle bundle) {
   1424    if (mAddonManagerDelegate == null) {
   1425      Log.e(LOGTAG, "no AddonManager delegate registered");
   1426      return;
   1427    }
   1428 
   1429    final GeckoBundle extensionBundle = bundle.getBundle("extension");
   1430    final WebExtension extension = new WebExtension(mDelegateControllerProvider, extensionBundle);
   1431    mAddonManagerDelegate.onInstalling(extension);
   1432  }
   1433 
   1434  private void onInstalled(final GeckoBundle bundle) {
   1435    if (mAddonManagerDelegate == null) {
   1436      Log.e(LOGTAG, "no AddonManager delegate registered");
   1437      return;
   1438    }
   1439 
   1440    final GeckoBundle extensionBundle = bundle.getBundle("extension");
   1441    final WebExtension extension = new WebExtension(mDelegateControllerProvider, extensionBundle);
   1442    mAddonManagerDelegate.onInstalled(extension);
   1443  }
   1444 
   1445  private void onReady(final GeckoBundle bundle) {
   1446    if (mAddonManagerDelegate == null) {
   1447      Log.e(LOGTAG, "no AddonManager delegate registered");
   1448      return;
   1449    }
   1450 
   1451    final GeckoBundle extensionBundle = bundle.getBundle("extension");
   1452    final WebExtension extension = new WebExtension(mDelegateControllerProvider, extensionBundle);
   1453    mAddonManagerDelegate.onReady(extension);
   1454  }
   1455 
   1456  private void onDisabledProcessSpawning() {
   1457    if (mExtensionProcessDelegate == null) {
   1458      Log.e(LOGTAG, "no extension process delegate registered");
   1459      return;
   1460    }
   1461 
   1462    mExtensionProcessDelegate.onDisabledProcessSpawning();
   1463  }
   1464 
   1465  @SuppressLint("WrongThread") // for .toGeckoBundle
   1466  private void getSettings(final Message message, final WebExtension extension) {
   1467    final WebExtension.BrowsingDataDelegate delegate = mListener.getBrowsingDataDelegate(extension);
   1468    if (delegate == null) {
   1469      mPendingBrowsingData.add(extension.id, message);
   1470      return;
   1471    }
   1472 
   1473    final GeckoResult<WebExtension.BrowsingDataDelegate.Settings> settingsResult =
   1474        delegate.onGetSettings();
   1475    if (settingsResult == null) {
   1476      message.callback.sendError("browsingData.settings is not supported");
   1477      return;
   1478    }
   1479    message.callback.resolveTo(settingsResult.map(settings -> settings.toGeckoBundle()));
   1480  }
   1481 
   1482  private void browsingDataClear(final Message message, final WebExtension extension) {
   1483    final WebExtension.BrowsingDataDelegate delegate = mListener.getBrowsingDataDelegate(extension);
   1484    if (delegate == null) {
   1485      mPendingBrowsingData.add(extension.id, message);
   1486      return;
   1487    }
   1488 
   1489    final long unixTimestamp = message.bundle.getLong("since");
   1490    final String dataType = message.bundle.getString("dataType");
   1491 
   1492    final GeckoResult<Void> response;
   1493    if ("downloads".equals(dataType)) {
   1494      response = delegate.onClearDownloads(unixTimestamp);
   1495    } else if ("formData".equals(dataType)) {
   1496      response = delegate.onClearFormData(unixTimestamp);
   1497    } else if ("history".equals(dataType)) {
   1498      response = delegate.onClearHistory(unixTimestamp);
   1499    } else if ("passwords".equals(dataType)) {
   1500      response = delegate.onClearPasswords(unixTimestamp);
   1501    } else {
   1502      throw new IllegalStateException("Illegal clear data type: " + dataType);
   1503    }
   1504 
   1505    message.callback.resolveTo(response);
   1506  }
   1507 
   1508  /* package */ void download(final Message message, final WebExtension extension) {
   1509    final WebExtension.DownloadDelegate delegate = mListener.getDownloadDelegate(extension);
   1510    if (delegate == null) {
   1511      mPendingDownload.add(extension.id, message);
   1512      return;
   1513    }
   1514 
   1515    final GeckoBundle optionsBundle = message.bundle.getBundle("options");
   1516 
   1517    final WebExtension.DownloadRequest request =
   1518        WebExtension.DownloadRequest.fromBundle(optionsBundle);
   1519 
   1520    final GeckoResult<WebExtension.DownloadInitData> result =
   1521        delegate.onDownload(extension, request);
   1522    if (result == null) {
   1523      message.callback.sendError("downloads.download is not supported");
   1524      return;
   1525    }
   1526 
   1527    message.callback.resolveTo(
   1528        result.map(
   1529            value -> {
   1530              if (value == null) {
   1531                Log.e(LOGTAG, "onDownload returned invalid null value");
   1532                throw new IllegalArgumentException("downloads.download is not supported");
   1533              }
   1534 
   1535              final GeckoBundle returnMessage =
   1536                  WebExtension.Download.downloadInfoToBundle(value.initData);
   1537              returnMessage.putInt("id", value.download.id);
   1538 
   1539              return returnMessage;
   1540            }));
   1541  }
   1542 
   1543  /* package */ void openOptionsPage(final Message message, final WebExtension extension) {
   1544    final GeckoBundle bundle = message.bundle;
   1545    final WebExtension.TabDelegate delegate = mListener.getTabDelegate(extension);
   1546 
   1547    if (delegate != null) {
   1548      delegate.onOpenOptionsPage(extension);
   1549    } else {
   1550      message.callback.sendError("runtime.openOptionsPage is not supported");
   1551    }
   1552 
   1553    message.callback.sendSuccess(null);
   1554  }
   1555 
   1556  /* package */
   1557  @SuppressLint("WrongThread") // for .isOpen
   1558  void newTab(final Message message, final WebExtension extension) {
   1559    final GeckoBundle bundle = message.bundle;
   1560 
   1561    final WebExtension.TabDelegate delegate = mListener.getTabDelegate(extension);
   1562    final WebExtension.CreateTabDetails details =
   1563        new WebExtension.CreateTabDetails(bundle.getBundle("createProperties"));
   1564 
   1565    final GeckoResult<GeckoSession> result;
   1566    if (delegate != null) {
   1567      result = delegate.onNewTab(extension, details);
   1568    } else {
   1569      mPendingNewTab.add(extension.id, message);
   1570      return;
   1571    }
   1572 
   1573    if (result == null) {
   1574      message.callback.sendSuccess(false);
   1575      return;
   1576    }
   1577 
   1578    final String newSessionId = message.bundle.getString("newSessionId");
   1579    message.callback.resolveTo(
   1580        result.map(
   1581            session -> {
   1582              if (session == null) {
   1583                return false;
   1584              }
   1585 
   1586              if (session.isOpen()) {
   1587                throw new IllegalArgumentException("Must use an unopened GeckoSession instance");
   1588              }
   1589 
   1590              session.open(mListener.runtime, newSessionId);
   1591              return true;
   1592            }));
   1593  }
   1594 
   1595  /* package */ void updateTab(final Message message, final WebExtension extension) {
   1596    final WebExtension.SessionTabDelegate delegate =
   1597        message.session.getWebExtensionController().getTabDelegate(extension);
   1598    final EventCallback callback = message.callback;
   1599 
   1600    if (delegate == null) {
   1601      callback.sendError("tabs.update is not supported");
   1602      return;
   1603    }
   1604 
   1605    final WebExtension.UpdateTabDetails details =
   1606        new WebExtension.UpdateTabDetails(message.bundle.getBundle("updateProperties"));
   1607    callback.resolveTo(
   1608        delegate
   1609            .onUpdateTab(extension, message.session, details)
   1610            .map(
   1611                value -> {
   1612                  if (value == AllowOrDeny.ALLOW) {
   1613                    return null;
   1614                  } else {
   1615                    throw new Exception("tabs.update is not supported");
   1616                  }
   1617                }));
   1618  }
   1619 
   1620  /* package */ void closeTab(final Message message, final WebExtension extension) {
   1621    final WebExtension.SessionTabDelegate delegate =
   1622        message.session.getWebExtensionController().getTabDelegate(extension);
   1623 
   1624    final GeckoResult<AllowOrDeny> result;
   1625    if (delegate != null) {
   1626      result = delegate.onCloseTab(extension, message.session);
   1627    } else {
   1628      result = GeckoResult.deny();
   1629    }
   1630 
   1631    message.callback.resolveTo(
   1632        result.map(
   1633            value -> {
   1634              if (value == AllowOrDeny.ALLOW) {
   1635                return null;
   1636              } else {
   1637                throw new Exception("tabs.remove is not supported");
   1638              }
   1639            }));
   1640  }
   1641 
   1642  /**
   1643   * Notifies extensions about a active tab change over the `tabs.onActivated` event.
   1644   *
   1645   * @param session The {@link GeckoSession} of the newly selected session/tab.
   1646   * @param active true if the tab became active, false if the tab became inactive.
   1647   */
   1648  @AnyThread
   1649  public void setTabActive(@NonNull final GeckoSession session, final boolean active) {
   1650    final GeckoBundle bundle = new GeckoBundle(1);
   1651    bundle.putBoolean("active", active);
   1652    session.getEventDispatcher().dispatch("GeckoView:WebExtension:SetTabActive", bundle);
   1653  }
   1654 
   1655  /* package */ void unregisterWebExtension(final WebExtension webExtension) {
   1656    mExtensions.remove(webExtension.id);
   1657    mListener.unregisterWebExtension(webExtension);
   1658  }
   1659 
   1660  private WebExtension.MessageSender fromBundle(
   1661      final WebExtension extension, final GeckoBundle sender, final GeckoSession session) {
   1662    if (extension == null) {
   1663      // All senders should have an extension
   1664      return null;
   1665    }
   1666 
   1667    final String envType = sender.getString("envType");
   1668    @WebExtension.MessageSender.EnvType final int environmentType;
   1669 
   1670    if ("content_child".equals(envType)) {
   1671      environmentType = WebExtension.MessageSender.ENV_TYPE_CONTENT_SCRIPT;
   1672    } else if ("addon_child".equals(envType)) {
   1673      // TODO Bug 1554277: check that this message is coming from the right process
   1674      environmentType = WebExtension.MessageSender.ENV_TYPE_EXTENSION;
   1675    } else {
   1676      environmentType = WebExtension.MessageSender.ENV_TYPE_UNKNOWN;
   1677    }
   1678 
   1679    if (environmentType == WebExtension.MessageSender.ENV_TYPE_UNKNOWN) {
   1680      if (BuildConfig.DEBUG_BUILD) {
   1681        throw new RuntimeException("Missing or unknown envType: " + envType);
   1682      }
   1683 
   1684      return null;
   1685    }
   1686 
   1687    final String url = sender.getString("url");
   1688    final boolean isTopLevel;
   1689    if (session == null || environmentType == WebExtension.MessageSender.ENV_TYPE_EXTENSION) {
   1690      // This message is coming from the background page, a popup, or an extension page
   1691      isTopLevel = true;
   1692    } else {
   1693      // If session is present we are either receiving this message from a content script or
   1694      // an extension page, let's make sure we have the proper identification so that
   1695      // embedders can check the origin of this message.
   1696      // -1 is an invalid frame id
   1697      final boolean hasFrameId =
   1698          sender.containsKey("frameId") && sender.getInt("frameId", -1) != -1;
   1699      final boolean hasUrl = sender.containsKey("url");
   1700      if (!hasFrameId || !hasUrl) {
   1701        if (BuildConfig.DEBUG_BUILD) {
   1702          throw new RuntimeException(
   1703              "Missing sender information. hasFrameId: " + hasFrameId + " hasUrl: " + hasUrl);
   1704        }
   1705 
   1706        // This message does not have the proper identification and may be compromised,
   1707        // let's ignore it.
   1708        return null;
   1709      }
   1710 
   1711      isTopLevel = sender.getInt("frameId", -1) == 0;
   1712    }
   1713 
   1714    return new WebExtension.MessageSender(extension, session, url, environmentType, isTopLevel);
   1715  }
   1716 
   1717  private WebExtension.MessageDelegate getDelegate(
   1718      final String nativeApp,
   1719      final WebExtension.MessageSender sender,
   1720      final EventCallback callback) {
   1721    if ((sender.webExtension.flags & WebExtension.Flags.ALLOW_CONTENT_MESSAGING) == 0
   1722        && sender.environmentType == WebExtension.MessageSender.ENV_TYPE_CONTENT_SCRIPT) {
   1723      callback.sendError("This NativeApp can't receive messages from Content Scripts.");
   1724      return null;
   1725    }
   1726 
   1727    WebExtension.MessageDelegate delegate = null;
   1728 
   1729    if (sender.session != null) {
   1730      delegate =
   1731          sender
   1732              .session
   1733              .getWebExtensionController()
   1734              .getMessageDelegate(sender.webExtension, nativeApp);
   1735    } else if (sender.environmentType == WebExtension.MessageSender.ENV_TYPE_EXTENSION) {
   1736      delegate = mListener.getMessageDelegate(sender.webExtension, nativeApp);
   1737    }
   1738 
   1739    return delegate;
   1740  }
   1741 
   1742  private static class MessageRecipient {
   1743    public final String webExtensionId;
   1744    public final String nativeApp;
   1745    public final GeckoSession session;
   1746 
   1747    public MessageRecipient(
   1748        final String webExtensionId, final String nativeApp, final GeckoSession session) {
   1749      this.webExtensionId = webExtensionId;
   1750      this.nativeApp = nativeApp;
   1751      this.session = session;
   1752    }
   1753 
   1754    private static boolean equals(final Object a, final Object b) {
   1755      return Objects.equals(a, b);
   1756    }
   1757 
   1758    @Override
   1759    public boolean equals(final Object other) {
   1760      if (!(other instanceof MessageRecipient)) {
   1761        return false;
   1762      }
   1763 
   1764      final MessageRecipient o = (MessageRecipient) other;
   1765      return equals(webExtensionId, o.webExtensionId)
   1766          && equals(nativeApp, o.nativeApp)
   1767          && equals(session, o.session);
   1768    }
   1769 
   1770    @Override
   1771    public int hashCode() {
   1772      return Arrays.hashCode(new Object[] {webExtensionId, nativeApp, session});
   1773    }
   1774  }
   1775 
   1776  private void connect(
   1777      final String nativeApp,
   1778      final long portId,
   1779      final Message message,
   1780      final WebExtension.MessageSender sender) {
   1781    if (portId == -1) {
   1782      message.callback.sendError("Missing portId.");
   1783      return;
   1784    }
   1785 
   1786    final WebExtension.Port port = new WebExtension.Port(nativeApp, portId, sender);
   1787 
   1788    final WebExtension.MessageDelegate delegate = getDelegate(nativeApp, sender, message.callback);
   1789    if (delegate == null) {
   1790      mPendingMessages.add(
   1791          new MessageRecipient(nativeApp, sender.webExtension.id, sender.session), message);
   1792      return;
   1793    }
   1794 
   1795    delegate.onConnect(port);
   1796    message.callback.sendSuccess(true);
   1797  }
   1798 
   1799  private void message(
   1800      final String nativeApp, final Message message, final WebExtension.MessageSender sender) {
   1801    final EventCallback callback = message.callback;
   1802 
   1803    final Object content;
   1804    try {
   1805      content = message.bundle.toJSONObject().get("data");
   1806    } catch (final JSONException ex) {
   1807      callback.sendError(ex.getMessage());
   1808      return;
   1809    }
   1810 
   1811    final WebExtension.MessageDelegate delegate = getDelegate(nativeApp, sender, callback);
   1812    if (delegate == null) {
   1813      mPendingMessages.add(
   1814          new MessageRecipient(nativeApp, sender.webExtension.id, sender.session), message);
   1815      return;
   1816    }
   1817 
   1818    final GeckoResult<Object> response = delegate.onMessage(nativeApp, content, sender);
   1819    if (response == null) {
   1820      callback.sendSuccess(null);
   1821      return;
   1822    }
   1823 
   1824    callback.resolveTo(response);
   1825  }
   1826 
   1827  private GeckoResult<WebExtension> extensionFromBundle(final GeckoBundle message) {
   1828    final String extensionId = message.getString("extensionId");
   1829    return mExtensions.get(extensionId);
   1830  }
   1831 
   1832  private void openPopup(
   1833      final Message message,
   1834      final WebExtension extension,
   1835      final @WebExtension.Action.ActionType int actionType) {
   1836    if (extension == null) {
   1837      return;
   1838    }
   1839 
   1840    final WebExtension.Action action =
   1841        new WebExtension.Action(actionType, message.bundle.getBundle("action"), extension);
   1842    final String popupUri = message.bundle.getString("popupUri");
   1843 
   1844    final WebExtension.ActionDelegate delegate = actionDelegateFor(extension, message.session);
   1845    if (delegate == null) {
   1846      return;
   1847    }
   1848 
   1849    final GeckoResult<GeckoSession> popup = delegate.onOpenPopup(extension, action);
   1850    action.openPopup(popup, popupUri);
   1851  }
   1852 
   1853  private WebExtension.ActionDelegate actionDelegateFor(
   1854      final WebExtension extension, final GeckoSession session) {
   1855    if (session == null) {
   1856      return mListener.getActionDelegate(extension);
   1857    }
   1858 
   1859    return session.getWebExtensionController().getActionDelegate(extension);
   1860  }
   1861 
   1862  private void actionUpdate(
   1863      final Message message,
   1864      final WebExtension extension,
   1865      final @WebExtension.Action.ActionType int actionType) {
   1866    if (extension == null) {
   1867      return;
   1868    }
   1869 
   1870    final WebExtension.ActionDelegate delegate = actionDelegateFor(extension, message.session);
   1871    if (delegate == null) {
   1872      return;
   1873    }
   1874 
   1875    final WebExtension.Action action =
   1876        new WebExtension.Action(actionType, message.bundle.getBundle("action"), extension);
   1877    if (actionType == WebExtension.Action.TYPE_BROWSER_ACTION) {
   1878      delegate.onBrowserAction(extension, message.session, action);
   1879    } else if (actionType == WebExtension.Action.TYPE_PAGE_ACTION) {
   1880      delegate.onPageAction(extension, message.session, action);
   1881    }
   1882  }
   1883 
   1884  // TODO: implement bug 1595822
   1885  /* package */ static GeckoResult<List<WebExtension.Menu>> getMenu(
   1886      final GeckoBundle menuArrayBundle) {
   1887    return null;
   1888  }
   1889 
   1890  /**
   1891   * Creates a new Download object with the specified ID for tracking WebExtension downloads.
   1892   *
   1893   * @param id The unique identifier for the download
   1894   * @return A new Download object for the specified ID
   1895   * @throws IllegalArgumentException if a download with this ID already exists
   1896   */
   1897  @Nullable
   1898  @UiThread
   1899  public WebExtension.Download createDownload(final int id) {
   1900    if (mDownloads.indexOfKey(id) >= 0) {
   1901      throw new IllegalArgumentException("Download with this id already exists");
   1902    } else {
   1903      final WebExtension.Download download = new WebExtension.Download(id);
   1904      mDownloads.put(id, download);
   1905 
   1906      return download;
   1907    }
   1908  }
   1909 }