Bug 734877 - Updates to pageaction based on the new design. r=wesj
authorShilpan Bhagat <sbhagat@mozilla.com>
Mon, 15 Jul 2013 17:19:05 -0700
changeset 138671 696633740d85c118ea187e59e26caa75f582dedc
parent 138670 c08b03cc4f324d0da1508b3ca22af97ef14b9ac8
child 138672 3e3d601a9ab2089f5937d9b96d34411d345033cf
push id24964
push userryanvm@gmail.com
push dateTue, 16 Jul 2013 20:04:09 +0000
treeherderautoland@fd10ead17ace [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerswesj
bugs734877
milestone25.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 734877 - Updates to pageaction based on the new design. r=wesj
mobile/android/base/BrowserToolbar.java
mobile/android/base/Makefile.in
mobile/android/base/PageActionLayout.java
mobile/android/base/gfx/BitmapUtils.java
mobile/android/base/menu/MenuPopup.java
mobile/android/base/resources/drawable-hdpi/icon_pageaction.png
mobile/android/base/resources/drawable-mdpi/icon_pageaction.png
mobile/android/base/resources/drawable-xhdpi/icon_pageaction.png
mobile/android/base/resources/layout-large-v11/browser_toolbar.xml
mobile/android/base/resources/layout/browser_toolbar.xml
mobile/android/base/resources/values/dimens.xml
mobile/android/base/strings.xml.in
mobile/android/base/widget/GeckoPopupMenu.java
mobile/android/chrome/content/browser.js
mobile/android/locales/en-US/chrome/browser.properties
--- a/mobile/android/base/BrowserToolbar.java
+++ b/mobile/android/base/BrowserToolbar.java
@@ -6,24 +6,24 @@
 package org.mozilla.gecko;
 
 import org.mozilla.gecko.animation.PropertyAnimator;
 import org.mozilla.gecko.animation.ViewHelper;
 import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
 import org.mozilla.gecko.gfx.LayerView;
 import org.mozilla.gecko.menu.GeckoMenu;
 import org.mozilla.gecko.menu.MenuPopup;
+import org.mozilla.gecko.PageActionLayout;
+import org.mozilla.gecko.PrefsHelper;
 import org.mozilla.gecko.util.Clipboard;
 import org.mozilla.gecko.util.StringUtils;
 import org.mozilla.gecko.util.HardwareUtils;
-
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.util.UiAsyncTask;
-
-import org.mozilla.gecko.PrefsHelper;
+import org.mozilla.gecko.util.GeckoEventListener;
 
 import org.json.JSONObject;
 
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
 import android.graphics.Color;
 import android.graphics.Rect;
@@ -58,24 +58,24 @@ import android.widget.Button;
 import android.widget.ImageButton;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.PopupWindow;
 import android.widget.RelativeLayout;
 import android.widget.RelativeLayout.LayoutParams;
 import android.widget.ViewSwitcher;
 
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 
 public class BrowserToolbar extends GeckoRelativeLayout
                             implements Tabs.OnTabsChangedListener,
                                        GeckoMenu.ActionItemBarPresenter,
-                                       Animation.AnimationListener {
+                                       Animation.AnimationListener,
+                                       GeckoEventListener {
     private static final String LOGTAG = "GeckoToolbar";
     public static final String PREF_TITLEBAR_MODE = "browser.chrome.titlebarMode";
     private LayoutParams mAwesomeBarParams;
     private View mUrlDisplayContainer;
     private View mAwesomeBarEntry;
     private ImageView mAwesomeBarRightEdge;
     private BrowserToolbarBackground mAddressBarBg;
     private GeckoTextView mTitle;
@@ -83,17 +83,17 @@ public class BrowserToolbar extends Geck
     private boolean mSiteSecurityVisible;
     private boolean mSwitchingTabs;
     private ShapedButton mTabs;
     private ImageButton mBack;
     private ImageButton mForward;
     public ImageButton mFavicon;
     public ImageButton mStop;
     public ImageButton mSiteSecurity;
-    public ImageButton mReader;
+    public PageActionLayout mPageActionLayout;
     private AnimationDrawable mProgressSpinner;
     private TabCounter mTabsCounter;
     private ImageView mShadow;
     private GeckoImageButton mMenu;
     private GeckoImageView mMenuIcon;
     private LinearLayout mActionItemBar;
     private MenuPopup mMenuPopup;
     private List<? extends View> mFocusOrder;
@@ -178,16 +178,19 @@ public class BrowserToolbar extends Geck
             }
         });
 
         Resources res = getResources();
         mUrlColor = new ForegroundColorSpan(res.getColor(R.color.url_bar_urltext));
         mDomainColor = new ForegroundColorSpan(res.getColor(R.color.url_bar_domaintext));
         mPrivateDomainColor = new ForegroundColorSpan(res.getColor(R.color.url_bar_domaintext_private));
 
+        registerEventListener("Reader:Click");
+        registerEventListener("Reader:LongClick");
+
         mShowSiteSecurity = false;
         mShowReader = false;
 
         mAnimatingEntry = false;
 
         mAddressBarBg = (BrowserToolbarBackground) findViewById(R.id.address_bar_bg);
         mAddressBarViewOffset = res.getDimensionPixelSize(R.dimen.addressbar_offset_left);
         mDefaultForwardMargin = res.getDimensionPixelSize(R.dimen.forward_default_offset);
@@ -216,35 +219,35 @@ public class BrowserToolbar extends Geck
 
         mSiteSecurity = (ImageButton) findViewById(R.id.site_security);
         mSiteSecurityVisible = (mSiteSecurity.getVisibility() == View.VISIBLE);
         mActivity.getSiteIdentityPopup().setAnchor(mSiteSecurity);
 
         mProgressSpinner = (AnimationDrawable) res.getDrawable(R.drawable.progress_spinner);
 
         mStop = (ImageButton) findViewById(R.id.stop);
-        mReader = (ImageButton) findViewById(R.id.reader);
         mShadow = (ImageView) findViewById(R.id.shadow);
+        mPageActionLayout = (PageActionLayout) findViewById(R.id.page_action_layout);
 
         if (Build.VERSION.SDK_INT >= 16) {
             mShadow.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
         }
 
         mMenu = (GeckoImageButton) findViewById(R.id.menu);
         mMenuIcon = (GeckoImageView) findViewById(R.id.menu_icon);
         mActionItemBar = (LinearLayout) findViewById(R.id.menu_items);
         mHasSoftMenuButton = !HardwareUtils.hasMenuButton();
 
         // We use different layouts on phones and tablets, so adjust the focus
         // order appropriately.
         if (HardwareUtils.isTablet()) {
             mFocusOrder = Arrays.asList(mTabs, mBack, mForward, this,
-                    mSiteSecurity, mReader, mStop, mActionItemBar, mMenu);
+                    mSiteSecurity, mPageActionLayout, mStop, mActionItemBar, mMenu);
         } else {
-            mFocusOrder = Arrays.asList(this, mSiteSecurity, mReader, mStop,
+            mFocusOrder = Arrays.asList(this, mSiteSecurity, mPageActionLayout, mStop,
                     mTabs, mMenu);
         }
     }
 
     @Override
     public void onAttachedToWindow() {
         super.onAttachedToWindow();
 
@@ -348,38 +351,16 @@ public class BrowserToolbar extends Geck
             public void onClick(View v) {
                 Tab tab = Tabs.getInstance().getSelectedTab();
                 if (tab != null)
                     tab.doStop();
                 setProgressVisibility(false);
             }
         });
 
-        mReader.setOnClickListener(new Button.OnClickListener() {
-            @Override
-            public void onClick(View view) {
-                Tab tab = Tabs.getInstance().getSelectedTab();
-                if (tab != null) {
-                    tab.toggleReaderMode();
-                }
-            }
-        });
-
-        mReader.setOnLongClickListener(new Button.OnLongClickListener() {
-            public boolean onLongClick(View v) {
-                Tab tab = Tabs.getInstance().getSelectedTab();
-                if (tab != null) {
-                    tab.addToReadingList();
-                    return true;
-                }
-
-                return false;
-            }
-        });
-
         mShadow.setOnClickListener(new Button.OnClickListener() {
             @Override
             public void onClick(View v) {
             }
         });
 
         float slideWidth = getResources().getDimension(R.dimen.browser_toolbar_lock_width);
 
@@ -469,17 +450,17 @@ public class BrowserToolbar extends Geck
             case START:
                 if (Tabs.getInstance().isSelectedTab(tab)) {
                     updateBackButton(tab.canDoBack());
                     updateForwardButton(tab.canDoForward());
                     Boolean showProgress = (Boolean)data;
                     if (showProgress && tab.getState() == Tab.STATE_LOADING)
                         setProgressVisibility(true);
                     setSecurityMode(tab.getSecurityMode());
-                    setReaderMode(tab.getReaderEnabled());
+                    setPageActionVisibility(mStop.getVisibility() == View.VISIBLE);
                 }
                 break;
             case STOP:
                 if (Tabs.getInstance().isSelectedTab(tab)) {
                     updateBackButton(tab.canDoBack());
                     updateForwardButton(tab.canDoForward());
                     setProgressVisibility(false);
                     // Reset the title in case we haven't navigated to a new page yet.
@@ -514,17 +495,17 @@ public class BrowserToolbar extends Geck
                 break;
             case SECURITY_CHANGE:
                 if (Tabs.getInstance().isSelectedTab(tab)) {
                     setSecurityMode(tab.getSecurityMode());
                 }
                 break;
             case READER_ENABLED:
                 if (Tabs.getInstance().isSelectedTab(tab)) {
-                    setReaderMode(tab.getReaderEnabled());
+                    setPageActionVisibility(mStop.getVisibility() == View.VISIBLE);
                 }
                 break;
         }
     }
 
     public boolean isVisible() {
         return getScrollY() == 0;
     }
@@ -532,17 +513,17 @@ public class BrowserToolbar extends Geck
     public void setNextFocusDownId(int nextId) {
         super.setNextFocusDownId(nextId);
         mTabs.setNextFocusDownId(nextId);
         mBack.setNextFocusDownId(nextId);
         mForward.setNextFocusDownId(nextId);
         mFavicon.setNextFocusDownId(nextId);
         mStop.setNextFocusDownId(nextId);
         mSiteSecurity.setNextFocusDownId(nextId);
-        mReader.setNextFocusDownId(nextId);
+        mPageActionLayout.setNextFocusDownId(nextId);
         mMenu.setNextFocusDownId(nextId);
     }
 
     @Override
     public void onAnimationStart(Animation animation) {
         if (animation.equals(mLockFadeIn)) {
             if (mSiteSecurityVisible)
                 mSiteSecurity.setVisibility(View.VISIBLE);
@@ -608,17 +589,17 @@ public class BrowserToolbar extends Geck
             ViewHelper.setTranslationX(mTabsCounter, curveTranslation);
             ViewHelper.setTranslationX(mActionItemBar, curveTranslation);
 
             if (mHasSoftMenuButton) {
                 ViewHelper.setTranslationX(mMenu, curveTranslation);
                 ViewHelper.setTranslationX(mMenuIcon, curveTranslation);
             }
 
-            ViewHelper.setAlpha(mReader, 0);
+            ViewHelper.setAlpha(mPageActionLayout, 0);
             ViewHelper.setAlpha(mStop, 0);
         }
 
         final PropertyAnimator contentAnimator = new PropertyAnimator(250);
         contentAnimator.setUseHardwareLayer(false);
 
         // Shrink the awesome entry back to its original size
 
@@ -656,17 +637,17 @@ public class BrowserToolbar extends Geck
             public void onPropertyAnimationEnd() {
                 // Turn off selected state on the entry
                 setSelected(false);
 
                 PropertyAnimator buttonsAnimator = new PropertyAnimator(300);
 
                 // Fade toolbar buttons (reader, stop) after the entry
                 // is schrunk back to its original size.
-                buttonsAnimator.attach(mReader,
+                buttonsAnimator.attach(mPageActionLayout,
                                        PropertyAnimator.Property.ALPHA,
                                        1);
                 buttonsAnimator.attach(mStop,
                                        PropertyAnimator.Property.ALPHA,
                                        1);
 
                 buttonsAnimator.start();
 
@@ -703,17 +684,17 @@ public class BrowserToolbar extends Geck
 
         final int entryTranslation = getAwesomeBarEntryTranslation();
         final int curveTranslation = getAwesomeBarCurveTranslation();
 
         // Keep the entry highlighted during the animation
         setSelected(true);
 
         // Hide stop/reader buttons immediately
-        ViewHelper.setAlpha(mReader, 0);
+        ViewHelper.setAlpha(mPageActionLayout, 0);
         ViewHelper.setAlpha(mStop, 0);
 
         // Slide the right side elements of the toolbar
 
         if (mAwesomeBarRightEdge != null) {
             contentAnimator.attach(mAwesomeBarRightEdge,
                                    PropertyAnimator.Property.TRANSLATION_X,
                                    entryTranslation);
@@ -822,32 +803,26 @@ public class BrowserToolbar extends Geck
 
     public void setPageActionVisibility(boolean isLoading) {
         // Handle the loading mode page actions
         mStop.setVisibility(isLoading ? View.VISIBLE : View.GONE);
 
         // Handle the viewing mode page actions
         setSiteSecurityVisibility(mShowSiteSecurity && !isLoading);
 
-        // Handle the readerMode image and visibility: We show the reader mode button if 1) you can
-        // enter reader mode for current page or 2) if you're already in reader mode,
-        // in which case we show the reader mode "close" (reader_active) icon.
         boolean inReaderMode = false;
         Tab tab = Tabs.getInstance().getSelectedTab();
         if (tab != null)
             inReaderMode = ReaderModeUtils.isAboutReader(tab.getURL());
-        mReader.setImageResource(inReaderMode ? R.drawable.reader_active : R.drawable.reader);
 
-        mReader.setVisibility(!isLoading && (mShowReader || inReaderMode) ? View.VISIBLE : View.GONE);
-
+        mPageActionLayout.setVisibility(!isLoading ? View.VISIBLE : View.GONE);
         // We want title to fill the whole space available for it when there are icons
         // being shown on the right side of the toolbar as the icons already have some
         // padding in them. This is just to avoid wasting space when icons are shown.
         mTitle.setPadding(0, 0, (!isLoading && !(mShowReader || inReaderMode) ? mTitlePadding : 0), 0);
-
         updateFocusOrder();
     }
 
     private void setSiteSecurityVisibility(final boolean visible) {
         if (visible == mSiteSecurityVisible)
             return;
 
         mSiteSecurityVisible = visible;
@@ -921,17 +896,17 @@ public class BrowserToolbar extends Geck
         Tab tab = Tabs.getInstance().getSelectedTab();
         if (tab == null) {
             return;
         }
 
         String url = tab.getURL();
 
         // Only set shadow to visible when not on about screens except about:blank.
-        visible &= !(url == null || (url.startsWith("about:") && 
+        visible &= !(url == null || (url.startsWith("about:") &&
                      !url.equals("about:blank")));
 
         if ((mShadow.getVisibility() == View.VISIBLE) != visible) {
             mShadow.setVisibility(visible ? View.VISIBLE : View.GONE);
         }
     }
 
     private void setTitle(CharSequence title) {
@@ -992,21 +967,16 @@ public class BrowserToolbar extends Geck
     private void setSecurityMode(String mode) {
         int imageLevel = SiteIdentityPopup.getSecurityImageLevel(mode);
         mSiteSecurity.setImageLevel(imageLevel);
         mShowSiteSecurity = (imageLevel != SiteIdentityPopup.LEVEL_UKNOWN);
 
         setPageActionVisibility(mStop.getVisibility() == View.VISIBLE);
     }
 
-    private void setReaderMode(boolean showReader) {
-        mShowReader = showReader;
-        setPageActionVisibility(mStop.getVisibility() == View.VISIBLE);
-    }
-
     public void prepareTabsAnimation(PropertyAnimator animator, boolean tabsAreShown) {
         if (!tabsAreShown) {
             PropertyAnimator buttonsAnimator =
                     new PropertyAnimator(animator.getDuration(), sButtonsInterpolator);
 
             buttonsAnimator.attach(mTabsCounter,
                                    PropertyAnimator.Property.ALPHA,
                                    1.0f);
@@ -1157,17 +1127,17 @@ public class BrowserToolbar extends Geck
 
     public void refresh() {
         Tab tab = Tabs.getInstance().getSelectedTab();
         if (tab != null) {
             updateTitle();
             setFavicon(tab.getFavicon());
             setProgressVisibility(tab.getState() == Tab.STATE_LOADING);
             setSecurityMode(tab.getSecurityMode());
-            setReaderMode(tab.getReaderEnabled());
+            setPageActionVisibility(mStop.getVisibility() == View.VISIBLE);
             setShadowVisibility(true);
             updateBackButton(tab.canDoBack());
             updateForwardButton(tab.canDoForward());
 
             final boolean isPrivate = tab.isPrivate();
             mAddressBarBg.setPrivateMode(isPrivate);
             setPrivateMode(isPrivate);
             mTabs.setPrivateMode(isPrivate);
@@ -1184,16 +1154,19 @@ public class BrowserToolbar extends Geck
     }
 
     public void onDestroy() {
         if (mPrefObserverId != null) {
              PrefsHelper.removeObserver(mPrefObserverId);
              mPrefObserverId = null;
         }
         Tabs.unregisterOnTabsChangedListener(this);
+
+        unregisterEventListener("Reader:Click");
+        unregisterEventListener("Reader:LongClick");
     }
 
     public boolean openOptionsMenu() {
         if (!mHasSoftMenuButton)
             return false;
 
         // Initialize the popup.
         if (mMenuPopup == null) {
@@ -1220,9 +1193,32 @@ public class BrowserToolbar extends Geck
         if (!mHasSoftMenuButton)
             return false;
 
         if (mMenuPopup != null && mMenuPopup.isShowing())
             mMenuPopup.dismiss();
 
         return true;
     }
+
+    protected void registerEventListener(String event) {
+        GeckoAppShell.getEventDispatcher().registerEventListener(event, this);
+    }
+
+    protected void unregisterEventListener(String event) {
+        GeckoAppShell.getEventDispatcher().unregisterEventListener(event, this);
+    }
+
+    @Override
+    public void handleMessage(String event, JSONObject message) {
+        if (event.equals("Reader:Click")) {
+            Tab tab = Tabs.getInstance().getSelectedTab();
+            if (tab != null) {
+                tab.toggleReaderMode();
+            }
+        } else if (event.equals("Reader:LongClick")) {
+            Tab tab = Tabs.getInstance().getSelectedTab();
+            if (tab != null) {
+                tab.addToReadingList();
+            }
+        }
+    }
 }
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -124,16 +124,17 @@ FENNEC_JAVA_FILES = \
   MotionEventInterceptor.java \
   MultiChoicePreference.java \
   NotificationClient.java \
   NotificationHandler.java \
   NotificationHelper.java \
   NotificationService.java \
   NSSBridge.java \
   OrderedBroadcastHelper.java \
+  PageActionLayout.java \
   PrefsHelper.java \
   PrivateDataPreference.java \
   PrivateTab.java \
   ProfileMigrator.java \
   Prompt.java \
   PromptInput.java \
   PromptService.java \
   Restarter.java \
@@ -636,16 +637,17 @@ RES_DRAWABLE_MDPI = \
   res/drawable-mdpi/ic_menu_bookmark_remove.png \
   res/drawable-mdpi/ic_menu_character_encoding.png \
   res/drawable-mdpi/ic_menu_close_all_tabs.png \
   res/drawable-mdpi/ic_menu_forward.png \
   res/drawable-mdpi/ic_menu_new_private_tab.png \
   res/drawable-mdpi/ic_menu_new_tab.png \
   res/drawable-mdpi/ic_menu_reload.png \
   res/drawable-mdpi/ic_status_logo.png \
+  res/drawable-mdpi/icon_pageaction.png \
   res/drawable-mdpi/progress_spinner_1.png \
   res/drawable-mdpi/progress_spinner_2.png \
   res/drawable-mdpi/progress_spinner_3.png \
   res/drawable-mdpi/progress_spinner_4.png \
   res/drawable-mdpi/progress_spinner_5.png \
   res/drawable-mdpi/progress_spinner_6.png \
   res/drawable-mdpi/progress_spinner_7.png \
   res/drawable-mdpi/progress_spinner_8.png \
@@ -758,16 +760,17 @@ RES_DRAWABLE_HDPI = \
   res/drawable-hdpi/ic_menu_bookmark_remove.png \
   res/drawable-hdpi/ic_menu_character_encoding.png \
   res/drawable-hdpi/ic_menu_close_all_tabs.png \
   res/drawable-hdpi/ic_menu_forward.png \
   res/drawable-hdpi/ic_menu_new_private_tab.png \
   res/drawable-hdpi/ic_menu_new_tab.png \
   res/drawable-hdpi/ic_menu_reload.png \
   res/drawable-hdpi/ic_status_logo.png \
+  res/drawable-hdpi/icon_pageaction.png \
   res/drawable-hdpi/tab_indicator_divider.9.png \
   res/drawable-hdpi/tab_indicator_selected.9.png \
   res/drawable-hdpi/tab_indicator_selected_focused.9.png \
   res/drawable-hdpi/spinner_default.9.png \
   res/drawable-hdpi/spinner_focused.9.png \
   res/drawable-hdpi/spinner_pressed.9.png \
   res/drawable-hdpi/tab_new.png \
   res/drawable-hdpi/tab_new_pb.png \
@@ -856,16 +859,17 @@ RES_DRAWABLE_XHDPI = \
   res/drawable-xhdpi/ic_menu_bookmark_remove.png \
   res/drawable-xhdpi/ic_menu_close_all_tabs.png \
   res/drawable-xhdpi/ic_menu_character_encoding.png \
   res/drawable-xhdpi/ic_menu_forward.png \
   res/drawable-xhdpi/ic_menu_new_private_tab.png \
   res/drawable-xhdpi/ic_menu_new_tab.png \
   res/drawable-xhdpi/ic_menu_reload.png \
   res/drawable-xhdpi/ic_status_logo.png \
+  res/drawable-xhdpi/icon_pageaction.png \
   res/drawable-xhdpi/spinner_default.9.png \
   res/drawable-xhdpi/spinner_focused.9.png \
   res/drawable-xhdpi/spinner_pressed.9.png \
   res/drawable-xhdpi/tab_new.png \
   res/drawable-xhdpi/tab_new_pb.png \
   res/drawable-xhdpi/tab_close.png \
   res/drawable-xhdpi/tab_thumbnail_default.png \
   res/drawable-xhdpi/tab_thumbnail_shadow.png \
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/PageActionLayout.java
@@ -0,0 +1,315 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko;
+
+import org.mozilla.gecko.gfx.BitmapUtils;
+import org.mozilla.gecko.util.GeckoEventListener;
+import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.gecko.widget.GeckoPopupMenu;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+
+import java.util.UUID;
+import java.util.LinkedHashMap;
+
+public class PageActionLayout extends LinearLayout implements GeckoEventListener,
+                                                              View.OnClickListener,
+                                                              View.OnLongClickListener {
+    private final String LOGTAG = "GeckoPageActionLayout";
+    private final String MENU_BUTTON_KEY = "MENU_BUTTON_KEY";
+    private final int DEFAULT_PAGE_ACTIONS_SHOWN = 2;
+
+    private LinkedHashMap<String, PageAction> mPageActionList;
+    private GeckoPopupMenu mPageActionsMenu;
+    private Context mContext;
+    private LinearLayout mLayout;
+
+    // By default it's two, can be changed by calling setNumberShown(int)
+    private int mMaxVisiblePageActions;
+
+    public PageActionLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mContext = context;
+        mLayout = this;
+
+        mPageActionList = new LinkedHashMap<String, PageAction>();
+        setNumberShown(DEFAULT_PAGE_ACTIONS_SHOWN);
+
+        registerEventListener("PageActions:Add");
+        registerEventListener("PageActions:Remove");
+    }
+
+    public void setNumberShown(int count) {
+        mMaxVisiblePageActions = count;
+
+        for(int index = 0; index < count; index++) {
+            if ((this.getChildCount() - 1) < index) {
+                mLayout.addView(createImageButton());
+            }
+        }
+    }
+
+    public void onDestroy() {
+        unregisterEventListener("PageActions:Add");
+        unregisterEventListener("PageActions:Remove");
+    }
+
+    protected void registerEventListener(String event) {
+        GeckoAppShell.getEventDispatcher().registerEventListener(event, this);
+    }
+
+    protected void unregisterEventListener(String event) {
+        GeckoAppShell.getEventDispatcher().unregisterEventListener(event, this);
+    }
+
+    @Override
+    public void handleMessage(String event, JSONObject message) {
+        try {
+            if (event.equals("PageActions:Add")) {
+                final String id = message.getString("id");
+                final String title = message.getString("title");
+                final String imageURL = message.optString("icon");
+
+                addPageAction(id, title, imageURL, new OnPageActionClickListeners() {
+                    @Override
+                    public void onClick(String id) {
+                        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("PageActions:Clicked", id));
+                    }
+
+                    @Override
+                    public boolean onLongClick(String id) {
+                        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("PageActions:LongClicked", id));
+                        return true;
+                    }
+                });
+            } else if (event.equals("PageActions:Remove")) {
+                final String id = message.getString("id");
+
+                removePageAction(id);
+            }
+        } catch(JSONException ex) {
+            Log.e(LOGTAG, "Error deocding", ex);
+        }
+    }
+
+    public void addPageAction(final String id, final String title, final String imageData, final OnPageActionClickListeners mOnPageActionClickListeners) {
+        final PageAction pageAction = new PageAction(id, title, null, mOnPageActionClickListeners);
+        mPageActionList.put(id, pageAction);
+
+        BitmapUtils.getDrawable(mContext, imageData, new BitmapUtils.BitmapLoader() {
+            @Override
+            public void onBitmapFound(final Drawable d) {
+                if (mPageActionList.containsKey(id)) {
+                    pageAction.setDrawable(d);
+                    refreshPageActionIcons();
+                }
+            }
+        });
+    }
+
+    public void removePageAction(String id) {
+        mPageActionList.remove(id);
+        refreshPageActionIcons();
+    }
+
+    private ImageButton createImageButton() {
+        ImageButton imageButton = new ImageButton(mContext, null, R.style.AddressBar_ImageButton_Icon);
+        imageButton.setLayoutParams(new LayoutParams(mContext.getResources().getDimensionPixelSize(R.dimen.page_action_button_width), LayoutParams.MATCH_PARENT));
+        imageButton.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
+        imageButton.setOnClickListener(this);
+        imageButton.setOnLongClickListener(this);
+        return imageButton;
+    }
+
+    @Override
+    public void onClick(View v) {
+        String buttonClickedId = (String)v.getTag();
+        if (buttonClickedId != null) {
+            if (buttonClickedId.equals(MENU_BUTTON_KEY)) {
+                showMenu(v, mPageActionList.size() - mMaxVisiblePageActions + 1);
+            } else {
+                mPageActionList.get(buttonClickedId).onClick();
+            }
+        }
+    }
+
+    @Override
+    public boolean onLongClick(View v) {
+        String buttonClickedId = (String)v.getTag();
+        if (buttonClickedId.equals(MENU_BUTTON_KEY)) {
+            showMenu(v, mPageActionList.size() - mMaxVisiblePageActions + 1);
+            return true;
+        } else {
+            return mPageActionList.get(buttonClickedId).onLongClick();
+        }
+    }
+
+    private void refreshPageActionIcons() {
+        final Resources resources = mContext.getResources();
+        for(int index = 0; index < this.getChildCount(); index++) {
+            final ImageButton v = (ImageButton)this.getChildAt(index);
+            final PageAction pageAction = getPageActionForViewAt(index);
+
+            if (index == (this.getChildCount() - 1)) {
+                String id = (pageAction != null) ? pageAction.getID() : null;
+                v.setTag((mPageActionList.size() > mMaxVisiblePageActions) ? MENU_BUTTON_KEY : id);
+                ThreadUtils.postToUiThread(new Runnable() {
+                    @Override
+                    public void run () {
+                        // If there are more pageactions then buttons, set the menu icon. Otherwise set the page action's icon if there is a page action.
+                        Drawable d = (pageAction != null) ? pageAction.getDrawable() : null;
+                        v.setImageDrawable((mPageActionList.size() > mMaxVisiblePageActions) ? resources.getDrawable(R.drawable.icon_pageaction) : d);
+                        v.setVisibility((pageAction != null) ? View.VISIBLE : View.GONE);
+                    }
+                });
+            } else {
+                v.setTag((pageAction != null) ? pageAction.getID() : null);
+                ThreadUtils.postToUiThread(new Runnable() {
+                    @Override
+                    public void run () {
+                        v.setImageDrawable((pageAction != null) ? pageAction.getDrawable() : null);
+                        v.setVisibility((pageAction != null) ? View.VISIBLE : View.GONE);
+                    }
+                });
+            }
+        }
+    }
+
+    private PageAction getPageActionForViewAt(int index) {
+        /**
+         * We show the user the most recent pageaction added since this keeps the user aware of any new page actions being added
+         * Also, the order of the pageAction is important i.e. if a page action is added, instead of shifting the pagactions to the
+         * left to make space for the new one, it would be more visually appealing to have the pageaction appear in the blank space.
+         *
+         * buttonIndex is needed for this reason because every new View added to PageActionLayout gets added to the right of its neighbouring View.
+         * Hence the button on the very leftmost has the index 0. We want our pageactions to start from the rightmost
+         * and hence we maintain the insertion order of the child Views which is essentially the reverse of their index
+         */
+
+        int buttonIndex = (this.getChildCount() - 1) - index;
+        int totalVisibleButtons = ((mPageActionList.size() < this.getChildCount()) ? mPageActionList.size() : this.getChildCount());
+
+        if (mPageActionList.size() > buttonIndex) {
+            // Return the pageactions starting from the end of the list for the number of visible pageactions.
+            return getPageActionAt((mPageActionList.size() - totalVisibleButtons) + buttonIndex);
+        }
+        return null;
+    }
+
+    private PageAction getPageActionAt(int index) {
+        int count = 0;
+        for(PageAction pageAction : mPageActionList.values()) {
+            if (count == index) {
+                return pageAction;
+            }
+            count++;
+        }
+        return null;
+    }
+
+    private void showMenu(View mPageActionButton, int toShow) {
+        if (mPageActionsMenu == null) {
+            mPageActionsMenu = new GeckoPopupMenu(mPageActionButton.getContext(), mPageActionButton);
+            mPageActionsMenu.inflate(0);
+            mPageActionsMenu.setOnMenuItemClickListener(new GeckoPopupMenu.OnMenuItemClickListener() {
+                @Override
+                public boolean onMenuItemClick(MenuItem item) {
+                    for(PageAction pageAction : mPageActionList.values()) {
+                        if (pageAction.key() == item.getItemId()) {
+                            pageAction.onClick();
+                        }
+                    }
+                    return true;
+                }
+            });
+        }
+        Menu menu = mPageActionsMenu.getMenu();
+        menu.clear();
+
+        int count = 0;
+        for(PageAction pageAction : mPageActionList.values()) {
+            if (count < toShow) {
+                MenuItem item = menu.add(Menu.NONE, pageAction.key(), Menu.NONE, pageAction.getTitle());
+                item.setIcon(pageAction.getDrawable());
+            }
+            count++;
+        }
+        mPageActionsMenu.show();
+    }
+
+    public static interface OnPageActionClickListeners {
+        public void onClick(String id);
+        public boolean onLongClick(String id);
+    }
+
+    private static class PageAction {
+        private OnPageActionClickListeners mOnPageActionClickListeners;
+        private Drawable mDrawable;
+        private String mTitle;
+        private String mId;
+        private int key;
+
+        public PageAction(String id, String title, Drawable image, OnPageActionClickListeners mOnPageActionClickListeners) {
+            this.mId = id;
+            this.mTitle = title;
+            this.mDrawable = image;
+            this.mOnPageActionClickListeners = mOnPageActionClickListeners;
+
+            this.key = UUID.fromString(mId.subSequence(1, mId.length() - 2).toString()).hashCode();
+        }
+
+        public Drawable getDrawable() {
+            return mDrawable;
+        }
+
+        public void setDrawable(Drawable d) {
+            this.mDrawable = d;
+        }
+
+        public String getTitle() {
+            return mTitle;
+        }
+
+        public String getID() {
+            return mId;
+        }
+
+        public int key() {
+            return key;
+        }
+
+        public void onClick() {
+            if (mOnPageActionClickListeners != null) {
+                mOnPageActionClickListeners.onClick(mId);
+            }
+        }
+
+        public boolean onLongClick() {
+            if (mOnPageActionClickListeners != null) {
+                return mOnPageActionClickListeners.onLongClick(mId);
+            }
+            return false;
+        }
+    }
+}
\ No newline at end of file
--- a/mobile/android/base/gfx/BitmapUtils.java
+++ b/mobile/android/base/gfx/BitmapUtils.java
@@ -1,37 +1,103 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.gfx;
 
+import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.gecko.util.GeckoJarReader;
+import org.mozilla.gecko.util.UiAsyncTask;
+
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.BitmapDrawable;
 import android.net.Uri;
+import android.os.AsyncTask;
 import android.util.Base64;
 import android.util.Log;
+import android.text.TextUtils;
 
 import org.mozilla.gecko.R;
 
 import java.io.IOException;
 import java.io.InputStream;
 import java.lang.reflect.Field;
 import java.net.MalformedURLException;
 import java.net.URL;
 
 public final class BitmapUtils {
     private static final String LOGTAG = "GeckoBitmapUtils";
 
     private BitmapUtils() {}
 
+    public interface BitmapLoader {
+        public void onBitmapFound(Drawable d);
+    }
+
+    public static void getDrawable(final Context context, final String data, final BitmapLoader loader) {
+        if (TextUtils.isEmpty(data)) {
+            loader.onBitmapFound(null);
+            return;
+        }
+
+        if (data.startsWith("data")) {
+            BitmapDrawable d = new BitmapDrawable(getBitmapFromDataURI(data));
+            loader.onBitmapFound(d);
+            return;
+        }
+
+        if (data.startsWith("jar:") || data.startsWith("file://")) {
+            (new UiAsyncTask<Void, Void, Drawable>(ThreadUtils.getBackgroundHandler()) {
+                @Override
+                public Drawable doInBackground(Void... params) {
+                    try {
+                        if (data.startsWith("jar:jar")) {
+                            return GeckoJarReader.getBitmapDrawable(context.getResources(), data);
+                        }
+
+                        URL url = new URL(data);
+                        InputStream is = (InputStream) url.getContent();
+                        try {
+                            return Drawable.createFromStream(is, "src");
+                        } finally {
+                            is.close();
+                        }
+                    } catch (Exception e) {
+                        Log.w(LOGTAG, "Unable to set icon", e);
+                    }
+                    return null;
+                }
+
+                @Override
+                public void onPostExecute(Drawable drawable) {
+                    loader.onBitmapFound(drawable);
+                }
+            }).execute();
+            return;
+        }
+
+        if(data.startsWith("drawable://")) {
+            Uri imageUri = Uri.parse(data);
+            int id = getResource(imageUri, R.drawable.ic_status_logo);
+            Drawable d = context.getResources().getDrawable(id);
+
+            loader.onBitmapFound(d);
+            return;
+        }
+
+        loader.onBitmapFound(null);
+    }
+
     public static Bitmap decodeByteArray(byte[] bytes) {
         return decodeByteArray(bytes, null);
     }
 
     public static Bitmap decodeByteArray(byte[] bytes, BitmapFactory.Options options) {
         if (bytes.length <= 0) {
             throw new IllegalArgumentException("bytes.length " + bytes.length
                                                + " must be a positive number");
--- a/mobile/android/base/menu/MenuPopup.java
+++ b/mobile/android/base/menu/MenuPopup.java
@@ -93,24 +93,24 @@ public class MenuPopup extends PopupWind
         }
 
         int[] anchorLocation = new int[2];
         anchor.getLocationOnScreen(anchorLocation);
 
         int screenWidth = mResources.getDisplayMetrics().widthPixels;
         int arrowWidth = mResources.getDimensionPixelSize(R.dimen.menu_popup_arrow_width);
         int arrowOffset = (anchor.getWidth() - arrowWidth)/2;
-       
+
         if (anchorLocation[0] + mPopupWidth <= screenWidth) {
             // left align
             ((LayoutParams) mArrowTop.getLayoutParams()).rightMargin = mPopupWidth - anchor.getWidth() + arrowOffset;
             ((LayoutParams) mArrowBottom.getLayoutParams()).rightMargin = mPopupWidth - anchor.getWidth() + arrowOffset;
         } else {
             // right align
-            ((LayoutParams) mArrowTop.getLayoutParams()).rightMargin = mArrowMargin;
+            ((LayoutParams) mArrowTop.getLayoutParams()).rightMargin = screenWidth - anchorLocation[0] - anchor.getWidth()/2 - arrowWidth/2;
             ((LayoutParams) mArrowBottom.getLayoutParams()).rightMargin = mArrowMargin;
         }
 
         // shown below anchor
         mArrowTop.setVisibility(View.VISIBLE);
         mArrowBottom.setVisibility(View.GONE);
         showAsDropDown(anchor, 0, -mYOffset);
     }
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..1a28b9e6629fff585c8d51eb30bd83447a0e514f
GIT binary patch
literal 187
zc%17D@N?(olHy`uVBq!ia0vp^av;pX1|+Qw)-3{3O`a}}Ar-gYUa{tCaNu#h7}@&c
z_UzW)*+S+^I(q-_Jz}D$(BWg$te}}!+r@sK@qoHy&W3|*?d_{~y)+28=Jmjk;g0tM
zrrRPfdQ^pGe2DE>HX|chbDw}#`_4%P@*(`jb$b>_KHIW^f1i*Pd)l8p3oV~LI@IHT
l!{LSVx2tT@IUo8TaLbl2wpe&_jvvr944$rjF6*2UngHcPNkjku
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..badb89012f364fc13daf2471eda784a99aa3f1d7
GIT binary patch
literal 174
zc%17D@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE;Asi%u$NX4zUmp5`Y7zngJJmSDL
zeanVIKK2W7){1kJmPT7NT%L3OkweoAnH#D{!)5<`=n8iEvRcMRFXC!%o3O?uL9TmN
zjvIP8vm}(0Y+2Nencpd!HB4i%+_mS3nuddC)0sp6I?g5l8T*9HA`Vt<|D);2rhj>x
W*`C}FCw2fGz~JfX=d#Wzp$PzGTR<HE
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..ced2327f6810391e30353b8531f764dea358712b
GIT binary patch
literal 289
zc%17D@N?(olHy`uVBq!ia0vp^8X(NU1|)m_?Z^dEFFaiwLn>~)y=KVm7$|Y<<99W#
z=95aP0jC}a-#wV{k++5^{y>I-*5aN`n%zbcEk0(EZ;EVQz0eVQSG~^K=5T$D|NeCA
zq#n-_79kIXMlf-);cS?-i+ReF-HQU4D;$pnr}HLUSKhFgktg6j^SX8y<{uiZZCyJU
zxvn&9Gm+AM;8wQb1!JtFNpHjK8|E(ZFHSA|$-hJLl+s7`6448vxbHAMo%n-&S69Yg
z#=KN^&l-nt&bRj;n3Z)L;V%ij@R9Xh!zqt{>?Kz(yzJckKx@)}=2+e>&e0+NKeYXK
jWa3nr;K0P`WATAc_9|apzD74Q(9aB>u6{1-oD!M<>`Qbt
--- a/mobile/android/base/resources/layout-large-v11/browser_toolbar.xml
+++ b/mobile/android/base/resources/layout-large-v11/browser_toolbar.xml
@@ -98,21 +98,22 @@
                                          android:paddingRight="8dp"
                                          android:textColor="@color/awesome_bar_title"
                                          android:textColorHint="@color/awesome_bar_title_hint"
                                          android:gravity="center_vertical|left"
                                          android:hint="@string/awesomebar_default_text"
                                          android:layout_gravity="center_vertical"
                                          gecko:autoUpdateTheme="false"/>
 
-        <ImageButton android:id="@+id/reader"
-                     style="@style/AddressBar.ImageButton.Icon"
-                     android:src="@drawable/reader"
-                     android:contentDescription="@string/reader"
-                     android:visibility="gone"/>
+        <org.mozilla.gecko.PageActionLayout android:id="@+id/page_action_layout"
+                                            android:layout_width="wrap_content"
+                                            android:layout_height="match_parent"
+                                            android:layout_marginRight="@dimen/browser_toolbar_button_padding"
+                                            android:visibility="gone"
+                                            android:orientation="horizontal"/>
 
         <ImageButton android:id="@+id/stop"
                      style="@style/AddressBar.ImageButton.Icon"
                      android:src="@drawable/urlbar_stop"
                      android:contentDescription="@string/stop"
                      android:visibility="gone"/>
 
     </LinearLayout>
--- a/mobile/android/base/resources/layout/browser_toolbar.xml
+++ b/mobile/android/base/resources/layout/browser_toolbar.xml
@@ -124,21 +124,22 @@
                                          android:paddingRight="8dp"
                                          android:textColor="@color/awesome_bar_title"
                                          android:textColorHint="@color/awesome_bar_title_hint"
                                          android:gravity="center_vertical|left"
                                          android:hint="@string/awesomebar_default_text"
                                          android:layout_gravity="center_vertical"
                                          gecko:autoUpdateTheme="false"/>
 
-        <ImageButton android:id="@+id/reader"
-                     style="@style/AddressBar.ImageButton.Icon"
-                     android:src="@drawable/reader"
-                     android:contentDescription="@string/reader"
-                     android:visibility="gone"/>
+        <org.mozilla.gecko.PageActionLayout android:id="@+id/page_action_layout"
+                                            android:layout_width="wrap_content"
+                                            android:layout_height="match_parent"
+                                            android:layout_marginRight="12dp"
+                                            android:visibility="gone"
+                                            android:orientation="horizontal"/>
 
         <ImageButton android:id="@+id/stop"
                      style="@style/AddressBar.ImageButton.Icon"
                      android:src="@drawable/urlbar_stop"
                      android:contentDescription="@string/stop"
                      android:visibility="gone"/>
 
     </LinearLayout>
--- a/mobile/android/base/resources/values/dimens.xml
+++ b/mobile/android/base/resources/values/dimens.xml
@@ -71,9 +71,12 @@
     <dimen name="tabs_tray_horizontal_height">156dp</dimen>
     <dimen name="text_selection_handle_width">47dp</dimen>
     <dimen name="text_selection_handle_height">58dp</dimen>
     <dimen name="text_selection_handle_shadow">11dp</dimen>
     <dimen name="validation_message_height">50dp</dimen>
     <dimen name="validation_message_margin_top">6dp</dimen>
     <dimen name="forward_default_offset">-13dip</dimen>
     <dimen name="addressbar_offset_left">32dp</dimen>
+
+    <!-- PageActionButtons dimensions -->
+    <dimen name="page_action_button_width">32dp</dimen>
 </resources>
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -188,17 +188,16 @@
   <string name="site_settings_clear">&site_settings_clear;</string>
   <string name="site_settings_no_settings">&site_settings_no_settings;</string>
 
   <string name="reading_list">&reading_list;</string>
   <string name="reading_list_added">&reading_list_added;</string>
   <string name="reading_list_removed">&reading_list_removed;</string>
   <string name="reading_list_failed">&reading_list_failed;</string>
   <string name="reading_list_duplicate">&reading_list_duplicate;</string>
-  <string name="reader">&reader;</string>
 
   <string name="contextmenu_open_new_tab">&contextmenu_open_new_tab;</string>
   <string name="contextmenu_open_private_tab">&contextmenu_open_private_tab;</string>
   <string name="contextmenu_open_in_reader">&contextmenu_open_in_reader;</string>
   <string name="contextmenu_remove_history">&contextmenu_remove_history;</string>
   <string name="contextmenu_remove_bookmark">&contextmenu_remove_bookmark;</string>
   <string name="contextmenu_add_to_launcher">&contextmenu_add_to_launcher;</string>
   <string name="contextmenu_share">&contextmenu_share;</string>
--- a/mobile/android/base/widget/GeckoPopupMenu.java
+++ b/mobile/android/base/widget/GeckoPopupMenu.java
@@ -12,20 +12,20 @@ import org.mozilla.gecko.menu.MenuPopup;
 
 import android.content.Context;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.View;
 
 /**
- * A PopupMenu that uses the custom GeckoMenu. This menu is 
+ * A PopupMenu that uses the custom GeckoMenu. This menu is
  * usually tied to an anchor, and show as a dropdrown from the anchor.
  */
-public class GeckoPopupMenu implements GeckoMenu.Callback, 
+public class GeckoPopupMenu implements GeckoMenu.Callback,
                                        GeckoMenu.MenuPresenter {
 
     // An interface for listeners for dismissal.
     public static interface OnDismissListener {
         public boolean onDismiss(GeckoMenu menu);
     }
 
     // An interface for listeners for menu item click events.
@@ -87,18 +87,19 @@ public class GeckoPopupMenu implements G
     }
 
     /**
      * Inflates a menu resource to the menu using the menu inflater.
      *
      * @param menuRes The menu resource to be inflated.
      */
     public void inflate(int menuRes) {
-        mMenuInflater.inflate(menuRes, mMenu);
-
+        if (menuRes > 0) {
+            mMenuInflater.inflate(menuRes, mMenu);
+        }
         mMenuPanel.addView(mMenu);
         mMenuPopup.setPanelView(mMenuPanel);
     }
 
     /**
      * Set a different anchor after the menu is inflated.
      *
      * @param anchor The new anchor for the popup.
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -723,17 +723,17 @@ var BrowserApp = {
   },
 
   getTabForWindow: function getTabForWindow(aWindow) {
     let tabs = this._tabs;
     for (let i = 0; i < tabs.length; i++) {
       if (tabs[i].browser.contentWindow == aWindow)
         return tabs[i];
     }
-    return null; 
+    return null;
   },
 
   getBrowserForWindow: function getBrowserForWindow(aWindow) {
     let tabs = this._tabs;
     for (let i = 0; i < tabs.length; i++) {
       if (tabs[i].browser.contentWindow == aWindow)
         return tabs[i].browser;
     }
@@ -1534,22 +1534,26 @@ var BrowserApp = {
         browser.gotoIndex(toIndex-selected);
     });
   },
 };
 
 var NativeWindow = {
   init: function() {
     Services.obs.addObserver(this, "Menu:Clicked", false);
+    Services.obs.addObserver(this, "PageActions:Clicked", false);
+    Services.obs.addObserver(this, "PageActions:LongClicked", false);
     Services.obs.addObserver(this, "Doorhanger:Reply", false);
     this.contextmenus.init();
   },
 
   uninit: function() {
     Services.obs.removeObserver(this, "Menu:Clicked");
+    Services.obs.removeObserver(this, "PageActions:Clicked");
+    Services.obs.removeObserver(this, "PageActions:LongClicked");
     Services.obs.removeObserver(this, "Doorhanger:Reply");
     this.contextmenus.uninit();
   },
 
   loadDex: function(zipFile, implClass) {
     sendMessageToJava({
       type: "Dex:Load",
       zipfile: zipFile,
@@ -1569,16 +1573,45 @@ var NativeWindow = {
       sendMessageToJava({
         type: "Toast:Show",
         message: aMessage,
         duration: aDuration
       });
     }
   },
 
+  pageactions: {
+    _items: { },
+    add: function(aOptions) {
+      let id = uuidgen.generateUUID().toString();
+      sendMessageToJava({
+        gecko: {
+          type: "PageActions:Add",
+          id: id,
+          title: aOptions.title,
+          icon: aOptions.icon,
+        }
+      });
+      this._items[id] = {
+        clickCallback: aOptions.clickCallback,
+        longClickCallback: aOptions.longClickCallback
+      };
+      return id;
+    },
+    remove: function(id) {
+      sendMessageToJava({
+        gecko: {
+          type: "PageActions:Remove",
+          id: id
+        }
+      });
+      delete this._items[id];
+    }
+  },
+
   menu: {
     _callbacks: [],
     _menuId: 1,
     toolsMenuID: -1,
     add: function() {
       let options;
       if (arguments.length == 1) {
         options = arguments[0];
@@ -1666,16 +1699,22 @@ var NativeWindow = {
       });
     }
   },
 
   observe: function(aSubject, aTopic, aData) {
     if (aTopic == "Menu:Clicked") {
       if (this.menu._callbacks[aData])
         this.menu._callbacks[aData]();
+    } else if (aTopic == "PageActions:Clicked") {
+        if (this.pageactions._items[aData].clickCallback)
+          this.pageactions._items[aData].clickCallback();
+    } else if (aTopic == "PageActions:LongClicked") {
+        if (this.pageactions._items[aData].longClickCallback)
+          this.pageactions._items[aData].longClickCallback();
     } else if (aTopic == "Doorhanger:Reply") {
       let data = JSON.parse(aData);
       let reply_id = data["callback"];
 
       if (this.doorhanger._callbacks[reply_id]) {
         // Pass the value of the optional checkbox to the callback
         let checked = data["checked"];
         this.doorhanger._callbacks[reply_id].cb(checked, data.inputs);
@@ -2464,16 +2503,18 @@ function Tab(aURL, aParams) {
   this.lastTouchedAt = Date.now();
   this.showProgress = true;
   this._zoom = 1.0;
   this._drawZoom = 1.0;
   this._fixedMarginLeft = 0;
   this._fixedMarginTop = 0;
   this._fixedMarginRight = 0;
   this._fixedMarginBottom = 0;
+  this._readerEnabled = false;
+  this._readerActive = false;
   this.userScrollPos = { x: 0, y: 0 };
   this.viewportExcludesHorizontalMargins = true;
   this.viewportExcludesVerticalMargins = true;
   this.viewportMeasureCallback = null;
   this.lastPageSizeUsedForViewportChange = { width: 0, height: 0 };
   this.contentDocumentIsDisplayed = true;
   this.pluginDoorhangerTimeout = null;
   this.shouldShowPluginDoorhanger = true;
@@ -2758,16 +2799,17 @@ Tab.prototype = {
   setActive: function setActive(aActive) {
     if (!this.browser || !this.browser.docShell)
       return;
 
     if (aActive) {
       this.browser.setAttribute("type", "content-primary");
       this.browser.focus();
       this.browser.docShellIsActive = true;
+      Reader.updatePageAction(this);
     } else {
       this.browser.setAttribute("type", "content-targetable");
       this.browser.docShellIsActive = false;
     }
   },
 
   getActive: function getActive() {
     return this.browser.docShellIsActive;
@@ -3450,28 +3492,37 @@ Tab.prototype = {
         // Once document is fully loaded, parse it
         Reader.parseDocumentFromTab(this.id, function (article) {
           // Do nothing if there's no article or the page in this tab has
           // changed
           let tabURL = this.browser.currentURI.specIgnoringRef;
           if (article == null || (article.url != tabURL)) {
             // Don't clear the article for about:reader pages since we want to
             // use the article from the previous page
-            if (!tabURL.startsWith("about:reader"))
+            if (!tabURL.startsWith("about:reader")) {
               this.savedArticle = null;
-
+              this.readerEnabled = false;
+            } else {
+              this.readerActive = true;
+            }
             return;
           }
 
           this.savedArticle = article;
 
           sendMessageToJava({
             type: "Content:ReaderEnabled",
             tabID: this.id
           });
+
+          if(this.readerActive)
+            this.readerActive = false;
+
+          if(!this.readerEnabled)
+            this.readerEnabled = true;
         }.bind(this));
       }
     }
   },
 
   onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) {
     let contentWin = aWebProgress.DOMWindow;
     if (contentWin != contentWin.top)
@@ -3917,16 +3968,36 @@ Tab.prototype = {
         break;
       case "nsPref:changed":
         if (aData == "browser.ui.zoom.force-user-scalable")
           ViewportHandler.updateMetadata(this, false);
         break;
     }
   },
 
+  set readerEnabled(isReaderEnabled) {
+    this._readerEnabled = isReaderEnabled;
+    if (this.getActive())
+      Reader.updatePageAction(this);
+  },
+
+  get readerEnabled() {
+    return this._readerEnabled;
+  },
+
+  set readerActive(isReaderActive) {
+    this._readerActive = isReaderActive;
+    if (this.getActive())
+      Reader.updatePageAction(this);
+  },
+
+  get readerActive() {
+    return this._readerActive;
+  },
+
   // nsIBrowserTab
   get window() {
     if (!this.browser)
       return null;
     return this.browser.contentWindow;
   },
 
   get scale() {
@@ -6855,16 +6926,56 @@ let Reader = {
     this.isEnabledForParseOnLoad = this.getStateForParseOnLoad();
 
     Services.obs.addObserver(this, "Reader:Add", false);
     Services.obs.addObserver(this, "Reader:Remove", false);
 
     Services.prefs.addObserver("reader.parse-on-load.", this, false);
   },
 
+  pageAction: {
+    readerModeCallback: function(){
+      sendMessageToJava({
+        gecko: {
+          type: "Reader:Click",
+        }
+      });
+    },
+
+    readerModeActiveCallback: function(){
+      sendMessageToJava({
+        gecko: {
+          type: "Reader:LongClick",
+        }
+      });
+    },
+  },
+
+  updatePageAction: function(tab) {
+    if(this.pageAction.id) {
+      NativeWindow.pageactions.remove(this.pageAction.id);
+      delete this.pageAction.id;
+    }
+
+    if (tab.readerActive) {
+      this.pageAction.id = NativeWindow.pageactions.add({
+        title: Strings.browser.GetStringFromName("readerMode.exit"),
+        icon: "drawable://reader_active",
+        clickCallback: this.pageAction.readerModeCallback
+      });
+    } else if (tab.readerEnabled) {
+      this.pageAction.id = NativeWindow.pageactions.add({
+        title: Strings.browser.GetStringFromName("readerMode.enter"),
+        icon: "drawable://reader",
+        clickCallback:this.pageAction.readerModeCallback,
+        longClickCallback: this.pageAction.readerModeActiveCallback
+      });
+    }
+  },
+
   observe: function(aMessage, aTopic, aData) {
     switch(aTopic) {
       case "Reader:Add": {
         let args = JSON.parse(aData);
         if ('fromAboutReader' in args) {
           // Ignore adds initiated from aboutReader menu banner
           break;
         }
--- a/mobile/android/locales/en-US/chrome/browser.properties
+++ b/mobile/android/locales/en-US/chrome/browser.properties
@@ -263,8 +263,12 @@ getUserMedia.videoDevice.back = Back fac
 getUserMedia.videoDevice.none = No Video
 getUserMedia.videoDevice.prompt = Camera to use
 getUserMedia.audioDevice.default = Microphone %S
 getUserMedia.audioDevice.none = No Audio
 getUserMedia.audioDevice.prompt = Microphone to use
 getUserMedia.sharingCamera.message2 = Camera is on
 getUserMedia.sharingMicrophone.message2 = Microphone is on
 getUserMedia.sharingCameraAndMicrophone.message2 = Camera and microphone are on
+
+#Reader mode
+readerMode.enter = Enter Reader Mode
+readerMode.exit = Exit Reader Mode