Backed out changeset 3e80b5052d0b (bug 1257319)
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Wed, 23 Mar 2016 10:55:10 +0100
changeset 290076 71cdfc0d1448cfed917140391d86799d85920753
parent 290075 f7d63892ad2c2dc18314ea6ba5f9c7db2f90cc3d
child 290077 88dbd01499c0463a55f25661e562bc17ca7f941d
push id19656
push usergwagner@mozilla.com
push dateMon, 04 Apr 2016 13:43:23 +0000
treeherderb2g-inbound@e99061fde28a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs1257319
milestone48.0a1
backs out3e80b5052d0b33bdbdeb7b670d38bfeea03ac7ae
Backed out changeset 3e80b5052d0b (bug 1257319)
mobile/android/base/java/org/mozilla/gecko/BaseGeckoInterface.java
mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
mobile/android/base/java/org/mozilla/gecko/BrowserLocaleManager.java
mobile/android/base/java/org/mozilla/gecko/ChromeCast.java
mobile/android/base/java/org/mozilla/gecko/ContactService.java
mobile/android/base/java/org/mozilla/gecko/DoorHangerPopup.java
mobile/android/base/java/org/mozilla/gecko/EventDispatcher.java
mobile/android/base/java/org/mozilla/gecko/FilePicker.java
mobile/android/base/java/org/mozilla/gecko/FindInPageBar.java
mobile/android/base/java/org/mozilla/gecko/FormAssistPopup.java
mobile/android/base/java/org/mozilla/gecko/GeckoAccessibility.java
mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
mobile/android/base/java/org/mozilla/gecko/GeckoAppShell.java
mobile/android/base/java/org/mozilla/gecko/GeckoMessageReceiver.java
mobile/android/base/java/org/mozilla/gecko/GeckoView.java
mobile/android/base/java/org/mozilla/gecko/MediaCastingBar.java
mobile/android/base/java/org/mozilla/gecko/MediaPlayerManager.java
mobile/android/base/java/org/mozilla/gecko/MemoryMonitor.java
mobile/android/base/java/org/mozilla/gecko/NotificationHelper.java
mobile/android/base/java/org/mozilla/gecko/OrderedBroadcastHelper.java
mobile/android/base/java/org/mozilla/gecko/SharedPreferencesHelper.java
mobile/android/base/java/org/mozilla/gecko/Tab.java
mobile/android/base/java/org/mozilla/gecko/Tabs.java
mobile/android/base/java/org/mozilla/gecko/TextSelection.java
mobile/android/base/java/org/mozilla/gecko/TextSelectionHandle.java
mobile/android/base/java/org/mozilla/gecko/ZoomedView.java
mobile/android/base/java/org/mozilla/gecko/db/FormHistoryProvider.java
mobile/android/base/java/org/mozilla/gecko/db/LocalReadingListAccessor.java
mobile/android/base/java/org/mozilla/gecko/distribution/Distribution.java
mobile/android/base/java/org/mozilla/gecko/distribution/ReferrerReceiver.java
mobile/android/base/java/org/mozilla/gecko/dlc/DownloadContentService.java
mobile/android/base/java/org/mozilla/gecko/gfx/GeckoLayerClient.java
mobile/android/base/java/org/mozilla/gecko/gfx/JavaPanZoomController.java
mobile/android/base/java/org/mozilla/gecko/gfx/SubdocumentScrollHelper.java
mobile/android/base/java/org/mozilla/gecko/home/BrowserSearch.java
mobile/android/base/java/org/mozilla/gecko/home/HistoryPanel.java
mobile/android/base/java/org/mozilla/gecko/home/HomeBanner.java
mobile/android/base/java/org/mozilla/gecko/home/HomeFragment.java
mobile/android/base/java/org/mozilla/gecko/home/PanelAuthLayout.java
mobile/android/base/java/org/mozilla/gecko/home/PanelInfoManager.java
mobile/android/base/java/org/mozilla/gecko/home/PanelRefreshLayout.java
mobile/android/base/java/org/mozilla/gecko/home/RecentTabsPanel.java
mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
mobile/android/base/java/org/mozilla/gecko/preferences/PrivateDataPreference.java
mobile/android/base/java/org/mozilla/gecko/preferences/SearchPreferenceCategory.java
mobile/android/base/java/org/mozilla/gecko/push/PushService.java
mobile/android/base/java/org/mozilla/gecko/reader/ReadingListHelper.java
mobile/android/base/java/org/mozilla/gecko/tabqueue/TabQueueHelper.java
mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryFragment.java
mobile/android/base/java/org/mozilla/gecko/tabs/TabsGridLayout.java
mobile/android/base/java/org/mozilla/gecko/tabs/TabsListLayout.java
mobile/android/base/java/org/mozilla/gecko/toolbar/PageActionLayout.java
mobile/android/base/java/org/mozilla/gecko/toolbar/SiteIdentityPopup.java
mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecNativeActions.java
mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/BaseRobocopTest.java
mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAccessibleCarets.java
mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFilePicker.java
--- a/mobile/android/base/java/org/mozilla/gecko/BaseGeckoInterface.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BaseGeckoInterface.java
@@ -135,15 +135,15 @@ public class BaseGeckoInterface implemen
     // Bug 908791: Implement this
     @Override
     public AbsoluteLayout getPluginContainer() {
         return null;
     }
 
     @Override
     public void notifyCheckUpdateResult(String result) {
-        GeckoAppShell.notifyObservers("Update:CheckResult", result);
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Update:CheckResult", result));
     }
 
     // Bug 908792: Implement this
     @Override
     public void invalidateOptionsMenu() {}
 }
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -1308,17 +1308,17 @@ public class BrowserApp extends GeckoApp
             Tab tab = Tabs.getInstance().getSelectedTab();
             if (tab != null && tab.hasFeeds()) {
                 JSONObject args = new JSONObject();
                 try {
                     args.put("tabId", tab.getId());
                 } catch (JSONException e) {
                     Log.e(LOGTAG, "error building json arguments", e);
                 }
-                GeckoAppShell.notifyObservers("Feeds:Subscribe", args.toString());
+                GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Feeds:Subscribe", args.toString()));
                 if (Versions.preHC) {
                     Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.CONTEXT_MENU, "subscribe");
                 }
             }
             return true;
         }
 
         if (itemId == R.id.add_search_engine) {
@@ -1327,17 +1327,17 @@ public class BrowserApp extends GeckoApp
             if (tab != null && tab.hasOpenSearch()) {
                 JSONObject args = new JSONObject();
                 try {
                     args.put("tabId", tab.getId());
                 } catch (JSONException e) {
                     Log.e(LOGTAG, "error building json arguments", e);
                     return true;
                 }
-                GeckoAppShell.notifyObservers("SearchEngines:Add", args.toString());
+                GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SearchEngines:Add", args.toString()));
 
                 if (Versions.preHC) {
                     Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.CONTEXT_MENU, "add_search_engine");
                 }
             }
             return true;
         }
 
@@ -1687,17 +1687,18 @@ public class BrowserApp extends GeckoApp
                 codeArray[i] = charset.getString("code");
             }
 
             final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
             dialogBuilder.setSingleChoiceItems(titleArray, selected,
                     new AlertDialog.OnClickListener() {
                 @Override
                 public void onClick(final DialogInterface dialog, final int which) {
-                    GeckoAppShell.notifyObservers("CharEncoding:Set", codeArray[which]);
+                    GeckoAppShell.sendEventToGecko(
+                        GeckoEvent.createBroadcastEvent("CharEncoding:Set", codeArray[which]));
                     dialog.dismiss();
                 }
             });
             dialogBuilder.setNegativeButton(R.string.button_cancel,
                     new AlertDialog.OnClickListener() {
                 @Override
                 public void onClick(final DialogInterface dialog, final int which) {
                     dialog.dismiss();
@@ -2948,17 +2949,17 @@ public class BrowserApp extends GeckoApp
             }
         }
 
         final MenuItem item = destination.add(Menu.NONE, info.id, Menu.NONE, info.label);
 
         item.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
             @Override
             public boolean onMenuItemClick(MenuItem item) {
-                GeckoAppShell.notifyObservers("Menu:Clicked", Integer.toString(info.id - ADDON_MENU_OFFSET));
+                GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Menu:Clicked", Integer.toString(info.id - ADDON_MENU_OFFSET)));
                 return true;
             }
         });
 
         item.setCheckable(info.checkable);
         item.setChecked(info.checked);
         item.setEnabled(info.enabled);
         item.setVisible(info.visible);
@@ -3520,17 +3521,17 @@ public class BrowserApp extends GeckoApp
             final String url = AboutPages.getURLForBuiltinPanelType(PanelType.HISTORY);
             Tabs.getInstance().loadUrl(url);
             Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, Experiments.BOOKMARKS_HISTORY_MENU);
             return true;
         }
 
         if (itemId == R.id.save_as_pdf) {
             Telemetry.sendUIEvent(TelemetryContract.Event.SAVE, TelemetryContract.Method.MENU, "pdf");
-            GeckoAppShell.notifyObservers("SaveAs:PDF", null);
+            GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SaveAs:PDF", null));
             return true;
         }
 
         if (itemId == R.id.print) {
             Telemetry.sendUIEvent(TelemetryContract.Event.SAVE, TelemetryContract.Method.MENU, "print");
             PrintHelper.printPDF(this);
             return true;
         }
@@ -3565,17 +3566,17 @@ public class BrowserApp extends GeckoApp
         }
 
         if (itemId == R.id.downloads) {
             Tabs.getInstance().loadUrlInTab(AboutPages.DOWNLOADS);
             return true;
         }
 
         if (itemId == R.id.char_encoding) {
-            GeckoAppShell.notifyObservers("CharEncoding:Get", null);
+            GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("CharEncoding:Get", null));
             return true;
         }
 
         if (itemId == R.id.find_in_page) {
             mFindInPageBar.show();
             return true;
         }
 
@@ -3585,17 +3586,17 @@ public class BrowserApp extends GeckoApp
                 return true;
             JSONObject args = new JSONObject();
             try {
                 args.put("desktopMode", !item.isChecked());
                 args.put("tabId", selectedTab.getId());
             } catch (JSONException e) {
                 Log.e(LOGTAG, "error building json arguments", e);
             }
-            GeckoAppShell.notifyObservers("DesktopMode:Change", args.toString());
+            GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("DesktopMode:Change", args.toString()));
             return true;
         }
 
         if (itemId == R.id.new_tab) {
             addTab();
             return true;
         }
 
@@ -3768,17 +3769,17 @@ public class BrowserApp extends GeckoApp
             int launchCount = settings.getInt(keyName, 0);
             if (launchCount < FEEDBACK_LAUNCH_COUNT) {
                 // Increment the launch count and store the new value.
                 launchCount++;
                 settings.edit().putInt(keyName, launchCount).apply();
 
                 // If we've reached our magic number, show the feedback page.
                 if (launchCount == FEEDBACK_LAUNCH_COUNT) {
-                    GeckoAppShell.notifyObservers("Feedback:Show", null);
+                    GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Feedback:Show", null));
                 }
             }
         } finally {
             StrictMode.setThreadPolicy(savedPolicy);
         }
     }
 
     private void showTabQueuePromptIfApplicable(final Intent intent) {
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserLocaleManager.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserLocaleManager.java
@@ -215,17 +215,18 @@ public class BrowserLocaleManager implem
         }
 
         // Store the Java-native form.
         prefs.edit().putString("osLocale", osLocaleString).apply();
 
         // The value we send to Gecko should be a language tag, not
         // a Java locale string.
         final String osLanguageTag = Locales.getLanguageTag(osLocale);
-        GeckoAppShell.notifyObservers("Locale:OS", osLanguageTag);
+        final GeckoEvent localeOSEvent = GeckoEvent.createBroadcastEvent("Locale:OS", osLanguageTag);
+        GeckoAppShell.sendEventToGecko(localeOSEvent);
     }
 
     @Override
     public String getAndApplyPersistedLocale(Context context) {
         initialize(context);
 
         final long t1 = android.os.SystemClock.uptimeMillis();
         final String localeCode = getPersistedLocale(context);
@@ -259,32 +260,34 @@ public class BrowserLocaleManager implem
         // We always persist and notify Gecko, even if nothing seemed to
         // change. This might happen if you're picking a locale that's the same
         // as the current OS locale. The OS locale might change next time we
         // launch, and we need the Gecko pref and persisted locale to have been
         // set by the time that happens.
         persistLocale(context, localeCode);
 
         // Tell Gecko.
-        GeckoAppShell.notifyObservers(EVENT_LOCALE_CHANGED, Locales.getLanguageTag(getCurrentLocale(context)));
+        GeckoEvent ev = GeckoEvent.createBroadcastEvent(EVENT_LOCALE_CHANGED, Locales.getLanguageTag(getCurrentLocale(context)));
+        GeckoAppShell.sendEventToGecko(ev);
 
         return resultant;
     }
 
     @Override
     public void resetToSystemLocale(Context context) {
         // Wipe the pref.
         final SharedPreferences settings = getSharedPreferences(context);
         settings.edit().remove(PREF_LOCALE).apply();
 
         // Apply the system locale.
         updateLocale(context, systemLocale);
 
         // Tell Gecko.
-        GeckoAppShell.notifyObservers(EVENT_LOCALE_CHANGED, "");
+        GeckoEvent ev = GeckoEvent.createBroadcastEvent(EVENT_LOCALE_CHANGED, "");
+        GeckoAppShell.sendEventToGecko(ev);
     }
 
     /**
      * This is public to allow for an activity to force the
      * current locale to be applied if necessary (e.g., when
      * a new activity launches).
      */
     @Override
--- a/mobile/android/base/java/org/mozilla/gecko/ChromeCast.java
+++ b/mobile/android/base/java/org/mozilla/gecko/ChromeCast.java
@@ -84,25 +84,25 @@ class ChromeCast implements GeckoMediaPl
         }
 
         @Override
         public void onStatusUpdated() {
             MediaStatus mediaStatus = remoteMediaPlayer.getMediaStatus();
 
             switch (mediaStatus.getPlayerState()) {
             case MediaStatus.PLAYER_STATE_PLAYING:
-                GeckoAppShell.notifyObservers("MediaPlayer:Playing", null);
+                GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("MediaPlayer:Playing", null));
                 break;
             case MediaStatus.PLAYER_STATE_PAUSED:
-                GeckoAppShell.notifyObservers("MediaPlayer:Paused", null);
+                GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("MediaPlayer:Paused", null));
                 break;
             case MediaStatus.PLAYER_STATE_IDLE:
                 // TODO: Do we want to shutdown when there are errors?
                 if (mediaStatus.getIdleReason() == MediaStatus.IDLE_REASON_FINISHED) {
-                    GeckoAppShell.notifyObservers("Casting:Stop", null);
+                    GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Casting:Stop", null));
                 }
                 break;
             default:
                 // TODO: Do we need to handle other status such as buffering / unknown?
                 break;
             }
         }
 
@@ -383,17 +383,17 @@ class ChromeCast implements GeckoMediaPl
         }
 
         /*
          * Receive message from the receiver app
          */
         @Override
         public void onMessageReceived(CastDevice castDevice, String namespace,
                                       String message) {
-            GeckoAppShell.notifyObservers("MediaPlayer:Response", message);
+            GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("MediaPlayer:Response", message));
         }
 
         public void sendMessage(String message) {
             if (apiClient != null && mMirrorChannel != null) {
                 try {
                     Cast.CastApi.sendMessage(apiClient, mMirrorChannel.getNamespace(), message)
                         .setResultCallback(
                                            new ResultCallback<Status>() {
@@ -432,17 +432,17 @@ class ChromeCast implements GeckoMediaPl
                                                              mMirrorChannel
                                                              .getNamespace(),
                                                              mMirrorChannel);
                     sendSuccess(callback, null);
                 } catch (IOException e) {
                     Log.e(LOGTAG, "Exception while creating channel", e);
                 }
 
-                GeckoAppShell.notifyObservers("Casting:Mirror", route.getId());
+                GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Casting:Mirror", route.getId()));
             } else {
                 sendError(callback, status.toString());
             }
         }
     }
 
     @Override
     public void message(String msg, final EventCallback callback) {
--- a/mobile/android/base/java/org/mozilla/gecko/ContactService.java
+++ b/mobile/android/base/java/org/mozilla/gecko/ContactService.java
@@ -1502,17 +1502,17 @@ public class ContactService implements G
             callbackMessage.put("requestID", requestID);
 
             if (argNames != null) {
                 for (int i = 0; i < argNames.length; i++) {
                     callbackMessage.put(argNames[i], argValues[i]);
                 }
             }
 
-            GeckoAppShell.notifyObservers(subject, callbackMessage.toString());
+            GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(subject, callbackMessage.toString()));
         } catch (JSONException e) {
             throw new IllegalArgumentException(e);
         }
     }
 
     private ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) {
         try {
             return mContentResolver.applyBatch(ContactsContract.AUTHORITY, operations);
--- a/mobile/android/base/java/org/mozilla/gecko/DoorHangerPopup.java
+++ b/mobile/android/base/java/org/mozilla/gecko/DoorHangerPopup.java
@@ -206,17 +206,18 @@ public class DoorHangerPopup extends Anc
     }
 
 
     /*
      * DoorHanger.OnButtonClickListener implementation
      */
     @Override
     public void onButtonClick(JSONObject response, DoorHanger doorhanger) {
-        GeckoAppShell.notifyObservers("Doorhanger:Reply", response.toString());
+        GeckoEvent e = GeckoEvent.createBroadcastEvent("Doorhanger:Reply", response.toString());
+        GeckoAppShell.sendEventToGecko(e);
         removeDoorHanger(doorhanger);
         updatePopup();
     }
 
     /**
      * Gets a doorhanger.
      *
      * This method must be called on the UI thread.
--- a/mobile/android/base/java/org/mozilla/gecko/EventDispatcher.java
+++ b/mobile/android/base/java/org/mozilla/gecko/EventDispatcher.java
@@ -345,19 +345,20 @@ public final class EventDispatcher {
         try {
             final String topic = message.getString("type") + ":Response";
             final JSONObject wrapper = new JSONObject();
             wrapper.put(GUID, message.getString(GUID));
             wrapper.put("status", status);
             wrapper.put("response", response);
 
             if (ThreadUtils.isOnGeckoThread()) {
-                GeckoAppShell.syncNotifyObservers(topic, wrapper.toString());
+                GeckoAppShell.notifyGeckoObservers(topic, wrapper.toString());
             } else {
-                GeckoAppShell.notifyObservers(topic, wrapper.toString());
+                GeckoAppShell.sendEventToGecko(
+                    GeckoEvent.createBroadcastEvent(topic, wrapper.toString()));
             }
         } catch (final JSONException e) {
             Log.e(LOGTAG, "Unable to send response", e);
         }
     }
 
     private static class GeckoEventCallback implements EventCallback {
         private final String guid;
@@ -390,18 +391,19 @@ public final class EventDispatcher {
             try {
                 final String topic = type + ":Response";
                 final JSONObject wrapper = new JSONObject();
                 wrapper.put(GUID, guid);
                 wrapper.put("status", status);
                 wrapper.put("response", response);
 
                 if (ThreadUtils.isOnGeckoThread()) {
-                    GeckoAppShell.syncNotifyObservers(topic, wrapper.toString());
+                    GeckoAppShell.notifyGeckoObservers(topic, wrapper.toString());
                 } else {
-                    GeckoAppShell.notifyObservers(topic, wrapper.toString());
+                    GeckoAppShell.sendEventToGecko(
+                        GeckoEvent.createBroadcastEvent(topic, wrapper.toString()));
                 }
             } catch (final JSONException e) {
                 Log.e(LOGTAG, "Unable to send response for: " + type, e);
             }
         }
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/FilePicker.java
+++ b/mobile/android/base/java/org/mozilla/gecko/FilePicker.java
@@ -65,17 +65,18 @@ public class FilePicker implements Gecko
                 public void gotFile(String filename) {
                     try {
                         message.put("file", filename);
                     } catch (JSONException ex) {
                         Log.i(LOGTAG, "Can't add filename to message " + filename);
                     }
 
 
-                    GeckoAppShell.notifyObservers("FilePicker:Result", message.toString());
+                    GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(
+                        "FilePicker:Result", message.toString()));
                 }
             }, tabId);
         }
     }
 
     private void addActivities(Intent intent, HashMap<String, Intent> intents, HashMap<String, Intent> filters) {
         PackageManager pm = context.getPackageManager();
         List<ResolveInfo> lri = pm.queryIntentActivities(intent, 0);
--- a/mobile/android/base/java/org/mozilla/gecko/FindInPageBar.java
+++ b/mobile/android/base/java/org/mozilla/gecko/FindInPageBar.java
@@ -73,18 +73,18 @@ public class FindInPageBar extends Linea
     public void show() {
         if (!mInflated)
             inflateContent();
 
         setVisibility(VISIBLE);
         mFindText.requestFocus();
 
         // handleMessage() receives response message and determines initial state of softInput
-        GeckoAppShell.notifyObservers("TextSelection:Get", REQUEST_ID);
-        GeckoAppShell.notifyObservers("FindInPage:Opened", null);
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("TextSelection:Get", REQUEST_ID));
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("FindInPage:Opened", null));
     }
 
     public void hide() {
         if (!mInflated || getVisibility() == View.GONE) {
             // There's nothing to hide yet.
             return;
         }
 
@@ -93,17 +93,17 @@ public class FindInPageBar extends Linea
 
         // Only close the IMM if its EditText is the one with focus.
         if (mFindText.isFocused()) {
           getInputMethodManager(mFindText).hideSoftInputFromWindow(mFindText.getWindowToken(), 0);
         }
 
         // Close the FIPB / FindHelper state.
         setVisibility(GONE);
-        GeckoAppShell.notifyObservers("FindInPage:Closed", null);
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("FindInPage:Closed", null));
     }
 
     private InputMethodManager getInputMethodManager(View view) {
         Context context = view.getContext();
         return (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
      }
 
     public void onDestroy() {
--- a/mobile/android/base/java/org/mozilla/gecko/FormAssistPopup.java
+++ b/mobile/android/base/java/org/mozilla/gecko/FormAssistPopup.java
@@ -400,17 +400,17 @@ public class FormAssistPopup extends Rel
             @Override
             public void run() {
                 positionAndShowPopup(aMetrics);
             }
         });
     }
 
     private static void broadcastGeckoEvent(String eventName, String eventData) {
-        GeckoAppShell.notifyObservers(eventName, eventData);
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(eventName, eventData));
     }
 
     private class AutoCompleteListAdapter extends ArrayAdapter<Pair<String, String>> {
         private final LayoutInflater mInflater;
         private final int mTextViewResourceId;
 
         public AutoCompleteListAdapter(Context context, int textViewResourceId) {
             super(context, textViewResourceId);
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoAccessibility.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoAccessibility.java
@@ -80,17 +80,18 @@ public class GeckoAccessibility {
                     }
 
                     try {
                         ret.put("enabled", sEnabled);
                     } catch (Exception ex) {
                         Log.e(LOGTAG, "Error building JSON arguments for Accessibility:Settings:", ex);
                     }
 
-                    GeckoAppShell.notifyObservers("Accessibility:Settings", ret.toString());
+                    GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Accessibility:Settings",
+                                                                                   ret.toString()));
                     return null;
                 }
 
                 @Override
                 public void onPostExecute(Void args) {
                     boolean isGeckoApp = false;
                     try {
                         isGeckoApp = context instanceof GeckoApp;
@@ -283,17 +284,18 @@ public class GeckoAccessibility {
                     updateAccessibilitySettings(context);
                 }
             });
         }
     }
 
     public static void onLayerViewFocusChanged(LayerView layerview, boolean gainFocus) {
         if (sEnabled)
-            GeckoAppShell.notifyObservers("Accessibility:Focus", gainFocus ? "true" : "false");
+            GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Accessibility:Focus",
+                                                                           gainFocus ? "true" : "false"));
     }
 
     public static class GeckoAccessibilityDelegate extends View.AccessibilityDelegate {
         AccessibilityNodeProvider mAccessibilityNodeProvider;
 
         @Override
         public AccessibilityNodeProvider getAccessibilityNodeProvider(final View host) {
             if (mAccessibilityNodeProvider == null)
@@ -345,44 +347,51 @@ public class GeckoAccessibility {
                         @Override
                         public boolean performAction (int virtualViewId, int action, Bundle arguments) {
                             if (action == AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS) {
                                 // The accessibility focus is permanently on the middle node, VIRTUAL_CURSOR_POSITION.
                                 // When we enter the view forward or backward we just ask Gecko to get focus, keeping the current position.
                                 if (virtualViewId == VIRTUAL_CURSOR_POSITION && sHoverEnter != null) {
                                     GeckoAccessibility.sendAccessibilityEvent(sHoverEnter, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
                                 } else {
-                                    GeckoAppShell.notifyObservers("Accessibility:Focus", "true");
+                                    GeckoAppShell.
+                                      sendEventToGecko(GeckoEvent.createBroadcastEvent("Accessibility:Focus", "true"));
                                 }
                                 return true;
                             } else if (action == AccessibilityNodeInfo.ACTION_CLICK && virtualViewId == VIRTUAL_CURSOR_POSITION) {
-                                GeckoAppShell.notifyObservers("Accessibility:ActivateObject", null);
+                                GeckoAppShell.
+                                    sendEventToGecko(GeckoEvent.createBroadcastEvent("Accessibility:ActivateObject", null));
                                 return true;
                             } else if (action == AccessibilityNodeInfo.ACTION_LONG_CLICK && virtualViewId == VIRTUAL_CURSOR_POSITION) {
-                                GeckoAppShell.notifyObservers("Accessibility:LongPress", null);
+                                GeckoAppShell.
+                                    sendEventToGecko(GeckoEvent.createBroadcastEvent("Accessibility:LongPress", null));
                                 return true;
                             } else if (action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD && virtualViewId == VIRTUAL_CURSOR_POSITION) {
-                                GeckoAppShell.notifyObservers("Accessibility:ScrollForward", null);
+                                GeckoAppShell.
+                                    sendEventToGecko(GeckoEvent.createBroadcastEvent("Accessibility:ScrollForward", null));
                                 return true;
                             } else if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD && virtualViewId == VIRTUAL_CURSOR_POSITION) {
-                                GeckoAppShell.notifyObservers("Accessibility:ScrollBackward", null);
+                                GeckoAppShell.
+                                    sendEventToGecko(GeckoEvent.createBroadcastEvent("Accessibility:ScrollBackward", null));
                                 return true;
                             } else if (action == AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT && virtualViewId == VIRTUAL_CURSOR_POSITION) {
                                 String traversalRule = "";
                                 if (arguments != null) {
                                     traversalRule = arguments.getString(AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING);
                                 }
-                                GeckoAppShell.notifyObservers("Accessibility:NextObject", traversalRule);
+                                GeckoAppShell.
+                                    sendEventToGecko(GeckoEvent.createBroadcastEvent("Accessibility:NextObject", traversalRule));
                                 return true;
                             } else if (action == AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT && virtualViewId == VIRTUAL_CURSOR_POSITION) {
                                 String traversalRule = "";
                                 if (arguments != null) {
                                     traversalRule = arguments.getString(AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING);
                                 }
-                                GeckoAppShell.notifyObservers("Accessibility:PreviousObject", traversalRule);
+                                GeckoAppShell.
+                                    sendEventToGecko(GeckoEvent.createBroadcastEvent("Accessibility:PreviousObject", traversalRule));
                                 return true;
                             } else if (action == AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY &&
                                        virtualViewId == VIRTUAL_CURSOR_POSITION) {
                                 // XXX: Self brailling gives this action with a bogus argument instead of an actual click action;
                                 // the argument value is the BRAILLE_CLICK_BASE_INDEX - the index of the routing key that was hit.
                                 // Other negative values are used by ChromeVox, but we don't support them.
                                 // FAKE_GRANULARITY_READ_CURRENT = -1
                                 // FAKE_GRANULARITY_READ_TITLE = -2
@@ -392,40 +401,43 @@ public class GeckoAccessibility {
                                 if (granularity <= BRAILLE_CLICK_BASE_INDEX) {
                                     int keyIndex = BRAILLE_CLICK_BASE_INDEX - granularity;
                                     JSONObject activationData = new JSONObject();
                                     try {
                                         activationData.put("keyIndex", keyIndex);
                                     } catch (JSONException e) {
                                         return true;
                                     }
-                                    GeckoAppShell.notifyObservers("Accessibility:ActivateObject", activationData.toString());
+                                    GeckoAppShell.
+                                        sendEventToGecko(GeckoEvent.createBroadcastEvent("Accessibility:ActivateObject", activationData.toString()));
                                 } else if (granularity > 0) {
                                     JSONObject movementData = new JSONObject();
                                     try {
                                         movementData.put("direction", "Next");
                                         movementData.put("granularity", granularity);
                                     } catch (JSONException e) {
                                         return true;
                                     }
-                                    GeckoAppShell.notifyObservers("Accessibility:MoveByGranularity", movementData.toString());
+                                    GeckoAppShell.
+                                        sendEventToGecko(GeckoEvent.createBroadcastEvent("Accessibility:MoveByGranularity", movementData.toString()));
                                 }
                                 return true;
                             } else if (action == AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY &&
                                        virtualViewId == VIRTUAL_CURSOR_POSITION) {
                                 JSONObject movementData = new JSONObject();
                                 int granularity = arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT);
                                 try {
                                     movementData.put("direction", "Previous");
                                     movementData.put("granularity", granularity);
                                 } catch (JSONException e) {
                                     return true;
                                 }
                                 if (granularity > 0) {
-                                    GeckoAppShell.notifyObservers("Accessibility:MoveByGranularity", movementData.toString());
+                                    GeckoAppShell.
+                                      sendEventToGecko(GeckoEvent.createBroadcastEvent("Accessibility:MoveByGranularity", movementData.toString()));
                                 }
                                 return true;
                             }
                             return host.performAccessibilityAction(action, arguments);
                         }
                     };
 
             return mAccessibilityNodeProvider;
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
@@ -434,17 +434,17 @@ public abstract class GeckoApp
 
         return super.onPreparePanel(featureId, view, menu);
     }
 
     @Override
     public boolean onMenuOpened(int featureId, Menu menu) {
         // exit full-screen mode whenever the menu is opened
         if (mLayerView != null && mLayerView.isFullScreen()) {
-            GeckoAppShell.notifyObservers("FullScreen:Exit", null);
+            GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("FullScreen:Exit", null));
         }
 
         if (featureId == Window.FEATURE_OPTIONS_PANEL) {
             if (mMenu == null) {
                 // getMenuPanel() will force the creation of the menu as well
                 MenuPanel panel = getMenuPanel();
                 onPreparePanel(featureId, panel, mMenu);
             }
@@ -491,17 +491,18 @@ public abstract class GeckoApp
                 final String sessionRestore = getSessionRestorePreference();
                 try {
                     res.put("dontSaveSession", "quit".equals(sessionRestore));
                 } catch(JSONException ex) {
                     Log.e(LOGTAG, "Error adding session restore data", ex);
                 }
             }
 
-            GeckoAppShell.notifyObservers("Browser:Quit", res.toString());
+            GeckoAppShell.sendEventToGeckoSync(
+                    GeckoEvent.createBroadcastEvent("Browser:Quit", res.toString()));
             doShutdown();
             return true;
         }
 
         return super.onOptionsItemSelected(item);
     }
 
     @Override
@@ -769,17 +770,18 @@ public abstract class GeckoApp
                 SparseBooleanArray checkedItemPositions = listView.getCheckedItemPositions();
 
                 // An array of the indices of the permissions we want to clear
                 JSONArray permissionsToClear = new JSONArray();
                 for (int i = 0; i < checkedItemPositions.size(); i++)
                     if (checkedItemPositions.get(i))
                         permissionsToClear.put(i);
 
-                GeckoAppShell.notifyObservers("Permissions:Clear", permissionsToClear.toString());
+                GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(
+                    "Permissions:Clear", permissionsToClear.toString()));
             }
         });
 
         builder.setNegativeButton(R.string.site_settings_cancel, new DialogInterface.OnClickListener(){
             @Override
             public void onClick(DialogInterface dialog, int id) {
                 dialog.cancel();
             }
@@ -1494,17 +1496,17 @@ public abstract class GeckoApp
                     restoreMessage = restoreSessionTabs(isExternalURL);
                 } catch (SessionRestoreException e) {
                     // If restore failed, do a normal startup
                     Log.e(LOGTAG, "An error occurred during restore", e);
                     mShouldRestore = false;
                 }
             }
 
-            GeckoAppShell.notifyObservers("Session:Restore", restoreMessage);
+            GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Session:Restore", restoreMessage));
         }
 
         // External URLs should always be loaded regardless of whether Gecko is
         // already running.
         if (isExternalURL) {
             // Restore tabs before opening an external URL so that the new tab
             // is animated properly.
             Tabs.getInstance().notifyListeners(null, Tabs.TabEvents.RESTORED);
@@ -1591,17 +1593,18 @@ public abstract class GeckoApp
         if (mIsRestoringActivity) {
             Tab selectedTab = Tabs.getInstance().getSelectedTab();
             if (selectedTab != null) {
                 Tabs.getInstance().notifyListeners(selectedTab, Tabs.TabEvents.SELECTED);
             }
 
             if (GeckoThread.isRunning()) {
                 geckoConnected();
-                GeckoAppShell.notifyObservers("Viewport:Flush", null);
+                GeckoAppShell.sendEventToGecko(
+                        GeckoEvent.createBroadcastEvent("Viewport:Flush", null));
             }
         }
 
         if (ACTION_ALERT_CALLBACK.equals(action)) {
             processAlertCallback(intent);
         } else if (NotificationHelper.HELPER_BROADCAST_ACTION.equals(action)) {
             NotificationHelper.getInstance(getApplicationContext()).handleNotificationIntent(intent);
         }
@@ -2326,17 +2329,17 @@ public abstract class GeckoApp
 
         if (mFullScreenPluginView != null) {
             GeckoAppShell.onFullScreenPluginHidden(mFullScreenPluginView);
             removeFullScreenPluginView(mFullScreenPluginView);
             return;
         }
 
         if (mLayerView != null && mLayerView.isFullScreen()) {
-            GeckoAppShell.notifyObservers("FullScreen:Exit", null);
+            GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("FullScreen:Exit", null));
             return;
         }
 
         final Tabs tabs = Tabs.getInstance();
         final Tab tab = tabs.getSelectedTab();
         if (tab == null) {
             moveTaskToBack(true);
             return;
@@ -2461,17 +2464,17 @@ public abstract class GeckoApp
         } else if (!state.equals("locked-foreground") && wl != null) {
             wl.release();
             mWakeLocks.remove(topic);
         }
     }
 
     @Override
     public void notifyCheckUpdateResult(String result) {
-        GeckoAppShell.notifyObservers("Update:CheckResult", result);
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Update:CheckResult", result));
     }
 
     private void geckoConnected() {
         mLayerView.setOverScrollMode(View.OVER_SCROLL_NEVER);
     }
 
     public void setAccessibilityEnabled(boolean enabled) {
     }
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoAppShell.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoAppShell.java
@@ -369,30 +369,29 @@ public class GeckoAppShell
                 if (!message.has(GECKOREQUEST_RESPONSE_KEY)) {
                     request.onError(message.getObject(GECKOREQUEST_ERROR_KEY));
                     return;
                 }
                 request.onResponse(message.getObject(GECKOREQUEST_RESPONSE_KEY));
             }
         }, responseMessage);
 
-        notifyObservers(request.getName(), request.getData());
+        sendEventToGecko(GeckoEvent.createBroadcastEvent(request.getName(), request.getData()));
     }
 
     // Tell the Gecko event loop that an event is available.
     public static native void notifyGeckoOfEvent(GeckoEvent event);
 
     // Synchronously notify a Gecko observer; must be called from Gecko thread.
     @WrapForJNI
     public static native void syncNotifyObservers(String topic, String data);
 
     @WrapForJNI(stubName = "NotifyObservers")
     private static native void nativeNotifyObservers(String topic, String data);
 
-    @RobocopTarget
     public static void notifyObservers(final String topic, final String data) {
         notifyObservers(topic, data, GeckoThread.State.RUNNING);
     }
 
     public static void notifyObservers(final String topic, final String data, final GeckoThread.State state) {
         if (GeckoThread.isStateAtLeast(state)) {
             nativeNotifyObservers(topic, data);
         } else {
@@ -2504,17 +2503,18 @@ public class GeckoAppShell
         GeckoView v = (GeckoView) getLayerView();
         if (v == null) {
             return;
         }
         boolean imeIsEnabled = v.isIMEEnabled();
         if (imeIsEnabled && !sImeWasEnabledOnLastResize) {
             // The IME just came up after not being up, so let's scroll
             // to the focused input.
-            notifyObservers("ScrollTo:FocusedInput", "");
+            sendEventToGecko(GeckoEvent.createBroadcastEvent(
+                    "ScrollTo:FocusedInput", ""));
         }
         sImeWasEnabledOnLastResize = imeIsEnabled;
     }
 
     @WrapForJNI(stubName = "GetCurrentNetworkInformationWrapper")
     public static double[] getCurrentNetworkInformation() {
         return GeckoNetworkManager.getInstance().getCurrentInformation();
     }
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoMessageReceiver.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoMessageReceiver.java
@@ -8,12 +8,12 @@ import android.content.BroadcastReceiver
 import android.content.Context;
 import android.content.Intent;
 
 public class GeckoMessageReceiver extends BroadcastReceiver {
     @Override
     public void onReceive(Context context, Intent intent) {
         final String action = intent.getAction();
         if (GeckoApp.ACTION_INIT_PW.equals(action)) {
-            GeckoAppShell.notifyObservers("Passwords:Init", null);
+            GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Passwords:Init", null));
         }
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoView.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoView.java
@@ -485,30 +485,31 @@ public class GeckoView extends LayerView
         for (Tab tab : tabs) {
             browsers.add(new Browser(tab.getId()));
         }
         return Collections.unmodifiableList(browsers);
     }
 
     public void importScript(final String url) {
         if (url.startsWith("resource://android/assets/")) {
-            GeckoAppShell.notifyObservers("GeckoView:ImportScript", url);
+            GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("GeckoView:ImportScript", url));
             return;
         }
 
         throw new IllegalArgumentException("Must import script from 'resources://android/assets/' location.");
     }
 
     private void connectToGecko() {
         Tab selectedTab = Tabs.getInstance().getSelectedTab();
         if (selectedTab != null) {
             Tabs.getInstance().notifyListeners(selectedTab, Tabs.TabEvents.SELECTED);
         }
 
-        GeckoAppShell.notifyObservers("Viewport:Flush", null);
+        GeckoAppShell.sendEventToGecko(
+                GeckoEvent.createBroadcastEvent("Viewport:Flush", null));
     }
 
     private void handleReady(final JSONObject message) {
         connectToGecko();
 
         if (mChromeDelegate != null) {
             mChromeDelegate.onReady(this);
         }
@@ -651,17 +652,17 @@ public class GeckoView extends LayerView
             try {
                 args.put("url", url);
                 args.put("parentId", -1);
                 args.put("newTab", false);
                 args.put("tabID", mId);
             } catch (Exception e) {
                 Log.w(LOGTAG, "Error building JSON arguments for loadUrl.", e);
             }
-            GeckoAppShell.notifyObservers("Tab:Load", args.toString());
+            GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Tab:Load", args.toString()));
         }
 
         /**
         * Reload the current URL resource into the Browser. The URL is force loaded from the
         * network and is not pulled from cache.
         */
         public void reload() {
             Tab tab = Tabs.getInstance().getTab(mId);
--- a/mobile/android/base/java/org/mozilla/gecko/MediaCastingBar.java
+++ b/mobile/android/base/java/org/mozilla/gecko/MediaCastingBar.java
@@ -79,25 +79,25 @@ public class MediaCastingBar extends Rel
     }
 
     // View.OnClickListener implementation
     @Override
     public void onClick(View v) {
         final int viewId = v.getId();
 
         if (viewId == R.id.media_play) {
-            GeckoAppShell.notifyObservers("Casting:Play", "");
+            GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Casting:Play", ""));
             mMediaPlay.setVisibility(GONE);
             mMediaPause.setVisibility(VISIBLE);
         } else if (viewId == R.id.media_pause) {
-            GeckoAppShell.notifyObservers("Casting:Pause", "");
+            GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Casting:Pause", ""));
             mMediaPause.setVisibility(GONE);
             mMediaPlay.setVisibility(VISIBLE);
         } else if (viewId == R.id.media_stop) {
-            GeckoAppShell.notifyObservers("Casting:Stop", "");
+            GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Casting:Stop", ""));
         }
     }
 
     // GeckoEventListener implementation
     @Override
     public void handleMessage(final String event, final JSONObject message) {
         final String device = message.optString("device");
 
--- a/mobile/android/base/java/org/mozilla/gecko/MediaPlayerManager.java
+++ b/mobile/android/base/java/org/mozilla/gecko/MediaPlayerManager.java
@@ -132,17 +132,18 @@ public class MediaPlayerManager extends 
     }
 
     private final MediaRouter.Callback callback =
         new MediaRouter.Callback() {
             @Override
             public void onRouteRemoved(MediaRouter router, RouteInfo route) {
                 debug("onRouteRemoved: route=" + route);
                 displays.remove(route.getId());
-                GeckoAppShell.notifyObservers("MediaPlayer:Removed", route.getId());
+                GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(
+                        "MediaPlayer:Removed", route.getId()));
                 updatePresentation();
             }
 
             @SuppressWarnings("unused")
             public void onRouteSelected(MediaRouter router, int type, MediaRouter.RouteInfo route) {
                 updatePresentation();
             }
 
@@ -184,17 +185,18 @@ public class MediaPlayerManager extends 
                 }
 
                 final JSONObject json = display.toJSON();
                 if (json == null) {
                     return;
                 }
 
                 displays.put(route.getId(), display);
-                GeckoAppShell.notifyObservers(eventName, json.toString());
+                final GeckoEvent event = GeckoEvent.createBroadcastEvent(eventName, json.toString());
+                GeckoAppShell.sendEventToGecko(event);
             }
         };
 
     private GeckoMediaPlayer getMediaPlayerForRoute(MediaRouter.RouteInfo route) {
         try {
             if (route.supportsControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {
                 return new ChromeCast(getActivity(), route);
             }
--- a/mobile/android/base/java/org/mozilla/gecko/MemoryMonitor.java
+++ b/mobile/android/base/java/org/mozilla/gecko/MemoryMonitor.java
@@ -134,17 +134,17 @@ class MemoryMonitor extends BroadcastRec
         } else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(intent.getAction())) {
             Log.d(LOGTAG, "Device storage is ok");
             mStoragePressure = false;
         } else if (ACTION_MEMORY_DUMP.equals(intent.getAction())) {
             String label = intent.getStringExtra("label");
             if (label == null) {
                 label = "default";
             }
-            GeckoAppShell.notifyObservers("Memory:Dump", label);
+            GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Memory:Dump", label));
         } else if (ACTION_FORCE_PRESSURE.equals(intent.getAction())) {
             increaseMemoryPressure(MEMORY_PRESSURE_HIGH);
         }
     }
 
     private boolean increaseMemoryPressure(int level) {
         int oldLevel;
         synchronized (this) {
--- a/mobile/android/base/java/org/mozilla/gecko/NotificationHelper.java
+++ b/mobile/android/base/java/org/mozilla/gecko/NotificationHelper.java
@@ -143,17 +143,17 @@ public final class NotificationHelper im
             args.put(COOKIE_ATTR, cookie);
 
             if (BUTTON_EVENT.equals(notificationType)) {
                 final String actionName = data.getQueryParameter(ACTION_ID_ATTR);
                 args.put(ACTION_ID_ATTR, actionName);
             }
 
             Log.i(LOGTAG, "Send " + args.toString());
-            GeckoAppShell.notifyObservers("Notification:Event", args.toString());
+            GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Notification:Event", args.toString()));
         } catch (JSONException e) {
             Log.e(LOGTAG, "Error building JSON notification arguments.", e);
         }
 
         // If the notification was clicked, we are closing it. This must be executed after
         // sending the event to js side because when the notification is canceled no event can be
         // handled.
         if (CLICK_EVENT.equals(notificationType) && !i.getBooleanExtra(ONGOING_ATTR, false)) {
@@ -331,17 +331,17 @@ public final class NotificationHelper im
     private void sendNotificationWasClosed(String id, String handlerKey, String cookie) {
         final JSONObject args = new JSONObject();
         try {
             args.put(ID_ATTR, id);
             args.put(HANDLER_ATTR, handlerKey);
             args.put(COOKIE_ATTR, cookie);
             args.put(EVENT_TYPE_ATTR, CLOSED_EVENT);
             Log.i(LOGTAG, "Send " + args.toString());
-            GeckoAppShell.notifyObservers("Notification:Event", args.toString());
+            GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Notification:Event", args.toString()));
         } catch (JSONException ex) {
             Log.e(LOGTAG, "sendNotificationWasClosed: error building JSON notification arguments.", ex);
         }
     }
 
     private void closeNotification(String id, String handlerKey, String cookie) {
         GeckoAppShell.notificationClient.remove(id.hashCode());
         sendNotificationWasClosed(id, handlerKey, cookie);
--- a/mobile/android/base/java/org/mozilla/gecko/OrderedBroadcastHelper.java
+++ b/mobile/android/base/java/org/mozilla/gecko/OrderedBroadcastHelper.java
@@ -96,17 +96,18 @@ public final class OrderedBroadcastHelpe
                             res.put("action", action);
                             res.put("token", token);
                             res.put("data", data);
                         } catch (JSONException e) {
                             Log.e(LOGTAG, "Got exception in onReceive handling action " + action, e);
                             return;
                         }
 
-                        GeckoAppShell.notifyObservers(responseEvent, res.toString());
+                        GeckoEvent event = GeckoEvent.createBroadcastEvent(responseEvent, res.toString());
+                        GeckoAppShell.sendEventToGecko(event);
                     }
                 }
             };
 
             Intent intent = new Intent(action);
             // OrderedBroadcast.jsm adds its callback ID to the caller's token;
             // this unwraps that wrapping.
             if (token != null && token.has("data")) {
--- a/mobile/android/base/java/org/mozilla/gecko/SharedPreferencesHelper.java
+++ b/mobile/android/base/java/org/mozilla/gecko/SharedPreferencesHelper.java
@@ -228,17 +228,17 @@ public final class SharedPreferencesHelp
                 msg.put("profileName", this.profileName);
                 msg.put("key", key);
 
                 // Truly, this is awful, but the API impedance is strong: there
                 // is no way to get a single untyped value from a
                 // SharedPreferences instance.
                 msg.put("value", sharedPreferences.getAll().get(key));
 
-                GeckoAppShell.notifyObservers("SharedPreferences:Changed", msg.toString());
+                GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SharedPreferences:Changed", msg.toString()));
             } catch (JSONException e) {
                 Log.e(LOGTAG, "Got exception creating JSON object", e);
                 return;
             }
         }
     }
 
     /**
--- a/mobile/android/base/java/org/mozilla/gecko/Tab.java
+++ b/mobile/android/base/java/org/mozilla/gecko/Tab.java
@@ -614,46 +614,50 @@ public class Tab {
         });
     }
 
     public boolean isEnteringReaderMode() {
         return mEnteringReaderMode;
     }
 
     public void doReload(boolean bypassCache) {
-        GeckoAppShell.notifyObservers("Session:Reload", "{\"bypassCache\":" + String.valueOf(bypassCache) + "}");
+        GeckoEvent e = GeckoEvent.createBroadcastEvent("Session:Reload", "{\"bypassCache\":" + String.valueOf(bypassCache) + "}");
+        GeckoAppShell.sendEventToGecko(e);
     }
 
     // Our version of nsSHistory::GetCanGoBack
     public boolean canDoBack() {
         return mCanDoBack;
     }
 
     public boolean doBack() {
         if (!canDoBack())
             return false;
 
-        GeckoAppShell.notifyObservers("Session:Back", "");
+        GeckoEvent e = GeckoEvent.createBroadcastEvent("Session:Back", "");
+        GeckoAppShell.sendEventToGecko(e);
         return true;
     }
 
     public void doStop() {
-        GeckoAppShell.notifyObservers("Session:Stop", "");
+        GeckoEvent e = GeckoEvent.createBroadcastEvent("Session:Stop", "");
+        GeckoAppShell.sendEventToGecko(e);
     }
 
     // Our version of nsSHistory::GetCanGoForward
     public boolean canDoForward() {
         return mCanDoForward;
     }
 
     public boolean doForward() {
         if (!canDoForward())
             return false;
 
-        GeckoAppShell.notifyObservers("Session:Forward", "");
+        GeckoEvent e = GeckoEvent.createBroadcastEvent("Session:Forward", "");
+        GeckoAppShell.sendEventToGecko(e);
         return true;
     }
 
     void handleLocationChange(JSONObject message) throws JSONException {
         final String uri = message.getString("uri");
         final String oldUrl = getURL();
         final boolean sameDocument = message.getBoolean("sameDocument");
         mEnteringReaderMode = ReaderModeUtils.isEnteringReaderMode(oldUrl, uri);
--- a/mobile/android/base/java/org/mozilla/gecko/Tabs.java
+++ b/mobile/android/base/java/org/mozilla/gecko/Tabs.java
@@ -277,17 +277,17 @@ public class Tabs implements GeckoEventL
         mSelectedTab = tab;
         notifyListeners(tab, TabEvents.SELECTED);
 
         if (oldTab != null) {
             notifyListeners(oldTab, TabEvents.UNSELECTED);
         }
 
         // Pass a message to Gecko to update tab state in BrowserApp.
-        GeckoAppShell.notifyObservers("Tab:Selected", String.valueOf(tab.getId()));
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Tab:Selected", String.valueOf(tab.getId())));
         return tab;
     }
 
     private int getIndexOf(Tab tab) {
         return mOrder.lastIndexOf(tab);
     }
 
     private Tab getNextTabFrom(Tab tab, boolean getPrivate) {
@@ -397,17 +397,17 @@ public class Tabs implements GeckoEventL
         try {
             args.put("tabId", String.valueOf(tabId));
             args.put("showUndoToast", showUndoToast);
         } catch (JSONException e) {
             Log.e(LOGTAG, "Error building Tab:Closed arguments: " + e);
         }
 
         // Pass a message to Gecko to update tab state in BrowserApp
-        GeckoAppShell.notifyObservers("Tab:Closed", args.toString());
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Tab:Closed", args.toString()));
     }
 
     /** Return the tab that will be selected by default after this one is closed */
     public Tab getNextTab(Tab tab) {
         Tab selectedTab = getSelectedTab();
         if (selectedTab != tab)
             return selectedTab;
 
@@ -924,17 +924,17 @@ public class Tabs implements GeckoEventL
                 tabToSelect = addTab(tabId, tabUrl, external, parentId, url, isPrivate, tabIndex);
                 tabToSelect.setDesktopMode(desktopMode);
                 tabToSelect.setApplicationId(applicationId);
             }
         } catch (Exception e) {
             Log.w(LOGTAG, "Error building JSON arguments for loadUrl.", e);
         }
 
-        GeckoAppShell.notifyObservers("Tab:Load", args.toString());
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Tab:Load", args.toString()));
 
         if (tabToSelect == null) {
             return null;
         }
 
         if (!delayLoad && !background) {
             selectTab(tabToSelect.getId());
         }
--- a/mobile/android/base/java/org/mozilla/gecko/TextSelection.java
+++ b/mobile/android/base/java/org/mozilla/gecko/TextSelection.java
@@ -80,17 +80,17 @@ class TextSelection extends Layer implem
         this.anchorHandle = anchorHandle;
         this.caretHandle = caretHandle;
         this.focusHandle = focusHandle;
 
         mDrawListener = new DrawListener() {
             @Override
             public void drawFinished() {
                 if (!mDraggingHandles) {
-                    GeckoAppShell.notifyObservers("TextSelection:LayerReflow", "");
+                    GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("TextSelection:LayerReflow", ""));
                 }
             }
         };
 
         // Only register listeners if we have valid start/middle/end handles
         if (anchorHandle == null || caretHandle == null || focusHandle == null) {
             Log.e(LOGTAG, "Failed to initialize text selection because at least one handle is null");
         } else {
@@ -363,17 +363,17 @@ class TextSelection extends Layer implem
             mActionMode = mode;
             return true;
         }
 
         @Override
         public boolean onActionItemClicked(ActionModeCompat mode, MenuItem item) {
             try {
                 final JSONObject obj = mItems.getJSONObject(item.getItemId());
-                GeckoAppShell.notifyObservers("TextSelection:Action", obj.optString("id"));
+                GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("TextSelection:Action", obj.optString("id")));
                 return true;
             } catch(Exception ex) {
                 Log.i(LOGTAG, "Exception calling action", ex);
             }
             return false;
         }
 
         // Called when the user exits the action mode
@@ -384,12 +384,14 @@ class TextSelection extends Layer implem
             final JSONObject args = new JSONObject();
             try {
                 args.put("selectionID", selectionID);
             } catch (JSONException e) {
                 Log.e(LOGTAG, "Error building JSON arguments for TextSelection:End", e);
                 return;
             }
 
-            GeckoAppShell.notifyObservers("TextSelection:End", args.toString());
+            final GeckoEvent event =
+                GeckoEvent.createBroadcastEvent("TextSelection:End", args.toString());
+            GeckoAppShell.sendEventToGecko(event);
         }
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/TextSelectionHandle.java
+++ b/mobile/android/base/java/org/mozilla/gecko/TextSelectionHandle.java
@@ -108,17 +108,17 @@ class TextSelectionHandle extends ImageV
 
                 // Reposition handles to line up with ends of selection
                 JSONObject args = new JSONObject();
                 try {
                     args.put("handleType", mHandleType.toString());
                 } catch (Exception e) {
                     Log.e(LOGTAG, "Error building JSON arguments for TextSelection:Position");
                 }
-                GeckoAppShell.notifyObservers("TextSelection:Position", args.toString());
+                GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("TextSelection:Position", args.toString()));
                 break;
             }
             case MotionEvent.ACTION_MOVE: {
                 move(event.getRawX(), event.getRawY());
                 break;
             }
         }
         return true;
@@ -150,17 +150,17 @@ class TextSelectionHandle extends ImageV
         JSONObject args = new JSONObject();
         try {
             args.put("handleType", mHandleType.toString());
             args.put("x", (int) geckoPoint.x);
             args.put("y", (int) geckoPoint.y);
         } catch (Exception e) {
             Log.e(LOGTAG, "Error building JSON arguments for TextSelection:Move");
         }
-        GeckoAppShell.notifyObservers("TextSelection:Move", args.toString());
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("TextSelection:Move", args.toString()));
 
         // If we're positioning a cursor, don't move the handle here. Gecko
         // will tell us the position of the caret, so we set the handle
         // position then. This allows us to lock the handle to wherever the
         // caret appears.
         if (mHandleType != HandleType.CARET) {
             setLayoutPosition();
         }
--- a/mobile/android/base/java/org/mozilla/gecko/ZoomedView.java
+++ b/mobile/android/base/java/org/mozilla/gecko/ZoomedView.java
@@ -155,17 +155,18 @@ public class ZoomedView extends FrameLay
                 }
                 break;
 
             case MotionEvent.ACTION_UP:
                 if (dragged) {
                     dragged = false;
                 } else {
                     if (isClickInZoomedView(event.getY())) {
-                        GeckoAppShell.notifyObservers("Gesture:ClickInZoomedView", "");
+                        GeckoEvent eClickInZoomedView = GeckoEvent.createBroadcastEvent("Gesture:ClickInZoomedView", "");
+                        GeckoAppShell.sendEventToGecko(eClickInZoomedView);
                         layerView.dispatchTouchEvent(actionDownEvent);
                         actionDownEvent.recycle();
                         PointF convertedPosition = getUnzoomedPositionFromPointInZoomedView(event.getX(), event.getY());
                         // the LayerView expects the coordinates relative to the window, not the surface, so we need
                         // to adjust that here.
                         convertedPosition.y += layerView.getSurfaceTranslation();
                         MotionEvent e = MotionEvent.obtain(event.getDownTime(), event.getEventTime(),
                                 MotionEvent.ACTION_UP, convertedPosition.x, convertedPosition.y,
--- a/mobile/android/base/java/org/mozilla/gecko/db/FormHistoryProvider.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/FormHistoryProvider.java
@@ -121,17 +121,17 @@ public class FormHistoryProvider extends
 
             default:
                 throw new UnsupportedOperationException("Unknown insert URI " + uri);
         }
     }
 
     @Override
     public void initGecko() {
-        GeckoAppShell.notifyObservers("FormHistory:Init", null);
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("FormHistory:Init", null));
     }
 
     @Override
     public void onPreInsert(ContentValues values, Uri uri, SQLiteBridge db) {
         if (!values.containsKey(FormHistory.GUID)) {
             return;
         }
 
--- a/mobile/android/base/java/org/mozilla/gecko/db/LocalReadingListAccessor.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/LocalReadingListAccessor.java
@@ -111,17 +111,17 @@ public class LocalReadingListAccessor im
         // We're adding locally, so we can specify these.
         values.put(ReadingListItems.ADDED_ON, System.currentTimeMillis());
         values.put(ReadingListItems.ADDED_BY, ReadingListProvider.PLACEHOLDER_THIS_DEVICE);
 
         // We never un-delete (and we can't; we wipe as we go).
         // Re-add if necessary and allow the server to resolve conflicts.
         final long id = ContentUris.parseId(cr.insert(mReadingListUriWithProfile, values));
 
-        GeckoAppShell.notifyObservers("Reader:Added", url);
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Reader:Added", url));
 
         return id;
     }
 
     @Override
     public long addBasicReadingListItem(ContentResolver cr, String url, String title) {
         if (url == null) {
             throw new IllegalArgumentException("URL must not be null.");
@@ -156,17 +156,17 @@ public class LocalReadingListAccessor im
     }
 
     @Override
     public void removeReadingListItemWithURL(final ContentResolver cr, String uri) {
         cr.delete(mReadingListUriWithProfile,
                   ReadingListItems.URL + " = ? OR " + ReadingListItems.RESOLVED_URL + " = ?",
                   new String[]{ uri, uri });
 
-        GeckoAppShell.notifyObservers("Reader:Removed", uri);
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Reader:Removed", uri));
     }
 
     @Override
     public void deleteItem(ContentResolver cr, long itemID) {
         // TODO: For completness, we should send a "Reader:Removed"
         // GeckoEvent, but we don't have the uri. Luckily, this is
         // only called in testing at the moment.
         cr.delete(ContentUris.appendId(mReadingListUriWithProfile.buildUpon(), itemID).build(),
--- a/mobile/android/base/java/org/mozilla/gecko/distribution/Distribution.java
+++ b/mobile/android/base/java/org/mozilla/gecko/distribution/Distribution.java
@@ -213,17 +213,17 @@ public class Distribution {
 
     private static Distribution init(final Distribution distribution) {
         // Read/write preferences and files on the background thread.
         ThreadUtils.postToBackgroundThread(new Runnable() {
             @Override
             public void run() {
                 boolean distributionSet = distribution.doInit();
                 if (distributionSet) {
-                    GeckoAppShell.notifyObservers("Distribution:Set", "");
+                    GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Distribution:Set", ""));
                 }
             }
         });
 
         return distribution;
     }
 
     /**
@@ -313,17 +313,17 @@ public class Distribution {
         // Just in case this isn't empty but doInit has finished.
         runReadyQueue();
 
         // Now process any tasks that already ran while we were in STATE_NONE
         // to tell them of our good news.
         runLateReadyQueue();
 
         // Make sure that changes to search defaults are applied immediately.
-        GeckoAppShell.notifyObservers("Distribution:Changed", "");
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Distribution:Changed", ""));
     }
 
     /**
      * Helper to grab a file in the distribution directory.
      *
      * Returns null if there is no distribution directory or the file
      * doesn't exist. Ensures init first.
      */
--- a/mobile/android/base/java/org/mozilla/gecko/distribution/ReferrerReceiver.java
+++ b/mobile/android/base/java/org/mozilla/gecko/distribution/ReferrerReceiver.java
@@ -89,14 +89,15 @@ public class ReferrerReceiver extends Br
 
         try {
             final JSONObject data = new JSONObject();
             data.put("id", "playstore");
             data.put("version", referrer.campaign);
             String payload = data.toString();
 
             // Try to make sure the prefs are written as a group.
-            GeckoAppShell.notifyObservers("Campaign:Set", payload);
+            final GeckoEvent event = GeckoEvent.createBroadcastEvent("Campaign:Set", payload);
+            GeckoAppShell.sendEventToGecko(event);
         } catch (JSONException e) {
             Log.e(LOGTAG, "Error propagating campaign identifier.", e);
         }
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/dlc/DownloadContentService.java
+++ b/mobile/android/base/java/org/mozilla/gecko/dlc/DownloadContentService.java
@@ -82,17 +82,17 @@ public class DownloadContentService exte
                 action = new StudyAction();
                 break;
 
             case ACTION_DOWNLOAD_CONTENT:
                 action = new DownloadAction(new DownloadAction.Callback() {
                     @Override
                     public void onContentDownloaded(DownloadContent content) {
                         if (content.isFont()) {
-                            GeckoAppShell.notifyObservers("Fonts:Reload", "");
+                            GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Fonts:Reload", ""));
                         }
                     }
                 });
                 break;
 
             case ACTION_VERIFY_CONTENT:
                 action = new VerifyAction();
                 break;
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/GeckoLayerClient.java
+++ b/mobile/android/base/java/org/mozilla/gecko/gfx/GeckoLayerClient.java
@@ -297,17 +297,17 @@ class GeckoLayerClient implements LayerV
                 jsonObj.put("x", scrollChange.x / mViewportMetrics.zoomFactor);
                 jsonObj.put("y", scrollChange.y / mViewportMetrics.zoomFactor);
                 jsonObj.put("id", id);
                 json = jsonObj.toString();
             }
         } catch (Exception e) {
             Log.e(LOGTAG, "Unable to convert point to JSON", e);
         }
-        GeckoAppShell.notifyObservers("Window:Resize", json);
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Window:Resize", json));
     }
 
     /** Sets the current page rect. You must hold the monitor while calling this. */
     private void setPageRect(RectF rect, RectF cssRect) {
         // Since the "rect" is always just a multiple of "cssRect" we don't need to
         // check both; this function assumes that both "rect" and "cssRect" are relative
         // the zoom factor in mViewportMetrics.
         if (mViewportMetrics.getCssPageRect().equals(cssRect))
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/JavaPanZoomController.java
+++ b/mobile/android/base/java/org/mozilla/gecko/gfx/JavaPanZoomController.java
@@ -212,17 +212,17 @@ class JavaPanZoomController
         // ease-out approx.
         // -(t-1)^2+1
         t = t-1;
         return -t*t+1;
     }
 
     private void setState(PanZoomState state) {
         if (state != mState) {
-            GeckoAppShell.notifyObservers("PanZoom:StateChange", state.toString());
+            GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("PanZoom:StateChange", state.toString()));
             mState = state;
 
             // Let the target know we've finished with it (for now)
             if (state == PanZoomState.NOTHING) {
                 mTarget.panZoomStopped();
             }
         }
     }
@@ -1326,17 +1326,17 @@ class JavaPanZoomController
                 return;
             }
             json = PointUtils.toJSON(point).toString();
         } catch (Exception e) {
             Log.e(LOGTAG, "Unable to convert point to JSON for " + event, e);
             return;
         }
 
-        GeckoAppShell.notifyObservers(event, json);
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(event, json));
     }
 
     @Override
     public boolean onDown(MotionEvent motionEvent) {
         mWaitForDoubleTap = mTarget.getZoomConstraints().getAllowDoubleTapZoom();
         return false;
     }
 
@@ -1392,17 +1392,18 @@ class JavaPanZoomController
 
     @Override
     public boolean onDoubleTap(MotionEvent motionEvent) {
         sendPointToGecko("Gesture:DoubleTap", motionEvent);
         return true;
     }
 
     private void cancelTouch() {
-        GeckoAppShell.notifyObservers("Gesture:CancelTouch", "");
+        GeckoEvent e = GeckoEvent.createBroadcastEvent("Gesture:CancelTouch", "");
+        GeckoAppShell.sendEventToGecko(e);
     }
 
     /**
      * Zoom to a specified rect IN CSS PIXELS.
      *
      * While we usually use device pixels, @zoomToRect must be specified in CSS
      * pixels.
      */
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/SubdocumentScrollHelper.java
+++ b/mobile/android/base/java/org/mozilla/gecko/gfx/SubdocumentScrollHelper.java
@@ -81,17 +81,17 @@ class SubdocumentScrollHelper implements
 
         JSONObject json = new JSONObject();
         try {
             json.put("x", displacement.x);
             json.put("y", displacement.y);
         } catch (JSONException e) {
             Log.e(LOGTAG, "Error forming subwindow scroll message: ", e);
         }
-        GeckoAppShell.notifyObservers(MESSAGE_SCROLL, json.toString());
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(MESSAGE_SCROLL, json.toString()));
 
         mOverrideScrollAck = false;
         mOverrideScrollPending = false;
         // clear the |mPendingDisplacement| after serializing |displacement| to
         // JSON because they might be the same object
         mPendingDisplacement.x = 0;
         mPendingDisplacement.y = 0;
 
--- a/mobile/android/base/java/org/mozilla/gecko/home/BrowserSearch.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/BrowserSearch.java
@@ -257,31 +257,31 @@ public class BrowserSearch extends HomeF
             final boolean isPrivate = (tab != null && tab.isPrivate());
 
             // Removes Search Suggestions Loader if in private browsing mode
             // Loader may have been inserted when browsing in normal tab
             if (isPrivate) {
                 getLoaderManager().destroyLoader(LOADER_ID_SUGGESTION);
             }
 
-            GeckoAppShell.notifyObservers("SearchEngines:GetVisible", null);
+            GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SearchEngines:GetVisible", null));
         }
         super.onHiddenChanged(hidden);
     }
 
     @Override
     public void onResume() {
         super.onResume();
 
         final SharedPreferences prefs = GeckoSharedPrefs.forApp(getContext());
         mSavedSearchesEnabled = prefs.getBoolean(GeckoPreferences.PREFS_HISTORY_SAVED_SEARCH, true);
 
         // Fetch engines if we need to.
         if (mSearchEngines.isEmpty() || !Locale.getDefault().equals(mLastLocale)) {
-            GeckoAppShell.notifyObservers("SearchEngines:GetVisible", null);
+            GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SearchEngines:GetVisible", null));
         } else {
             updateSearchEngineBar();
         }
 
         Telemetry.startUISession(TelemetryContract.Session.FRECENCY);
     }
 
     @Override
@@ -430,17 +430,17 @@ public class BrowserSearch extends HomeF
         final boolean searchPath = searchTerm.indexOf('/') > 0;
         final String autocompletion = findAutocompletion(searchTerm, c, searchPath);
 
         if (autocompletion == null || mAutocompleteHandler == null) {
             return;
         }
 
         // Prefetch auto-completed domain since it's a likely target
-        GeckoAppShell.notifyObservers("Session:Prefetch", "http://" + autocompletion);
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Session:Prefetch", "http://" + autocompletion));
 
         mAutocompleteHandler.onAutocomplete(autocompletion);
         mAutocompleteHandler = null;
     }
 
     /**
      * Returns the substring of a provided URI, starting at the given offset,
      * and extending up to the end of the path segment in which the provided
@@ -529,17 +529,17 @@ public class BrowserSearch extends HomeF
         final int urlIndex = c.getColumnIndexOrThrow(History.URL);
         int searchCount = 0;
 
         do {
             final String url = c.getString(urlIndex);
 
             if (searchCount == 0) {
                 // Prefetch the first item in the list since it's weighted the highest
-                GeckoAppShell.notifyObservers("Session:Prefetch", url);
+                GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Session:Prefetch", url));
             }
 
             // Does the completion match against the whole URL? This will match
             // about: pages, as well as user input including "http://...".
             if (url.startsWith(searchTerm)) {
                 return uriSubstringUpToMatchedPath(url, 0,
                         (searchLength > HTTPS_PREFIX_LENGTH) ? searchLength : HTTPS_PREFIX_LENGTH);
             }
--- a/mobile/android/base/java/org/mozilla/gecko/home/HistoryPanel.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/HistoryPanel.java
@@ -204,17 +204,17 @@ public class HistoryPanel extends HomeFr
                         // Send message to Java to clear history.
                         final JSONObject json = new JSONObject();
                         try {
                             json.put("history", true);
                         } catch (JSONException e) {
                             Log.e(LOGTAG, "JSON error", e);
                         }
 
-                        GeckoAppShell.notifyObservers("Sanitize:ClearData", json.toString());
+                        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Sanitize:ClearData", json.toString()));
 
                         Telemetry.sendUIEvent(TelemetryContract.Event.SANITIZE, TelemetryContract.Method.BUTTON, "history");
                     }
                 });
 
                 dialogBuilder.show();
             }
         });
--- a/mobile/android/base/java/org/mozilla/gecko/home/HomeBanner.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/HomeBanner.java
@@ -96,27 +96,27 @@ public class HomeBanner extends LinearLa
         closeButton.getDrawable().setAlpha(127);
 
         closeButton.setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View view) {
                 HomeBanner.this.dismiss();
 
                 // Send the current message id back to JS.
-                GeckoAppShell.notifyObservers("HomeBanner:Dismiss", (String) getTag());
+                GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("HomeBanner:Dismiss", (String) getTag()));
             }
         });
 
         setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
                 HomeBanner.this.dismiss();
 
                 // Send the current message id back to JS.
-                GeckoAppShell.notifyObservers("HomeBanner:Click", (String) getTag());
+                GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("HomeBanner:Click", (String) getTag()));
             }
         });
 
         EventDispatcher.getInstance().registerGeckoThreadListener(this, "HomeBanner:Data");
     }
 
     @Override
     public void onDetachedFromWindow() {
@@ -155,17 +155,17 @@ public class HomeBanner extends LinearLa
             mOnDismissListener.onDismiss();
         }
     }
 
     /**
      * Sends a message to gecko to request a new banner message. UI is updated in handleMessage.
      */
     public void update() {
-        GeckoAppShell.notifyObservers("HomeBanner:Get", null);
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("HomeBanner:Get", null));
     }
 
     @Override
     public void handleMessage(String event, JSONObject message) {
         final String id = message.optString("id");
         final String text = message.optString("text");
         final String iconURI = message.optString("iconURI");
 
@@ -190,17 +190,17 @@ public class HomeBanner extends LinearLa
                             mIconView.setVisibility(View.GONE);
                         } else {
                             mIconView.setImageDrawable(d);
                             mIconView.setVisibility(View.VISIBLE);
                         }
                     }
                 });
 
-                GeckoAppShell.notifyObservers("HomeBanner:Shown", id);
+                GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("HomeBanner:Shown", id));
 
                 // Enable the banner after a message is set.
                 setEnabled(true);
 
                 // Animate the banner if it is currently active.
                 if (mActive) {
                     animateUp();
                 }
--- a/mobile/android/base/java/org/mozilla/gecko/home/HomeFragment.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/HomeFragment.java
@@ -406,17 +406,17 @@ public abstract class HomeFragment exten
 
                 case HISTORY:
                     mDB.removeHistoryEntry(cr, mUrl);
                     break;
 
                 case READING_LIST:
                     Telemetry.sendUIEvent(TelemetryContract.Event.UNSAVE, TelemetryContract.Method.CONTEXT_MENU, "reading_list");
                     mDB.getReadingListAccessor().removeReadingListItemWithURL(cr, mUrl);
-                    GeckoAppShell.notifyObservers("Reader:Removed", mUrl);
+                    GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Reader:Removed", mUrl));
                     break;
 
                 default:
                     Log.e(LOGTAG, "Can't remove item type " + mType.toString());
                     break;
             }
             return null;
         }
--- a/mobile/android/base/java/org/mozilla/gecko/home/PanelAuthLayout.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/PanelAuthLayout.java
@@ -40,17 +40,17 @@ class PanelAuthLayout extends LinearLayo
 
         final Button buttonView = (Button) findViewById(R.id.button);
         buttonView.setText(authConfig.getButtonText());
 
         final String panelId = panelConfig.getId();
         buttonView.setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
-                GeckoAppShell.notifyObservers("HomePanels:Authenticate", panelId);
+                GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("HomePanels:Authenticate", panelId));
             }
         });
 
         final ImageView imageView = (ImageView) findViewById(R.id.image);
         final String imageUrl = authConfig.getImageUrl();
 
         if (TextUtils.isEmpty(imageUrl)) {
             // Use a default image if an image URL isn't specified.
--- a/mobile/android/base/java/org/mozilla/gecko/home/PanelInfoManager.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/PanelInfoManager.java
@@ -96,17 +96,17 @@ public class PanelInfoManager implements
 
                 message.put("ids", idsArray);
             }
         } catch (JSONException e) {
             Log.e(LOGTAG, "Failed to build event to request panels by id", e);
             return;
         }
 
-        GeckoAppShell.notifyObservers("HomePanels:Get", message.toString());
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("HomePanels:Get", message.toString()));
     }
 
     /**
      * Asynchronously fetches list of available panels from Gecko.
      *
      * @param callback onComplete will be called on the UI thread.
      */
     public void requestAvailablePanels(RequestCallback callback) {
--- a/mobile/android/base/java/org/mozilla/gecko/home/PanelRefreshLayout.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/PanelRefreshLayout.java
@@ -80,12 +80,14 @@ class PanelRefreshLayout extends SwipeRe
             try {
                 response.put(JSON_KEY_PANEL_ID, panelId);
                 response.put(JSON_KEY_VIEW_INDEX, viewIndex);
             } catch (JSONException e) {
                 Log.e(LOGTAG, "Could not create refresh message", e);
                 return;
             }
 
-            GeckoAppShell.notifyObservers("HomePanels:RefreshView", response.toString());
+            final GeckoEvent event =
+                GeckoEvent.createBroadcastEvent("HomePanels:RefreshView", response.toString());
+            GeckoAppShell.sendEventToGecko(event);
         }
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/home/RecentTabsPanel.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/RecentTabsPanel.java
@@ -75,17 +75,17 @@ public class RecentTabsPanel extends Hom
     private void restoreSessionWithHistory(List<String> dataList) {
         JSONObject json = new JSONObject();
         try {
             json.put("tabs", new JSONArray(dataList));
         } catch (JSONException e) {
             Log.e(LOGTAG, "JSON error", e);
         }
 
-        GeckoAppShell.notifyObservers("Session:RestoreRecentTabs", json.toString());
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Session:RestoreRecentTabs", json.toString()));
     }
 
     private static final class ClosedTab {
         public final String url;
         public final String title;
         public final String data;
 
         public ClosedTab(String url, String title, String data) {
@@ -159,32 +159,32 @@ public class RecentTabsPanel extends Hom
                 info.title = cursor.getString(cursor.getColumnIndexOrThrow(RecentTabs.TITLE));
                 return info;
             }
         });
 
         registerForContextMenu(mList);
 
         EventDispatcher.getInstance().registerGeckoThreadListener(this, "ClosedTabs:Data");
-        GeckoAppShell.notifyObservers("ClosedTabs:StartNotifications", null);
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("ClosedTabs:StartNotifications", null));
     }
 
     @Override
     public void onDestroyView() {
         super.onDestroyView();
 
         // Discard any additional item clicks on the list as the
         // panel is getting destroyed (bug 1210243).
         mList.setOnItemClickListener(null);
 
         mList = null;
         mEmptyView = null;
 
         EventDispatcher.getInstance().unregisterGeckoThreadListener(this, "ClosedTabs:Data");
-        GeckoAppShell.notifyObservers("ClosedTabs:StopNotifications", null);
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("ClosedTabs:StopNotifications", null));
     }
 
     @Override
     public void onActivityCreated(Bundle savedInstanceState) {
         super.onActivityCreated(savedInstanceState);
 
         // Intialize adapter
         mAdapter = new RecentTabsAdapter(getActivity());
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
@@ -942,20 +942,20 @@ OnSharedPreferenceChangeListener
 
         return true;
     }
 
     /**
      * Restore default search engines in Gecko and retrigger a search engine refresh.
      */
     protected void restoreDefaultSearchEngines() {
-        GeckoAppShell.notifyObservers("SearchEngines:RestoreDefaults", null);
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SearchEngines:RestoreDefaults", null));
 
         // Send message to Gecko to get engines. SearchPreferenceCategory listens for the response.
-        GeckoAppShell.notifyObservers("SearchEngines:GetVisible", null);
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SearchEngines:GetVisible", null));
     }
 
     @Override
     public boolean onOptionsItemSelected(MenuItem item) {
         int itemId = item.getItemId();
         switch (itemId) {
             case android.R.id.home:
                 finishChoosingTransition();
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/PrivateDataPreference.java
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/PrivateDataPreference.java
@@ -50,11 +50,11 @@ class PrivateDataPreference extends Mult
             try {
                 json.put(key, true);
             } catch (JSONException e) {
                 Log.e(LOGTAG, "JSON error", e);
             }
         }
 
         // clear private data in gecko
-        GeckoAppShell.notifyObservers("Sanitize:ClearData", json.toString());
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Sanitize:ClearData", json.toString()));
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/SearchPreferenceCategory.java
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/SearchPreferenceCategory.java
@@ -38,17 +38,17 @@ public class SearchPreferenceCategory ex
     }
 
     @Override
     protected void onAttachedToActivity() {
         super.onAttachedToActivity();
 
         // Register for SearchEngines messages and request list of search engines from Gecko.
         EventDispatcher.getInstance().registerGeckoThreadListener(this, "SearchEngines:Data");
-        GeckoAppShell.notifyObservers("SearchEngines:GetVisible", null);
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SearchEngines:GetVisible", null));
     }
 
     @Override
     protected void onPrepareForRemoval() {
         super.onPrepareForRemoval();
 
         EventDispatcher.getInstance().unregisterGeckoThreadListener(this, "SearchEngines:Data");
     }
@@ -135,11 +135,11 @@ public class SearchPreferenceCategory ex
     private void sendGeckoEngineEvent(String event, String engineName) {
         JSONObject json = new JSONObject();
         try {
             json.put("engine", engineName);
         } catch (JSONException e) {
             Log.e(LOGTAG, "JSONException creating search engine configuration change message for Gecko.", e);
             return;
         }
-        GeckoAppShell.notifyObservers(event, json.toString());
+        GeckoAppShell.notifyGeckoOfEvent(GeckoEvent.createBroadcastEvent(event, json.toString()));
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/push/PushService.java
+++ b/mobile/android/base/java/org/mozilla/gecko/push/PushService.java
@@ -174,17 +174,17 @@ public class PushService implements Bund
                 data.put("enckey", bundle.getString("enckey"));
                 data.put("message", bundle.getString("body"));
             } catch (JSONException e) {
                 Log.e(LOG_TAG, "Got exception delivering dom/push message to Gecko!", e);
                 return;
             }
 
             Log.i(LOG_TAG, "Delivering dom/push message to Gecko!");
-            GeckoAppShell.notifyObservers("PushServiceAndroidGCM:ReceivedPushMessage", data.toString());
+            GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("PushServiceAndroidGCM:ReceivedPushMessage", data.toString()));
         } else {
             Log.e(LOG_TAG, "Message directed to unknown service; dropping: " + subscription.service);
         }
     }
 
     protected void registerGeckoEventListener() {
         Log.d(LOG_TAG, "Registered Gecko event listener.");
         EventDispatcher.getInstance().registerBackgroundThreadListener(this, GECKO_EVENTS);
--- a/mobile/android/base/java/org/mozilla/gecko/reader/ReadingListHelper.java
+++ b/mobile/android/base/java/org/mozilla/gecko/reader/ReadingListHelper.java
@@ -258,17 +258,18 @@ public final class ReadingListHelper imp
                     return;
                 }
                 try {
                     while (c.moveToNext()) {
                         JSONObject json = new JSONObject();
                         try {
                             json.put("id", c.getInt(c.getColumnIndexOrThrow(ReadingListItems._ID)));
                             json.put("url", c.getString(c.getColumnIndexOrThrow(ReadingListItems.URL)));
-                            GeckoAppShell.notifyObservers("Reader:FetchContent", json.toString());
+                            GeckoAppShell.sendEventToGecko(
+                                    GeckoEvent.createBroadcastEvent("Reader:FetchContent", json.toString()));
                         } catch (JSONException e) {
                             Log.e(LOGTAG, "Failed to fetch reading list content for item");
                         }
                     }
                 } finally {
                     c.close();
                 }
             }
--- a/mobile/android/base/java/org/mozilla/gecko/tabqueue/TabQueueHelper.java
+++ b/mobile/android/base/java/org/mozilla/gecko/tabqueue/TabQueueHelper.java
@@ -260,17 +260,17 @@ public class TabQueueHelper {
 
         JSONArray jsonArray = profile.readJSONArrayFromFile(filename);
 
         if (jsonArray.length() > 0) {
             JSONObject data = new JSONObject();
             try {
                 data.put("urls", jsonArray);
                 data.put("shouldNotifyTabsOpenedToJava", shouldPerformJavaScriptCallback);
-                GeckoAppShell.notifyObservers("Tabs:OpenMultiple", data.toString());
+                GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Tabs:OpenMultiple", data.toString()));
             } catch (JSONException e) {
                 // Don't exit early as we perform cleanup at the end of this function.
                 Log.e(LOGTAG, "Error sending tab queue data", e);
             }
         }
 
         try {
             profile.deleteFileFromProfileDir(filename);
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryFragment.java
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabHistoryFragment.java
@@ -82,17 +82,17 @@ public class TabHistoryFragment extends 
         final ArrayAdapter<TabHistoryPage> urlAdapter = new TabHistoryAdapter(getActivity(), historyPageList);
         dialogList.setAdapter(urlAdapter);
         dialogList.setOnItemClickListener(this);
     }
 
     @Override
     public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
         String index = String.valueOf(toIndex - position);
-        GeckoAppShell.notifyObservers("Session:Navigate", index);
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Session:Navigate", index));
         dismiss();
     }
 
     @Override
     public void onClick(View v) {
         // Since the fragment view fills the entire screen, any clicks outside of the history
         // ListView will end up here.
         dismiss();
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsGridLayout.java
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsGridLayout.java
@@ -184,17 +184,17 @@ class TabsGridLayout extends GridView
         }
     }
 
     @Override
     public void hide() {
         lastSelectedTabId = Tabs.getInstance().getSelectedTab().getId();
         setVisibility(View.GONE);
         Tabs.unregisterOnTabsChangedListener(this);
-        GeckoAppShell.notifyObservers("Tab:Screenshot:Cancel", "");
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Tab:Screenshot:Cancel", ""));
         tabsAdapter.clear();
     }
 
     @Override
     public boolean shouldExpand() {
         return true;
     }
 
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsListLayout.java
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsListLayout.java
@@ -90,17 +90,17 @@ class TabsListLayout extends TwoWayView
         Tabs.registerOnTabsChangedListener(this);
         refreshTabsData();
     }
 
     @Override
     public void hide() {
         setVisibility(View.GONE);
         Tabs.unregisterOnTabsChangedListener(this);
-        GeckoAppShell.notifyObservers("Tab:Screenshot:Cancel", "");
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Tab:Screenshot:Cancel", ""));
         tabsAdapter.clear();
     }
 
     @Override
     public boolean shouldExpand() {
         return isVertical();
     }
 
--- a/mobile/android/base/java/org/mozilla/gecko/toolbar/PageActionLayout.java
+++ b/mobile/android/base/java/org/mozilla/gecko/toolbar/PageActionLayout.java
@@ -100,22 +100,22 @@ public class PageActionLayout extends Li
             final String id = message.getString("id");
             final String title = message.getString("title");
             final String imageURL = message.getString("icon");
             final boolean important = message.getBoolean("important");
 
             addPageAction(id, title, imageURL, new OnPageActionClickListeners() {
                 @Override
                 public void onClick(String id) {
-                    GeckoAppShell.notifyObservers("PageActions:Clicked", id);
+                    GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("PageActions:Clicked", id));
                 }
 
                 @Override
                 public boolean onLongClick(String id) {
-                    GeckoAppShell.notifyObservers("PageActions:LongClicked", id);
+                    GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("PageActions:LongClicked", id));
                     return true;
                 }
             }, important);
         } else if (event.equals("PageActions:Remove")) {
             final String id = message.getString("id");
 
             removePageAction(id);
         }
--- a/mobile/android/base/java/org/mozilla/gecko/toolbar/SiteIdentityPopup.java
+++ b/mobile/android/base/java/org/mozilla/gecko/toolbar/SiteIdentityPopup.java
@@ -146,17 +146,18 @@ public class SiteIdentityPopup extends A
                                          siteIdentity.getSecurityMode() == SecurityMode.VERIFIED);
         updateConnectionState(siteIdentity);
         toggleIdentityKnownContainerVisibility(isIdentityKnown);
 
         if (isIdentityKnown) {
             updateIdentityInformation(siteIdentity);
         }
 
-        GeckoAppShell.notifyObservers("Permissions:Check", null);
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(
+            "Permissions:Check", null));
     }
 
     @Override
     public void handleMessage(String event, JSONObject geckoObject) {
         if ("Doorhanger:Logins".equals(event)) {
             try {
                 final Tab selectedTab = Tabs.getInstance().getSelectedTab();
                 if (selectedTab != null) {
@@ -170,17 +171,17 @@ public class SiteIdentityPopup extends A
                 Log.e(LOGTAG, "Error accessing logins in Doorhanger:Logins message", e);
             }
         } else if ("Permissions:CheckResult".equals(event)) {
             final boolean hasPermissions = geckoObject.optBoolean("hasPermissions", false);
             if (hasPermissions) {
                 mSiteSettingsLink.setOnClickListener(new View.OnClickListener() {
                     @Override
                     public void onClick(View v) {
-                        GeckoAppShell.notifyObservers("Permissions:Get", null);
+                        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Permissions:Get", null));
                         dismiss();
                     }
                 });
             }
 
             ThreadUtils.postToUiThread(new Runnable() {
                 @Override
                 public void run() {
@@ -559,13 +560,14 @@ public class SiteIdentityPopup extends A
         removeSelectLoginDoorhanger();
         mTitle.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
         mDivider.setVisibility(View.GONE);
     }
 
     private class ContentNotificationButtonListener implements OnButtonClickListener {
         @Override
         public void onButtonClick(JSONObject response, DoorHanger doorhanger) {
-            GeckoAppShell.notifyObservers("Session:Reload", response.toString());
+            GeckoEvent e = GeckoEvent.createBroadcastEvent("Session:Reload", response.toString());
+            GeckoAppShell.sendEventToGecko(e);
             dismiss();
         }
     }
 }
--- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecNativeActions.java
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/FennecNativeActions.java
@@ -183,17 +183,17 @@ public class FennecNativeActions impleme
     }
 
     public RepeatedEventExpecter expectGeckoEvent(final String geckoEvent) {
         FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG, "waiting for " + geckoEvent);
         return new GeckoEventExpecter(geckoEvent);
     }
 
     public void sendGeckoEvent(final String geckoEvent, final String data) {
-        GeckoAppShell.notifyObservers(geckoEvent, data);
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(geckoEvent, data));
     }
 
     public static final class PrefProxy implements PrefsHelper.PrefHandler, PrefWaiter {
         public static final int MAX_WAIT_MS = 180000;
 
         /* package */ final PrefHandlerBase target;
         private final String[] expectedPrefs;
         private final ArrayList<String> seenPrefs = new ArrayList<>();
--- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/BaseRobocopTest.java
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/BaseRobocopTest.java
@@ -212,17 +212,17 @@ public abstract class BaseRobocopTest ex
             // manually inspect an activity's state after a test
             // run. runtestsremote.py sets this to "1".  Testers running via an
             // IDE will not have this set at all.
             final String quitAndFinish = FennecInstrumentationTestRunner.getFennecArguments()
                     .getString("quit_and_finish"); // null means not specified.
             if ("1".equals(quitAndFinish)) {
                 // Request the browser force quit and wait for it to take effect.
                 Log.i(LOGTAG, "Requesting force quit.");
-                mActions.sendGeckoEvent("Robocop:Quit", null);
+                GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Robocop:Quit", null));
                 mSolo.sleep(ROBOCOP_QUIT_WAIT_MS);
 
                 // If still running, finish activities as recommended by Robotium.
                 Log.i(LOGTAG, "Finishing all opened activities.");
                 mSolo.finishOpenedActivities();
             } else {
                 // This has the effect of keeping the activity-under-test
                 // around; if we don't set it to null, it is killed, either by
--- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAccessibleCarets.java
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAccessibleCarets.java
@@ -65,14 +65,16 @@ public class testAccessibleCarets extend
                     final JSONObject args = new JSONObject();
                     try {
                         args.put("tabId", tab.getId());
                         args.put("event", msg.toString());
                     } catch (JSONException e) {
                         Log.e(LOGTAG, "Error building JSON arguments for " + TAB_CHANGE_EVENT, e);
                         return;
                     }
-                    mActions.sendGeckoEvent(TAB_CHANGE_EVENT, args.toString());
+                    final GeckoEvent event =
+                        GeckoEvent.createBroadcastEvent(TAB_CHANGE_EVENT, args.toString());
+                    GeckoAppShell.sendEventToGecko(event);
                     break;
             }
         }
     }
 }
--- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFilePicker.java
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testFilePicker.java
@@ -27,17 +27,17 @@ public class testFilePicker extends Java
         // don't want to try to emulate "picking" a file using the Android intent chooser.
         if (event.equals("FilePicker:Show")) {
             try {
                 message.put("file", TEST_FILENAME);
             } catch (JSONException ex) {
                 fFail("Can't add filename to message " + TEST_FILENAME);
             }
 
-            mActions.sendGeckoEvent("FilePicker:Result", message.toString());
+            GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("FilePicker:Result", message.toString()));
         }
     }
 
     @Override
     public void setUp() throws Exception {
         super.setUp();
 
         EventDispatcher.getInstance().registerGeckoThreadListener(this, "FilePicker:Show");