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 146250 696633740d85c118ea187e59e26caa75f582dedc
parent 146249 c08b03cc4f324d0da1508b3ca22af97ef14b9ac8
child 146251 3e3d601a9ab2089f5937d9b96d34411d345033cf
push idunknown
push userunknown
push dateunknown
reviewerswesj
bugs734877
milestone25.0a1
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