Bug 1530402 - Add {Browser,Page}Action support to GVE. r=snorp
☠☠ backed out by 8f3cbd66bbc3 ☠ ☠
authorAgi Sferro <agi@sferro.dev>
Wed, 13 Nov 2019 20:29:17 +0000
changeset 501844 f6af9d6a5482ee9fabebecb8e7a6dd8c445769eb
parent 501843 bf09025d6f98793965458b5805a8f53564ce540a
child 501845 9e55fee783ff8a1dc0c512316689145a57f8b6aa
push id114172
push userdluca@mozilla.com
push dateTue, 19 Nov 2019 11:31:10 +0000
treeherdermozilla-inbound@b5c5ba07d3db [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssnorp
bugs1530402
milestone72.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1530402 - Add {Browser,Page}Action support to GVE. r=snorp Differential Revision: https://phabricator.services.mozilla.com/D49042
mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/ActionButton.java
mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/TabSession.java
mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/TabSessionManager.java
mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/ToolbarLayout.java
mobile/android/geckoview_example/src/main/res/drawable/rounded_bg.xml
mobile/android/geckoview_example/src/main/res/layout/browser_action.xml
mobile/android/geckoview_example/src/main/res/layout/browser_action_popup.xml
mobile/android/geckoview_example/src/main/res/values/colors.xml
mobile/android/geckoview_example/src/main/res/values/ids.xml
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/ActionButton.java
@@ -0,0 +1,18 @@
+package org.mozilla.geckoview_example;
+
+import android.graphics.Bitmap;
+
+public class ActionButton {
+    final Bitmap icon;
+    final String text;
+    final Integer textColor;
+    final Integer backgroundColor;
+
+    public ActionButton(final Bitmap icon, final String text, final Integer textColor,
+                        final Integer backgroundColor) {
+        this.icon = icon;
+        this.text = text;
+        this.textColor = textColor;
+        this.backgroundColor = backgroundColor;
+    }
+}
--- a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
@@ -39,71 +39,238 @@ import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.drawable.Icon;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Environment;
-import android.os.Handler;
 import android.os.SystemClock;
-import android.security.keystore.KeyProperties;
 import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
 import android.support.v4.app.ActivityCompat;
 import android.support.v4.app.NotificationCompat;
 import android.support.v4.app.NotificationManagerCompat;
 import android.support.v4.content.ContextCompat;
 import android.support.v7.app.ActionBar;
 import android.support.v7.app.AppCompatActivity;
 import android.util.Log;
+import android.util.LruCache;
+import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.View;
+import android.view.ViewGroup;
 import android.view.WindowManager;
 import android.widget.ProgressBar;
+import android.widget.RelativeLayout;
 
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
-import java.security.KeyPair;
-import java.security.KeyPairGenerator;
-import java.security.NoSuchAlgorithmException;
-import java.security.interfaces.ECPublicKey;
-import java.security.spec.ECGenParameterSpec;
+import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.Locale;
 
-public class GeckoViewActivity extends AppCompatActivity {
+interface BrowserActionDelegate {
+    default GeckoSession toggleBrowserActionPopup(boolean force) {
+        return null;
+    }
+    default void onActionButton(ActionButton button) {}
+    default TabSession getSession(GeckoSession session) {
+        return null;
+    }
+    default TabSession getCurrentSession() {
+        return null;
+    }
+}
+
+class WebExtensionManager implements WebExtension.ActionDelegate, TabSessionManager.TabObserver {
+    public WebExtension extension;
+
+    private LruCache<WebExtension.ActionIcon, Bitmap> mBitmapCache = new LruCache<>(5);
+    private GeckoRuntime mRuntime;
+    private WebExtension.Action mDefaultAction;
+
+    private WeakReference<BrowserActionDelegate> mActionDelegate;
+
+    // We only support either one browserAction or one pageAction
+    private void onAction(final WebExtension extension, final GeckoSession session,
+                          final WebExtension.Action action) {
+        BrowserActionDelegate delegate = mActionDelegate.get();
+        if (delegate == null) {
+            return;
+        }
+
+        WebExtension.Action resolved;
+
+        if (session == null) {
+            // This is the default action
+            mDefaultAction = action;
+            resolved = actionFor(delegate.getCurrentSession());
+        } else {
+            if (delegate.getSession(session) == null) {
+                return;
+            }
+            delegate.getSession(session).action = action;
+            if (delegate.getCurrentSession() != session) {
+                // This update is not for the session that we are currently displaying,
+                // no need to update the UI
+                return;
+            }
+            resolved = action.withDefault(mDefaultAction);
+        }
+
+        updateAction(resolved);
+    }
+
+    @Override
+    public void onPageAction(final WebExtension extension,
+                                final GeckoSession session,
+                                final WebExtension.Action action) {
+        onAction(extension, session, action);
+    }
+
+    @Override
+    public void onBrowserAction(final WebExtension extension,
+                                final GeckoSession session,
+                                final WebExtension.Action action) {
+        onAction(extension, session, action);
+    }
+
+    private GeckoResult<GeckoSession> togglePopup(boolean force) {
+        BrowserActionDelegate actionDelegate = mActionDelegate.get();
+        if (actionDelegate == null) {
+            return null;
+        }
+
+        GeckoSession session = actionDelegate.toggleBrowserActionPopup(false);
+        if (session == null) {
+            return null;
+        }
+
+        return GeckoResult.fromValue(session);
+    }
+
+    @Override
+    public GeckoResult<GeckoSession> onTogglePopup(final @NonNull WebExtension extension,
+                                                   final @NonNull WebExtension.Action action) {
+        return togglePopup(false);
+    }
+
+    @Override
+    public GeckoResult<GeckoSession> onOpenPopup(final @NonNull WebExtension extension,
+                                                 final @NonNull WebExtension.Action action) {
+        return togglePopup(true);
+    }
+
+    private WebExtension.Action actionFor(TabSession session) {
+        if (session.action == null) {
+            return mDefaultAction;
+        } else {
+            return session.action.withDefault(mDefaultAction);
+        }
+    }
+
+    private void updateAction(WebExtension.Action resolved) {
+        BrowserActionDelegate actionDelegate = mActionDelegate.get();
+        if (actionDelegate == null) {
+            return;
+        }
+
+        if (resolved.enabled == null || !resolved.enabled) {
+            actionDelegate.onActionButton(null);
+            return;
+        }
+
+        if (resolved.icon != null) {
+            if (mBitmapCache.get(resolved.icon) != null) {
+                actionDelegate.onActionButton(new ActionButton(
+                        mBitmapCache.get(resolved.icon), resolved.badgeText,
+                        resolved.badgeTextColor,
+                        resolved.badgeBackgroundColor
+                ));
+            } else {
+                resolved.icon.get(100).accept(bitmap -> {
+                    mBitmapCache.put(resolved.icon, bitmap);
+                    actionDelegate.onActionButton(new ActionButton(
+                        bitmap, resolved.badgeText,
+                        resolved.badgeTextColor,
+                        resolved.badgeBackgroundColor));
+                });
+            }
+        } else {
+            actionDelegate.onActionButton(null);
+        }
+    }
+
+    public void onClicked(TabSession session) {
+        actionFor(session).click();
+    }
+
+    public void setActionDelegate(BrowserActionDelegate delegate) {
+        mActionDelegate = new WeakReference<>(delegate);
+    }
+
+    @Override
+    public void onCurrentSession(TabSession session) {
+        if (mDefaultAction == null) {
+            // No action was ever defined, so nothing to do
+            return;
+        }
+
+        if (session.action != null) {
+            updateAction(session.action.withDefault(mDefaultAction));
+        } else {
+            updateAction(mDefaultAction);
+        }
+    }
+
+    public WebExtensionManager(GeckoRuntime runtime) {
+        mRuntime = runtime;
+        // TODO: allow users to install an extension from file
+        // extension = new WebExtension("resource://android/assets/chill-out/");
+        // extension.setActionDelegate(this);
+        // mRuntime.registerWebExtension(extension);
+    }
+}
+
+public class GeckoViewActivity
+        extends AppCompatActivity
+        implements ToolbarLayout.TabListener, BrowserActionDelegate {
     private static final String LOGTAG = "GeckoViewActivity";
     private static final String USE_MULTIPROCESS_EXTRA = "use_multiprocess";
     private static final String FULL_ACCESSIBILITY_TREE_EXTRA = "full_accessibility_tree";
     private static final String SEARCH_URI_BASE = "https://www.google.com/search?q=";
     private static final String ACTION_SHUTDOWN = "org.mozilla.geckoview_example.SHUTDOWN";
     private static final String CHANNEL_ID = "GeckoViewExample";
     private static final int REQUEST_FILE_PICKER = 1;
     private static final int REQUEST_PERMISSIONS = 2;
     private static final int REQUEST_WRITE_EXTERNAL_STORAGE = 3;
 
     private static GeckoRuntime sGeckoRuntime;
+
+    private static WebExtensionManager sExtensionManager;
+
     private TabSessionManager mTabSessionManager;
     private GeckoView mGeckoView;
     private boolean mUseMultiprocess;
     private boolean mFullAccessibilityTree;
     private boolean mUseTrackingProtection;
     private boolean mUsePrivateBrowsing;
     private boolean mEnableRemoteDebugging;
     private boolean mKillProcessOnDestroy;
     private boolean mDesktopMode;
+    private TabSession mPopupSession;
+    private View mPopupView;
 
     private boolean mShowNotificationsRejected;
     private ArrayList<String> mAcceptedPersistentStorage = new ArrayList<String>();
 
     private ToolbarLayout mToolbarView;
     private String mCurrentUri;
     private boolean mCanGoBack;
     private boolean mCanGoForward;
@@ -139,17 +306,17 @@ public class GeckoViewActivity extends A
         mGeckoView = findViewById(R.id.gecko_view);
 
         mTabSessionManager = new TabSessionManager();
 
         setSupportActionBar(findViewById(R.id.toolbar));
 
         mToolbarView = new ToolbarLayout(this, mTabSessionManager);
         mToolbarView.setId(R.id.toolbar_layout);
-        mToolbarView.setTabListener(this::switchToSessionAtIndex);
+        mToolbarView.setTabListener(this);
 
         getSupportActionBar().setCustomView(mToolbarView,
                 new ActionBar.LayoutParams(ActionBar.LayoutParams.MATCH_PARENT,
                         ActionBar.LayoutParams.WRAP_CONTENT));
         getSupportActionBar().setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
 
         mUseMultiprocess = getIntent().getBooleanExtra(USE_MULTIPROCESS_EXTRA, true);
         mEnableRemoteDebugging = true;
@@ -197,16 +364,19 @@ public class GeckoViewActivity extends A
                 @Override
                 public GeckoResult<AllowOrDeny> onCloseTab(WebExtension source, GeckoSession session) {
                     TabSession tabSession = mTabSessionManager.getSession(session);
                     closeTab(tabSession);
                     return GeckoResult.fromValue(AllowOrDeny.ALLOW);
                 }
             });
 
+            sExtensionManager = new WebExtensionManager(sGeckoRuntime);
+            mTabSessionManager.setTabObserver(sExtensionManager);
+
             // `getSystemService` call requires API level 23
             if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
                 sGeckoRuntime.setWebNotificationDelegate(new WebNotificationDelegate() {
                     NotificationManager notificationManager = getSystemService(NotificationManager.class);
                     @Override
                     public void onShowNotification(@NonNull WebNotification notification) {
                         Intent clickIntent = new Intent(GeckoViewActivity.this, GeckoViewActivity.class);
                         clickIntent.putExtra("onClick",notification.tag);
@@ -255,16 +425,18 @@ public class GeckoViewActivity extends A
             }
 
             sGeckoRuntime.setDelegate(() -> {
                 mKillProcessOnDestroy = true;
                 finish();
             });
         }
 
+        sExtensionManager.setActionDelegate(this);
+
         if(savedInstanceState == null) {
             TabSession session = getIntent().getParcelableExtra("session");
             if (session != null) {
                 connectSession(session);
 
                 if (!session.isOpen()) {
                     session.open(sGeckoRuntime);
                 }
@@ -282,16 +454,79 @@ public class GeckoViewActivity extends A
             }
             loadFromIntent(getIntent());
         }
 
         mToolbarView.getLocationView().setCommitListener(mCommitListener);
         mToolbarView.updateTabCount();
     }
 
+    @Override
+    public TabSession getSession(GeckoSession session) {
+        return mTabSessionManager.getSession(session);
+    }
+
+    @Override
+    public TabSession getCurrentSession() {
+        return mTabSessionManager.getCurrentSession();
+    }
+
+    @Override
+    public void onActionButton(ActionButton button) {
+        mToolbarView.setBrowserActionButton(button);
+    }
+
+    @Override
+    public GeckoSession toggleBrowserActionPopup(boolean force) {
+        if (mPopupSession == null) {
+            openPopupSession();
+        }
+
+        ViewGroup.LayoutParams params = mPopupView.getLayoutParams();
+        boolean shouldShow = force || params.width == 0;
+
+        if (shouldShow) {
+            params.height = 1100;
+            params.width = 1200;
+        } else {
+            params.height = 0;
+            params.width = 0;
+        }
+
+        mPopupView.setLayoutParams(params);
+        return shouldShow ? mPopupSession : null;
+    }
+
+    private void openPopupSession() {
+        LayoutInflater inflater = (LayoutInflater)
+                getSystemService(LAYOUT_INFLATER_SERVICE);
+        mPopupView = inflater.inflate(R.layout.browser_action_popup, null);
+        GeckoView geckoView = mPopupView.findViewById(R.id.gecko_view_popup);
+        geckoView.setViewBackend(GeckoView.BACKEND_TEXTURE_VIEW);
+        mPopupSession = new TabSession();
+        mPopupSession.open(sGeckoRuntime);
+        geckoView.setSession(mPopupSession);
+
+        mPopupView.setOnFocusChangeListener(this::hideBrowserAction);
+        RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(0, 0);
+        params.addRule(RelativeLayout.ABOVE, R.id.toolbar);
+        mPopupView.setLayoutParams(params);
+        mPopupView.setFocusable(true);
+        ((ViewGroup) findViewById(R.id.main)).addView(mPopupView);
+    }
+
+    private void hideBrowserAction(View view, boolean hasFocus) {
+        if (!hasFocus) {
+            ViewGroup.LayoutParams params = mPopupView.getLayoutParams();
+            params.height = 0;
+            params.width = 0;
+            mPopupView.setLayoutParams(params);
+        }
+    }
+
     private void createNotificationChannel() {
         // Create the NotificationChannel, but only on API 26+ because
         // the NotificationChannel class is new and not in the support library
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
             CharSequence name = getString(R.string.app_name);
             String description = getString(R.string.activity_label);
             int importance = NotificationManager.IMPORTANCE_DEFAULT;
             NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance);
@@ -335,16 +570,19 @@ public class GeckoViewActivity extends A
 
         final ExamplePermissionDelegate permission = new ExamplePermissionDelegate();
         permission.androidPermissionRequestCode = REQUEST_PERMISSIONS;
         session.setPermissionDelegate(permission);
 
         session.setMediaDelegate(new ExampleMediaDelegate(this));
 
         session.setSelectionActionDelegate(new BasicSelectionActionDelegate(this));
+        if (sExtensionManager.extension != null) {
+            session.setWebExtensionActionDelegate(sExtensionManager.extension, sExtensionManager);
+        }
 
         updateTrackingProtection(session);
         updateDesktopMode(session);
     }
 
     private void recreateSession() {
         recreateSession(mTabSessionManager.getCurrentSession());
     }
@@ -493,17 +731,21 @@ public class GeckoViewActivity extends A
             setGeckoViewSession(tabSession);
             tabSession.reload();
             mToolbarView.updateTabCount();
         } else {
             recreateSession(session);
         }
     }
 
-    private void switchToSessionAtIndex(int index) {
+    public void onBrowserActionClick() {
+        sExtensionManager.onClicked(mTabSessionManager.getCurrentSession());
+    }
+
+    public void switchToTab(int index) {
         TabSession nextSession = mTabSessionManager.getSession(index);
         TabSession currentSession = mTabSessionManager.getCurrentSession();
         if(nextSession != currentSession) {
             setGeckoViewSession(nextSession);
             mCurrentUri = nextSession.getUri();
             mToolbarView.getLocationView().setText(mCurrentUri);
         }
     }
--- a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/TabSession.java
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/TabSession.java
@@ -2,20 +2,22 @@ package org.mozilla.geckoview_example;
 
 import android.os.Parcel;
 import android.support.annotation.AnyThread;
 import android.support.annotation.NonNull;
 import android.support.annotation.UiThread;
 
 import org.mozilla.geckoview.GeckoSession;
 import org.mozilla.geckoview.GeckoSessionSettings;
+import org.mozilla.geckoview.WebExtension;
 
 public class TabSession extends GeckoSession {
     private String mTitle;
     private String mUri;
+    public WebExtension.Action action;
 
     public TabSession() { super(); }
 
     public TabSession(GeckoSessionSettings settings) {
         super(settings);
     }
 
     public String getTitle() {
--- a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/TabSessionManager.java
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/TabSessionManager.java
@@ -3,22 +3,31 @@ package org.mozilla.geckoview_example;
 import android.support.annotation.Nullable;
 
 import org.mozilla.geckoview.GeckoSession;
 import org.mozilla.geckoview.GeckoSessionSettings;
 
 import java.util.ArrayList;
 
 public class TabSessionManager {
-    private static ArrayList<TabSession> mTabSessions = new ArrayList<TabSession>();
+    private static ArrayList<TabSession> mTabSessions = new ArrayList<>();
     private int mCurrentSessionIndex = 0;
+    private TabObserver mTabObserver;
+
+    public interface TabObserver {
+        void onCurrentSession(TabSession session);
+    }
 
     public TabSessionManager() {
     }
 
+    public void setTabObserver(TabObserver observer) {
+        mTabObserver = observer;
+    }
+
     public void addSession(TabSession session) {
         mTabSessions.add(session);
     }
 
     public TabSession getSession(int index) {
         return mTabSessions.get(index);
     }
 
@@ -36,16 +45,20 @@ public class TabSessionManager {
 
     public void setCurrentSession(TabSession session) {
         int index = mTabSessions.indexOf(session);
         if (index == -1) {
             mTabSessions.add(session);
             index = mTabSessions.size() - 1;
         }
         mCurrentSessionIndex = index;
+
+        if (mTabObserver != null) {
+            mTabObserver.onCurrentSession(session);
+        }
     }
 
     private boolean isCurrentSession(TabSession session) {
         return session == getCurrentSession();
     }
 
     public void closeSession(@Nullable TabSession session) {
         if (session == null) { return; }
--- a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/ToolbarLayout.java
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/ToolbarLayout.java
@@ -1,26 +1,35 @@
 package org.mozilla.geckoview_example;
 
 import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.PorterDuff;
 import android.graphics.Typeface;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.GradientDrawable;
+import android.graphics.drawable.ShapeDrawable;
 import android.support.v4.content.ContextCompat;
+import android.view.LayoutInflater;
 import android.view.View;
 import android.widget.Button;
+import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.PopupMenu;
+import android.widget.TextView;
 
 public class ToolbarLayout extends LinearLayout {
-
     public interface TabListener {
         void switchToTab(int tabId);
+        void onBrowserActionClick();
     }
 
     private LocationView mLocationView;
     private Button mTabsCountButton;
+    private View mBrowserAction;
     private TabListener mTabListener;
     private TabSessionManager mSessionManager;
 
     public ToolbarLayout(Context context, TabSessionManager sessionManager) {
         super(context);
         mSessionManager = sessionManager;
         initView();
     }
@@ -30,28 +39,72 @@ public class ToolbarLayout extends Linea
         setOrientation(LinearLayout.HORIZONTAL);
         mLocationView = new LocationView(getContext());
         mLocationView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT, 1.0f));
         mLocationView.setId(R.id.url_bar);
         addView(mLocationView);
 
         mTabsCountButton = getTabsCountButton();
         addView(mTabsCountButton);
+
+        mBrowserAction = getBrowserAction();
+        addView(mBrowserAction);
     }
 
     private Button getTabsCountButton() {
         Button button = new Button(getContext());
         button.setLayoutParams(new LayoutParams(150, LayoutParams.MATCH_PARENT));
         button.setId(R.id.tabs_button);
         button.setOnClickListener(this::onTabButtonClicked);
         button.setBackground(ContextCompat.getDrawable(getContext(), R.drawable.tab_number_background));
         button.setTypeface(button.getTypeface(), Typeface.BOLD);
         return button;
     }
 
+    private View getBrowserAction() {
+        View browserAction = ((LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE))
+            .inflate(R.layout.browser_action, this, false);
+        browserAction.setVisibility(GONE);
+        return browserAction;
+    }
+
+    public void setBrowserActionButton(ActionButton button) {
+        if (button == null) {
+            mBrowserAction.setVisibility(GONE);
+            return;
+        }
+
+        BitmapDrawable drawable = new BitmapDrawable(getContext().getResources(), button.icon);
+        ImageView view = mBrowserAction.findViewById(R.id.browser_action_icon);
+        view.setOnClickListener(this::onBrowserActionButtonClicked);
+        view.setBackground(drawable);
+
+        TextView badge = mBrowserAction.findViewById(R.id.browser_action_badge);
+        if (button.text != null && !button.text.equals("")) {
+            if (button.backgroundColor != null) {
+                GradientDrawable backgroundDrawable = ((GradientDrawable) badge.getBackground().mutate());
+                backgroundDrawable.setColor(button.backgroundColor);
+                backgroundDrawable.invalidateSelf();
+            }
+            if (button.textColor != null) {
+                badge.setTextColor(button.textColor);
+            }
+            badge.setText(button.text);
+            badge.setVisibility(VISIBLE);
+        } else {
+            badge.setVisibility(GONE);
+        }
+
+        mBrowserAction.setVisibility(VISIBLE);
+    }
+
+    public void onBrowserActionButtonClicked(View view) {
+        mTabListener.onBrowserActionClick();
+    }
+
     public LocationView getLocationView() {
         return mLocationView;
     }
 
     public void setTabListener(TabListener listener) {
         this.mTabListener = listener;
     }
 
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/drawable/rounded_bg.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid
+        android:id="@+id/browser_action_badge_background"
+        android:color="#176d7a"
+        />
+    <corners android:radius="5dp" />
+</shape>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/layout/browser_action.xml
@@ -0,0 +1,32 @@
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="?android:actionBarSize"
+    android:layout_height="?android:actionBarSize"
+    android:gravity="center"
+    android:orientation="vertical">
+
+    <RelativeLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        >
+        <ImageView
+            android:id="@+id/browser_action_icon"
+            android:layout_width="36dp"
+            android:layout_height="36dp"
+            android:layout_centerInParent="true"
+            />
+    </RelativeLayout>
+
+    <TextView
+        android:id="@+id/browser_action_badge"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:background="@drawable/rounded_bg"
+        android:textColor="@color/colorPrimaryDark"
+        android:layout_alignParentRight="true"
+        android:paddingLeft="3dp"
+        android:paddingRight="3dp"
+        android:layout_marginTop="3dp"
+        android:layout_marginRight="3dp"
+        android:text="12"
+        />
+</RelativeLayout>
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/layout/browser_action_popup.xml
@@ -0,0 +1,13 @@
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <org.mozilla.geckoview.GeckoView
+        android:id="@+id/gecko_view_popup"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:scrollbars="none"
+        />
+</RelativeLayout>
--- a/mobile/android/geckoview_example/src/main/res/values/colors.xml
+++ b/mobile/android/geckoview_example/src/main/res/values/colors.xml
@@ -1,6 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
     <color name="colorPrimary">#3F51B5</color>
-    <color name="colorPrimaryDark">#303F9F</color>
+    <color name="colorBackgroundDark">#3F51B5</color>
+    <color name="colorPrimaryDark">#FFFFFF</color>
     <color name="colorAccent">#FF4081</color>
 </resources>
--- a/mobile/android/geckoview_example/src/main/res/values/ids.xml
+++ b/mobile/android/geckoview_example/src/main/res/values/ids.xml
@@ -1,6 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
     <item name="toolbar_layout" type="id"/>
     <item name="url_bar" type="id"/>
+    <item name="browser_action" type="id"/>
     <item name="tabs_button" type="id"/>
 </resources>