Bug 1321418 - Use GekcoBundle events in GeckoApp/BrowserApp; r=snorp r=sebastian r=gbrown
authorJim Chen <nchen@mozilla.com>
Fri, 09 Dec 2016 12:32:45 -0500
changeset 325598 1cce3b69bfc76377f5a0c015372d790b81f7b2bd
parent 325597 6ed38cf05aaf57389f9a8b6680a454d247916c50
child 325599 c9d3669dc5133a29944acb1353101c7bf905e914
push id24
push usermaklebus@msu.edu
push dateTue, 20 Dec 2016 03:11:33 +0000
reviewerssnorp, sebastian, gbrown
bugs1321418
milestone53.0a1
Bug 1321418 - Use GekcoBundle events in GeckoApp/BrowserApp; r=snorp r=sebastian r=gbrown Bug 1321418 - 1. Use GekcoBundle events in GeckoApp; r=snorp r=sebastian Switch GeckoApp to using GeckoBundle events everywhere. UI or Gecko events are used if the event requires the UI or Gecko thread, respectively, and background events are used for all other events. There are changes to some other Java classes, such as SnackbarBuilder and GeckoAccessibility, due to the switch to GeckoBundle. For "Snackbar:Show", we need the global EventDispatcher because the event can be sent to both GeckoApp and GeckoPreferences. Howveer, we only want one listener registered at the same time, so we register and unregister in GeckoApp's and GeckoPreferences's onPause and onResume methods. Bug 1321418 - 2. Use appropriate JS EventDispatcher to send GeckoApp events; r=snorp r=sebastian Change JS code that sends events to GeckoApp to use either the global EventDispatcher or the per-window EventDispatcher. "Session:StatePurged" is not used so it's removed. "Gecko:Ready" in geckoview.js is not necessary because it's only used for GeckoApp, so it's removed from geckoview.js. Bug 1321418 - 3. Use GeckoBundle events in BrowserApp; r=snorp r=sebastian Switch BrowserApp to using GeckoBundle events, in a similar vein as GeckoApp. UI or Gecko events are used if the event handlers required UI or Gecko thread, respectively, and background events are used for all other events. Some other Java classes also have to be modified as a result of switching to GeckoBundle. Bug 1321418 - 4. Use global EventDispatcher to send BrowserApp events; r=snorp r=sebastian Change JS code that sends events to BrowserApp to use the global EventDispatcher instead of "Messaging". Bug 1321418 - 5. Update usages of events in tests; r=gbrown Update cases where we use or wait for events in tests.
accessible/jsat/AccessFu.jsm
mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
mobile/android/base/java/org/mozilla/gecko/SnackbarBuilder.java
mobile/android/base/java/org/mozilla/gecko/Tabs.java
mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryPanel.java
mobile/android/base/java/org/mozilla/gecko/lwt/LightweightTheme.java
mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
mobile/android/chrome/content/ActionBarHandler.js
mobile/android/chrome/content/Feedback.js
mobile/android/chrome/content/PermissionsHelper.js
mobile/android/chrome/content/Reader.js
mobile/android/chrome/content/RemoteDebugger.js
mobile/android/chrome/content/about.js
mobile/android/chrome/content/aboutHealthReport.js
mobile/android/chrome/content/aboutLogins.js
mobile/android/chrome/content/browser.js
mobile/android/chrome/content/geckoview.js
mobile/android/components/HelperAppDialog.js
mobile/android/components/SessionStore.js
mobile/android/geckoview/src/main/java/org/mozilla/gecko/EventDispatcher.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAccessibility.java
mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoThread.java
mobile/android/modules/LightweightThemeConsumer.jsm
mobile/android/modules/RuntimePermissions.jsm
mobile/android/modules/Sanitizer.jsm
mobile/android/modules/Snackbars.jsm
mobile/android/modules/WebsiteMetadata.jsm
mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/BaseTest.java
mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/ContentContextMenuTest.java
mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/GeckoHelper.java
mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAboutPage.java
mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAddonManager.java
mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarksPanel.java
mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testDoorHanger.java
mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPrivateBrowsing.java
mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testRuntimePermissionsAPI.java
mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSnackbarAPI.java
mobile/android/tests/browser/robocop/testSnackbarAPI.js
toolkit/content/aboutTelemetry.js
--- a/accessible/jsat/AccessFu.jsm
+++ b/accessible/jsat/AccessFu.jsm
@@ -28,18 +28,17 @@ this.AccessFu = { // jshint ignore:line
    * Initialize chrome-layer accessibility functionality.
    * If accessibility is enabled on the platform, then a special accessibility
    * mode is started.
    */
   attach: function attach(aWindow) {
     Utils.init(aWindow);
 
     try {
-      Services.androidBridge.handleGeckoMessage(
-          { type: 'Accessibility:Ready' });
+      Services.androidBridge.dispatch('Accessibility:Ready');
       Services.obs.addObserver(this, 'Accessibility:Settings', false);
     } catch (x) {
       // Not on Android
       if (aWindow.navigator.mozSettings) {
         let lock = aWindow.navigator.mozSettings.createLock();
         let req = lock.get(SCREENREADER_SETTING);
         req.addEventListener('success', () => {
           this._systemPref = req.result[SCREENREADER_SETTING];
@@ -597,17 +596,16 @@ var Output = {
     const ANDROID_VIEW_TEXT_CHANGED = 0x10;
     const ANDROID_VIEW_TEXT_SELECTION_CHANGED = 0x2000;
 
     if (!this.androidBridge) {
       return;
     }
 
     for (let androidEvent of aDetails) {
-      androidEvent.type = 'Accessibility:Event';
       if (androidEvent.bounds) {
         androidEvent.bounds = AccessFu.adjustContentBounds(
           androidEvent.bounds, aBrowser);
       }
 
       switch(androidEvent.eventType) {
         case ANDROID_VIEW_TEXT_CHANGED:
           androidEvent.brailleOutput = this.brailleState.adjustText(
@@ -617,17 +615,19 @@ var Output = {
           androidEvent.brailleOutput = this.brailleState.adjustSelection(
             androidEvent.brailleOutput);
           break;
         default:
           androidEvent.brailleOutput = this.brailleState.init(
             androidEvent.brailleOutput);
           break;
       }
-      this.androidBridge.handleGeckoMessage(androidEvent);
+      let win = Utils.win;
+      let view = win && win.QueryInterface(Ci.nsIAndroidView);
+      view.dispatch('Accessibility:Event', androidEvent);
     }
   },
 
   Braille: function Braille(aDetails) {
     Logger.debug('Braille output: ' + aDetails.output);
   }
 };
 
@@ -813,18 +813,19 @@ var Input = {
             return;
           } else {
             target.blur();
           }
         }
 
         if (Utils.MozBuildApp == 'mobile/android') {
           // Return focus to native Android browser chrome.
-          Services.androidBridge.handleGeckoMessage(
-              { type: 'ToggleChrome:Focus' });
+          let win = Utils.win;
+          let view = win && win.QueryInterface(Ci.nsIAndroidView);
+          view.dispatch('ToggleChrome:Focus');
         }
         break;
       case aEvent.DOM_VK_RETURN:
         if (this.editState.editing) {
           return;
         }
         this.activateCurrent();
         break;
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -98,22 +98,19 @@ import org.mozilla.gecko.updater.PostUpd
 import org.mozilla.gecko.updater.UpdateServiceHelper;
 import org.mozilla.gecko.util.ActivityUtils;
 import org.mozilla.gecko.util.Clipboard;
 import org.mozilla.gecko.util.BundleEventListener;
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.FloatUtils;
 import org.mozilla.gecko.util.GamepadUtils;
 import org.mozilla.gecko.util.GeckoBundle;
-import org.mozilla.gecko.util.GeckoEventListener;
 import org.mozilla.gecko.util.HardwareUtils;
 import org.mozilla.gecko.util.IntentUtils;
 import org.mozilla.gecko.util.MenuUtils;
-import org.mozilla.gecko.util.NativeEventListener;
-import org.mozilla.gecko.util.NativeJSObject;
 import org.mozilla.gecko.util.PrefUtils;
 import org.mozilla.gecko.util.StringUtils;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.widget.AnchoredPopup;
 
 import org.mozilla.gecko.widget.GeckoActionProvider;
 
 import android.app.Activity;
@@ -197,18 +194,17 @@ public class BrowserApp extends GeckoApp
                                    View.OnKeyListener,
                                    LayerView.DynamicToolbarListener,
                                    BrowserSearch.OnSearchListener,
                                    BrowserSearch.OnEditSuggestionListener,
                                    OnUrlOpenListener,
                                    OnUrlOpenInBackgroundListener,
                                    AnchoredPopup.OnVisibilityChangeListener,
                                    ActionModeCompat.Presenter,
-                                   LayoutInflater.Factory,
-                                   BundleEventListener {
+                                   LayoutInflater.Factory {
     private static final String LOGTAG = "GeckoBrowserApp";
 
     private static final int TABS_ANIMATION_DURATION = 450;
 
     // Intent String extras used to specify custom Switchboard configurations.
     private static final String INTENT_KEY_SWITCHBOARD_SERVER = "switchboard-server";
 
     // TODO: Replace with kinto endpoint.
@@ -721,42 +717,46 @@ public class BrowserApp extends GeckoApp
 
         setBrowserToolbarListeners();
 
         mFindInPageBar = (FindInPageBar) findViewById(R.id.find_in_page);
         mMediaCastingBar = (MediaCastingBar) findViewById(R.id.media_casting);
 
         mDoorhangerOverlay = findViewById(R.id.doorhanger_overlay);
 
-        EventDispatcher.getInstance().registerGeckoThreadListener((GeckoEventListener)this,
-            "Gecko:DelayedStartup",
+        EventDispatcher.getInstance().registerGeckoThreadListener(this,
+            "Search:Keyword",
+            "Favicon:CacheLoad",
+            null);
+
+        EventDispatcher.getInstance().registerUiThreadListener(this,
             "Menu:Open",
             "Menu:Update",
+            "Menu:Add",
+            "Menu:Remove",
             "LightweightTheme:Update",
-            "Search:Keyword",
             "Tab:Added",
-            "Video:Play");
-
-        EventDispatcher.getInstance().registerGeckoThreadListener((NativeEventListener)this,
+            "Video:Play",
             "CharEncoding:Data",
             "CharEncoding:State",
-            "Download:AndroidDownloadManager",
+            "Settings:Show",
+            "Updater:Launch",
+            null);
+
+        EventDispatcher.getInstance().registerBackgroundThreadListener(this,
             "Experiments:GetActive",
             "Experiments:SetOverride",
             "Experiments:ClearOverride",
-            "Favicon:CacheLoad",
             "Feedback:MaybeLater",
-            "Menu:Add",
-            "Menu:Remove",
             "Sanitize:ClearHistory",
             "Sanitize:ClearSyncedTabs",
-            "Settings:Show",
             "Telemetry:Gather",
-            "Updater:Launch",
-            "Website:Metadata");
+            "Download:AndroidDownloadManager",
+            "Website:Metadata",
+            null);
 
         getAppEventDispatcher().registerUiThreadListener(this, "Prompt:ShowTop");
 
         final GeckoProfile profile = getProfile();
 
         // We want to upload the telemetry core ping as soon after startup as possible. It relies on the
         // Distribution being initialized. If you move this initialization, ensure it plays well with telemetry.
         final Distribution distribution = Distribution.init(getApplicationContext());
@@ -1425,42 +1425,46 @@ public class BrowserApp extends GeckoApp
         }
 
         if (mZoomedView != null) {
             mZoomedView.destroy();
         }
 
         mSearchEngineManager.unregisterListeners();
 
-        EventDispatcher.getInstance().unregisterGeckoThreadListener((GeckoEventListener) this,
-            "Gecko:DelayedStartup",
+        EventDispatcher.getInstance().unregisterGeckoThreadListener(this,
+            "Search:Keyword",
+            "Favicon:CacheLoad",
+            null);
+
+        EventDispatcher.getInstance().unregisterUiThreadListener(this,
             "Menu:Open",
             "Menu:Update",
+            "Menu:Add",
+            "Menu:Remove",
             "LightweightTheme:Update",
-            "Search:Keyword",
             "Tab:Added",
-            "Video:Play");
-
-        EventDispatcher.getInstance().unregisterGeckoThreadListener((NativeEventListener) this,
+            "Video:Play",
             "CharEncoding:Data",
             "CharEncoding:State",
-            "Download:AndroidDownloadManager",
+            "Settings:Show",
+            "Updater:Launch",
+            null);
+
+        EventDispatcher.getInstance().unregisterBackgroundThreadListener(this,
             "Experiments:GetActive",
             "Experiments:SetOverride",
             "Experiments:ClearOverride",
-            "Favicon:CacheLoad",
             "Feedback:MaybeLater",
-            "Menu:Add",
-            "Menu:Remove",
             "Sanitize:ClearHistory",
             "Sanitize:ClearSyncedTabs",
-            "Settings:Show",
             "Telemetry:Gather",
-            "Updater:Launch",
-            "Website:Metadata");
+            "Download:AndroidDownloadManager",
+            "Website:Metadata",
+            null);
 
         getAppEventDispatcher().unregisterUiThreadListener(this, "Prompt:ShowTop");
 
         if (AppConstants.MOZ_ANDROID_BEAM) {
             NfcAdapter nfc = NfcAdapter.getDefaultAdapter(this);
             if (nfc != null) {
                 // null this out even though the docs say it's not needed,
                 // because the source code looks like it will only do this
@@ -1543,35 +1547,16 @@ public class BrowserApp extends GeckoApp
     @Override
     public void onDoorHangerHide() {
         final Animator alphaAnimator = ObjectAnimator.ofFloat(mDoorhangerOverlay, "alpha", 0);
         alphaAnimator.setDuration(200);
 
         alphaAnimator.start();
     }
 
-    private void handleClearHistory(final boolean clearSearchHistory) {
-        final BrowserDB db = BrowserDB.from(getProfile());
-        ThreadUtils.postToBackgroundThread(new Runnable() {
-            @Override
-            public void run() {
-                db.clearHistory(getContentResolver(), clearSearchHistory);
-            }
-        });
-    }
-
-    private void handleClearSyncedTabs() {
-        ThreadUtils.postToBackgroundThread(new Runnable() {
-            @Override
-            public void run() {
-                FennecTabsRepository.deleteNonLocalClientsAndTabs(getContext());
-            }
-        });
-    }
-
     private void setToolbarMargin(int margin) {
         ((RelativeLayout.LayoutParams) mGeckoLayout.getLayoutParams()).topMargin = margin;
         mGeckoLayout.requestLayout();
     }
 
     @Override
     public void onTranslationChanged(float aToolbarTranslation, float aLayerViewTranslation) {
         if (mBrowserChrome == null) {
@@ -1658,39 +1643,31 @@ public class BrowserApp extends GeckoApp
             mToolbarHeight = height;
             mLayerView.setMaxTranslation(height);
             mDynamicToolbar.setVisible(true, VisibilityTransition.IMMEDIATE);
         }
     }
 
     @Override
     void toggleChrome(final boolean aShow) {
-        ThreadUtils.postToUiThread(new Runnable() {
-            @Override
-            public void run() {
-                if (aShow) {
-                    mBrowserChrome.setVisibility(View.VISIBLE);
-                } else {
-                    mBrowserChrome.setVisibility(View.GONE);
-                }
-            }
-        });
+        if (aShow) {
+            mBrowserChrome.setVisibility(View.VISIBLE);
+        } else {
+            mBrowserChrome.setVisibility(View.GONE);
+        }
 
         super.toggleChrome(aShow);
     }
 
     @Override
     void focusChrome() {
-        ThreadUtils.postToUiThread(new Runnable() {
-            @Override
-            public void run() {
-                mBrowserChrome.setVisibility(View.VISIBLE);
-                mActionBarFlipper.requestFocusFromTouch();
-            }
-        });
+        mBrowserChrome.setVisibility(View.VISIBLE);
+        mActionBarFlipper.requestFocusFromTouch();
+
+        super.focusChrome();
     }
 
     @Override
     public void refreshChrome() {
         invalidateOptionsMenu();
 
         if (mTabsPanel != null) {
             mTabsPanel.refresh();
@@ -1702,39 +1679,168 @@ public class BrowserApp extends GeckoApp
 
         mBrowserToolbar.refresh();
     }
 
     @Override // BundleEventListener
     public void handleMessage(final String event, final GeckoBundle message,
                               final EventCallback callback) {
         switch (event) {
+            case "Gecko:Ready":
+                EventDispatcher.getInstance().registerUiThreadListener(this, "Gecko:DelayedStartup");
+
+                // Handle this message in GeckoApp, but also enable the Settings
+                // menuitem, which is specific to BrowserApp.
+                super.handleMessage(event, message, callback);
+
+                final Menu menu = mMenu;
+                ThreadUtils.postToUiThread(new Runnable() {
+                    @Override
+                    public void run() {
+                        if (menu != null) {
+                            menu.findItem(R.id.settings).setEnabled(true);
+                            menu.findItem(R.id.help).setEnabled(true);
+                        }
+                    }
+                });
+
+                // Display notification for Mozilla data reporting, if data should be collected.
+                if (AppConstants.MOZ_DATA_REPORTING &&
+                        Restrictions.isAllowed(this, Restrictable.DATA_CHOICES)) {
+                    DataReportingNotification.checkAndNotifyPolicy(GeckoAppShell.getContext());
+                }
+                break;
+
+            case "Gecko:DelayedStartup":
+                EventDispatcher.getInstance().unregisterUiThreadListener(this, "Gecko:DelayedStartup");
+
+                // Force tabs panel inflation once the initial pageload is finished.
+                ensureTabsPanelExists();
+
+                if (AppConstants.NIGHTLY_BUILD && mZoomedView == null) {
+                    ViewStub stub = (ViewStub) findViewById(R.id.zoomed_view_stub);
+                    mZoomedView = (ZoomedView) stub.inflate();
+                }
+
+                if (AppConstants.MOZ_MEDIA_PLAYER) {
+                    // Check if the fragment is already added. This should never be true
+                    // here, but this is a nice safety check. If casting is disabled,
+                    // these classes aren't built. We use reflection to initialize them.
+                    final Class<?> mediaManagerClass = getMediaPlayerManager();
+
+                    if (mediaManagerClass != null) {
+                        try {
+                            final String tag = "";
+                            mediaManagerClass.getDeclaredField("MEDIA_PLAYER_TAG").get(tag);
+                            Log.i(LOGTAG, "Found tag " + tag);
+                            final Fragment frag = getSupportFragmentManager().findFragmentByTag(tag);
+                            if (frag == null) {
+                                final Method getInstance = mediaManagerClass.getMethod(
+                                        "getInstance", (Class[]) null);
+                                final Fragment mpm = (Fragment) getInstance.invoke(null);
+                                getSupportFragmentManager().beginTransaction()
+                                        .disallowAddToBackStack().add(mpm, tag).commit();
+                            }
+                        } catch (Exception ex) {
+                            Log.e(LOGTAG, "Error initializing media manager", ex);
+                        }
+                    }
+                }
+
+                if (AppConstants.MOZ_STUMBLER_BUILD_TIME_ENABLED &&
+                        Restrictions.isAllowed(this, Restrictable.DATA_CHOICES)) {
+                    // Start (this acts as ping if started already) the stumbler lib; if
+                    // the stumbler has queued data it will upload it.  Stumbler operates
+                    // on its own thread, and startup impact is further minimized by
+                    // delaying work (such as upload) a few seconds.  Avoid any potential
+                    // startup CPU/thread contention by delaying the pref broadcast.
+                    GeckoPreferences.broadcastStumblerPref(BrowserApp.this);
+                }
+
+                if (AppConstants.MOZ_ANDROID_DOWNLOAD_CONTENT_SERVICE) {
+                    // TODO: Better scheduling of sync action (Bug 1257492)
+                    DownloadContentService.startSync(this);
+                    DownloadContentService.startVerification(this);
+                }
+
+                FeedService.setup(this);
+                break;
+
+            case "Menu:Open":
+                if (mBrowserToolbar.isEditing()) {
+                    mBrowserToolbar.cancelEdit();
+                }
+                openOptionsMenu();
+                break;
+
+            case "Menu:Update":
+                updateAddonMenuItem(message.getInt("id"), message.getBundle("options"));
+                break;
+
+            case "Menu:Add":
+                final MenuItemInfo info = new MenuItemInfo();
+                info.label = message.getString("name");
+                info.id = message.getInt("id") + ADDON_MENU_OFFSET;
+                info.checked = message.getBoolean("checked", false);
+                info.enabled = message.getBoolean("enabled", true);
+                info.visible = message.getBoolean("visible", true);
+                info.checkable = message.getBoolean("checkable", false);
+                final int parent = message.getInt("parent", 0);
+                info.parent = parent <= 0 ? parent : parent + ADDON_MENU_OFFSET;
+                addAddonMenuItem(info);
+                break;
+
+            case "Menu:Remove":
+                removeAddonMenuItem(message.getInt("id") + ADDON_MENU_OFFSET);
+                break;
+
+            case "LightweightTheme:Update":
+                mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
+                break;
+
+            case "Search:Keyword":
+                storeSearchQuery(message.getString("query"));
+                recordSearch(GeckoSharedPrefs.forProfile(this), message.getString("identifier"),
+                        TelemetryContract.Method.ACTIONBAR);
+                break;
+
             case "Prompt:ShowTop":
                 // Bring this activity to front so the prompt is visible..
                 Intent bringToFrontIntent = new Intent();
                 bringToFrontIntent.setClassName(AppConstants.ANDROID_PACKAGE_NAME,
                                                 AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
                 bringToFrontIntent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
                 startActivity(bringToFrontIntent);
                 break;
-        }
-    }
-
-    @Override
-    public void handleMessage(final String event, final NativeJSObject message,
-                              final EventCallback callback) {
-        switch (event) {
+
+            case "Tab:Added":
+                if (message.getBoolean("cancelEditMode")) {
+                    // Set the target tab to null so it does not get selected (on editing
+                    // mode exit) in lieu of the tab that we're going to open and select.
+                    mTargetTabForEditingMode = null;
+                    mBrowserToolbar.cancelEdit();
+                }
+                break;
+
+            case "Video:Play":
+                if (SwitchBoard.isInExperiment(this, Experiments.HLS_VIDEO_PLAYBACK)) {
+                    mVideoPlayer.start(Uri.parse(message.getString("uri")));
+                    Telemetry.sendUIEvent(TelemetryContract.Event.SHOW,
+                                          TelemetryContract.Method.CONTENT, "playhls");
+                }
+                break;
+
             case "CharEncoding:Data":
-                final NativeJSObject[] charsets = message.getObjectArray("charsets");
+                final GeckoBundle[] charsets = message.getBundleArray("charsets");
                 final int selected = message.getInt("selected");
 
                 final String[] titleArray = new String[charsets.length];
                 final String[] codeArray = new String[charsets.length];
                 for (int i = 0; i < charsets.length; i++) {
-                    final NativeJSObject charset = charsets[i];
+                    final GeckoBundle charset = charsets[i];
                     titleArray[i] = charset.getString("title");
                     codeArray[i] = charset.getString("code");
                 }
 
                 final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
                 dialogBuilder.setSingleChoiceItems(titleArray, selected,
                         new AlertDialog.OnClickListener() {
                             @Override
@@ -1745,197 +1851,183 @@ public class BrowserApp extends GeckoApp
                         });
                 dialogBuilder.setNegativeButton(R.string.button_cancel,
                         new AlertDialog.OnClickListener() {
                             @Override
                             public void onClick(final DialogInterface dialog, final int which) {
                                 dialog.dismiss();
                             }
                         });
-                ThreadUtils.postToUiThread(new Runnable() {
-                    @Override
-                    public void run() {
-                        dialogBuilder.show();
-                    }
-                });
+                dialogBuilder.show();
                 break;
 
             case "CharEncoding:State":
-                final boolean visible = message.getString("visible").equals("true");
+                final boolean visible = "true".equals(message.getString("visible"));
                 GeckoPreferences.setCharEncodingState(visible);
-                final Menu menu = mMenu;
-                ThreadUtils.postToUiThread(new Runnable() {
-                    @Override
-                    public void run() {
-                        if (menu != null) {
-                            menu.findItem(R.id.char_encoding).setVisible(visible);
-                        }
-                    }
-                });
+                if (mMenu != null) {
+                    mMenu.findItem(R.id.char_encoding).setVisible(visible);
+                }
                 break;
 
             case "Experiments:GetActive":
                 final List<String> experiments = SwitchBoard.getActiveExperiments(this);
                 final JSONArray json = new JSONArray(experiments);
                 callback.sendSuccess(json.toString());
                 break;
 
             case "Experiments:SetOverride":
-                Experiments.setOverride(getContext(), message.getString("name"), message.getBoolean("isEnabled"));
+                Experiments.setOverride(getContext(), message.getString("name"),
+                                        message.getBoolean("isEnabled"));
                 break;
 
             case "Experiments:ClearOverride":
                 Experiments.clearOverride(getContext(), message.getString("name"));
                 break;
 
             case "Favicon:CacheLoad":
                 final String url = message.getString("url");
                 getFaviconFromCache(callback, url);
                 break;
 
             case "Feedback:MaybeLater":
-                resetFeedbackLaunchCount();
-                break;
-
-            case "Menu:Add":
-                final MenuItemInfo info = new MenuItemInfo();
-                info.label = message.getString("name");
-                info.id = message.getInt("id") + ADDON_MENU_OFFSET;
-                info.checked = message.optBoolean("checked", false);
-                info.enabled = message.optBoolean("enabled", true);
-                info.visible = message.optBoolean("visible", true);
-                info.checkable = message.optBoolean("checkable", false);
-                final int parent = message.optInt("parent", 0);
-                info.parent = parent <= 0 ? parent : parent + ADDON_MENU_OFFSET;
-                final MenuItemInfo menuItemInfo = info;
-                ThreadUtils.postToUiThread(new Runnable() {
-                    @Override
-                    public void run() {
-                        addAddonMenuItem(menuItemInfo);
-                    }
-                });
-                break;
-
-            case "Menu:Remove":
-                final int id = message.getInt("id") + ADDON_MENU_OFFSET;
-                ThreadUtils.postToUiThread(new Runnable() {
-                    @Override
-                    public void run() {
-                        removeAddonMenuItem(id);
-                    }
-                });
+                SharedPreferences settings = getPreferences(Activity.MODE_PRIVATE);
+                settings.edit().putInt(getPackageName() + ".feedback_launch_count", 0).apply();
                 break;
 
             case "Sanitize:ClearHistory":
-                handleClearHistory(message.optBoolean("clearSearchHistory", false));
-                callback.sendSuccess(true);
+                BrowserDB.from(getProfile()).clearHistory(
+                        getContentResolver(), message.getBoolean("clearSearchHistory", false));
+                callback.sendSuccess(null);
                 break;
 
             case "Sanitize:ClearSyncedTabs":
-                handleClearSyncedTabs();
-                callback.sendSuccess(true);
+                FennecTabsRepository.deleteNonLocalClientsAndTabs(getContext());
+                callback.sendSuccess(null);
                 break;
 
             case "Settings:Show":
-                final String resource =
-                        message.optString(GeckoPreferences.INTENT_EXTRA_RESOURCES, null);
                 final Intent settingsIntent = new Intent(this, GeckoPreferences.class);
+                final String resource = message.getString(GeckoPreferences.INTENT_EXTRA_RESOURCES);
+
                 GeckoPreferences.setResourceToOpen(settingsIntent, resource);
                 startActivityForResult(settingsIntent, ACTIVITY_REQUEST_PREFERENCES);
 
                 // Don't use a transition to settings if we're on a device where that
                 // would look bad.
                 if (HardwareUtils.IS_KINDLE_DEVICE) {
                     overridePendingTransition(0, 0);
                 }
                 break;
 
             case "Telemetry:Gather":
                 final BrowserDB db = BrowserDB.from(getProfile());
                 final ContentResolver cr = getContentResolver();
+
                 Telemetry.addToHistogram("PLACES_PAGES_COUNT", db.getCount(cr, "history"));
                 Telemetry.addToHistogram("FENNEC_BOOKMARKS_COUNT", db.getCount(cr, "bookmarks"));
-                Telemetry.addToHistogram("BROWSER_IS_USER_DEFAULT", (isDefaultBrowser(Intent.ACTION_VIEW) ? 1 : 0));
-                Telemetry.addToHistogram("FENNEC_CUSTOM_HOMEPAGE", (TextUtils.isEmpty(getHomepage()) ? 0 : 1));
+                Telemetry.addToHistogram("BROWSER_IS_USER_DEFAULT",
+                        (isDefaultBrowser(Intent.ACTION_VIEW) ? 1 : 0));
+                Telemetry.addToHistogram("FENNEC_CUSTOM_HOMEPAGE",
+                        (TextUtils.isEmpty(getHomepage()) ? 0 : 1));
+
                 final SharedPreferences prefs = GeckoSharedPrefs.forProfile(getContext());
                 final boolean hasCustomHomepanels =
-                        prefs.contains(HomeConfigPrefsBackend.PREFS_CONFIG_KEY) || prefs.contains(HomeConfigPrefsBackend.PREFS_CONFIG_KEY_OLD);
+                        prefs.contains(HomeConfigPrefsBackend.PREFS_CONFIG_KEY) ||
+                        prefs.contains(HomeConfigPrefsBackend.PREFS_CONFIG_KEY_OLD);
+
                 Telemetry.addToHistogram("FENNEC_HOMEPANELS_CUSTOM", hasCustomHomepanels ? 1 : 0);
 
                 Telemetry.addToHistogram("FENNEC_READER_VIEW_CACHE_SIZE",
-                        SavedReaderViewHelper.getSavedReaderViewHelper(getContext()).getDiskSpacedUsedKB());
+                        SavedReaderViewHelper.getSavedReaderViewHelper(getContext())
+                                             .getDiskSpacedUsedKB());
 
                 if (Versions.feature16Plus) {
-                    Telemetry.addToHistogram("BROWSER_IS_ASSIST_DEFAULT", (isDefaultBrowser(Intent.ACTION_ASSIST) ? 1 : 0));
+                    Telemetry.addToHistogram("BROWSER_IS_ASSIST_DEFAULT",
+                            (isDefaultBrowser(Intent.ACTION_ASSIST) ? 1 : 0));
                 }
                 break;
 
             case "Updater:Launch":
-                handleUpdaterLaunch();
+                /**
+                 * Launch UI that lets the user update Firefox.
+                 *
+                 * This depends on the current channel: Release and Beta both direct to
+                 * the Google Play Store. If updating is enabled, Aurora, Nightly, and
+                 * custom builds open about:, which provides an update interface.
+                 *
+                 * If updating is not enabled, this simply logs an error.
+                 */
+                if (AppConstants.RELEASE_OR_BETA) {
+                    Intent intent = new Intent(Intent.ACTION_VIEW);
+                    intent.setData(Uri.parse("market://details?id=" + getPackageName()));
+                    startActivity(intent);
+                    break;
+                }
+
+                if (AppConstants.MOZ_UPDATER) {
+                    Tabs.getInstance().loadUrlInTab(AboutPages.UPDATER);
+                    break;
+                }
+
+                Log.w(LOGTAG, "No candidate updater found; ignoring launch request.");
                 break;
 
             case "Download:AndroidDownloadManager":
                 // Downloading via Android's download manager
-
                 final String uri = message.getString("uri");
                 final String filename = message.getString("filename");
                 final String mimeType = message.getString("mimeType");
 
                 final DownloadManager.Request request = new DownloadManager.Request(Uri.parse(uri));
                 request.setMimeType(mimeType);
 
                 try {
-                    request.setDestinationInExternalFilesDir(this, Environment.DIRECTORY_DOWNLOADS, filename);
+                    request.setDestinationInExternalFilesDir(
+                            this, Environment.DIRECTORY_DOWNLOADS, filename);
                 } catch (IllegalStateException e) {
                     Log.e(LOGTAG, "Cannot create download directory");
-                    return;
+                    break;
                 }
 
                 request.allowScanningByMediaScanner();
-                request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
+                request.setNotificationVisibility(
+                        DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
                 request.addRequestHeader("User-Agent", HardwareUtils.isTablet() ?
                         AppConstants.USER_AGENT_FENNEC_TABLET :
                         AppConstants.USER_AGENT_FENNEC_MOBILE);
 
                 try {
-                    DownloadManager manager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
+                    DownloadManager manager = (DownloadManager)
+                            getSystemService(Context.DOWNLOAD_SERVICE);
                     manager.enqueue(request);
-
-                    Log.d(LOGTAG, "Enqueued download (Download Manager)");
                 } catch (RuntimeException e) {
                     Log.e(LOGTAG, "Download failed: " + e);
                 }
                 break;
 
             case "Website:Metadata":
-                final NativeJSObject metadata = message.getObject("metadata");
                 final String location = message.getString("location");
-
-                final boolean hasImage = !TextUtils.isEmpty(metadata.optString("image_url", null));
-                final String metadataJSON = metadata.toString();
-
-                ThreadUtils.postToBackgroundThread(new Runnable() {
-                    @Override
-                    public void run() {
-                        final ContentProviderClient contentProviderClient = getContentResolver()
-                                .acquireContentProviderClient(BrowserContract.PageMetadata.CONTENT_URI);
-                        if (contentProviderClient == null) {
-                            Log.w(LOGTAG, "Failed to obtain content provider client for: " + BrowserContract.PageMetadata.CONTENT_URI);
-                            return;
-                        }
-                        try {
-                            GlobalPageMetadata.getInstance().add(
-                                    BrowserDB.from(getProfile()),
-                                    contentProviderClient,
-                                    location, hasImage, metadataJSON);
-                        } finally {
-                            contentProviderClient.release();
-                        }
-                    }
-                });
+                final boolean hasImage = message.getBoolean("hasImage");
+                final String metadata = message.getString("metadata");
+
+                final ContentProviderClient contentProviderClient = getContentResolver()
+                        .acquireContentProviderClient(BrowserContract.PageMetadata.CONTENT_URI);
+                if (contentProviderClient == null) {
+                    Log.w(LOGTAG, "Failed to obtain content provider client for: " +
+                                  BrowserContract.PageMetadata.CONTENT_URI);
+                    return;
+                }
+                try {
+                    GlobalPageMetadata.getInstance().add(
+                            BrowserDB.from(getProfile()),
+                            contentProviderClient,
+                            location, hasImage, metadata);
+                } finally {
+                    contentProviderClient.release();
+                }
 
                 break;
 
             default:
                 super.handleMessage(event, message, callback);
                 break;
         }
     }
@@ -1990,174 +2082,16 @@ public class BrowserApp extends GeckoApp
             return false;
         }
 
         final String packageName = info.activityInfo.packageName;
         return (TextUtils.equals(packageName, getPackageName()));
     }
 
     @Override
-    public void handleMessage(String event, JSONObject message) {
-        try {
-            switch (event) {
-                case "Menu:Open":
-                    if (mBrowserToolbar.isEditing()) {
-                        mBrowserToolbar.cancelEdit();
-                    }
-
-                    openOptionsMenu();
-                    break;
-
-                case "Menu:Update":
-                    final int id = message.getInt("id") + ADDON_MENU_OFFSET;
-                    final JSONObject options = message.getJSONObject("options");
-                    ThreadUtils.postToUiThread(new Runnable() {
-                        @Override
-                        public void run() {
-                            updateAddonMenuItem(id, options);
-                        }
-                    });
-                    break;
-
-                case "Gecko:DelayedStartup":
-                    ThreadUtils.postToUiThread(new Runnable() {
-                        @Override
-                        public void run() {
-                            // Force tabs panel inflation once the initial
-                            // pageload is finished.
-                            ensureTabsPanelExists();
-                            if (AppConstants.NIGHTLY_BUILD && mZoomedView == null) {
-                                ViewStub stub = (ViewStub) findViewById(R.id.zoomed_view_stub);
-                                mZoomedView = (ZoomedView) stub.inflate();
-                            }
-                        }
-                    });
-
-                    if (AppConstants.MOZ_MEDIA_PLAYER) {
-                        // Check if the fragment is already added. This should never be true here, but this is
-                        // a nice safety check.
-                        // If casting is disabled, these classes aren't built. We use reflection to initialize them.
-                        final Class<?> mediaManagerClass = getMediaPlayerManager();
-
-                        if (mediaManagerClass != null) {
-                            try {
-                                final String tag = "";
-                                mediaManagerClass.getDeclaredField("MEDIA_PLAYER_TAG").get(tag);
-                                Log.i(LOGTAG, "Found tag " + tag);
-                                final Fragment frag = getSupportFragmentManager().findFragmentByTag(tag);
-                                if (frag == null) {
-                                    final Method getInstance = mediaManagerClass.getMethod("getInstance", (Class[]) null);
-                                    final Fragment mpm = (Fragment) getInstance.invoke(null);
-                                    getSupportFragmentManager().beginTransaction().disallowAddToBackStack().add(mpm, tag).commit();
-                                }
-                            } catch (Exception ex) {
-                                Log.e(LOGTAG, "Error initializing media manager", ex);
-                            }
-                        }
-                    }
-
-                    if (AppConstants.MOZ_STUMBLER_BUILD_TIME_ENABLED && Restrictions.isAllowed(this, Restrictable.DATA_CHOICES)) {
-                        // Start (this acts as ping if started already) the stumbler lib; if the stumbler has queued data it will upload it.
-                        // Stumbler operates on its own thread, and startup impact is further minimized by delaying work (such as upload) a few seconds.
-                        // Avoid any potential startup CPU/thread contention by delaying the pref broadcast.
-                        final long oneSecondInMillis = 1000;
-                        ThreadUtils.getBackgroundHandler().postDelayed(new Runnable() {
-                            @Override
-                            public void run() {
-                                GeckoPreferences.broadcastStumblerPref(BrowserApp.this);
-                            }
-                        }, oneSecondInMillis);
-                    }
-
-                    if (AppConstants.MOZ_ANDROID_DOWNLOAD_CONTENT_SERVICE) {
-                        // TODO: Better scheduling of sync action (Bug 1257492)
-                        DownloadContentService.startSync(this);
-
-                        DownloadContentService.startVerification(this);
-                    }
-
-                    FeedService.setup(this);
-
-                    super.handleMessage(event, message);
-                    break;
-
-                case "Gecko:Ready":
-                    // Handle this message in GeckoApp, but also enable the Settings
-                    // menuitem, which is specific to BrowserApp.
-                    super.handleMessage(event, message);
-                    final Menu menu = mMenu;
-                    ThreadUtils.postToUiThread(new Runnable() {
-                        @Override
-                        public void run() {
-                            if (menu != null) {
-                                menu.findItem(R.id.settings).setEnabled(true);
-                                menu.findItem(R.id.help).setEnabled(true);
-                            }
-                        }
-                    });
-
-                    // Display notification for Mozilla data reporting, if data should be collected.
-                    if (AppConstants.MOZ_DATA_REPORTING && Restrictions.isAllowed(this, Restrictable.DATA_CHOICES)) {
-                        DataReportingNotification.checkAndNotifyPolicy(GeckoAppShell.getContext());
-                    }
-                    break;
-
-                case "Search:Keyword":
-                    storeSearchQuery(message.getString("query"));
-                    recordSearch(GeckoSharedPrefs.forProfile(this), message.getString("identifier"),
-                            TelemetryContract.Method.ACTIONBAR);
-                    break;
-
-                case "LightweightTheme:Update":
-                    ThreadUtils.postToUiThread(new Runnable() {
-                        @Override
-                        public void run() {
-                            mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
-                        }
-                    });
-                    break;
-
-                case "Video:Play":
-                    if (SwitchBoard.isInExperiment(this, Experiments.HLS_VIDEO_PLAYBACK)) {
-                        final String uri = message.getString("uri");
-                        final String uuid = message.getString("uuid");
-                        ThreadUtils.postToUiThread(new Runnable() {
-                            @Override
-                            public void run() {
-                                mVideoPlayer.start(Uri.parse(uri));
-                                Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.CONTENT, "playhls");
-                            }
-                        });
-                    }
-                    break;
-
-                case "Tab:Added":
-                    if (message.getBoolean("cancelEditMode")) {
-                        ThreadUtils.postToUiThread(new Runnable() {
-                            @Override
-                            public void run() {
-                                // Set the target tab to null so it does not get selected (on editing
-                                // mode exit) in lieu of the tab that we're going to open and select.
-                                mTargetTabForEditingMode = null;
-                                mBrowserToolbar.cancelEdit();
-                            }
-                        });
-                    }
-                    break;
-
-                default:
-                    super.handleMessage(event, message);
-                    break;
-            }
-        } catch (Exception e) {
-            Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
-        }
-    }
-
-    @Override
     public void addTab() {
         Tabs.getInstance().addTab();
     }
 
     @Override
     public void addPrivateTab() {
         Tabs.getInstance().addPrivateTab();
     }
@@ -3243,43 +3177,43 @@ public class BrowserApp extends GeckoApp
         if (mMenu == null)
             return;
 
         final MenuItem menuItem = mMenu.findItem(id);
         if (menuItem != null)
             mMenu.removeItem(id);
     }
 
-    private void updateAddonMenuItem(int id, JSONObject options) {
+    private void updateAddonMenuItem(int id, final GeckoBundle options) {
         // Set attribute for the menu item in cache, if available
         if (mAddonMenuItemsCache != null && !mAddonMenuItemsCache.isEmpty()) {
             for (MenuItemInfo item : mAddonMenuItemsCache) {
                 if (item.id == id) {
-                    item.label = options.optString("name", item.label);
-                    item.checkable = options.optBoolean("checkable", item.checkable);
-                    item.checked = options.optBoolean("checked", item.checked);
-                    item.enabled = options.optBoolean("enabled", item.enabled);
-                    item.visible = options.optBoolean("visible", item.visible);
+                    item.label = options.getString("name", item.label);
+                    item.checkable = options.getBoolean("checkable", item.checkable);
+                    item.checked = options.getBoolean("checked", item.checked);
+                    item.enabled = options.getBoolean("enabled", item.enabled);
+                    item.visible = options.getBoolean("visible", item.visible);
                     item.added = (mMenu != null);
                     break;
                 }
             }
         }
 
         if (mMenu == null) {
             return;
         }
 
         final MenuItem menuItem = mMenu.findItem(id);
         if (menuItem != null) {
-            menuItem.setTitle(options.optString("name", menuItem.getTitle().toString()));
-            menuItem.setCheckable(options.optBoolean("checkable", menuItem.isCheckable()));
-            menuItem.setChecked(options.optBoolean("checked", menuItem.isChecked()));
-            menuItem.setEnabled(options.optBoolean("enabled", menuItem.isEnabled()));
-            menuItem.setVisible(options.optBoolean("visible", menuItem.isVisible()));
+            menuItem.setTitle(options.getString("name", menuItem.getTitle().toString()));
+            menuItem.setCheckable(options.getBoolean("checkable", menuItem.isCheckable()));
+            menuItem.setChecked(options.getBoolean("checked", menuItem.isChecked()));
+            menuItem.setEnabled(options.getBoolean("enabled", menuItem.isEnabled()));
+            menuItem.setVisible(options.getBoolean("visible", menuItem.isVisible()));
         }
     }
 
     @Override
     public boolean onCreateOptionsMenu(Menu menu) {
         // Sets mMenu = menu.
         super.onCreateOptionsMenu(menu);
 
@@ -4024,21 +3958,16 @@ public class BrowserApp extends GeckoApp
                                                      && TabQueueHelper.shouldShowTabQueuePrompt(BrowserApp.this)) {
                     Intent promptIntent = new Intent(BrowserApp.this, TabQueuePrompt.class);
                     startActivityForResult(promptIntent, ACTIVITY_REQUEST_TAB_QUEUE);
                 }
             }
         });
     }
 
-    private void resetFeedbackLaunchCount() {
-        SharedPreferences settings = getPreferences(Activity.MODE_PRIVATE);
-        settings.edit().putInt(getPackageName() + ".feedback_launch_count", 0).apply();
-    }
-
     // HomePager.OnUrlOpenListener
     @Override
     public void onUrlOpen(String url, EnumSet<OnUrlOpenListener.Flags> flags) {
         if (flags.contains(OnUrlOpenListener.Flags.OPEN_WITH_INTENT)) {
             Intent intent = new Intent(Intent.ACTION_VIEW);
             intent.setData(Uri.parse(url));
             startActivity(intent);
         } else {
@@ -4142,44 +4071,16 @@ public class BrowserApp extends GeckoApp
     }
 
     // For use from tests only.
     @RobocopTarget
     public ReadingListHelper getReadingListHelper() {
         return mReadingListHelper;
     }
 
-    /**
-     * Launch UI that lets the user update Firefox.
-     *
-     * This depends on the current channel: Release and Beta both direct to the
-     * Google Play Store.  If updating is enabled, Aurora, Nightly, and custom
-     * builds open about:, which provides an update interface.
-     *
-     * If updating is not enabled, this simply logs an error.
-     *
-     * @return true if update UI was launched.
-     */
-    protected boolean handleUpdaterLaunch() {
-        if (AppConstants.RELEASE_OR_BETA) {
-            Intent intent = new Intent(Intent.ACTION_VIEW);
-            intent.setData(Uri.parse("market://details?id=" + getPackageName()));
-            startActivity(intent);
-            return true;
-        }
-
-        if (AppConstants.MOZ_UPDATER) {
-            Tabs.getInstance().loadUrlInTab(AboutPages.UPDATER);
-            return true;
-        }
-
-        Log.w(LOGTAG, "No candidate updater found; ignoring launch request.");
-        return false;
-    }
-
     /* Implementing ActionModeCompat.Presenter */
     @Override
     public void startActionModeCompat(final ActionModeCompat.Callback callback) {
         // If actionMode is null, we're not currently showing one. Flip to the action mode view
         if (mActionMode == null) {
             mActionBarFlipper.showNext();
             DynamicToolbarAnimator toolbar = mLayerView.getDynamicToolbarAnimator();
 
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
@@ -34,22 +34,22 @@ import org.mozilla.gecko.preferences.Gec
 import org.mozilla.gecko.prompts.PromptService;
 import org.mozilla.gecko.restrictions.Restrictions;
 import org.mozilla.gecko.tabqueue.TabQueueHelper;
 import org.mozilla.gecko.text.FloatingToolbarTextSelection;
 import org.mozilla.gecko.text.TextSelection;
 import org.mozilla.gecko.updater.UpdateServiceHelper;
 import org.mozilla.gecko.util.ActivityResultHandler;
 import org.mozilla.gecko.util.ActivityUtils;
+import org.mozilla.gecko.util.BundleEventListener;
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.FileUtils;
-import org.mozilla.gecko.util.GeckoEventListener;
+import org.mozilla.gecko.util.GeckoBundle;
 import org.mozilla.gecko.util.GeckoRequest;
 import org.mozilla.gecko.util.HardwareUtils;
-import org.mozilla.gecko.util.NativeEventListener;
 import org.mozilla.gecko.util.NativeJSObject;
 import org.mozilla.gecko.util.PrefUtils;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import android.annotation.SuppressLint;
 import android.annotation.TargetApi;
 import android.app.Activity;
 import android.app.AlertDialog;
@@ -125,22 +125,21 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
 public abstract class GeckoApp
     extends GeckoActivity
     implements
+    BundleEventListener,
     ContextGetter,
     GeckoAppShell.GeckoInterface,
-    GeckoEventListener,
     GeckoMenu.Callback,
     GeckoMenu.MenuPresenter,
-    NativeEventListener,
     Tabs.OnTabsChangedListener,
     ViewTreeObserver.OnGlobalLayoutListener {
 
     private static final String LOGTAG = "GeckoApp";
     private static final long ONE_DAY_MS = TimeUnit.MILLISECONDS.convert(1, TimeUnit.DAYS);
 
     public static final String ACTION_ALERT_CALLBACK       = "org.mozilla.gecko.ALERT_CALLBACK";
     public static final String ACTION_HOMESCREEN_SHORTCUT  = "org.mozilla.gecko.BOOKMARK";
@@ -611,46 +610,62 @@ public abstract class GeckoApp
      * @return True if the tab UI was hidden.
      */
     public boolean autoHideTabs() { return false; }
 
     @Override
     public boolean areTabsShown() { return false; }
 
     @Override
-    public void handleMessage(final String event, final NativeJSObject message,
+    public void handleMessage(final String event, final GeckoBundle message,
                               final EventCallback callback) {
-        if ("Accessibility:Ready".equals(event)) {
+        if (event.equals("Gecko:Ready")) {
+            mGeckoReadyStartupTimer.stop();
+            geckoConnected();
+
+            // This method is already running on the background thread, so we
+            // know that mHealthRecorder will exist. That doesn't stop us being
+            // paranoid.
+            // This method is cheap, so don't spawn a new runnable.
+            final HealthRecorder rec = mHealthRecorder;
+            if (rec != null) {
+              rec.recordGeckoStartupTime(mGeckoReadyStartupTimer.getElapsed());
+            }
+
+            GeckoApplication.get().onDelayedStartup();
+
+        } else if (event.equals("Gecko:Exited")) {
+            // Gecko thread exited first; let GeckoApp die too.
+            doShutdown();
+
+        } else if ("Accessibility:Ready".equals(event)) {
             GeckoAccessibility.updateAccessibilitySettings(this);
 
+        } else if ("Accessibility:Event".equals(event)) {
+            GeckoAccessibility.sendAccessibilityEvent(message);
+
         } else if ("Bookmark:Insert".equals(event)) {
-            final String url = message.getString("url");
-            final String title = message.getString("title");
-            final Context context = this;
             final BrowserDB db = BrowserDB.from(getProfile());
-            ThreadUtils.postToBackgroundThread(new Runnable() {
+            final boolean bookmarkAdded = db.addBookmark(
+                    getContentResolver(), message.getString("title"), message.getString("url"));
+            final int resId = bookmarkAdded ? R.string.bookmark_added
+                                            : R.string.bookmark_already_added;
+            ThreadUtils.postToUiThread(new Runnable() {
                 @Override
                 public void run() {
-                    final boolean bookmarkAdded = db.addBookmark(getContentResolver(), title, url);
-                    final int resId = bookmarkAdded ? R.string.bookmark_added : R.string.bookmark_already_added;
-                    ThreadUtils.postToUiThread(new Runnable() {
-                        @Override
-                        public void run() {
-                            SnackbarBuilder.builder(GeckoApp.this)
-                                    .message(resId)
-                                    .duration(Snackbar.LENGTH_LONG)
-                                    .buildAndShow();
-                        }
-                    });
+                    SnackbarBuilder.builder(GeckoApp.this)
+                            .message(resId)
+                            .duration(Snackbar.LENGTH_LONG)
+                            .buildAndShow();
                 }
             });
 
         } else if ("Contact:Add".equals(event)) {
-            final String email = message.optString("email", null);
-            final String phone = message.optString("phone", null);
+            final String email = message.getString("email", null);
+            final String phone = message.getString("phone", null);
             if (email != null) {
                 Uri contactUri = Uri.parse(email);
                 Intent i = new Intent(ContactsContract.Intents.SHOW_OR_CREATE_CONTACT, contactUri);
                 startActivity(i);
             } else if (phone != null) {
                 Uri contactUri = Uri.parse(phone);
                 Intent i = new Intent(ContactsContract.Intents.SHOW_OR_CREATE_CONTACT, contactUri);
                 startActivity(i);
@@ -680,24 +695,42 @@ public abstract class GeckoApp
         } else if ("Image:SetAs".equals(event)) {
             String src = message.getString("url");
             setImageAs(src);
 
         } else if ("Locale:Set".equals(event)) {
             setLocale(message.getString("locale"));
 
         } else if ("Permissions:Data".equals(event)) {
-            final NativeJSObject[] permissions = message.getObjectArray("permissions");
+            final GeckoBundle[] permissions = message.getBundleArray("permissions");
             showSiteSettingsDialog(permissions);
 
         } else if ("PrivateBrowsing:Data".equals(event)) {
-            mPrivateBrowsingSession = message.optString("session", null);
-
-        } else if ("Session:StatePurged".equals(event)) {
-            onStatePurged();
+            mPrivateBrowsingSession = message.getString("session", null);
+
+        } else if ("RuntimePermissions:Prompt".equals(event)) {
+            String[] permissions = message.getStringArray("permissions");
+            if (callback == null || permissions == null) {
+                return;
+            }
+
+            Permissions.from(this)
+                       .withPermissions(permissions)
+                       .andFallback(new Runnable() {
+                           @Override
+                           public void run() {
+                               callback.sendSuccess(false);
+                           }
+                       })
+                       .run(new Runnable() {
+                           @Override
+                           public void run() {
+                               callback.sendSuccess(true);
+                           }
+                       });
 
         } else if ("Share:Text".equals(event)) {
             final String text = message.getString("text");
             final Tab tab = Tabs.getInstance().getSelectedTab();
             String title = "";
             if (tab != null) {
                 title = tab.getDisplayTitle();
             }
@@ -708,101 +741,54 @@ public abstract class GeckoApp
 
         } else if ("Snackbar:Show".equals(event)) {
             SnackbarBuilder.builder(this)
                     .fromEvent(message)
                     .callback(callback)
                     .buildAndShow();
 
         } else if ("SystemUI:Visibility".equals(event)) {
-            setSystemUiVisible(message.getBoolean("visible"));
+            if (message.getBoolean("visible", true)) {
+                mMainLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
+            } else {
+                mMainLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE);
+            }
 
         } else if ("ToggleChrome:Focus".equals(event)) {
             focusChrome();
 
         } else if ("ToggleChrome:Hide".equals(event)) {
             toggleChrome(false);
 
         } else if ("ToggleChrome:Show".equals(event)) {
             toggleChrome(true);
 
         } else if ("Update:Check".equals(event)) {
             UpdateServiceHelper.checkForUpdate(this);
+
         } else if ("Update:Download".equals(event)) {
             UpdateServiceHelper.downloadUpdate(this);
+
         } else if ("Update:Install".equals(event)) {
             UpdateServiceHelper.applyUpdate(this);
-        } else if ("RuntimePermissions:Prompt".equals(event)) {
-            String[] permissions = message.getStringArray("permissions");
-            if (callback == null || permissions == null) {
-                return;
-            }
-
-            Permissions.from(this)
-                       .withPermissions(permissions)
-                       .andFallback(new Runnable() {
-                           @Override
-                           public void run() {
-                               callback.sendSuccess(false);
-                           }
-                       })
-                       .run(new Runnable() {
-                           @Override
-                           public void run() {
-                               callback.sendSuccess(true);
-                           }
-                       });
         }
     }
 
-    @Override
-    public void handleMessage(String event, JSONObject message) {
-        try {
-            if (event.equals("Gecko:Ready")) {
-                mGeckoReadyStartupTimer.stop();
-                geckoConnected();
-
-                // This method is already running on the background thread, so we
-                // know that mHealthRecorder will exist. That doesn't stop us being
-                // paranoid.
-                // This method is cheap, so don't spawn a new runnable.
-                final HealthRecorder rec = mHealthRecorder;
-                if (rec != null) {
-                  rec.recordGeckoStartupTime(mGeckoReadyStartupTimer.getElapsed());
-                }
-
-                GeckoApplication.get().onDelayedStartup();
-
-            } else if (event.equals("Gecko:Exited")) {
-                // Gecko thread exited first; let GeckoApp die too.
-                doShutdown();
-                return;
-
-            } else if (event.equals("Accessibility:Event")) {
-                GeckoAccessibility.sendAccessibilityEvent(message);
-            }
-        } catch (Exception e) {
-            Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
-        }
-    }
-
-    void onStatePurged() { }
-
     /**
      * @param permissions
      *        Array of JSON objects to represent site permissions.
      *        Example: { type: "offline-app", setting: "Store Offline Data", value: "Allow" }
      */
-    private void showSiteSettingsDialog(final NativeJSObject[] permissions) {
+    private void showSiteSettingsDialog(final GeckoBundle[] permissions) {
         final AlertDialog.Builder builder = new AlertDialog.Builder(this);
         builder.setTitle(R.string.site_settings_title);
 
         final ArrayList<HashMap<String, String>> itemList =
                 new ArrayList<HashMap<String, String>>();
-        for (final NativeJSObject permObj : permissions) {
+        for (final GeckoBundle permObj : permissions) {
             final HashMap<String, String> map = new HashMap<String, String>();
             map.put("setting", permObj.getString("setting"));
             map.put("value", permObj.getString("value"));
             itemList.add(map);
         }
 
         // setMultiChoiceItems doesn't support using an adapter, so we're creating a hack with
         // setSingleChoiceItems and changing the choiceMode below when we create the dialog
@@ -835,46 +821,39 @@ public abstract class GeckoApp
 
         builder.setNegativeButton(R.string.site_settings_cancel, new DialogInterface.OnClickListener() {
             @Override
             public void onClick(DialogInterface dialog, int id) {
                 dialog.cancel();
             }
         });
 
-        ThreadUtils.postToUiThread(new Runnable() {
+        AlertDialog dialog = builder.create();
+        dialog.show();
+
+        final ListView listView = dialog.getListView();
+        if (listView != null) {
+            listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
+        }
+
+        final Button clearButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
+        clearButton.setEnabled(false);
+
+        dialog.getListView().setOnItemClickListener(new AdapterView.OnItemClickListener() {
             @Override
-            public void run() {
-                AlertDialog dialog = builder.create();
-                dialog.show();
-
-                final ListView listView = dialog.getListView();
-                if (listView != null) {
-                    listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
+            public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
+                if (listView.getCheckedItemCount() == 0) {
+                    clearButton.setEnabled(false);
+                } else {
+                    clearButton.setEnabled(true);
                 }
-
-                final Button clearButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
-                clearButton.setEnabled(false);
-
-                dialog.getListView().setOnItemClickListener(new AdapterView.OnItemClickListener() {
-                    @Override
-                    public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
-                        if (listView.getCheckedItemCount() == 0) {
-                            clearButton.setEnabled(false);
-                        } else {
-                            clearButton.setEnabled(true);
-                        }
-                    }
-                });
             }
         });
     }
 
-
-
     /* package */ void addFullScreenPluginView(View view) {
         if (mFullScreenPluginView != null) {
             Log.w(LOGTAG, "Already have a fullscreen plugin view");
             return;
         }
 
         setFullScreen(true);
 
@@ -953,16 +932,45 @@ public abstract class GeckoApp
                 @Override
                 public void run() {
                     removeFullScreenPluginView(view);
                 }
             });
         }
     }
 
+    private void showSetImageResult(final boolean success, final int message, final String path) {
+        ThreadUtils.postToUiThread(new Runnable() {
+            @Override
+            public void run() {
+                if (!success) {
+                    SnackbarBuilder.builder(GeckoApp.this)
+                            .message(message)
+                            .duration(Snackbar.LENGTH_LONG)
+                            .buildAndShow();
+                    return;
+                }
+
+                final Intent intent = new Intent(Intent.ACTION_ATTACH_DATA);
+                intent.addCategory(Intent.CATEGORY_DEFAULT);
+                intent.setData(Uri.parse(path));
+
+                // Removes the image from storage once the chooser activity ends.
+                Intent chooser = Intent.createChooser(intent, getString(message));
+                ActivityResultHandler handler = new ActivityResultHandler() {
+                    @Override
+                    public void onActivityResult (int resultCode, Intent data) {
+                        getContentResolver().delete(intent.getData(), null, null);
+                    }
+                };
+                ActivityHandlerHelper.startIntentForActivity(GeckoApp.this, chooser, handler);
+            }
+        });
+    }
+
     // This method starts downloading an image synchronously and displays the Chooser activity to set the image as wallpaper.
     private void setImageAs(final String aSrc) {
         boolean isDataURI = aSrc.startsWith("data:");
         Bitmap image = null;
         InputStream is = null;
         ByteArrayOutputStream os = null;
         try {
             if (isDataURI) {
@@ -985,48 +993,27 @@ public abstract class GeckoApp
                 byte[] imgBuffer = os.toByteArray();
                 image = BitmapUtils.decodeByteArray(imgBuffer);
             }
             if (image != null) {
                 // Some devices don't have a DCIM folder and the Media.insertImage call will fail.
                 File dcimDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
 
                 if (!dcimDir.mkdirs() && !dcimDir.isDirectory()) {
-                    SnackbarBuilder.builder(this)
-                            .message(R.string.set_image_path_fail)
-                            .duration(Snackbar.LENGTH_LONG)
-                            .buildAndShow();
+                    showSetImageResult(/* success */ false, R.string.set_image_path_fail, null);
                     return;
                 }
                 String path = Media.insertImage(getContentResolver(), image, null, null);
                 if (path == null) {
-                    SnackbarBuilder.builder(this)
-                            .message(R.string.set_image_path_fail)
-                            .duration(Snackbar.LENGTH_LONG)
-                            .buildAndShow();
+                    showSetImageResult(/* success */ false, R.string.set_image_path_fail, null);
                     return;
                 }
-                final Intent intent = new Intent(Intent.ACTION_ATTACH_DATA);
-                intent.addCategory(Intent.CATEGORY_DEFAULT);
-                intent.setData(Uri.parse(path));
-
-                // Removes the image from storage once the chooser activity ends.
-                Intent chooser = Intent.createChooser(intent, getString(R.string.set_image_chooser_title));
-                ActivityResultHandler handler = new ActivityResultHandler() {
-                    @Override
-                    public void onActivityResult (int resultCode, Intent data) {
-                        getContentResolver().delete(intent.getData(), null, null);
-                    }
-                };
-                ActivityHandlerHelper.startIntentForActivity(this, chooser, handler);
+                showSetImageResult(/* success */ true, R.string.set_image_chooser_title, path);
             } else {
-                SnackbarBuilder.builder(this)
-                        .message(R.string.set_image_fail)
-                        .duration(Snackbar.LENGTH_LONG)
-                        .buildAndShow();
+                showSetImageResult(/* success */ false, R.string.set_image_fail, null);
             }
         } catch (OutOfMemoryError ome) {
             Log.e(LOGTAG, "Out of Memory when converting to byte array", ome);
         } catch (IOException ioe) {
             Log.e(LOGTAG, "I/O Exception while setting wallpaper", ioe);
         } finally {
             if (is != null) {
                 try {
@@ -1188,25 +1175,22 @@ public abstract class GeckoApp
 
             final String uri = getURIFromIntent(intent);
             if (!TextUtils.isEmpty(uri)) {
                 // Start a speculative connection as soon as Gecko loads.
                 GeckoThread.speculativeConnect(uri);
             }
         }
 
-        // GeckoThread has to register for "Gecko:Ready" first, so GeckoApp registers
-        // for events after initializing GeckoThread but before launching it.
-
-        EventDispatcher.getInstance().registerGeckoThreadListener((GeckoEventListener)this,
+        // To prevent races, register startup events before launching the Gecko thread.
+        EventDispatcher.getInstance().registerGeckoThreadListener(this,
+            "Accessibility:Ready",
+            "Gecko:Exited",
             "Gecko:Ready",
-            "Gecko:Exited");
-
-        EventDispatcher.getInstance().registerGeckoThreadListener((NativeEventListener)this,
-            "Accessibility:Ready");
+            null);
 
         GeckoThread.launch();
 
         Bundle stateBundle = IntentUtils.getBundleExtraSafe(getIntent(), EXTRA_STATE_BUNDLE);
         if (stateBundle != null) {
             // Use the state bundle if it was given as an intent extra. This is
             // only intended to be used internally via Robocop, so a boolean
             // is read from a private shared pref to prevent other apps from
@@ -1228,40 +1212,43 @@ public abstract class GeckoApp
         setContentView(getLayout());
 
         // Set up Gecko layout.
         mRootLayout = (RelativeLayout) findViewById(R.id.root_layout);
         mGeckoLayout = (RelativeLayout) findViewById(R.id.gecko_layout);
         mMainLayout = (RelativeLayout) findViewById(R.id.main_layout);
         mLayerView = (GeckoView) findViewById(R.id.layer_view);
 
-        getAppEventDispatcher().registerGeckoThreadListener((GeckoEventListener)this,
-            "Accessibility:Event");
-
-        getAppEventDispatcher().registerGeckoThreadListener((NativeEventListener)this,
+        getAppEventDispatcher().registerGeckoThreadListener(this,
+            "Accessibility:Event",
+            "Locale:Set",
+            null);
+
+        getAppEventDispatcher().registerBackgroundThreadListener(this,
             "Bookmark:Insert",
+            "Image:SetAs",
+            null);
+
+        getAppEventDispatcher().registerUiThreadListener(this,
             "Contact:Add",
             "DevToolsAuth:Scan",
             "DOMFullScreen:Start",
             "DOMFullScreen:Stop",
-            "Image:SetAs",
-            "Locale:Set",
             "Permissions:Data",
             "PrivateBrowsing:Data",
             "RuntimePermissions:Prompt",
-            "Session:StatePurged",
             "Share:Text",
-            "Snackbar:Show",
             "SystemUI:Visibility",
             "ToggleChrome:Focus",
             "ToggleChrome:Hide",
             "ToggleChrome:Show",
             "Update:Check",
             "Update:Download",
-            "Update:Install");
+            "Update:Install",
+            null);
 
         Tabs.getInstance().attachToContext(this, mLayerView);
 
         // Use global layout state change to kick off additional initialization
         mMainLayout.getViewTreeObserver().addOnGlobalLayoutListener(this);
 
         if (Versions.preMarshmallow) {
             mTextSelection = new ActionBarTextSelection(this);
@@ -1678,19 +1665,19 @@ public abstract class GeckoApp
         // site loaded from the intent is on top (last loaded) and selected with all other tabs
         // being opened behind it. We process the tab queue first and request a callback from the JS - the
         // listener will open the url from the intent as normal when the tab queue has been processed.
         ThreadUtils.postToBackgroundThread(new Runnable() {
             @Override
             public void run() {
                 if (TabQueueHelper.TAB_QUEUE_ENABLED && TabQueueHelper.shouldOpenTabQueueUrls(GeckoApp.this)) {
 
-                    getAppEventDispatcher().registerGeckoThreadListener(new NativeEventListener() {
+                    getAppEventDispatcher().registerGeckoThreadListener(new BundleEventListener() {
                         @Override
-                        public void handleMessage(String event, NativeJSObject message, EventCallback callback) {
+                        public void handleMessage(String event, GeckoBundle message, EventCallback callback) {
                             if ("Tabs:TabsOpened".equals(event)) {
                                 getAppEventDispatcher().unregisterGeckoThreadListener(this, "Tabs:TabsOpened");
                                 openTabsRunnable.run();
                             }
                         }
                     }, "Tabs:TabsOpened");
                     TabQueueHelper.openQueuedUrls(GeckoApp.this, getProfile(), TabQueueHelper.FILE_NAME, true);
                 } else {
@@ -2078,16 +2065,19 @@ public abstract class GeckoApp
     }
 
     @Override
     public void onResume()
     {
         // After an onPause, the activity is back in the foreground.
         // Undo whatever we did in onPause.
         super.onResume();
+
+        EventDispatcher.getInstance().registerUiThreadListener(this, "Snackbar:Show");
+
         if (mIsAbortingAppLaunch) {
             return;
         }
 
         foregrounded = true;
 
         GeckoAppShell.setGeckoInterface(this);
 
@@ -2159,16 +2149,18 @@ public abstract class GeckoApp
             mLayerView.setFocusableInTouchMode(true);
             getWindow().setBackgroundDrawable(null);
         }
     }
 
     @Override
     public void onPause()
     {
+        EventDispatcher.getInstance().unregisterUiThreadListener(this, "Snackbar:Show");
+
         if (mIsAbortingAppLaunch) {
             super.onPause();
             return;
         }
 
         foregrounded = false;
 
         final Tab selectedTab = Tabs.getInstance().getSelectedTab();
@@ -2242,47 +2234,49 @@ public abstract class GeckoApp
     public void onDestroy() {
         if (mIsAbortingAppLaunch) {
             // This build does not support the Android version of the device:
             // We did not initialize anything, so skip cleaning up.
             super.onDestroy();
             return;
         }
 
-        EventDispatcher.getInstance().unregisterGeckoThreadListener((GeckoEventListener)this,
+        EventDispatcher.getInstance().unregisterGeckoThreadListener(this,
+            "Accessibility:Ready",
+            "Gecko:Exited",
             "Gecko:Ready",
-            "Gecko:Exited");
-
-        EventDispatcher.getInstance().unregisterGeckoThreadListener((NativeEventListener)this,
-            "Accessibility:Ready");
-
-        getAppEventDispatcher().unregisterGeckoThreadListener((GeckoEventListener)this,
-            "Accessibility:Event");
-
-        getAppEventDispatcher().unregisterGeckoThreadListener((NativeEventListener)this,
+            null);
+
+        getAppEventDispatcher().unregisterGeckoThreadListener(this,
+            "Accessibility:Event",
+            "Locale:Set",
+            null);
+
+        getAppEventDispatcher().unregisterBackgroundThreadListener(this,
             "Bookmark:Insert",
+            "Image:SetAs",
+            null);
+
+        getAppEventDispatcher().unregisterUiThreadListener(this,
             "Contact:Add",
             "DevToolsAuth:Scan",
             "DOMFullScreen:Start",
             "DOMFullScreen:Stop",
-            "Image:SetAs",
-            "Locale:Set",
             "Permissions:Data",
             "PrivateBrowsing:Data",
             "RuntimePermissions:Prompt",
-            "Session:StatePurged",
             "Share:Text",
-            "Snackbar:Show",
             "SystemUI:Visibility",
             "ToggleChrome:Focus",
             "ToggleChrome:Hide",
             "ToggleChrome:Show",
             "Update:Check",
             "Update:Download",
-            "Update:Install");
+            "Update:Install",
+            null);
 
         if (mPromptService != null)
             mPromptService.destroy();
 
         final HealthRecorder rec = mHealthRecorder;
         mHealthRecorder = null;
         if (rec != null && rec.isEnabled()) {
             // Closing a HealthRecorder could incur a write.
@@ -2794,29 +2788,16 @@ public abstract class GeckoApp
         final String resultant = BrowserLocaleManager.getInstance().setSelectedLocale(this, locale);
         if (resultant == null) {
             return;
         }
 
         onLocaleChanged(resultant);
     }
 
-    private void setSystemUiVisible(final boolean visible) {
-        ThreadUtils.postToUiThread(new Runnable() {
-            @Override
-            public void run() {
-                if (visible) {
-                    mMainLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
-                } else {
-                    mMainLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE);
-                }
-            }
-        });
-    }
-
     protected HealthRecorder createHealthRecorder(final Context context,
                                                   final String profilePath,
                                                   final EventDispatcher dispatcher,
                                                   final String osLocale,
                                                   final String appLocale,
                                                   final SessionInformation previousSession) {
         // GeckoApp does not need to record any health information - return a stub.
         return new StubbedHealthRecorder();
--- a/mobile/android/base/java/org/mozilla/gecko/SnackbarBuilder.java
+++ b/mobile/android/base/java/org/mozilla/gecko/SnackbarBuilder.java
@@ -1,17 +1,17 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * 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.gecko;
 
 import org.mozilla.gecko.util.EventCallback;
-import org.mozilla.gecko.util.NativeJSObject;
+import org.mozilla.gecko.util.GeckoBundle;
 
 import android.app.Activity;
 import android.graphics.Color;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.InsetDrawable;
 import android.support.annotation.StringRes;
 import android.support.design.widget.Snackbar;
 import android.support.v4.content.ContextCompat;
@@ -168,32 +168,32 @@ public class SnackbarBuilder {
     public SnackbarBuilder actionColor(final Integer actionColor) {
         this.actionColor = actionColor;
         return this;
     }
 
     /**
      * @param object Populate the builder with data from a Gecko Snackbar:Show event.
      */
-    public SnackbarBuilder fromEvent(final NativeJSObject object) {
+    public SnackbarBuilder fromEvent(final GeckoBundle object) {
         message = object.getString("message");
         duration = object.getInt("duration");
 
-        if (object.has("backgroundColor")) {
+        if (object.containsKey("backgroundColor")) {
             final String providedColor = object.getString("backgroundColor");
             try {
                 backgroundColor = Color.parseColor(providedColor);
             } catch (IllegalArgumentException e) {
                 Log.w(LOGTAG, "Failed to parse color string: " + providedColor);
             }
         }
 
-        NativeJSObject actionObject = object.optObject("action", null);
+        GeckoBundle actionObject = object.getBundle("action");
         if (actionObject != null) {
-            action = actionObject.optString("label", null);
+            action = actionObject.getString("label", null);
         }
         return this;
     }
 
     public void buildAndShow() {
         final View parentView = findBestParentView(activity);
         final Snackbar snackbar = Snackbar.make(parentView, message, duration);
 
--- a/mobile/android/base/java/org/mozilla/gecko/Tabs.java
+++ b/mobile/android/base/java/org/mozilla/gecko/Tabs.java
@@ -18,16 +18,19 @@ import org.json.JSONObject;
 import org.mozilla.gecko.annotation.JNITarget;
 import org.mozilla.gecko.annotation.RobocopTarget;
 import org.mozilla.gecko.AppConstants.Versions;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.gfx.LayerView;
 import org.mozilla.gecko.mozglue.SafeIntent;
 import org.mozilla.gecko.notifications.WhatsNewReceiver;
 import org.mozilla.gecko.reader.ReaderModeUtils;
+import org.mozilla.gecko.util.BundleEventListener;
+import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.GeckoBundle;
 import org.mozilla.gecko.util.GeckoEventListener;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import android.accounts.Account;
 import android.accounts.AccountManager;
 import android.accounts.OnAccountsUpdateListener;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -35,17 +38,17 @@ import android.database.ContentObserver;
 import android.database.sqlite.SQLiteException;
 import android.graphics.Color;
 import android.net.Uri;
 import android.os.Handler;
 import android.provider.Browser;
 import android.support.v4.content.ContextCompat;
 import android.util.Log;
 
-public class Tabs implements GeckoEventListener {
+public class Tabs implements BundleEventListener, GeckoEventListener {
     private static final String LOGTAG = "GeckoTabs";
 
     // mOrder and mTabs are always of the same cardinality, and contain the same values.
     private final CopyOnWriteArrayList<Tab> mOrder = new CopyOnWriteArrayList<Tab>();
 
     // All writes to mSelectedTab must be synchronized on the Tabs instance.
     // In general, it's preferred to always use selectTab()).
     private volatile Tab mSelectedTab;
@@ -99,18 +102,21 @@ public class Tabs implements GeckoEventL
                 db.getTabsAccessor().persistLocalTabs(context.getContentResolver(), tabs);
             } catch (SQLiteException e) {
                 Log.w(LOGTAG, "Error persisting local tabs", e);
             }
         }
     };
 
     private Tabs() {
-        EventDispatcher.getInstance().registerGeckoThreadListener(this,
+        EventDispatcher.getInstance().registerUiThreadListener(this,
             "Tab:Added",
+            null);
+
+        EventDispatcher.getInstance().registerGeckoThreadListener((GeckoEventListener) this,
             "Tab:Close",
             "Tab:Select",
             "Tab:SelectAndForeground",
             "Content:LocationChange",
             "Content:SecurityChange",
             "Content:StateChange",
             "Content:LoadError",
             "Content:PageShow",
@@ -468,60 +474,66 @@ public class Tabs implements GeckoEventL
         private static final Tabs INSTANCE = new Tabs();
     }
 
     @RobocopTarget
     public static Tabs getInstance() {
        return Tabs.TabsInstanceHolder.INSTANCE;
     }
 
+    @Override // BundleEventListener
+    public synchronized void handleMessage(final String event, final GeckoBundle message,
+                                           final EventCallback callback) {
+        // "Tab:Added" is a special case because tab will be null if the tab was just added
+        if ("Tab:Added".equals(event)) {
+            int id = message.getInt("tabID");
+            Tab tab = getTab(id);
+
+            String url = message.getString("uri");
+
+            if (message.getBoolean("cancelEditMode")) {
+                final Tab oldTab = getSelectedTab();
+                if (oldTab != null) {
+                    oldTab.setIsEditing(false);
+                }
+            }
+
+            if (message.getBoolean("stub")) {
+                if (tab == null) {
+                    // Tab was already closed; abort
+                    return;
+                }
+            } else {
+                tab = addTab(id, url, message.getBoolean("external"),
+                                      message.getInt("parentId"),
+                                      message.getString("title"),
+                                      message.getBoolean("isPrivate"),
+                                      message.getInt("tabIndex"));
+                // If we added the tab as a stub, we should have already
+                // selected it, so ignore this flag for stubbed tabs.
+                if (message.getBoolean("selected"))
+                    selectTab(id);
+            }
+
+            if (message.getBoolean("delayLoad"))
+                tab.setState(Tab.STATE_DELAYED);
+            if (message.getBoolean("desktopMode"))
+                tab.setDesktopMode(true);
+        }
+    }
+
     // GeckoEventListener implementation
     @Override
-    public void handleMessage(String event, JSONObject message) {
+    public synchronized void handleMessage(String event, JSONObject message) {
         Log.d(LOGTAG, "handleMessage: " + event);
         try {
             // All other events handled below should contain a tabID property
             int id = message.getInt("tabID");
             Tab tab = getTab(id);
 
-            // "Tab:Added" is a special case because tab will be null if the tab was just added
-            if (event.equals("Tab:Added")) {
-                String url = message.isNull("uri") ? null : message.getString("uri");
-
-                if (message.getBoolean("cancelEditMode")) {
-                    final Tab oldTab = getSelectedTab();
-                    if (oldTab != null) {
-                        oldTab.setIsEditing(false);
-                    }
-                }
-
-                if (message.getBoolean("stub")) {
-                    if (tab == null) {
-                        // Tab was already closed; abort
-                        return;
-                    }
-                } else {
-                    tab = addTab(id, url, message.getBoolean("external"),
-                                          message.getInt("parentId"),
-                                          message.getString("title"),
-                                          message.getBoolean("isPrivate"),
-                                          message.getInt("tabIndex"));
-                    // If we added the tab as a stub, we should have already
-                    // selected it, so ignore this flag for stubbed tabs.
-                    if (message.getBoolean("selected"))
-                        selectTab(id);
-                }
-
-                if (message.getBoolean("delayLoad"))
-                    tab.setState(Tab.STATE_DELAYED);
-                if (message.getBoolean("desktopMode"))
-                    tab.setDesktopMode(true);
-                return;
-            }
-
             // Tab was already closed; abort
             if (tab == null)
                 return;
 
             if (event.equals("Tab:Close")) {
                 closeTab(tab);
             } else if (event.equals("Tab:Select")) {
                 selectTab(tab.getId());
--- a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryPanel.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryPanel.java
@@ -533,24 +533,17 @@ public class CombinedHistoryPanel extend
 
         final SpannableStringBuilder ssb = new SpannableStringBuilder(text);
 
         // Set clickable text.
         final ClickableSpan clickableSpan = new ClickableSpan() {
             @Override
             public void onClick(View widget) {
                 Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "hint_private_browsing");
-                try {
-                    final JSONObject json = new JSONObject();
-                    json.put("type", "Menu:Open");
-                    GeckoApp.getEventDispatcher().dispatchEvent(json, null);
-                    EventDispatcher.getInstance().dispatchEvent(json, null);
-                } catch (JSONException e) {
-                    Log.e(LOGTAG, "Error forming JSON for Private Browsing contextual hint", e);
-                }
+                EventDispatcher.getInstance().dispatch("Menu:Open", null);
             }
         };
 
         ssb.setSpan(clickableSpan, 0, text.length(), 0);
 
         // Remove underlining set by ClickableSpan.
         final UnderlineSpan noUnderlineSpan = new UnderlineSpan() {
             @Override
--- a/mobile/android/base/java/org/mozilla/gecko/lwt/LightweightTheme.java
+++ b/mobile/android/base/java/org/mozilla/gecko/lwt/LightweightTheme.java
@@ -10,17 +10,19 @@ import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.List;
 
 import org.json.JSONObject;
 import org.mozilla.gecko.AppConstants.Versions;
 import org.mozilla.gecko.EventDispatcher;
 import org.mozilla.gecko.GeckoSharedPrefs;
 import org.mozilla.gecko.gfx.BitmapUtils;
-import org.mozilla.gecko.util.GeckoEventListener;
+import org.mozilla.gecko.util.BundleEventListener;
+import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.GeckoBundle;
 import org.mozilla.gecko.util.WindowUtils;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.util.ThreadUtils.AssertBehavior;
 
 import android.app.Application;
 import android.content.SharedPreferences;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
@@ -32,17 +34,17 @@ import android.graphics.Shader;
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.Gravity;
 import android.view.View;
 import android.view.ViewParent;
 
-public class LightweightTheme implements GeckoEventListener {
+public class LightweightTheme implements BundleEventListener {
     private static final String LOGTAG = "GeckoLightweightTheme";
 
     private static final String PREFS_URL = "lightweightTheme.headerURL";
     private static final String PREFS_COLOR = "lightweightTheme.color";
 
     private static final String ASSETS_PREFIX = "resource://android/assets/";
 
     private final Application mApplication;
@@ -158,17 +160,17 @@ public class LightweightTheme implements
         }
     }
 
     public LightweightTheme(Application application) {
         mApplication = application;
         mListeners = new ArrayList<OnChangeListener>();
 
         // unregister isn't needed as the lifetime is same as the application.
-        EventDispatcher.getInstance().registerGeckoThreadListener(this,
+        EventDispatcher.getInstance().registerUiThreadListener(this,
             "LightweightTheme:Update",
             "LightweightTheme:Disable");
 
         ThreadUtils.postToBackgroundThread(new LightweightThemeRunnable());
     }
 
     public void addListener(final OnChangeListener listener) {
         // Don't inform the listeners that attached late.
@@ -176,38 +178,29 @@ public class LightweightTheme implements
         mListeners.add(listener);
     }
 
     public void removeListener(OnChangeListener listener) {
         mListeners.remove(listener);
     }
 
     @Override
-    public void handleMessage(String event, JSONObject message) {
-        try {
-            if (event.equals("LightweightTheme:Update")) {
-                JSONObject lightweightTheme = message.getJSONObject("data");
-                final String headerURL = lightweightTheme.getString("headerURL");
-                final String color = lightweightTheme.optString("accentcolor");
+    public void handleMessage(String event, GeckoBundle message, EventCallback callback) {
+        if (event.equals("LightweightTheme:Update")) {
+            GeckoBundle lightweightTheme = message.getBundle("data");
+            final String headerURL = lightweightTheme.getString("headerURL");
+            final String color = lightweightTheme.getString("accentcolor");
 
-                ThreadUtils.postToBackgroundThread(new LightweightThemeRunnable(headerURL, color));
-            } else if (event.equals("LightweightTheme:Disable")) {
-                // Clear the saved data when a theme is disabled.
-                // Called on the Gecko thread, but should be very lightweight.
-                clearPrefs();
+            ThreadUtils.postToBackgroundThread(new LightweightThemeRunnable(headerURL, color));
 
-                ThreadUtils.postToUiThread(new Runnable() {
-                    @Override
-                    public void run() {
-                        resetLightweightTheme();
-                    }
-                });
-            }
-        } catch (Exception e) {
-            Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
+        } else if (event.equals("LightweightTheme:Disable")) {
+            // Clear the saved data when a theme is disabled.
+            // Called on the Gecko thread, but should be very lightweight.
+            clearPrefs();
+            resetLightweightTheme();
         }
     }
 
     /**
      * Clear the data stored in preferences for fast path loading during startup
      */
     private void clearPrefs() {
         GeckoSharedPrefs.forProfile(mApplication)
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
@@ -36,18 +36,20 @@ import org.mozilla.gecko.feeds.FeedServi
 import org.mozilla.gecko.feeds.action.CheckForUpdatesAction;
 import org.mozilla.gecko.permissions.Permissions;
 import org.mozilla.gecko.restrictions.Restrictable;
 import org.mozilla.gecko.restrictions.Restrictions;
 import org.mozilla.gecko.tabqueue.TabQueueHelper;
 import org.mozilla.gecko.tabqueue.TabQueuePrompt;
 import org.mozilla.gecko.updater.UpdateService;
 import org.mozilla.gecko.updater.UpdateServiceHelper;
+import org.mozilla.gecko.util.BundleEventListener;
 import org.mozilla.gecko.util.ContextUtils;
 import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.GeckoBundle;
 import org.mozilla.gecko.util.HardwareUtils;
 import org.mozilla.gecko.util.InputOptionsUtils;
 import org.mozilla.gecko.util.NativeEventListener;
 import org.mozilla.gecko.util.NativeJSObject;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import android.annotation.TargetApi;
 import android.app.AlertDialog;
@@ -96,22 +98,22 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 
 public class GeckoPreferences
-extends AppCompatPreferenceActivity
-implements
-GeckoActivityStatus,
-NativeEventListener,
-OnPreferenceChangeListener,
-OnSharedPreferenceChangeListener
+    extends AppCompatPreferenceActivity
+    implements BundleEventListener,
+               GeckoActivityStatus,
+               NativeEventListener,
+               OnPreferenceChangeListener,
+               OnSharedPreferenceChangeListener
 {
     private static final String LOGTAG = "GeckoPreferences";
 
     // We have a white background, which makes transitions on
     // some devices look bad. Don't use transitions on those
     // devices.
     private static final boolean NO_TRANSITIONS = HardwareUtils.IS_KINDLE_DEVICE;
     private static final int NO_SUCH_ID = 0;
@@ -364,19 +366,18 @@ OnSharedPreferenceChangeListener
                 localeSwitchingIsEnabled = false;
                 throw new IllegalStateException("foobar");
             }
         }
 
         // Use setResourceToOpen to specify these extras.
         Bundle intentExtras = getIntent().getExtras();
 
-        EventDispatcher.getInstance().registerGeckoThreadListener(this,
-                                                                  "Sanitize:Finished",
-                                                                  "Snackbar:Show");
+        EventDispatcher.getInstance().registerGeckoThreadListener((NativeEventListener) this,
+                                                                  "Sanitize:Finished");
 
         // Add handling for long-press click.
         // This is only for Android 3.0 and below (which use the long-press-context-menu paradigm).
         final ListView mListView = getListView();
         mListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
             @Override
             public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
                 // Call long-click handler if it the item implements it.
@@ -501,28 +502,29 @@ OnSharedPreferenceChangeListener
 
         Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, Method.BACK, "settings");
     }
 
     @Override
     protected void onDestroy() {
         super.onDestroy();
 
-        EventDispatcher.getInstance().unregisterGeckoThreadListener(this,
-                                                                    "Sanitize:Finished",
-                                                                    "Snackbar:Show");
+        EventDispatcher.getInstance().unregisterGeckoThreadListener((NativeEventListener) this,
+                                                                    "Sanitize:Finished");
 
         if (mPrefsRequest != null) {
             PrefsHelper.removeObserver(mPrefsRequest);
             mPrefsRequest = null;
         }
     }
 
     @Override
     public void onPause() {
+        EventDispatcher.getInstance().unregisterUiThreadListener(this, "Snackbar:Show");
+
         // Symmetric with onResume.
         if (isMultiPane()) {
             SharedPreferences prefs = GeckoSharedPrefs.forApp(this);
             prefs.unregisterOnSharedPreferenceChangeListener(this);
         }
 
         super.onPause();
 
@@ -530,16 +532,18 @@ OnSharedPreferenceChangeListener
             ((GeckoApplication) getApplication()).onActivityPause(this);
         }
     }
 
     @Override
     public void onResume() {
         super.onResume();
 
+        EventDispatcher.getInstance().registerUiThreadListener(this, "Snackbar:Show");
+
         if (getApplication() instanceof GeckoApplication) {
             ((GeckoApplication) getApplication()).onActivityResume(this);
         }
 
         // Watch prefs, otherwise we don't reliably get told when they change.
         // See documentation for onSharedPreferenceChange for more.
         // Inexplicably only needed on tablet.
         if (isMultiPane()) {
@@ -603,34 +607,39 @@ OnSharedPreferenceChangeListener
     }
 
     @Override
     public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
         Permissions.onRequestPermissionsResult(this, permissions, grantResults);
     }
 
     @Override
+    public void handleMessage(final String event, final GeckoBundle message,
+                              final EventCallback callback) {
+        if ("Snackbar:Show".equals(event)) {
+            SnackbarBuilder.builder(this)
+                    .fromEvent(message)
+                    .callback(callback)
+                    .buildAndShow();
+        }
+    }
+
+    @Override
     public void handleMessage(final String event, final NativeJSObject message, final EventCallback callback) {
         try {
             switch (event) {
                 case "Sanitize:Finished":
                     boolean success = message.getBoolean("success");
                     final int stringRes = success ? R.string.private_data_success : R.string.private_data_fail;
 
                     SnackbarBuilder.builder(GeckoPreferences.this)
                             .message(stringRes)
                             .duration(Snackbar.LENGTH_LONG)
                             .buildAndShow();
                     break;
-                case "Snackbar:Show":
-                    SnackbarBuilder.builder(this)
-                            .fromEvent(message)
-                            .callback(callback)
-                            .buildAndShow();
-                    break;
             }
         } catch (Exception e) {
             Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
         }
     }
 
     /**
       * Initialize all of the preferences (native of Gecko ones) for this screen.
--- a/mobile/android/chrome/content/ActionBarHandler.js
+++ b/mobile/android/chrome/content/ActionBarHandler.js
@@ -586,17 +586,17 @@ var ActionBarHandler = {
             return false;
           }
           // Allow if selected text exists.
           return (ActionBarHandler._getSelectedText().length > 0);
         },
       },
 
       action: function(element, win) {
-        Messaging.sendRequest({
+        WindowEventDispatcher.sendRequest({
           type: "Share:Text",
           text: ActionBarHandler._getSelectedText(),
         });
 
         ActionBarHandler._uninit();
         UITelemetry.addEvent("action.1", "actionbar", null, "share");
       },
     },
--- a/mobile/android/chrome/content/Feedback.js
+++ b/mobile/android/chrome/content/Feedback.js
@@ -43,17 +43,17 @@ var Feedback = {
     }
 
     switch (event.type) {
       case "FeedbackClose":
         // Do nothing.
         break;
 
       case "FeedbackMaybeLater":
-        Messaging.sendRequest({ type: "Feedback:MaybeLater" });
+        GlobalEventDispatcher.sendRequest({ type: "Feedback:MaybeLater" });
         break;
     }
 
     let win = event.target.ownerDocument.defaultView.top;
     BrowserApp.closeTab(BrowserApp.getTabForWindow(win));
   },
 
   _isAllowed: function(node) {
--- a/mobile/android/chrome/content/PermissionsHelper.js
+++ b/mobile/android/chrome/content/PermissionsHelper.js
@@ -101,17 +101,17 @@ var PermissionsHelper = {
             hasPermissions: false
           });
           return;
         }
 
         // Keep track of permissions, so we know which ones to clear
         this._currentPermissions = permissions;
 
-        Messaging.sendRequest({
+        WindowEventDispatcher.sendRequest({
           type: "Permissions:Data",
           permissions: permissions
         });
         break;
  
       case "Permissions:Clear":
         // An array of the indices of the permissions we want to clear
         let permissionsToClear = JSON.parse(aData);
--- a/mobile/android/chrome/content/Reader.js
+++ b/mobile/android/chrome/content/Reader.js
@@ -121,17 +121,17 @@ var Reader = {
           url: message.data.url
         }).then(data => {
           message.target.messageManager.sendAsyncMessage("Reader:FaviconReturn", JSON.parse(data));
         });
         break;
       }
 
       case "Reader:SystemUIVisibility":
-        Messaging.sendRequest({
+        WindowEventDispatcher.sendRequest({
           type: "SystemUI:Visibility",
           visible: message.data.visible
         });
         break;
 
       case "Reader:ToolbarHidden":
         if (!this._hasUsedToolbar) {
           Snackbars.show(Strings.browser.GetStringFromName("readerMode.toolbarTip"), Snackbars.LENGTH_LONG);
--- a/mobile/android/chrome/content/RemoteDebugger.js
+++ b/mobile/android/chrome/content/RemoteDebugger.js
@@ -146,17 +146,17 @@ var RemoteDebugger = {
    *         * k     : K(random 128-bit number)
    *         A promise that will be resolved to the above is also allowed.
    */
   receiveOOB() {
     if (this._receivingOOB) {
       return this._receivingOOB;
     }
 
-    this._receivingOOB = Messaging.sendRequestForResult({
+    this._receivingOOB = WindowEventDispatcher.sendRequestForResult({
       type: "DevToolsAuth:Scan"
     }).then(data => {
       return JSON.parse(data);
     }, () => {
       let title = Strings.browser.GetStringFromName("remoteQRScanFailedPromptTitle");
       let msg = Strings.browser.GetStringFromName("remoteQRScanFailedPromptMessage");
       let ok = Strings.browser.GetStringFromName("remoteQRScanFailedPromptOK");
       let prompt = new Prompt({
--- a/mobile/android/chrome/content/about.js
+++ b/mobile/android/chrome/content/about.js
@@ -73,27 +73,30 @@ function init() {
     },
   };
 
   Updater.init();
 
   function checkForUpdates() {
     showCheckingMessage();
 
-    Services.androidBridge.handleGeckoMessage({ type: "Update:Check" });
+    let window = Services.wm.getMostRecentWindow("navigator:browser");
+    window.WindowEventDispatcher.sendRequest({ type: "Update:Check" });
   }
 
   function downloadUpdate() {
-    Services.androidBridge.handleGeckoMessage({ type: "Update:Download" });
+    let window = Services.wm.getMostRecentWindow("navigator:browser");
+    window.WindowEventDispatcher.sendRequest({ type: "Update:Download" });
   }
 
   function installUpdate() {
     showCheckAction();
 
-    Services.androidBridge.handleGeckoMessage({ type: "Update:Install" });
+    let window = Services.wm.getMostRecentWindow("navigator:browser");
+    window.WindowEventDispatcher.sendRequest({ type: "Update:Install" });
   }
 
   let updateLink = document.getElementById("updateLink");
   let checkingSpan = document.getElementById("update-message-checking");
   let noneSpan = document.getElementById("update-message-none");
   let foundSpan = document.getElementById("update-message-found");
   let downloadingSpan = document.getElementById("update-message-downloading");
   let downloadedSpan = document.getElementById("update-message-downloaded");
--- a/mobile/android/chrome/content/aboutHealthReport.js
+++ b/mobile/android/chrome/content/aboutHealthReport.js
@@ -3,18 +3,21 @@
  * 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/. */
 
 "use strict";
 
 var { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/Messaging.jsm");
 Cu.import("resource://gre/modules/SharedPreferences.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "EventDispatcher",
+                                  "resource://gre/modules/Messaging.jsm");
 
 // Name of Android SharedPreference controlling whether to upload
 // health reports.
 const PREF_UPLOAD_ENABLED = "android.not_a_preference.healthreport.uploadEnabled";
 
 // Name of Gecko Pref specifying report content location.
 const PREF_REPORTURL = "datareporting.healthreport.about.reportUrl";
 
@@ -114,25 +117,25 @@ var healthReportWrapper = {
     };
 
     let iframe = document.getElementById("remote-report");
     iframe.contentWindow.postMessage(data, reportUrl);
   },
 
   showSettings: function () {
     console.log("AboutHealthReport: showing settings.");
-    Messaging.sendRequest({
+    EventDispatcher.instance.sendRequest({
       type: "Settings:Show",
       resource: "preferences_vendor",
     });
   },
 
   launchUpdater: function () {
     console.log("AboutHealthReport: launching updater.");
-    Messaging.sendRequest({
+    EventDispatcher.instance.sendRequest({
       type: "Updater:Launch",
     });
   },
 
   handleRemoteCommand: function (evt) {
     switch (evt.detail.command) {
       case "DisableDataSubmission":
         this.onOptOut();
--- a/mobile/android/chrome/content/aboutLogins.js
+++ b/mobile/android/chrome/content/aboutLogins.js
@@ -1,29 +1,30 @@
 /* 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/. */
 
 var Ci = Components.interfaces, Cc = Components.classes, Cu = Components.utils;
 
 Cu.import("resource://services-common/utils.js"); /*global: CommonUtils */
-Cu.import("resource://gre/modules/Messaging.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/TelemetryStopwatch.jsm");
 
 XPCOMUtils.defineLazyGetter(window, "gChromeWin", () =>
   window.QueryInterface(Ci.nsIInterfaceRequestor)
     .getInterface(Ci.nsIWebNavigation)
     .QueryInterface(Ci.nsIDocShellTreeItem)
     .rootTreeItem
     .QueryInterface(Ci.nsIInterfaceRequestor)
     .getInterface(Ci.nsIDOMWindow)
     .QueryInterface(Ci.nsIDOMChromeWindow));
 
+XPCOMUtils.defineLazyModuleGetter(this, "EventDispatcher",
+                                  "resource://gre/modules/Messaging.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Snackbars", "resource://gre/modules/Snackbars.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Prompt",
                                   "resource://gre/modules/Prompt.jsm");
 
 var debug = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.d.bind(null, "AboutLogins");
 
 var gStringBundle = Services.strings.createBundle("chrome://browser/locale/aboutLogins.properties");
 
@@ -411,17 +412,17 @@ var Logins = {
             }
           });
       }
     });
   },
 
   _loadFavicon: function (aImg, aHostname) {
     // Load favicon from cache.
-    Messaging.sendRequestForResult({
+    EventDispatcher.instance.sendRequestForResult({
       type: "Favicon:CacheLoad",
       url: aHostname,
     }).then(function(faviconUrl) {
       aImg.style.backgroundImage= "url('" + faviconUrl + "')";
       aImg.style.visibility = "visible";
     }, function(data) {
       debug("Favicon cache failure : " + data);
       aImg.style.visibility = "visible";
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -385,36 +385,35 @@ var BrowserApp = {
     Services.obs.addObserver(this, "Sanitize:ClearData", false);
     Services.obs.addObserver(this, "FullScreen:Exit", false);
     Services.obs.addObserver(this, "Passwords:Init", false);
     Services.obs.addObserver(this, "FormHistory:Init", false);
     Services.obs.addObserver(this, "android-get-pref", false);
     Services.obs.addObserver(this, "android-set-pref", false);
     Services.obs.addObserver(this, "gather-telemetry", false);
     Services.obs.addObserver(this, "keyword-search", false);
-    Services.obs.addObserver(this, "sessionstore-state-purge-complete", false);
     Services.obs.addObserver(this, "Fonts:Reload", false);
     Services.obs.addObserver(this, "Vibration:Request", false);
 
     Messaging.addListener(this.getHistory.bind(this), "Session:GetHistory");
 
     window.addEventListener("fullscreen", function() {
-      Messaging.sendRequest({
+      WindowEventDispatcher.sendRequest({
         type: window.fullScreen ? "ToggleChrome:Hide" : "ToggleChrome:Show"
       });
     }, false);
 
     window.addEventListener("fullscreenchange", (e) => {
       // This event gets fired on the document and its entire ancestor chain
       // of documents. When enabling fullscreen, it is fired on the top-level
       // document first and goes down; when disabling the order is reversed
       // (per spec). This means the last event on enabling will be for the innermost
       // document, which will have fullscreenElement set correctly.
       let doc = e.target;
-      Messaging.sendRequest({
+      WindowEventDispatcher.sendRequest({
         type: doc.fullscreenElement ? "DOMFullScreen:Start" : "DOMFullScreen:Stop",
         rootElement: doc.fullscreenElement == doc.documentElement
       });
 
       if (this.fullscreenTransitionTab) {
         // Tab selection has changed during a fullscreen transition, handle it now.
         let tab = this.fullscreenTransitionTab;
         this.fullscreenTransitionTab = null;
@@ -503,26 +502,26 @@ var BrowserApp = {
       console.log("browser.js: loading Firefox Accounts WebChannel");
       Cu.import("resource://gre/modules/FxAccountsWebChannel.jsm");
       EnsureFxAccountsWebChannel();
     } else {
       console.log("browser.js: not loading Firefox Accounts WebChannel; this profile cannot connect to Firefox Accounts.");
     }
 
     // Notify Java that Gecko has loaded.
-    Messaging.sendRequest({ type: "Gecko:Ready" });
+    GlobalEventDispatcher.sendRequest({ type: "Gecko:Ready" });
 
     this.deck.addEventListener("DOMContentLoaded", function BrowserApp_delayedStartup() {
       BrowserApp.deck.removeEventListener("DOMContentLoaded", BrowserApp_delayedStartup, false);
 
       InitLater(() => Cu.import("resource://gre/modules/NotificationDB.jsm"));
       InitLater(() => Cu.import("resource://gre/modules/PresentationDeviceInfoManager.jsm"));
 
       InitLater(() => Services.obs.notifyObservers(window, "browser-delayed-startup-finished", ""));
-      InitLater(() => Messaging.sendRequest({ type: "Gecko:DelayedStartup" }));
+      InitLater(() => GlobalEventDispatcher.sendRequest({ type: "Gecko:DelayedStartup" }));
 
       if (!AppConstants.RELEASE_OR_BETA) {
         InitLater(() => WebcompatReporter.init());
       }
 
       // Collect telemetry data.
       // We do this at startup because we want to move away from "gather-telemetry" (bug 1127907)
       InitLater(() => {
@@ -568,17 +567,17 @@ var BrowserApp = {
     return this._startupStatus;
   },
 
   /**
    * Pass this a locale string, such as "fr" or "es_ES".
    */
   setLocale: function (locale) {
     console.log("browser.js: requesting locale set: " + locale);
-    Messaging.sendRequest({ type: "Locale:Set", locale: locale });
+    WindowEventDispatcher.sendRequest({ type: "Locale:Set", locale: locale });
   },
 
   initContextMenu: function () {
     // We pass a thunk in place of a raw label string. This allows the
     // context menu to automatically accommodate locale changes without
     // having to be rebuilt.
     let stringGetter = name => () => Strings.browser.GetStringFromName(name);
 
@@ -721,43 +720,43 @@ var BrowserApp = {
     });
 
     NativeWindow.contextmenus.add(stringGetter("contextmenu.addToContacts"),
       NativeWindow.contextmenus._disableRestricted("ADD_CONTACT", NativeWindow.contextmenus.emailLinkContext),
       function(aTarget) {
         UITelemetry.addEvent("action.1", "contextmenu", null, "web_contact_email");
 
         let url = NativeWindow.contextmenus._getLinkURL(aTarget);
-        Messaging.sendRequest({
+        WindowEventDispatcher.sendRequest({
           type: "Contact:Add",
           email: url
         });
       });
 
     NativeWindow.contextmenus.add(stringGetter("contextmenu.addToContacts"),
       NativeWindow.contextmenus._disableRestricted("ADD_CONTACT", NativeWindow.contextmenus.phoneNumberLinkContext),
       function(aTarget) {
         UITelemetry.addEvent("action.1", "contextmenu", null, "web_contact_phone");
 
         let url = NativeWindow.contextmenus._getLinkURL(aTarget);
-        Messaging.sendRequest({
+        WindowEventDispatcher.sendRequest({
           type: "Contact:Add",
           phone: url
         });
       });
 
     NativeWindow.contextmenus.add(stringGetter("contextmenu.bookmarkLink"),
       NativeWindow.contextmenus._disableRestricted("BOOKMARK", NativeWindow.contextmenus.linkBookmarkableContext),
       function(aTarget) {
         UITelemetry.addEvent("action.1", "contextmenu", null, "web_bookmark");
         UITelemetry.addEvent("save.1", "contextmenu", null, "bookmark");
 
         let url = NativeWindow.contextmenus._getLinkURL(aTarget);
         let title = aTarget.textContent || aTarget.title || url;
-        Messaging.sendRequest({
+        WindowEventDispatcher.sendRequest({
           type: "Bookmark:Insert",
           url: url,
           title: title
         });
       });
 
     NativeWindow.contextmenus.add(stringGetter("contextmenu.playMedia"),
       NativeWindow.contextmenus.mediaContext("media-paused"),
@@ -884,17 +883,17 @@ var BrowserApp = {
       });
 
     NativeWindow.contextmenus.add(stringGetter("contextmenu.setImageAs"),
       NativeWindow.contextmenus._disableRestricted("SET_IMAGE", NativeWindow.contextmenus.imageSaveableContext),
       function(aTarget) {
         UITelemetry.addEvent("action.1", "contextmenu", null, "web_background_image");
 
         let src = aTarget.src;
-        Messaging.sendRequest({
+        WindowEventDispatcher.sendRequest({
           type: "Image:SetAs",
           url: src
         });
       });
 
     NativeWindow.contextmenus.add(
       function(aTarget) {
         if (aTarget instanceof HTMLVideoElement) {
@@ -1751,17 +1750,17 @@ var BrowserApp = {
         // perform a keyword search on the selected tab.
         this.selectedTab.isSearch = true;
 
         // Don't store queries in private browsing mode.
         let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(this.selectedTab.browser);
         let query = isPrivate ? "" : aData;
 
         let engine = aSubject.QueryInterface(Ci.nsISearchEngine);
-        Messaging.sendRequest({
+        GlobalEventDispatcher.sendRequest({
           type: "Search:Keyword",
           identifier: engine.identifier,
           name: engine.name,
           query: query
         });
         break;
 
       case "Browser:Quit":
@@ -1897,22 +1896,18 @@ var BrowserApp = {
               Services.appinfo.submitReports = value;
               aSubject.setAsEmpty();
             }
             break;
         }
         break;
       }
 
-      case "sessionstore-state-purge-complete":
-        Messaging.sendRequest({ type: "Session:StatePurged" });
-        break;
-
       case "gather-telemetry":
-        Messaging.sendRequest({ type: "Telemetry:Gather" });
+        GlobalEventDispatcher.sendRequest({ type: "Telemetry:Gather" });
         break;
 
       case "Locale:OS":
         // We know the system locale. We use this for generating Accept-Language headers.
         console.log("Locale:OS: " + aData);
         let currentOSLocale = this.getOSLocalePref();
         if (currentOSLocale == aData) {
           break;
@@ -2174,31 +2169,31 @@ var NativeWindow = {
           };
       } else {
          throw "Incorrect number of parameters";
       }
 
       options.type = "Menu:Add";
       options.id = this._menuId;
 
-      Messaging.sendRequest(options);
+      GlobalEventDispatcher.sendRequest(options);
       this._callbacks[this._menuId] = options.callback;
       this._menuId++;
       return this._menuId - 1;
     },
 
     remove: function(aId) {
-      Messaging.sendRequest({ type: "Menu:Remove", id: aId });
+      GlobalEventDispatcher.sendRequest({ type: "Menu:Remove", id: aId });
     },
 
     update: function(aId, aOptions) {
       if (!aOptions)
         return;
 
-      Messaging.sendRequest({
+      GlobalEventDispatcher.sendRequest({
         type: "Menu:Update",
         id: aId,
         options: aOptions
       });
     }
   },
 
   doorhanger: {
@@ -3499,17 +3494,17 @@ Tab.prototype = {
                   ? aParams.selected !== false || aParams.cancelEditMode === true : true,
         cancelEditMode: aParams.cancelEditMode === true,
         title: truncate(title, MAX_TITLE_LENGTH),
         delayLoad: aParams.delayLoad || false,
         desktopMode: this.desktopMode,
         isPrivate: isPrivate,
         stub: stub
       };
-      Messaging.sendRequest(message);
+      GlobalEventDispatcher.sendRequest(message);
     }
 
     let flags = Ci.nsIWebProgress.NOTIFY_STATE_ALL |
                 Ci.nsIWebProgress.NOTIFY_LOCATION |
                 Ci.nsIWebProgress.NOTIFY_SECURITY;
     this.filter = Cc["@mozilla.org/appshell/component/browser-status-filter;1"].createInstance(Ci.nsIWebProgress);
     this.filter.addProgressListener(this, flags)
     this.browser.addProgressListener(this.filter, flags);
@@ -4530,17 +4525,17 @@ var BrowserEventHandler = {
         }
         break;
       case 'MozMouseHittest':
         this._handleRetargetedTouchStart(aEvent);
         break;
       case 'OpenMediaWithExternalApp': {
         let mediaSrc = aEvent.target.currentSrc || aEvent.target.src;
         let uuid = uuidgen.generateUUID().toString();
-        Services.androidBridge.handleGeckoMessage({
+        GlobalEventDispatcher.sendRequest({
           type: "Video:Play",
           uri: mediaSrc,
           uuid: uuid
         });
         break;
       }
     }
   },
@@ -5627,17 +5622,17 @@ var CharacterEncoding = {
   },
 
   sendState: function sendState() {
     let showCharEncoding = "false";
     try {
       showCharEncoding = Services.prefs.getComplexValue("browser.menu.showCharacterEncoding", Ci.nsIPrefLocalizedString).data;
     } catch (e) { /* Optional */ }
 
-    Messaging.sendRequest({
+    GlobalEventDispatcher.sendRequest({
       type: "CharEncoding:State",
       visible: showCharEncoding
     });
   },
 
   getEncoding: function getEncoding() {
     function infoToCharset(info) {
       return { code: info.value, title: info.label };
@@ -5661,17 +5656,17 @@ var CharacterEncoding = {
 
     for (let i = 0; i < charsetCount; i++) {
       if (this._charsets[i].code === docCharset) {
         selected = i;
         break;
       }
     }
 
-    Messaging.sendRequest({
+    GlobalEventDispatcher.sendRequest({
       type: "CharEncoding:Data",
       charsets: this._charsets,
       selected: selected
     });
   },
 
   setEncoding: function setEncoding(aEncoding) {
     let browser = BrowserApp.selectedBrowser;
@@ -6329,17 +6324,17 @@ var Telemetry = {
 var Experiments = {
   // Enable malware download protection (bug 936041)
   MALWARE_DOWNLOAD_PROTECTION: "malware-download-protection",
 
   // Try to load pages from disk cache when network is offline (bug 935190)
   OFFLINE_CACHE: "offline-cache",
 
   init() {
-    Messaging.sendRequestForResult({
+    GlobalEventDispatcher.sendRequestForResult({
       type: "Experiments:GetActive"
     }).then(experiments => {
       let names = JSON.parse(experiments);
       for (let name of names) {
         switch (name) {
           case this.MALWARE_DOWNLOAD_PROTECTION: {
             // Apply experiment preferences on the default branch. This allows
             // us to avoid migrating user prefs when experiments are enabled/disabled,
@@ -6356,25 +6351,25 @@ var Experiments = {
             continue;
           }
         }
       }
     });
   },
 
   setOverride(name, isEnabled) {
-    Messaging.sendRequest({
+    GlobalEventDispatcher.sendRequest({
       type: "Experiments:SetOverride",
       name: name,
       isEnabled: isEnabled
     });
   },
 
   clearOverride(name) {
-    Messaging.sendRequest({
+    GlobalEventDispatcher.sendRequest({
       type: "Experiments:ClearOverride",
       name: name
     });
   }
 };
 
 var ExternalApps = {
   _contextMenuId: null,
--- a/mobile/android/chrome/content/geckoview.js
+++ b/mobile/android/chrome/content/geckoview.js
@@ -9,24 +9,18 @@ var Cu = Components.utils;
 var Cr = Components.results;
 
 Cu.import("resource://gre/modules/AppConstants.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Log",
  "resource://gre/modules/AndroidLog.jsm", "AndroidLog");
 
-XPCOMUtils.defineLazyModuleGetter(this, "Messaging",
- "resource://gre/modules/Messaging.jsm", "Messaging");
-
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
  "resource://gre/modules/Services.jsm", "Services");
 
 function dump(msg) {
   Log.d("View", msg);
 }
 
 function startup() {
     dump("zerdatime " + Date.now() + " - geckoivew chrome startup finished.");
-
-    // Notify Java that Gecko has loaded.
-    Messaging.sendRequest({ type: "Gecko:Ready" });
 }
--- a/mobile/android/components/HelperAppDialog.js
+++ b/mobile/android/components/HelperAppDialog.js
@@ -21,17 +21,17 @@ Cu.import("resource://gre/modules/Downlo
 Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://gre/modules/HelperApps.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "RuntimePermissions", "resource://gre/modules/RuntimePermissions.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Messaging", "resource://gre/modules/Messaging.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "EventDispatcher", "resource://gre/modules/Messaging.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Snackbars", "resource://gre/modules/Snackbars.jsm");
 
 // -----------------------------------------------------------------------
 // HelperApp Launcher Dialog
 // -----------------------------------------------------------------------
 
 XPCOMUtils.defineLazyGetter(this, "ContentAreaUtils", function() {
   let ContentAreaUtils = {};
@@ -234,17 +234,17 @@ HelperAppLauncherDialog.prototype = {
   },
 
   _downloadWithAndroidDownloadManager(aLauncher) {
     let mimeType = aLauncher.MIMEInfo.MIMEType;
     if (!mimeType) {
       mimeType = ContentAreaUtils.getMIMETypeForURI(aLauncher.source) || "";
     }
 
-    Messaging.sendRequest({
+    EventDispatcher.instance.sendRequest({
       'type': 'Download:AndroidDownloadManager',
       'uri': aLauncher.source.spec,
       'mimeType': mimeType,
       'filename': aLauncher.suggestedFileName
     });
   },
 
   _getPrefName: function getPrefName(mimetype) {
--- a/mobile/android/components/SessionStore.js
+++ b/mobile/android/components/SessionStore.js
@@ -306,17 +306,18 @@ SessionStore.prototype = {
         break;
       }
       case "Tabs:OpenMultiple": {
         let data = JSON.parse(aData);
 
         this._openTabs(data);
 
         if (data.shouldNotifyTabsOpenedToJava) {
-          Messaging.sendRequest({
+          let window = Services.wm.getMostRecentWindow("navigator:browser");
+          window.WindowEventDispatcher.sendRequest({
             type: "Tabs:TabsOpened"
           });
         }
         break;
       }
       case "Tab:KeepZombified": {
         if (aData >= 0) {
           this._keepAsZombieTabId = aData;
@@ -974,17 +975,18 @@ SessionStore.prototype = {
            normalData.windows[0].tabs.length + " tabs in window[0]");
     } else {
       log("_saveState() writing empty normal data");
     }
     this._writeFile(this._sessionFile, this._sessionFileTemp, normalData, aAsync);
 
     // If we have private data, send it to Java; otherwise, send null to
     // indicate that there is no private data
-    Messaging.sendRequest({
+    let window = Services.wm.getMostRecentWindow("navigator:browser");
+    window.WindowEventDispatcher.sendRequest({
       type: "PrivateBrowsing:Data",
       session: (privateData.windows.length > 0 && privateData.windows[0].tabs.length > 0) ? JSON.stringify(privateData) : null
     });
 
     this._lastSaveTime = Date.now();
   },
 
   _getCurrentState: function ss_getCurrentState() {
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/EventDispatcher.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/EventDispatcher.java
@@ -101,16 +101,19 @@ public final class EventDispatcher exten
 
     private <T> void registerListener(final Class<?> listType,
                                       final Map<String, List<T>> listenersMap,
                                       final T listener,
                                       final String[] events) {
         try {
             synchronized (listenersMap) {
                 for (final String event : events) {
+                    if (event == null) {
+                        continue;
+                    }
                     List<T> listeners = listenersMap.get(event);
                     if (listeners == null) {
                         // Java doesn't let us put Class<? extends List<T>> as the type for listType.
                         @SuppressWarnings("unchecked")
                         final Class<? extends List<T>> type = (Class) listType;
                         listeners = type.newInstance();
                         listenersMap.put(event, listeners);
                     }
@@ -151,16 +154,19 @@ public final class EventDispatcher exten
         }
     }
 
     private <T> void unregisterListener(final Map<String, List<T>> listenersMap,
                                         final T listener,
                                         final String[] events) {
         synchronized (listenersMap) {
             for (final String event : events) {
+                if (event == null) {
+                    continue;
+                }
                 List<T> listeners = listenersMap.get(event);
                 if ((listeners == null ||
                      !listeners.remove(listener)) && !AppConstants.RELEASE_OR_BETA) {
                     throw new IllegalArgumentException(event + " was not registered");
                 }
             }
         }
     }
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAccessibility.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAccessibility.java
@@ -1,19 +1,19 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * 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.gecko;
 
-import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.mozilla.gecko.AppConstants.Versions;
+import org.mozilla.gecko.util.GeckoBundle;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.util.UIAsyncTask;
 
 import android.content.Context;
 import android.graphics.Rect;
 import android.os.Bundle;
 import android.util.Log;
 import android.view.View;
@@ -29,17 +29,17 @@ import com.googlecode.eyesfree.braille.s
 public class GeckoAccessibility {
     private static final String LOGTAG = "GeckoAccessibility";
     private static final int VIRTUAL_ENTRY_POINT_BEFORE = 1;
     private static final int VIRTUAL_CURSOR_POSITION = 2;
     private static final int VIRTUAL_ENTRY_POINT_AFTER = 3;
 
     private static boolean sEnabled;
     // Used to store the JSON message and populate the event later in the code path.
-    private static JSONObject sHoverEnter;
+    private static GeckoBundle sHoverEnter;
     private static AccessibilityNodeInfo sVirtualCursorNode;
     private static int sCurrentNode;
 
     // This is the number Brailleback uses to start indexing routing keys.
     private static final int BRAILLE_CLICK_BASE_INDEX = -275000000;
     private static SelfBrailleClient sSelfBrailleClient;
 
     public static void updateAccessibilitySettings (final Context context) {
@@ -71,42 +71,42 @@ public class GeckoAccessibility {
                     if (geckoInterface == null) {
                         return;
                     }
                     geckoInterface.setAccessibilityEnabled(sEnabled);
                 }
             }.execute();
     }
 
-    private static void populateEventFromJSON (AccessibilityEvent event, JSONObject message) {
-        final JSONArray textArray = message.optJSONArray("text");
+    private static void populateEventFromJSON (AccessibilityEvent event, GeckoBundle message) {
+        final String[] textArray = message.getStringArray("text");
         if (textArray != null) {
-            for (int i = 0; i < textArray.length(); i++)
-                event.getText().add(textArray.optString(i));
+            for (int i = 0; i < textArray.length; i++)
+                event.getText().add(textArray[i]);
         }
 
-        event.setContentDescription(message.optString("description"));
-        event.setEnabled(message.optBoolean("enabled", true));
-        event.setChecked(message.optBoolean("checked"));
-        event.setPassword(message.optBoolean("password"));
-        event.setAddedCount(message.optInt("addedCount", -1));
-        event.setRemovedCount(message.optInt("removedCount", -1));
-        event.setFromIndex(message.optInt("fromIndex", -1));
-        event.setItemCount(message.optInt("itemCount", -1));
-        event.setCurrentItemIndex(message.optInt("currentItemIndex", -1));
-        event.setBeforeText(message.optString("beforeText"));
-        event.setToIndex(message.optInt("toIndex", -1));
-        event.setScrollable(message.optBoolean("scrollable"));
-        event.setScrollX(message.optInt("scrollX", -1));
-        event.setScrollY(message.optInt("scrollY", -1));
-        event.setMaxScrollX(message.optInt("maxScrollX", -1));
-        event.setMaxScrollY(message.optInt("maxScrollY", -1));
+        event.setContentDescription(message.getString("description"));
+        event.setEnabled(message.getBoolean("enabled", true));
+        event.setChecked(message.getBoolean("checked"));
+        event.setPassword(message.getBoolean("password"));
+        event.setAddedCount(message.getInt("addedCount", -1));
+        event.setRemovedCount(message.getInt("removedCount", -1));
+        event.setFromIndex(message.getInt("fromIndex", -1));
+        event.setItemCount(message.getInt("itemCount", -1));
+        event.setCurrentItemIndex(message.getInt("currentItemIndex", -1));
+        event.setBeforeText(message.getString("beforeText"));
+        event.setToIndex(message.getInt("toIndex", -1));
+        event.setScrollable(message.getBoolean("scrollable"));
+        event.setScrollX(message.getInt("scrollX", -1));
+        event.setScrollY(message.getInt("scrollY", -1));
+        event.setMaxScrollX(message.getInt("maxScrollX", -1));
+        event.setMaxScrollY(message.getInt("maxScrollY", -1));
     }
 
-    private static void sendDirectAccessibilityEvent(int eventType, JSONObject message) {
+    private static void sendDirectAccessibilityEvent(int eventType, GeckoBundle message) {
         final Context context = GeckoAppShell.getApplicationContext();
         final AccessibilityEvent accEvent = AccessibilityEvent.obtain(eventType);
         accEvent.setClassName(GeckoAccessibility.class.getName());
         accEvent.setPackageName(context.getPackageName());
         populateEventFromJSON(accEvent, message);
         AccessibilityManager accessibilityManager =
             (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
         try {
@@ -115,34 +115,34 @@ public class GeckoAccessibility {
             // Accessibility is off.
         }
     }
 
     public static boolean isEnabled() {
         return sEnabled;
     }
 
-    public static void sendAccessibilityEvent (final JSONObject message) {
+    public static void sendAccessibilityEvent(final GeckoBundle message) {
         if (!sEnabled)
             return;
 
-        final int eventType = message.optInt("eventType", -1);
+        final int eventType = message.getInt("eventType", -1);
         if (eventType < 0) {
             Log.e(LOGTAG, "No accessibility event type provided");
             return;
         }
 
         sendAccessibilityEvent(message, eventType);
     }
 
-    public static void sendAccessibilityEvent (final JSONObject message, final int eventType) {
+    public static void sendAccessibilityEvent(final GeckoBundle message, final int eventType) {
         if (!sEnabled)
             return;
 
-        final String exitView = message.optString("exitView");
+        final String exitView = message.getString("exitView");
         if (exitView.equals("moveNext")) {
             sCurrentNode = VIRTUAL_ENTRY_POINT_AFTER;
         } else if (exitView.equals("movePrevious")) {
             sCurrentNode = VIRTUAL_ENTRY_POINT_BEFORE;
         } else {
             sCurrentNode = VIRTUAL_CURSOR_POSITION;
         }
 
@@ -157,51 +157,52 @@ public class GeckoAccessibility {
             });
         } else {
             // In Jelly Bean we populate an AccessibilityNodeInfo with the minimal amount of data to have
             // it work with TalkBack.
             final View view = GeckoAppShell.getLayerView();
             if (view == null)
                 return;
 
-            if (sVirtualCursorNode == null)
+            if (sVirtualCursorNode == null) {
                 sVirtualCursorNode = AccessibilityNodeInfo.obtain(view, VIRTUAL_CURSOR_POSITION);
-            sVirtualCursorNode.setEnabled(message.optBoolean("enabled", true));
-            sVirtualCursorNode.setClickable(message.optBoolean("clickable"));
-            sVirtualCursorNode.setCheckable(message.optBoolean("checkable"));
-            sVirtualCursorNode.setChecked(message.optBoolean("checked"));
-            sVirtualCursorNode.setPassword(message.optBoolean("password"));
+            }
+            sVirtualCursorNode.setEnabled(message.getBoolean("enabled", true));
+            sVirtualCursorNode.setClickable(message.getBoolean("clickable"));
+            sVirtualCursorNode.setCheckable(message.getBoolean("checkable"));
+            sVirtualCursorNode.setChecked(message.getBoolean("checked"));
+            sVirtualCursorNode.setPassword(message.getBoolean("password"));
 
-            final JSONArray textArray = message.optJSONArray("text");
+            final String[] textArray = message.getStringArray("text");
             StringBuilder sb = new StringBuilder();
-            if (textArray != null && textArray.length() > 0) {
-                sb.append(textArray.optString(0));
-                for (int i = 1; i < textArray.length(); i++) {
-                    sb.append(" ").append(textArray.optString(i));
+            if (textArray != null && textArray.length > 0) {
+                sb.append(textArray[0]);
+                for (int i = 1; i < textArray.length; i++) {
+                    sb.append(" ").append(textArray[i]);
                 }
                 sVirtualCursorNode.setText(sb.toString());
             }
-            sVirtualCursorNode.setContentDescription(message.optString("description"));
+            sVirtualCursorNode.setContentDescription(message.getString("description"));
 
-            JSONObject bounds = message.optJSONObject("bounds");
+            final GeckoBundle bounds = message.getBundle("bounds");
             if (bounds != null) {
-                Rect relativeBounds = new Rect(bounds.optInt("left"), bounds.optInt("top"),
-                                               bounds.optInt("right"), bounds.optInt("bottom"));
+                Rect relativeBounds = new Rect(bounds.getInt("left"), bounds.getInt("top"),
+                                               bounds.getInt("right"), bounds.getInt("bottom"));
                 sVirtualCursorNode.setBoundsInParent(relativeBounds);
                 int[] locationOnScreen = new int[2];
                 view.getLocationOnScreen(locationOnScreen);
                 Rect screenBounds = new Rect(relativeBounds);
                 screenBounds.offset(locationOnScreen[0], locationOnScreen[1]);
                 sVirtualCursorNode.setBoundsInScreen(screenBounds);
             }
 
-            final JSONObject braille = message.optJSONObject("brailleOutput");
+            final GeckoBundle braille = message.getBundle("brailleOutput");
             if (braille != null) {
-                sendBrailleText(view, braille.optString("text"),
-                                braille.optInt("selectionStart"), braille.optInt("selectionEnd"));
+                sendBrailleText(view, braille.getString("text"),
+                                braille.getInt("selectionStart"), braille.getInt("selectionEnd"));
             }
 
             if (eventType == AccessibilityEvent.TYPE_VIEW_HOVER_ENTER) {
                 sHoverEnter = message;
             }
 
             ThreadUtils.postToUiThread(new Runnable() {
                     @Override
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoThread.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoThread.java
@@ -546,23 +546,17 @@ public class GeckoThread extends Thread 
         }
 
         // And go.
         GeckoLoader.nativeRun(args, mCrashFileDescriptor, mIPCFileDescriptor);
 
         // And... we're done.
         setState(State.EXITED);
 
-        try {
-            final JSONObject msg = new JSONObject();
-            msg.put("type", "Gecko:Exited");
-            EventDispatcher.getInstance().dispatchEvent(msg, null);
-        } catch (final JSONException e) {
-            Log.e(LOGTAG, "unable to dispatch event", e);
-        }
+        EventDispatcher.getInstance().dispatch("Gecko:Exited", null);
 
         // Remove pumpMessageLoop() idle handler
         Looper.myQueue().removeIdleHandler(idleHandler);
     }
 
     @WrapForJNI(calledFrom = "gecko")
     private static boolean pumpMessageLoop(final Message msg) {
         final Handler geckoHandler = ThreadUtils.sGeckoHandler;
--- a/mobile/android/modules/LightweightThemeConsumer.jsm
+++ b/mobile/android/modules/LightweightThemeConsumer.jsm
@@ -3,16 +3,20 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 var EXPORTED_SYMBOLS = ["LightweightThemeConsumer"];
 var Cc = Components.classes;
 var Ci = Components.interfaces;
 
 Components.utils.import("resource://gre/modules/Services.jsm");
 Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "EventDispatcher",
+                                  "resource://gre/modules/Messaging.jsm");
 
 function LightweightThemeConsumer(aDocument) {
   this._doc = aDocument;
   Services.obs.addObserver(this, "lightweight-theme-styling-update", false);
   Services.obs.addObserver(this, "lightweight-theme-apply", false);
 
   this._update(LightweightThemeManager.currentThemeForDisplay);
 }
@@ -34,11 +38,11 @@ LightweightThemeConsumer.prototype = {
   _update: function (aData) {
     if (!aData)
       aData = { headerURL: "", footerURL: "", textcolor: "", accentcolor: "" };
 
     let active = !!aData.headerURL;
 
     let msg = active ? { type: "LightweightTheme:Update", data: aData } :
                        { type: "LightweightTheme:Disable" };
-    Services.androidBridge.handleGeckoMessage(msg);
+    EventDispatcher.instance.sendRequest(msg);
   }
 }
--- a/mobile/android/modules/RuntimePermissions.jsm
+++ b/mobile/android/modules/RuntimePermissions.jsm
@@ -3,16 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 this.EXPORTED_SYMBOLS = ["RuntimePermissions"];
 
+Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Messaging", "resource://gre/modules/Messaging.jsm");
 
 // See: http://developer.android.com/reference/android/Manifest.permission.html
 const CAMERA = "android.permission.CAMERA";
 const WRITE_EXTERNAL_STORAGE = "android.permission.WRITE_EXTERNAL_STORAGE";
 const RECORD_AUDIO = "android.permission.RECORD_AUDIO";
@@ -31,11 +32,12 @@ var RuntimePermissions = {
   waitForPermissions: function(permission) {
     let permissions = [].concat(permission);
 
     let msg = {
       type: 'RuntimePermissions:Prompt',
       permissions: permissions
     };
 
-    return Messaging.sendRequestForResult(msg);
+    let window = Services.wm.getMostRecentWindow("navigator:browser");
+    return window.WindowEventDispatcher.sendRequestForResult(msg);
   }
-};
\ No newline at end of file
+};
--- a/mobile/android/modules/Sanitizer.jsm
+++ b/mobile/android/modules/Sanitizer.jsm
@@ -8,24 +8,25 @@
 var Cc = Components.classes;
 var Ci = Components.interfaces;
 var Cu = Components.utils;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/LoadContextInfo.jsm");
 Cu.import("resource://gre/modules/FormHistory.jsm");
-Cu.import("resource://gre/modules/Messaging.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/Downloads.jsm");
 Cu.import("resource://gre/modules/osfile.jsm");
 Cu.import("resource://gre/modules/Accounts.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "DownloadIntegration",
                                   "resource://gre/modules/DownloadIntegration.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "EventDispatcher",
+                                  "resource://gre/modules/Messaging.jsm");
 
 function dump(a) {
   Services.console.logStringMessage(a);
 }
 
 this.EXPORTED_SYMBOLS = ["Sanitizer"];
 
 function Sanitizer() {}
@@ -139,17 +140,17 @@ Sanitizer.prototype = {
       {
           return true;
       }
     },
 
     history: {
       clear: function ()
       {
-        return Messaging.sendRequestForResult({ type: "Sanitize:ClearHistory" })
+        return EventDispatcher.instance.sendRequestForResult({ type: "Sanitize:ClearHistory" })
           .catch(e => Cu.reportError("Java-side history clearing failed: " + e))
           .then(function() {
             try {
               Services.obs.notifyObservers(null, "browser:purge-session-history", "");
             }
             catch (e) { }
 
             try {
@@ -165,17 +166,17 @@ Sanitizer.prototype = {
         // the browser:purge-session-history notification. (like error console)
         return true;
       }
     },
 
     searchHistory: {
       clear: function ()
       {
-        return Messaging.sendRequestForResult({ type: "Sanitize:ClearHistory", clearSearchHistory: true })
+        return EventDispatcher.instance.sendRequestForResult({ type: "Sanitize:ClearHistory", clearSearchHistory: true })
           .catch(e => Cu.reportError("Java-side search history clearing failed: " + e))
       },
 
       get canClear()
       {
         return true;
       }
     },
@@ -278,17 +279,17 @@ Sanitizer.prototype = {
       {
         return true;
       }
     },
 
     syncedTabs: {
       clear: function ()
       {
-        return Messaging.sendRequestForResult({ type: "Sanitize:ClearSyncedTabs" })
+        return EventDispatcher.instance.sendRequestForResult({ type: "Sanitize:ClearSyncedTabs" })
           .catch(e => Cu.reportError("Java-side synced tabs clearing failed: " + e));
       },
 
       canClear: function(aCallback)
       {
         Accounts.anySyncAccountsExist().then(aCallback)
           .catch(function(err) {
             Cu.reportError("Java-side synced tabs clearing failed: " + err)
--- a/mobile/android/modules/Snackbars.jsm
+++ b/mobile/android/modules/Snackbars.jsm
@@ -5,17 +5,17 @@
 "use strict";
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 this.EXPORTED_SYMBOLS = ["Snackbars"];
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "Messaging", "resource://gre/modules/Messaging.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "EventDispatcher", "resource://gre/modules/Messaging.jsm");
 
 const LENGTH_INDEFINITE = -2;
 const LENGTH_LONG = 0;
 const LENGTH_SHORT = -1;
 
 var Snackbars = {
   LENGTH_INDEFINITE: LENGTH_INDEFINITE,
   LENGTH_LONG: LENGTH_LONG,
@@ -40,19 +40,19 @@ var Snackbars = {
 
     if (aOptions && aOptions.action) {
       msg.action = {};
 
       if (aOptions.action.label) {
         msg.action.label = aOptions.action.label;
       }
 
-      Messaging.sendRequestForResult(msg).then(result => aOptions.action.callback());
+      EventDispatcher.instance.sendRequestForResult(msg).then(result => aOptions.action.callback());
     } else {
-      Messaging.sendRequest(msg);
+      EventDispatcher.instance.sendRequest(msg);
     }
   }
 };
 
 function migrateToastIfNeeded(aDuration, aOptions) {
   let duration;
   if (aDuration === "long") {
     duration = LENGTH_LONG;
@@ -64,9 +64,9 @@ function migrateToastIfNeeded(aDuration,
   let options = {};
   if (aOptions && aOptions.button) {
     options.action = {
       label: aOptions.button.label,
       callback: () => aOptions.button.callback(),
     };
   }
   return [duration, options];
-}
\ No newline at end of file
+}
--- a/mobile/android/modules/WebsiteMetadata.jsm
+++ b/mobile/android/modules/WebsiteMetadata.jsm
@@ -5,17 +5,17 @@
 'use strict';
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 this.EXPORTED_SYMBOLS = ["WebsiteMetadata"];
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "Messaging", "resource://gre/modules/Messaging.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "EventDispatcher", "resource://gre/modules/Messaging.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
 
 var WebsiteMetadata = {
   /**
    * Asynchronously parse the document extract metadata. A 'Website:Metadata' event with the metadata
    * will be sent.
    */
   parseAsynchronously: function(doc) {
@@ -27,20 +27,21 @@ var WebsiteMetadata = {
       // No metadata was extracted, so don't bother sending it.
       if (Object.keys(metadata).length === 0) {
         return;
       }
 
       let msg = {
         type: 'Website:Metadata',
         location: doc.location.href,
-        metadata: metadata,
+        hasImage: metadata.image_url && metadata.image_url !== "",
+        metadata: JSON.stringify(metadata),
       };
 
-      Messaging.sendRequest(msg);
+      EventDispatcher.instance.sendRequest(msg);
     });
   }
 };
 
 // #################################################################################################
 // # Modified version of makeUrlAbsolute() to not import url parser library (and dependencies)
 // #################################################################################################
 
--- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/BaseTest.java
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/BaseTest.java
@@ -63,27 +63,29 @@ abstract class BaseTest extends BaseRobo
     public Device mDevice;
     protected DatabaseHelper mDatabaseHelper;
     protected int mScreenMidWidth;
     protected int mScreenMidHeight;
     private final HashSet<Integer> mKnownTabIDs = new HashSet<Integer>();
 
     protected void blockForDelayedStartup() {
         try {
-            Actions.EventExpecter delayedStartupExpector = mActions.expectGeckoEvent("Gecko:DelayedStartup");
+            Actions.EventExpecter delayedStartupExpector =
+                    mActions.expectGlobalEvent(Actions.EventType.UI, "Gecko:DelayedStartup");
             delayedStartupExpector.blockForEvent(GECKO_READY_WAIT_MS, true);
             delayedStartupExpector.unregisterListener();
         } catch (Exception e) {
             mAsserter.dumpLog("Exception in blockForDelayedStartup", e);
         }
     }
 
     protected void blockForGeckoReady() {
         try {
-            Actions.EventExpecter geckoReadyExpector = mActions.expectGeckoEvent("Gecko:Ready");
+            Actions.EventExpecter geckoReadyExpector =
+                    mActions.expectGlobalEvent(Actions.EventType.GECKO, "Gecko:Ready");
             if (!GeckoThread.isRunning()) {
                 geckoReadyExpector.blockForEvent(GECKO_READY_WAIT_MS, true);
             }
             geckoReadyExpector.unregisterListener();
         } catch (Exception e) {
             mAsserter.dumpLog("Exception in blockForGeckoReady", e);
         }
     }
--- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/ContentContextMenuTest.java
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/ContentContextMenuTest.java
@@ -43,17 +43,17 @@ abstract class ContentContextMenuTest ex
             mAsserter.ok(mSolo.searchText(option), "Checking that the option: " + option + " is available", "The option is available");
         }
     }
 
     protected void openTabFromContextMenu(String contextMenuOption, int expectedTabCount) {
         if (!mSolo.searchText(contextMenuOption)) {
             openWebContentContextMenu(contextMenuOption); // Open the context menu if it is not already
         }
-        Actions.EventExpecter tabEventExpecter = mActions.expectGeckoEvent("Tab:Added");
+        Actions.EventExpecter tabEventExpecter = mActions.expectGlobalEvent(Actions.EventType.UI, "Tab:Added");
         mSolo.clickOnText(contextMenuOption);
         tabEventExpecter.blockForEvent();
         tabEventExpecter.unregisterListener();
         verifyTabCount(expectedTabCount);
     }
 
     protected void verifyTabs(String[] items) {
         if (!mSolo.searchText(items[0])) {
--- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/GeckoHelper.java
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/helpers/GeckoHelper.java
@@ -21,29 +21,29 @@ public final class GeckoHelper {
     private GeckoHelper() { /* To disallow instantiation. */ }
 
     protected static void init(final UITestContext context) {
         sActivity = context.getActivity();
         sActions = context.getActions();
     }
 
     public static void blockForReady() {
-        blockForEvent("Gecko:Ready");
+        blockForEvent(Actions.EventType.GECKO, "Gecko:Ready");
     }
 
     /**
      * Blocks for the "Gecko:DelayedStartup" event, which occurs after "Gecko:Ready" and the
      * first page load.
      */
     public static void blockForDelayedStartup() {
-        blockForEvent("Gecko:DelayedStartup");
+        blockForEvent(Actions.EventType.UI, "Gecko:DelayedStartup");
     }
 
-    private static void blockForEvent(final String eventName) {
-        final EventExpecter eventExpecter = sActions.expectGeckoEvent(eventName);
+    private static void blockForEvent(final Actions.EventType type, final String eventName) {
+        final EventExpecter eventExpecter = sActions.expectGlobalEvent(type, eventName);
 
         if (!GeckoThread.isRunning()) {
             eventExpecter.blockForEvent();
         }
 
         eventExpecter.unregisterListener();
     }
 }
--- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAboutPage.java
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAboutPage.java
@@ -24,17 +24,17 @@ public class testAboutPage extends Pixel
         // Open a new page to remove the about: page from the current tab.
         url = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL);
         loadUrlAndWait(url);
 
         // At this point the page title should have been set.
         verifyUrlInContentDescription(url);
 
         // Set up listeners to catch the page load we're about to do.
-        Actions.EventExpecter tabEventExpecter = mActions.expectGeckoEvent("Tab:Added");
+        Actions.EventExpecter tabEventExpecter = mActions.expectGlobalEvent(Actions.EventType.UI, "Tab:Added");
         Actions.EventExpecter contentEventExpecter = mActions.expectGeckoEvent("DOMContentLoaded");
 
         selectSettingsItem(mStringHelper.MOZILLA_SECTION_LABEL, mStringHelper.ABOUT_LABEL);
 
         // Wait for the new tab and page to load
         tabEventExpecter.blockForEvent();
         contentEventExpecter.blockForEvent();
 
--- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAddonManager.java
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAddonManager.java
@@ -24,17 +24,17 @@ public class testAddonManager extends Pi
         final String aboutAddonsURL = mStringHelper.ABOUT_ADDONS_URL;
 
         blockForGeckoReady();
 
         // Use the menu to open the Addon Manger
         selectMenuItem(mStringHelper.ADDONS_LABEL);
 
         // Set up listeners to catch the page load we're about to do
-        tabEventExpecter = mActions.expectGeckoEvent("Tab:Added");
+        tabEventExpecter = mActions.expectGlobalEvent(Actions.EventType.UI, "Tab:Added");
         contentEventExpecter = mActions.expectGeckoEvent("DOMContentLoaded");
 
         // Wait for the new tab and page to load
         tabEventExpecter.blockForEvent();
         contentEventExpecter.blockForEvent();
 
         tabEventExpecter.unregisterListener();
         contentEventExpecter.unregisterListener();
@@ -45,17 +45,17 @@ public class testAddonManager extends Pi
         // Close the Add-on Manager
         mSolo.goBack();
 
         // Load the about:addons page and verify it was loaded
         loadAndPaint(aboutAddonsURL);
         verifyUrlBarTitle(aboutAddonsURL);
 
         // Setup wait for tab to spawn and load
-        tabEventExpecter = mActions.expectGeckoEvent("Tab:Added");
+        tabEventExpecter = mActions.expectGlobalEvent(Actions.EventType.UI, "Tab:Added");
         contentEventExpecter = mActions.expectGeckoEvent("DOMContentLoaded");
 
         // Open a new tab
         final String blankURL = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL);
         addTab(blankURL);
 
         // Wait for the new tab and page to load
         tabEventExpecter.blockForEvent();
--- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarksPanel.java
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testBookmarksPanel.java
@@ -1,25 +1,24 @@
 /* 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.gecko.tests;
 
-import org.json.JSONException;
-import org.json.JSONObject;
 import org.mozilla.gecko.Actions;
 import org.mozilla.gecko.Element;
 import org.mozilla.gecko.R;
+import org.mozilla.gecko.util.GeckoBundle;
 import org.mozilla.gecko.util.StringUtils;
 
 public class testBookmarksPanel extends AboutHomeTest {
     public void testBookmarksPanel() {
         final String BOOKMARK_URL = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL);
-        JSONObject data = null;
+        GeckoBundle data = null;
 
         // Make sure our default bookmarks are loaded.
         // Technically this will race with the check below.
         initializeProfile();
 
         // Add a mobile bookmark.
         mDatabaseHelper.addMobileBookmark(mStringHelper.ROBOCOP_BLANK_PAGE_01_TITLE, BOOKMARK_URL);
 
@@ -35,53 +34,37 @@ public class testBookmarksPanel extends 
         assertAllContextMenuOptionsArePresent(mStringHelper.DEFAULT_BOOKMARKS_URLS[1],
                 mStringHelper.DEFAULT_BOOKMARKS_URLS[0]);
 
         openBookmarkContextMenu(mStringHelper.DEFAULT_BOOKMARKS_URLS[0]);
 
         // Test that "Open in New Tab" works
         final Element tabCount = mDriver.findElement(getActivity(), R.id.tabs_counter);
         final int tabCountInt = Integer.parseInt(tabCount.getText());
-        Actions.EventExpecter tabEventExpecter = mActions.expectGeckoEvent("Tab:Added");
+        Actions.EventExpecter tabEventExpecter = mActions.expectGlobalEvent(Actions.EventType.UI, "Tab:Added");
         mSolo.clickOnText(mStringHelper.BOOKMARK_CONTEXT_MENU_ITEMS[0]);
-        try {
-            data = new JSONObject(tabEventExpecter.blockForEventData());
-        } catch (JSONException e) {
-            mAsserter.ok(false, "exception getting event data", e.toString());
-        }
+
+        data = tabEventExpecter.blockForBundle();
         tabEventExpecter.unregisterListener();
         mAsserter.ok(mSolo.searchText(mStringHelper.TITLE_PLACE_HOLDER), "Checking that the tab is not changed", "The tab was not changed");
         // extra check here on the Tab:Added message to be sure the right tab opened
-        int tabID = 0;
-        try {
-            mAsserter.is(mStringHelper.ABOUT_FIREFOX_URL, data.getString("uri"), "Checking tab uri");
-            tabID = data.getInt("tabID");
-        } catch (JSONException e) {
-            mAsserter.ok(false, "exception accessing event data", e.toString());
-        }
+        mAsserter.is(mStringHelper.ABOUT_FIREFOX_URL, data.getString("uri"), "Checking tab uri");
         // close tab so about:firefox can be selected again
-        closeTab(tabID);
+        closeTab(data.getInt("tabID"));
 
         // Test that "Open in Private Tab" works
         openBookmarkContextMenu(mStringHelper.DEFAULT_BOOKMARKS_URLS[0]);
-        tabEventExpecter = mActions.expectGeckoEvent("Tab:Added");
+        tabEventExpecter = mActions.expectGlobalEvent(Actions.EventType.UI, "Tab:Added");
         mSolo.clickOnText(mStringHelper.BOOKMARK_CONTEXT_MENU_ITEMS[1]);
-        try {
-            data = new JSONObject(tabEventExpecter.blockForEventData());
-        } catch (JSONException e) {
-            mAsserter.ok(false, "exception getting event data", e.toString());
-        }
+
+        data = tabEventExpecter.blockForBundle();
         tabEventExpecter.unregisterListener();
         mAsserter.ok(mSolo.searchText(mStringHelper.TITLE_PLACE_HOLDER), "Checking that the tab is not changed", "The tab was not changed");
         // extra check here on the Tab:Added message to be sure the right tab opened, again
-        try {
-            mAsserter.is(mStringHelper.ABOUT_FIREFOX_URL, data.getString("uri"), "Checking tab uri");
-        } catch (JSONException e) {
-            mAsserter.ok(false, "exception accessing event data", e.toString());
-        }
+        mAsserter.is(mStringHelper.ABOUT_FIREFOX_URL, data.getString("uri"), "Checking tab uri");
 
         // Test that "Edit" works
         String[] editedBookmarkValues = new String[] { "New bookmark title", "www.NewBookmark.url", "newBookmarkKeyword" };
         editBookmark(BOOKMARK_URL, editedBookmarkValues);
         checkBookmarkEdit(editedBookmarkValues[1], editedBookmarkValues);
 
         // Test that "Remove" works
         openBookmarkContextMenu(editedBookmarkValues[1]);
--- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testDoorHanger.java
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testDoorHanger.java
@@ -2,20 +2,18 @@
  * 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.gecko.tests;
 
 import android.widget.CheckBox;
 import android.view.View;
 import com.robotium.solo.Condition;
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
 import org.mozilla.gecko.Actions;
+import org.mozilla.gecko.util.GeckoBundle;
 
 /* This test will test if doorhangers are displayed and dismissed
    The test will test:
    * geolocation doorhangers - sharing and not sharing the location dismisses the doorhanger
    * opening a new tab hides the doorhanger
    * offline storage permission doorhangers - allowing and not allowing offline storage dismisses the doorhanger
    * Password Manager doorhangers - Remember and Not Now options dismiss the doorhanger
 */
@@ -128,36 +126,32 @@ public class testDoorHanger extends Base
         setPreferenceAndWaitForChange("dom.disable_open_during_load", true);
 
         // Load page with popup
         loadUrlAndWait(POPUP_URL);
         waitForText(mStringHelper.POPUP_MESSAGE);
         mAsserter.is(mSolo.searchText(mStringHelper.POPUP_MESSAGE), true, "Popup blocker is displayed");
 
         // Wait for the popup to be shown.
-        Actions.EventExpecter tabEventExpecter = mActions.expectGeckoEvent("Tab:Added");
+        Actions.EventExpecter tabEventExpecter = mActions.expectGlobalEvent(Actions.EventType.UI, "Tab:Added");
 
         waitForCheckBox();
         mSolo.clickOnCheckBox(0);
         mSolo.clickOnButton(mStringHelper.POPUP_ALLOW);
         waitForTextDismissed(mStringHelper.POPUP_MESSAGE);
         mAsserter.is(mSolo.searchText(mStringHelper.POPUP_MESSAGE), false, "Popup blocker is hidden when popup allowed");
 
-        try {
-            final JSONObject data = new JSONObject(tabEventExpecter.blockForEventData());
-
-            // Check to make sure the popup window was opened.
-            mAsserter.is("data:text/plain;charset=utf-8,a", data.getString("uri"), "Checking popup URL");
+        final GeckoBundle data = tabEventExpecter.blockForBundle();
 
-            // Close the popup window.
-            closeTab(data.getInt("tabID"));
+        // Check to make sure the popup window was opened.
+        mAsserter.is("data:text/plain;charset=utf-8,a", data.getString("uri"), "Checking popup URL");
 
-        } catch (JSONException e) {
-            mAsserter.ok(false, "exception getting event data", e.toString());
-        }
+        // Close the popup window.
+        closeTab(data.getInt("tabID"));
+
         tabEventExpecter.unregisterListener();
 
         // Load page with popup
         loadUrlAndWait(POPUP_URL);
         waitForText(mStringHelper.POPUP_MESSAGE);
         mAsserter.is(mSolo.searchText(mStringHelper.POPUP_MESSAGE), true, "Popup blocker is displayed");
 
         waitForCheckBox();
--- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPrivateBrowsing.java
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testPrivateBrowsing.java
@@ -1,20 +1,19 @@
 /* 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.gecko.tests;
 
 import java.util.ArrayList;
 
-import org.json.JSONException;
-import org.json.JSONObject;
 import org.mozilla.gecko.Actions;
 import org.mozilla.gecko.Tabs;
+import org.mozilla.gecko.util.GeckoBundle;
 
 /**
  * The test loads a new private tab and loads a page with a big link on it
  * Opens the link in a new private tab and checks that it is private
  * Adds a new normal tab and loads a 3rd URL
  * Checks that the bigLinkUrl loaded in the normal tab is present in the browsing history but the 2 urls opened in private tabs are not
  */
 public class testPrivateBrowsing extends ContentContextMenuTest {
@@ -22,17 +21,17 @@ public class testPrivateBrowsing extends
     public void testPrivateBrowsing() {
         String bigLinkUrl = getAbsoluteUrl(mStringHelper.ROBOCOP_BIG_LINK_URL);
         String blank1Url = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_01_URL);
         String blank2Url = getAbsoluteUrl(mStringHelper.ROBOCOP_BLANK_PAGE_02_URL);
         Tabs tabs = Tabs.getInstance();
 
         blockForGeckoReady();
 
-        Actions.EventExpecter tabExpecter = mActions.expectGeckoEvent("Tab:Added");
+        Actions.EventExpecter tabExpecter = mActions.expectGlobalEvent(Actions.EventType.UI, "Tab:Added");
         Actions.EventExpecter contentExpecter = mActions.expectGeckoEvent("Content:PageShow");
         tabs.loadUrl(bigLinkUrl, Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_PRIVATE);
         tabExpecter.blockForEvent();
         tabExpecter.unregisterListener();
         contentExpecter.blockForEvent();
         contentExpecter.unregisterListener();
         verifyTabCount(1);
 
@@ -41,28 +40,29 @@ public class testPrivateBrowsing extends
 
         // Open the link context menu and verify the options
         verifyContextMenuItems(mStringHelper.CONTEXT_MENU_ITEMS_IN_PRIVATE_TAB);
 
         // Check that "Open Link in New Tab" is not in the menu
         mAsserter.ok(!mSolo.searchText(mStringHelper.CONTEXT_MENU_ITEMS_IN_NORMAL_TAB[0]), "Checking that 'Open Link in New Tab' is not displayed in the context menu", "'Open Link in New Tab' is not displayed in the context menu");
 
         // Open the link in a new private tab and check that it is private
-        tabExpecter = mActions.expectGeckoEvent("Tab:Added");
+        tabExpecter = mActions.expectGlobalEvent(Actions.EventType.UI, "Tab:Added");
         contentExpecter = mActions.expectGeckoEvent("Content:PageShow");
         mSolo.clickOnText(mStringHelper.CONTEXT_MENU_ITEMS_IN_PRIVATE_TAB[0]);
-        String eventData = tabExpecter.blockForEventData();
+
+        final GeckoBundle eventData = tabExpecter.blockForBundle();
         tabExpecter.unregisterListener();
         contentExpecter.blockForEvent();
         contentExpecter.unregisterListener();
-        mAsserter.ok(isTabPrivate(eventData), "Checking if the new tab opened from the context menu was a private tab", "The tab was a private tab");
+        mAsserter.ok(eventData.getBoolean("isPrivate"), "Checking if the new tab opened from the context menu was a private tab", "The tab was a private tab");
         verifyTabCount(2);
 
         // Open a normal tab to check later that it was registered in the Firefox Browser History
-        tabExpecter = mActions.expectGeckoEvent("Tab:Added");
+        tabExpecter = mActions.expectGlobalEvent(Actions.EventType.UI, "Tab:Added");
         contentExpecter = mActions.expectGeckoEvent("Content:PageShow");
         tabs.loadUrl(blank2Url, Tabs.LOADURL_NEW_TAB);
         tabExpecter.blockForEvent();
         tabExpecter.unregisterListener();
         contentExpecter.blockForEvent();
         contentExpecter.unregisterListener();
         verifyTabCount(2);
 
@@ -71,19 +71,9 @@ public class testPrivateBrowsing extends
 
         // Get the history list and check that the links open in private browsing are not saved
         final ArrayList<String> firefoxHistory = mDatabaseHelper.getBrowserDBUrls(DatabaseHelper.BrowserDataType.HISTORY);
 
         mAsserter.ok(!firefoxHistory.contains(bigLinkUrl), "Check that the link opened in the first private tab was not saved", bigLinkUrl + " was not added to history");
         mAsserter.ok(!firefoxHistory.contains(blank1Url), "Check that the link opened in the private tab from the context menu was not saved", blank1Url + " was not added to history");
         mAsserter.ok(firefoxHistory.contains(blank2Url), "Check that the link opened in the normal tab was saved", blank2Url + " was added to history");
     }
-
-    private boolean isTabPrivate(String eventData) {
-        try {
-            JSONObject data = new JSONObject(eventData);
-            return data.getBoolean("isPrivate");
-        } catch (JSONException e) {
-            mAsserter.ok(false, "Error parsing the event data", e.toString());
-            return false;
-        }
-    }
 }
--- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testRuntimePermissionsAPI.java
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testRuntimePermissionsAPI.java
@@ -1,42 +1,43 @@
 /* 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.gecko.tests;
 
 import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.GeckoApp;
+import org.mozilla.gecko.util.BundleEventListener;
 import org.mozilla.gecko.util.EventCallback;
-import org.mozilla.gecko.util.NativeEventListener;
-import org.mozilla.gecko.util.NativeJSObject;
+import org.mozilla.gecko.util.GeckoBundle;
 
 import static org.mozilla.gecko.tests.helpers.AssertionHelper.fFail;
 
-public class testRuntimePermissionsAPI extends JavascriptTest implements NativeEventListener {
+public class testRuntimePermissionsAPI extends JavascriptTest implements BundleEventListener {
     public testRuntimePermissionsAPI() {
         super("testRuntimePermissionsAPI.js");
     }
 
     @Override
     public void setUp() throws Exception {
         super.setUp();
 
-        EventDispatcher.getInstance().registerGeckoThreadListener(this, "RuntimePermissions:Prompt");
+        GeckoApp.getEventDispatcher().registerUiThreadListener(this, "RuntimePermissions:Prompt");
     }
 
     @Override
     public void tearDown() throws Exception {
         super.tearDown();
 
-        EventDispatcher.getInstance().unregisterGeckoThreadListener(this, "RuntimePermissions:Prompt");
+        GeckoApp.getEventDispatcher().unregisterUiThreadListener(this, "RuntimePermissions:Prompt");
     }
 
     @Override
-    public void handleMessage(String event, NativeJSObject message, EventCallback callback) {
+    public void handleMessage(String event, GeckoBundle message, EventCallback callback) {
         mAsserter.is(event, "RuntimePermissions:Prompt", "Received RuntimePermissions:Prompt event");
 
         try {
             String[] permissions = message.getStringArray("permissions");
             mAsserter.is(3, permissions.length, "Received three permissions");
 
             mAsserter.is("android.permission.CAMERA", permissions[0], "Received CAMERA permission");
             mAsserter.is("android.permission.WRITE_EXTERNAL_STORAGE", permissions[1], "Received WRITE_EXTERNAL_STORAGE permission");
--- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSnackbarAPI.java
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testSnackbarAPI.java
@@ -2,51 +2,58 @@
  * 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.gecko.tests;
 
 import static org.mozilla.gecko.tests.helpers.AssertionHelper.fFail;
 
 import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.util.BundleEventListener;
 import org.mozilla.gecko.util.EventCallback;
-import org.mozilla.gecko.util.NativeEventListener;
-import org.mozilla.gecko.util.NativeJSObject;
+import org.mozilla.gecko.util.GeckoBundle;
 
-public class testSnackbarAPI extends JavascriptTest implements NativeEventListener {
+public class testSnackbarAPI extends JavascriptTest implements BundleEventListener {
     // Snackbar.LENGTH_INDEFINITE: To avoid tests depending on the android design support library
     private static final int  SNACKBAR_LENGTH_INDEFINITE = -2;
 
     public testSnackbarAPI() {
         super("testSnackbarAPI.js");
     }
 
     @Override
-    public void handleMessage(String event, NativeJSObject message, EventCallback callback) {
+    public void handleMessage(String event, GeckoBundle message, EventCallback callback) {
+        if ("Robocop:WaitOnUI".equals(event)) {
+            callback.sendSuccess(null);
+            return;
+        }
+
         mAsserter.is(event, "Snackbar:Show", "Received Snackbar:Show event");
 
         try {
             mAsserter.is(message.getString("message"), "This is a Snackbar", "Snackbar message");
             mAsserter.is(message.getInt("duration"), SNACKBAR_LENGTH_INDEFINITE, "Snackbar duration");
 
-            NativeJSObject action = message.getObject("action");
+            GeckoBundle action = message.getBundle("action");
 
             mAsserter.is(action.getString("label"), "Click me", "Snackbar action label");
 
         } catch (Exception e) {
             fFail("Event does not contain expected data: " + e.getMessage());
         }
     }
 
     @Override
     public void setUp() throws Exception {
         super.setUp();
 
-        EventDispatcher.getInstance().registerGeckoThreadListener(this, "Snackbar:Show");
+        EventDispatcher.getInstance().registerUiThreadListener(this, "Snackbar:Show");
+        EventDispatcher.getInstance().registerUiThreadListener(this, "Robocop:WaitOnUI");
     }
 
     @Override
     public void tearDown() throws Exception {
         super.tearDown();
 
-        EventDispatcher.getInstance().unregisterGeckoThreadListener(this, "Snackbar:Show");
+        EventDispatcher.getInstance().unregisterUiThreadListener(this, "Snackbar:Show");
+        EventDispatcher.getInstance().unregisterUiThreadListener(this, "Robocop:WaitOnUI");
     }
 }
--- a/mobile/android/tests/browser/robocop/testSnackbarAPI.js
+++ b/mobile/android/tests/browser/robocop/testSnackbarAPI.js
@@ -3,19 +3,24 @@
  * 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/. */
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Snackbars", "resource://gre/modules/Snackbars.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "EventDispatcher", "resource://gre/modules/Messaging.jsm");
 
 add_task(function* test_snackbar_api() {
   Snackbars.show("This is a Snackbar", Snackbars.LENGTH_INDEFINITE, {
     action: {
       label: "Click me",
       callback: function () {}
     }
   });
+
+  yield EventDispatcher.instance.sendRequestForResult({
+    type: "Robocop:WaitOnUI"
+  });
 });
 
 run_next_test();
--- a/toolkit/content/aboutTelemetry.js
+++ b/toolkit/content/aboutTelemetry.js
@@ -229,17 +229,17 @@ var Settings = {
       Preferences.observe(setting.pref, this.render, this);
     }
 
     let elements = document.getElementsByClassName("change-data-choices-link");
     for (let el of elements) {
       el.addEventListener("click", function() {
         if (AppConstants.platform == "android") {
           Cu.import("resource://gre/modules/Messaging.jsm");
-          Messaging.sendRequest({
+          EventDispatcher.instance.sendRequest({
             type: "Settings:Show",
             resource: "preferences_privacy",
           });
         } else {
           // Show the data choices preferences on desktop.
           let mainWindow = getMainWindowWithPreferencesPane();
           mainWindow.openAdvancedPreferences("dataChoicesTab");
         }