Merge fx-team to mozilla-central a=merge
authorWes Kocher <wkocher@mozilla.com>
Wed, 31 Dec 2014 16:52:30 -0800
changeset 247605 3c296aa11c51df594590eccee16c9f639d2b5f1d
parent 247595 271f40e3c3ad2e0ab4839ceac77f887e14de9db4 (current diff)
parent 247604 2e9b1569f4c85cad19ade8903482e3c35a47582f (diff)
child 247606 feeb347e67ec566c5fa534f71f825f49a46a248a
child 247618 9261135074f81237a4605abe449e031ca626b2e8
child 247636 f9f2edd5c4ea0632093949edb74ee5bde0d6936d
push id4489
push userraliiev@mozilla.com
push dateMon, 23 Feb 2015 15:17:55 +0000
treeherdermozilla-beta@fd7c3dc24146 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone37.0a1
first release with
nightly linux32
3c296aa11c51 / 37.0a1 / 20150101030214 / files
nightly linux64
3c296aa11c51 / 37.0a1 / 20150101030214 / files
nightly mac
3c296aa11c51 / 37.0a1 / 20150101030214 / files
nightly win32
3c296aa11c51 / 37.0a1 / 20150101030214 / files
nightly win64
3c296aa11c51 / 37.0a1 / 20150101030214 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fx-team to mozilla-central a=merge
--- a/mobile/android/base/menu/GeckoMenu.java
+++ b/mobile/android/base/menu/GeckoMenu.java
@@ -10,16 +10,17 @@ import org.mozilla.gecko.util.ThreadUtil
 import org.mozilla.gecko.util.ThreadUtils.AssertBehavior;
 import org.mozilla.gecko.widget.GeckoActionProvider;
 
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.util.AttributeSet;
 import android.util.Log;
+import android.util.SparseArray;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.SubMenu;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.AdapterView;
@@ -85,16 +86,19 @@ public class GeckoMenu extends ListView
         public void removeActionItem(View actionItem);
     }
 
     protected static final int NO_ID = 0;
 
     // List of all menu items.
     private final List<GeckoMenuItem> mItems;
 
+    // Quick lookup array used to make a fast path in findItem.
+    private final SparseArray<MenuItem> mItemsById;
+
     // Map of "always" action-items in action-bar and their views.
     private final Map<GeckoMenuItem, View> mPrimaryActionItems;
 
     // Map of "ifRoom" action-items in action-bar and their views.
     private final Map<GeckoMenuItem, View> mSecondaryActionItems;
 
     // Reference to a callback for menu events.
     private Callback mCallback;
@@ -129,16 +133,17 @@ public class GeckoMenu extends ListView
                                          LayoutParams.MATCH_PARENT));
 
         // Attach an adapter.
         mAdapter = new MenuItemsAdapter();
         setAdapter(mAdapter);
         setOnItemClickListener(this);
 
         mItems = new ArrayList<GeckoMenuItem>();
+        mItemsById = new SparseArray<MenuItem>();
         mPrimaryActionItems = new HashMap<GeckoMenuItem, View>();
         mSecondaryActionItems = new HashMap<GeckoMenuItem, View>();
 
         mPrimaryActionItemBar = (DefaultActionItemBar) LayoutInflater.from(context).inflate(R.layout.menu_action_bar, null);
         mSecondaryActionItemBar = (DefaultActionItemBar) LayoutInflater.from(context).inflate(R.layout.menu_secondary_action_bar, null);
     }
 
     private static void assertOnUiThread() {
@@ -357,25 +362,34 @@ public class GeckoMenu extends ListView
 
     private void showMenu(View viewForMenu) {
         if (mMenuPresenter != null)
             mMenuPresenter.showMenu(viewForMenu);
     }
 
     @Override
     public MenuItem findItem(int id) {
+        assertOnUiThread();
+        MenuItem quickItem = mItemsById.get(id);
+        if (quickItem != null) {
+            return quickItem;
+        }
+
         for (GeckoMenuItem menuItem : mItems) {
             if (menuItem.getItemId() == id) {
+                mItemsById.put(id, menuItem);
                 return menuItem;
             } else if (menuItem.hasSubMenu()) {
                 if (!menuItem.hasActionProvider()) {
                     SubMenu subMenu = menuItem.getSubMenu();
                     MenuItem item = subMenu.findItem(id);
-                    if (item != null)
+                    if (item != null) {
+                        mItemsById.put(id, item);
                         return item;
+                    }
                 }
             }
         }
         return null;
     }
 
     @Override
     public MenuItem getItem(int index) {
@@ -419,16 +433,19 @@ public class GeckoMenu extends ListView
 
     @Override
     public void removeItem(int id) {
         assertOnUiThread();
         GeckoMenuItem item = (GeckoMenuItem) findItem(id);
         if (item == null)
             return;
 
+        // Remove it from the cache.
+        mItemsById.remove(id);
+
         // Remove it from any sub-menu.
         for (GeckoMenuItem menuItem : mItems) {
             if (menuItem.hasSubMenu()) {
                 SubMenu subMenu = menuItem.getSubMenu();
                 if (subMenu != null && subMenu.findItem(id) != null) {
                     subMenu.removeItem(id);
                     return;
                 }
@@ -806,16 +823,17 @@ public class GeckoMenu extends ListView
 
         public void removeMenuItem(GeckoMenuItem menuItem) {
             // Remove it from the list.
             mItems.remove(menuItem);
             notifyDataSetChanged();
         }
 
         public void clear() {
+            mItemsById.clear();
             mItems.clear();
             notifyDataSetChanged();
         }
 
         public GeckoMenuItem getMenuItem(int id) {
             for (GeckoMenuItem item : mItems) {
                 if (item.getItemId() == id)
                     return item;
--- a/mobile/android/base/menu/GeckoMenuInflater.java
+++ b/mobile/android/base/menu/GeckoMenuInflater.java
@@ -151,19 +151,35 @@ public class GeckoMenuInflater extends M
         if (Versions.feature11Plus) {
             item.showAsAction = a.getInt(R.styleable.MenuItem_android_showAsAction, 0);
         }
 
         a.recycle();
     }
 
     public void setValues(ParsedItem item, MenuItem menuItem) {
+        // We are blocking any presenter updates during inflation.
+        GeckoMenuItem geckoItem = null;
+        if (menuItem instanceof GeckoMenuItem) {
+            geckoItem = (GeckoMenuItem) menuItem;
+        }
+
+        if (geckoItem != null) {
+            geckoItem.stopDispatchingChanges();
+        }
+
         menuItem.setChecked(item.checked)
                 .setVisible(item.visible)
                 .setEnabled(item.enabled)
                 .setCheckable(item.checkable)
                 .setIcon(item.iconRes);
 
         if (Versions.feature11Plus) {
             menuItem.setShowAsAction(item.showAsAction);
         }
+
+        if (geckoItem != null) {
+            // We don't need to allow presenter updates during inflation,
+            // so we use the weak form of re-enabling changes.
+            geckoItem.resumeDispatchingChanges();
+        }
     }
 }
--- a/mobile/android/base/menu/GeckoMenuItem.java
+++ b/mobile/android/base/menu/GeckoMenuItem.java
@@ -47,28 +47,62 @@ public class GeckoMenuItem implements Me
     private Drawable mIcon;
     private int mIconRes;
     private GeckoActionProvider mActionProvider;
     private GeckoSubMenu mSubMenu;
     private MenuItem.OnMenuItemClickListener mMenuItemClickListener;
     final GeckoMenu mMenu;
     OnShowAsActionChangedListener mShowAsActionChangedListener;
 
+    private volatile boolean mShouldDispatchChanges = true;
+    private volatile boolean mDidChange;
+
     public GeckoMenuItem(GeckoMenu menu, int id, int order, int titleRes) {
         mMenu = menu;
         mId = id;
         mOrder = order;
-        setTitle(titleRes);
+        mTitle = mMenu.getResources().getString(titleRes);
     }
 
     public GeckoMenuItem(GeckoMenu menu, int id, int order, CharSequence title) {
         mMenu = menu;
         mId = id;
         mOrder = order;
-        setTitle(title);
+        mTitle = title;
+    }
+
+    /**
+     * Stop dispatching item changed events to presenters until
+     * [start|resume]DispatchingItemsChanged() is called. Useful when
+     * many menu operations are going to be performed as a batch.
+     */
+    public void stopDispatchingChanges() {
+        mDidChange = false;
+        mShouldDispatchChanges = false;
+    }
+
+    /**
+     * Resume dispatching item changed events to presenters. This method
+     * will NOT call onItemChanged if any menu operations were queued.
+     * Only future menu operations will call onItemChanged. Useful for
+     * sequelching presenter updates.
+     */
+    public void resumeDispatchingChanges() {
+        mShouldDispatchChanges = true;
+    }
+
+    /**
+     * Start dispatching item changed events to presenters. This method
+     * will call onItemChanged if any menu operations were queued.
+     */
+    public void startDispatchingChanges() {
+        if (mDidChange) {
+            mMenu.onItemChanged(this);
+        }
+        mShouldDispatchChanges = true;
     }
 
     @Override
     public boolean collapseActionView() {
         return false;
     }
 
     @Override
@@ -242,53 +276,73 @@ public class GeckoMenuItem implements Me
     public MenuItem setAlphabeticShortcut(char alphaChar) {
         return this;
     }
 
     @Override
     public MenuItem setCheckable(boolean checkable) {
         if (mCheckable != checkable) {
             mCheckable = checkable;
-            mMenu.onItemChanged(this);
+            if (mShouldDispatchChanges) {
+                mMenu.onItemChanged(this);
+            } else {
+                mDidChange = true;
+            }
         }
         return this;
     }
 
     @Override
     public MenuItem setChecked(boolean checked) {
         if (mChecked != checked) {
             mChecked = checked;
-            mMenu.onItemChanged(this);
+            if (mShouldDispatchChanges) {
+                mMenu.onItemChanged(this);
+            } else {
+                mDidChange = true;
+            }
         }
         return this;
     }
 
     @Override
     public MenuItem setEnabled(boolean enabled) {
         if (mEnabled != enabled) {
             mEnabled = enabled;
-            mMenu.onItemChanged(this);
+            if (mShouldDispatchChanges) {
+                mMenu.onItemChanged(this);
+            } else {
+                mDidChange = true;
+            }
         }
         return this;
     }
 
     @Override
     public MenuItem setIcon(Drawable icon) {
         if (mIcon != icon) {
             mIcon = icon;
-            mMenu.onItemChanged(this);
+            if (mShouldDispatchChanges) {
+                mMenu.onItemChanged(this);
+            } else {
+                mDidChange = true;
+            }
         }
         return this;
     }
 
     @Override
     public MenuItem setIcon(int iconRes) {
         if (mIconRes != iconRes) {
             mIconRes = iconRes;
-            mMenu.onItemChanged(this);
+            if (mShouldDispatchChanges) {
+                mMenu.onItemChanged(this);
+            } else {
+                mDidChange = true;
+            }
         }
         return this;
     }
 
     @Override
     public MenuItem setIntent(Intent intent) {
         return this;
     }
@@ -362,17 +416,21 @@ public class GeckoMenuItem implements Me
         mSubMenu = subMenu;
         return this;
     }
 
     @Override
     public MenuItem setTitle(CharSequence title) {
         if (!TextUtils.equals(mTitle, title)) {
             mTitle = title;
-            mMenu.onItemChanged(this);
+            if (mShouldDispatchChanges) {
+                mMenu.onItemChanged(this);
+            } else {
+                mDidChange = true;
+            }
         }
         return this;
     }
 
     @Override
     public MenuItem setTitle(int title) {
         CharSequence newTitle = mMenu.getResources().getString(title);
         return setTitle(newTitle);
@@ -383,17 +441,21 @@ public class GeckoMenuItem implements Me
         mTitleCondensed = title;
         return this;
     }
 
     @Override
     public MenuItem setVisible(boolean visible) {
         if (mVisible != visible) {
             mVisible = visible;
-            mMenu.onItemChanged(this);
+            if (mShouldDispatchChanges) {
+                mMenu.onItemChanged(this);
+            } else {
+                mDidChange = true;
+            }
         }
         return this;
     }
 
     public boolean invoke() {
         if (mMenuItemClickListener != null)
             return mMenuItemClickListener.onMenuItemClick(this);
         else
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -468,16 +468,18 @@ gbjar.sources += [
     'widget/BasicColorPicker.java',
     'widget/ButtonToast.java',
     'widget/CheckableLinearLayout.java',
     'widget/ClickableWhenDisabledEditText.java',
     'widget/DateTimePicker.java',
     'widget/Divider.java',
     'widget/DoorHanger.java',
     'widget/EllipsisTextView.java',
+    'widget/FadedMultiColorTextView.java',
+    'widget/FadedSingleColorTextView.java',
     'widget/FadedTextView.java',
     'widget/FaviconView.java',
     'widget/FloatingHintEditText.java',
     'widget/FlowLayout.java',
     'widget/GeckoActionProvider.java',
     'widget/GeckoPopupMenu.java',
     'widget/GeckoSwipeRefreshLayout.java',
     'widget/GeckoViewFlipper.java',
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/color/toolbar_display_layout_bg.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- These colors are defined from the drawables url_bar_entry_default_* -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+          xmlns:gecko="http://schemas.android.com/apk/res-auto">
+
+    <item gecko:state_private="true" android:color="#080A0B"/>
+
+    <item android:color="#FFFFFF"/>
+
+</selector>
--- a/mobile/android/base/resources/layout-large-v11/tab_strip_item_view.xml
+++ b/mobile/android/base/resources/layout-large-v11/tab_strip_item_view.xml
@@ -9,17 +9,17 @@
    <ImageView
         android:id="@+id/favicon"
         android:layout_width="@dimen/new_tablet_tab_strip_favicon_size"
         android:layout_height="match_parent"
         android:layout_marginRight="8dp"
         android:scaleType="centerInside"
         android:duplicateParentState="true"/>
 
-    <org.mozilla.gecko.widget.FadedTextView
+    <org.mozilla.gecko.widget.FadedSingleColorTextView
         android:id="@+id/title"
         android:layout_width="0dip"
         android:layout_height="match_parent"
         android:layout_weight="1.0"
         android:layout_marginRight="-5dp"
         android:gravity="center_vertical"
         android:textSize="14sp"
         android:ellipsize="end"
--- a/mobile/android/base/resources/layout-v11/new_tablet_tabs_item_cell.xml
+++ b/mobile/android/base/resources/layout-v11/new_tablet_tabs_item_cell.xml
@@ -16,27 +16,28 @@
     <LinearLayout android:layout_width="fill_parent"
                   android:layout_height="wrap_content"
                   android:orientation="horizontal"
                   android:duplicateParentState="true"
                   android:paddingLeft="@dimen/new_tablet_tab_highlight_stroke_width"
                   android:paddingRight="@dimen/new_tablet_tab_highlight_stroke_width"
                   android:paddingBottom="@dimen/new_tablet_tab_highlight_stroke_width">
 
-        <org.mozilla.gecko.widget.FadedTextView android:id="@+id/title"
-                                                android:layout_width="0dip"
-                                                android:layout_height="wrap_content"
-                                                android:layout_weight="1.0"
-                                                style="@style/TabLayoutItemTextAppearance"
-                                                android:textSize="14sp"
-                                                android:textColor="@color/new_tablet_tab_item_title"
-                                                android:singleLine="true"
-                                                android:duplicateParentState="true"
-                                                gecko:fadeWidth="15dp"
-                                                android:paddingRight="5dp"/>
+       <org.mozilla.gecko.widget.FadedSingleColorTextView
+               android:id="@+id/title"
+               android:layout_width="0dip"
+               android:layout_height="wrap_content"
+               android:layout_weight="1.0"
+               style="@style/TabLayoutItemTextAppearance"
+               android:textSize="14sp"
+               android:textColor="@color/new_tablet_tab_item_title"
+               android:singleLine="true"
+               android:duplicateParentState="true"
+               gecko:fadeWidth="15dp"
+               android:paddingRight="5dp"/>
 
 
         <!-- Use of baselineAlignBottom only supported from API 11+ - if this needs to work on lower API versions
              we'll need to override getBaseLine() and return image height, but we assume this won't happen -->
         <ImageButton android:id="@+id/close"
                      style="@style/TabsItemClose"
                      android:layout_width="wrap_content"
                      android:layout_height="wrap_content"
--- a/mobile/android/base/resources/layout/home_remote_tabs_group.xml
+++ b/mobile/android/base/resources/layout/home_remote_tabs_group.xml
@@ -24,17 +24,17 @@
         tools:src="@drawable/sync_mobile"/>
 
     <LinearLayout
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_weight="1"
         android:orientation="vertical">
 
-        <org.mozilla.gecko.widget.FadedTextView
+        <org.mozilla.gecko.widget.FadedSingleColorTextView
             android:id="@+id/client"
             style="@style/Widget.TwoLinePageRow.Title"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             gecko:fadeWidth="30dp"
             tools:text="Firefox on Nexus 5"/>
 
         <TextView
--- a/mobile/android/base/resources/layout/tab_history_item_row.xml
+++ b/mobile/android/base/resources/layout/tab_history_item_row.xml
@@ -35,17 +35,17 @@
         <ImageView android:id="@+id/tab_history_timeline_bottom"
                    android:layout_width="@dimen/tab_history_timeline_width"
                    android:layout_height="@dimen/tab_history_timeline_height"
                    android:layout_gravity="center_horizontal"
                    android:background="@color/tab_history_timeline_separator" />
 
     </LinearLayout>
 
-    <org.mozilla.gecko.widget.FadedTextView
+    <org.mozilla.gecko.widget.FadedSingleColorTextView
             android:id="@+id/tab_history_title"
             style="@style/Widget.TwoLinePageRow.Title"
             android:layout_centerVertical="true"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:layout_gravity="center_vertical"
             android:paddingRight="@dimen/tab_history_title_margin_right"
             android:text="@+id/tab_history_title"
--- a/mobile/android/base/resources/layout/toolbar_display_layout.xml
+++ b/mobile/android/base/resources/layout/toolbar_display_layout.xml
@@ -20,23 +20,25 @@
                  android:layout_width="@dimen/browser_toolbar_site_security_width"
                  android:scaleType="fitCenter"
                  android:layout_marginRight="4dip"
                  android:layout_marginBottom="@dimen/site_security_bottom_margin"
                  android:src="@drawable/site_security_level"
                  android:contentDescription="@string/site_security"
                  android:visibility="gone"/>
 
-    <org.mozilla.gecko.widget.FadedTextView android:id="@+id/url_bar_title"
-                                            style="@style/UrlBar.Title"
-                                            android:layout_width="match_parent"
-                                            android:layout_height="match_parent"
-                                            android:layout_weight="1.0"
-                                            gecko:fadeWidth="40dip"
-                                            gecko:autoUpdateTheme="false"/>
+    <org.mozilla.gecko.widget.FadedMultiColorTextView
+            android:id="@+id/url_bar_title"
+            style="@style/UrlBar.Title"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:layout_weight="1.0"
+            gecko:fadeWidth="40dip"
+            gecko:fadeBackgroundColor="@color/toolbar_display_layout_bg"
+            gecko:autoUpdateTheme="false"/>
 
     <org.mozilla.gecko.toolbar.PageActionLayout android:id="@+id/page_action_layout"
                                                 android:layout_width="wrap_content"
                                                 android:layout_height="match_parent"
                                                 android:visibility="gone"
                                                 android:orientation="horizontal"/>
 
     <ImageButton android:id="@+id/stop"
--- a/mobile/android/base/resources/layout/top_sites_grid_item_view.xml
+++ b/mobile/android/base/resources/layout/top_sites_grid_item_view.xml
@@ -7,17 +7,17 @@
        xmlns:gecko="http://schemas.android.com/apk/res-auto">
 
     <org.mozilla.gecko.home.TopSitesThumbnailView
             android:id="@+id/thumbnail"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:layout_alignParentTop="true"/>
 
-    <org.mozilla.gecko.widget.FadedTextView
+    <org.mozilla.gecko.widget.FadedSingleColorTextView
             android:id="@+id/title"
             style="@style/Widget.TopSitesGridItemTitle"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:layout_below="@id/thumbnail"
             android:duplicateParentState="true"
             android:drawablePadding="4dip"
             gecko:fadeWidth="20dip"/>
--- a/mobile/android/base/resources/layout/two_line_page_row.xml
+++ b/mobile/android/base/resources/layout/two_line_page_row.xml
@@ -15,17 +15,17 @@
                                           tools:background="@drawable/favicon"/>
 
     <LinearLayout android:layout_width="match_parent"
                   android:layout_height="wrap_content"
                   android:layout_gravity="center_vertical"
                   android:orientation="vertical"
                   android:paddingRight="25dp">
 
-        <org.mozilla.gecko.widget.FadedTextView
+        <org.mozilla.gecko.widget.FadedSingleColorTextView
                 android:id="@+id/title"
                 style="@style/Widget.TwoLinePageRow.Title"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 gecko:fadeWidth="30dp"
                 tools:text="This is a long test title"/>
 
         <TextView android:id="@+id/url"
--- a/mobile/android/base/resources/values/attrs.xml
+++ b/mobile/android/base/resources/values/attrs.xml
@@ -141,16 +141,22 @@
     <declare-styleable name="HomePagerTabStrip">
         <attr name="tabIndicatorColor" format="color"/>
     </declare-styleable>
 
     <declare-styleable name="FadedTextView">
         <attr name="fadeWidth" format="dimension"/>
     </declare-styleable>
 
+    <declare-styleable name="FadedMultiColorTextView">
+        <!-- The background color we should be fading over. Useful because the
+             background is full alpha and we need to copy the background underneath. -->
+        <attr name="fadeBackgroundColor" format="dimension"/>
+    </declare-styleable>
+
     <declare-styleable name="BookmarkFolderView">
         <attr name="state_open" format="boolean"/>
     </declare-styleable>
 
     <declare-styleable name="GeckoView">
         <attr name="url" format="string"/>
         <attr name="doinit" format="boolean"/>
     </declare-styleable>
--- a/mobile/android/base/tests/components/AppMenuComponent.java
+++ b/mobile/android/base/tests/components/AppMenuComponent.java
@@ -14,16 +14,17 @@ import java.util.List;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.menu.MenuItemActionBar;
 import org.mozilla.gecko.menu.MenuItemDefault;
 import org.mozilla.gecko.tests.UITestContext;
 import org.mozilla.gecko.tests.helpers.DeviceHelper;
 import org.mozilla.gecko.tests.helpers.WaitHelper;
 import org.mozilla.gecko.util.HardwareUtils;
 
+import android.text.TextUtils;
 import android.view.View;
 import android.widget.TextView;
 
 import com.jayway.android.robotium.solo.Condition;
 import com.jayway.android.robotium.solo.RobotiumUtils;
 import com.jayway.android.robotium.solo.Solo;
 
 /**
@@ -158,35 +159,35 @@ public class AppMenuComponent extends Ba
      */
     private View findAppMenuItemView(String text) {
         mSolo.waitForText(text, 1, MAX_WAITTIME_FOR_MENU_UPDATE_IN_MS);
 
         final List<View> views = mSolo.getViews();
 
         final List<MenuItemActionBar> menuItemActionBarList = RobotiumUtils.filterViews(MenuItemActionBar.class, views);
         for (MenuItemActionBar menuItem : menuItemActionBarList) {
-            if (menuItem.getContentDescription().equals(text)) {
+            if (TextUtils.equals(menuItem.getContentDescription(), text)) {
                 return menuItem;
             }
         }
 
         final List<MenuItemDefault> menuItemDefaultList = RobotiumUtils.filterViews(MenuItemDefault.class, views);
         for (MenuItemDefault menuItem : menuItemDefaultList) {
-            if (menuItem.getText().equals(text)) {
+            if (TextUtils.equals(menuItem.getText(), text)) {
                 return menuItem;
             }
         }
 
         // On Android 2.3, menu items may be instances of
         // com.android.internal.view.menu.ListMenuItemView, each with a child
         // android.widget.RelativeLayout which in turn has a child
         // TextView with the appropriate text.
         final List<TextView> textViewList = RobotiumUtils.filterViews(TextView.class, views);
         for (TextView textView : textViewList) {
-            if (textView.getText().equals(text)) {
+            if (TextUtils.equals(textView.getText(), text)) {
                 View relativeLayout = (View) textView.getParent();
                 View listMenuItemView = (View)relativeLayout.getParent();
                 return listMenuItemView;
             }
         }
         return null;
     }
 
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/widget/FadedMultiColorTextView.java
@@ -0,0 +1,104 @@
+/* -*- 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.widget;
+
+import org.mozilla.gecko.R;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.LinearGradient;
+import android.graphics.Paint;
+import android.graphics.Shader;
+import android.util.AttributeSet;
+
+/**
+ * Fades the end of the text by gecko:fadeWidth amount,
+ * if the text is too long and requires an ellipsis.
+ *
+ * This implementation is an improvement over Android's built-in fadingEdge
+ * but potentially slower than the {@link org.mozilla.gecko.widget.FadedSingleColorTextView}.
+ * It works for text of multiple colors but only one background color. It works by
+ * drawing a gradient rectangle with the background color over the text, fading it out.
+ */
+public class FadedMultiColorTextView extends FadedTextView {
+    private final ColorStateList fadeBackgroundColorList;
+
+    private final Paint fadePaint;
+    private FadedTextGradient backgroundGradient;
+
+    public FadedMultiColorTextView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        fadePaint = new Paint();
+
+        final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FadedMultiColorTextView);
+        fadeBackgroundColorList =
+                a.getColorStateList(R.styleable.FadedMultiColorTextView_fadeBackgroundColor);
+        a.recycle();
+    }
+
+    @Override
+    public void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+
+        final boolean needsEllipsis = needsEllipsis();
+        if (needsEllipsis) {
+            final int right = getWidth() - getCompoundPaddingRight();
+            final float left = right - fadeWidth;
+
+            updateGradientShader(needsEllipsis, right);
+
+            final float center = getHeight() / 2;
+            final float top = center - getTextSize();
+            final float bottom = center + getTextSize();
+
+            canvas.drawRect(left, top, right, bottom, fadePaint);
+        }
+    }
+
+    private void updateGradientShader(final boolean needsEllipsis, final int gradientEndRight) {
+        final int backgroundColor =
+                fadeBackgroundColorList.getColorForState(getDrawableState(), Color.RED);
+
+        final boolean needsNewGradient = (backgroundGradient == null ||
+                                          backgroundGradient.getBackgroundColor() != backgroundColor ||
+                                          backgroundGradient.getEndRight() != gradientEndRight);
+
+        if (needsEllipsis && needsNewGradient) {
+            backgroundGradient = new FadedTextGradient(gradientEndRight, fadeWidth, backgroundColor);
+            fadePaint.setShader(backgroundGradient);
+        }
+    }
+
+    private static class FadedTextGradient extends LinearGradient {
+        private final int endRight;
+        private final int backgroundColor;
+
+        public FadedTextGradient(final int gradientEndRight, final int fadeWidth,
+                final int backgroundColor) {
+            super(gradientEndRight - fadeWidth, 0, gradientEndRight, 0,
+                  getColorWithZeroedAlpha(backgroundColor), backgroundColor, Shader.TileMode.CLAMP);
+
+            this.endRight = gradientEndRight;
+            this.backgroundColor = backgroundColor;
+        }
+
+        private static int getColorWithZeroedAlpha(final int color) {
+            return Color.argb(0, Color.red(color), Color.green(color), Color.blue(color));
+        }
+
+        public int getEndRight() {
+            return endRight;
+        }
+
+        public int getBackgroundColor() {
+            return backgroundColor;
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/widget/FadedSingleColorTextView.java
@@ -0,0 +1,74 @@
+/* -*- 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.widget;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.LinearGradient;
+import android.graphics.Shader;
+import android.util.AttributeSet;
+
+/**
+ * Fades the end of the text by gecko:fadeWidth amount,
+ * if the text is too long and requires an ellipsis.
+ *
+ * This implementation is an improvement over Android's built-in fadingEdge
+ * and the fastest of Fennec's implementations. However, it only works for
+ * text of one color. It works by applying a linear gradient directly to the text.
+ */
+public class FadedSingleColorTextView extends FadedTextView {
+    // Shader for the fading edge.
+    private FadedTextGradient mTextGradient;
+
+    public FadedSingleColorTextView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    private void updateGradientShader() {
+        final int color = getCurrentTextColor();
+        final int width = getAvailableWidth();
+
+        final boolean needsNewGradient = (mTextGradient == null ||
+                                          mTextGradient.getColor() != color ||
+                                          mTextGradient.getWidth() != width);
+
+        final boolean needsEllipsis = needsEllipsis();
+        if (needsEllipsis && needsNewGradient) {
+            mTextGradient = new FadedTextGradient(width, fadeWidth, color);
+        }
+
+        getPaint().setShader(needsEllipsis ? mTextGradient : null);
+    }
+
+    @Override
+    public void onDraw(Canvas canvas) {
+        updateGradientShader();
+        super.onDraw(canvas);
+    }
+
+    private static class FadedTextGradient extends LinearGradient {
+        private final int mWidth;
+        private final int mColor;
+
+        public FadedTextGradient(int width, int fadeWidth, int color) {
+            super(0, 0, width, 0,
+                  new int[] { color, color, 0x0 },
+                  new float[] { 0,  ((float) (width - fadeWidth) / width), 1.0f },
+                  Shader.TileMode.CLAMP);
+
+            mWidth = width;
+            mColor = color;
+        }
+
+        public int getWidth() {
+            return mWidth;
+        }
+
+        public int getColor() {
+            return mColor;
+        }
+    }
+}
--- a/mobile/android/base/widget/FadedTextView.java
+++ b/mobile/android/base/widget/FadedTextView.java
@@ -2,104 +2,46 @@
  * 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.widget;
 
 import android.content.Context;
 import android.content.res.TypedArray;
-import android.graphics.Canvas;
-import android.graphics.LinearGradient;
-import android.graphics.Shader;
-import android.graphics.drawable.Drawable;
 import android.text.Layout;
 import android.util.AttributeSet;
 
 import org.mozilla.gecko.R;
-import org.mozilla.gecko.widget.ThemedTextView;
 
 /**
- * FadedTextView fades the ends of the text by fadeWidth amount,
- * if the text is too long and requires an ellipsis.
+ * An implementation of FadedTextView should fade the end of the text
+ * by gecko:fadeWidth amount, if the text is too long and requires an ellipsis.
  */
-public class FadedTextView extends ThemedTextView {
-
+public abstract class FadedTextView extends ThemedTextView {
     // Width of the fade effect from end of the view.
-    private final int mFadeWidth;
+    protected final int fadeWidth;
 
-    // Shader for the fading edge.
-    private FadedTextGradient mTextGradient;
-
-    public FadedTextView(Context context) {
-        this(context, null);
-    }
-
-    public FadedTextView(Context context, AttributeSet attrs) {
+    public FadedTextView(final Context context, final AttributeSet attrs) {
         super(context, attrs);
 
         setSingleLine(true);
         setEllipsize(null);
 
-        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FadedTextView);
-        mFadeWidth = a.getDimensionPixelSize(R.styleable.FadedTextView_fadeWidth, 0);
+        final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.FadedTextView);
+        fadeWidth = a.getDimensionPixelSize(R.styleable.FadedTextView_fadeWidth, 0);
         a.recycle();
     }
 
-    private int getAvailableWidth() {
+    protected int getAvailableWidth() {
         return getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight();
     }
 
-    private boolean needsEllipsis() {
+    protected boolean needsEllipsis() {
         final int width = getAvailableWidth();
         if (width <= 0) {
             return false;
         }
 
         final Layout layout = getLayout();
         return (layout != null && layout.getLineWidth(0) > width);
     }
-
-    private void updateGradientShader() {
-        final int color = getCurrentTextColor();
-        final int width = getAvailableWidth();
-
-        final boolean needsNewGradient = (mTextGradient == null ||
-                                          mTextGradient.getColor() != color ||
-                                          mTextGradient.getWidth() != width);
-
-        final boolean needsEllipsis = needsEllipsis();
-        if (needsEllipsis && needsNewGradient) {
-            mTextGradient = new FadedTextGradient(width, mFadeWidth, color);
-        }
-
-        getPaint().setShader(needsEllipsis ? mTextGradient : null);
-    }
-
-    @Override
-    public void onDraw(Canvas canvas) {
-        updateGradientShader();
-        super.onDraw(canvas);
-    }
-
-    private static class FadedTextGradient extends LinearGradient {
-        private final int mWidth;
-        private final int mColor;
-
-        public FadedTextGradient(int width, int fadeWidth, int color) {
-            super(0, 0, width, 0,
-                  new int[] { color, color, 0x0 },
-                  new float[] { 0,  ((float) (width - fadeWidth) / width), 1.0f },
-                  Shader.TileMode.CLAMP);
-
-            mWidth = width;
-            mColor = color;
-        }
-
-        public int getWidth() {
-            return mWidth;
-        }
-
-        public int getColor() {
-            return mColor;
-        }
-    }
 }
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -5006,18 +5006,21 @@ AddonInstall.prototype = {
   /**
    * Cancels installation of this add-on.
    *
    * @throws if installation cannot be cancelled from the current state
    */
   cancel: function AI_cancel() {
     switch (this.state) {
     case AddonManager.STATE_DOWNLOADING:
-      if (this.channel)
+      if (this.channel) {
+        logger.debug("Cancelling download of " + this.sourceURI.spec);
         this.channel.cancel(Cr.NS_BINDING_ABORTED);
+      }
+      break;
     case AddonManager.STATE_AVAILABLE:
     case AddonManager.STATE_DOWNLOADED:
       logger.debug("Cancelling download of " + this.sourceURI.spec);
       this.state = AddonManager.STATE_CANCELLED;
       XPIProvider.removeActiveInstall(this);
       AddonManagerPrivate.callInstallListeners("onDownloadCancelled",
                                                this.listeners, this.wrapper);
       this.removeTemporaryFile();
@@ -5494,18 +5497,30 @@ AddonInstall.prototype = {
    * @see nsIStreamListener
    */
   onStopRequest: function AI_onStopRequest(aRequest, aContext, aStatus) {
     this.stream.close();
     this.channel = null;
     this.badCerthandler = null;
     Services.obs.removeObserver(this, "network:offline-about-to-go-offline");
 
-    // If the download was cancelled then all events will have already been sent
+    // If the download was cancelled then update the state and send events
     if (aStatus == Cr.NS_BINDING_ABORTED) {
+      if (this.state == AddonManager.STATE_DOWNLOADING) {
+        logger.debug("Cancelled download of " + this.sourceURI.spec);
+        this.state = AddonManager.STATE_CANCELLED;
+        XPIProvider.removeActiveInstall(this);
+        AddonManagerPrivate.callInstallListeners("onDownloadCancelled",
+                                                 this.listeners, this.wrapper);
+        // If a listener restarted the download then there is no need to
+        // remove the temporary file
+        if (this.state != AddonManager.STATE_CANCELLED)
+          return;
+      }
+
       this.removeTemporaryFile();
       if (this.restartDownload)
         this.openChannel();
       return;
     }
 
     logger.debug("Download of " + this.sourceURI.spec + " completed.");
 
--- a/toolkit/mozapps/extensions/test/xpcshell/test_install.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_install.js
@@ -559,27 +559,31 @@ function run_test_9() {
       install.install();
     });
   }, "application/x-xpinstall", null, "Real Test 4", null, "1.0");
 }
 
 function check_test_9(install) {
   prepare_test({}, [
     "onDownloadCancelled"
-  ]);
+  ], function() {
+    let file = install.file;
+
+    // Allow the file removal to complete
+    do_execute_soon(function() {
+      AddonManager.getAllInstalls(function(activeInstalls) {
+        do_check_eq(activeInstalls.length, 0);
+        do_check_false(file.exists());
+
+        run_test_10();
+      });
+    });
+  });
 
   install.cancel();
-
-  ensure_test_completed();
-
-  AddonManager.getAllInstalls(function(activeInstalls) {
-    do_check_eq(activeInstalls.length, 0);
-
-    run_test_10();
-  });
 }
 
 // Tests that after cancelling a pending install it is removed from the active
 // installs
 function run_test_10() {
   prepare_test({ }, [
     "onNewInstall"
   ]);
@@ -1008,38 +1012,41 @@ function run_test_14() {
     ], check_test_14);
     install.install();
   }, "application/x-xpinstall");
 }
 
 function check_test_14(install) {
   prepare_test({ }, [
     "onDownloadCancelled"
-  ]);
-
-  install.cancel();
-
-  ensure_test_completed();
+  ], function() {
+    let file = install.file;
 
-  install.addListener({
-    onDownloadProgress: function() {
-      do_throw("Download should not have continued");
-    },
-    onDownloadEnded: function() {
-      do_throw("Download should not have continued");
-    }
+    install.addListener({
+      onDownloadProgress: function() {
+        do_throw("Download should not have continued");
+      },
+      onDownloadEnded: function() {
+        do_throw("Download should not have continued");
+      }
+    });
+
+    // Allow the listener to return to see if it continues downloading. The
+    // The listener only really tests if we give it time to see progress, the
+    // file check isn't ideal either
+    do_execute_soon(function() {
+      do_check_false(file.exists());
+
+      run_test_15();
+    });
   });
 
-  // Allow the listener to return to see if it continues downloading. The
-  // The listener only really tests if we give it time to see progress, the
-  // file check isn't ideal either
+  // Wait for the channel to be ready to cancel
   do_execute_soon(function() {
-    do_check_eq(install.file, null);
-
-    run_test_15();
+    install.cancel();
   });
 }
 
 // Checks that cancelling the install from onDownloadEnded actually cancels it
 function run_test_15() {
   prepare_test({ }, [
     "onNewInstall"
   ]);
@@ -1628,17 +1635,20 @@ function check_test_27(aInstall) {
     ]
   }, [
     "onDownloadStarted",
     "onDownloadEnded",
     "onInstallStarted",
     "onInstallEnded"
   ], finish_test_27);
 
+  let file = aInstall.file;
   aInstall.install();
+  do_check_neq(file.path, aInstall.file.path);
+  do_check_false(file.exists());
 }
 
 function finish_test_27(aInstall) {
   prepare_test({
     "addon3@tests.mozilla.org": [
       "onOperationCancelled"
     ]
   }, [
--- a/toolkit/mozapps/extensions/test/xpcshell/test_install_strictcompat.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_install_strictcompat.js
@@ -548,27 +548,31 @@ function run_test_9() {
       install.install();
     });
   }, "application/x-xpinstall", null, "Real Test 4", null, "1.0");
 }
 
 function check_test_9(install) {
   prepare_test({}, [
     "onDownloadCancelled"
-  ]);
+  ], function() {
+    let file = install.file;
+
+    // Allow the file removal to complete
+    do_execute_soon(function() {
+      AddonManager.getAllInstalls(function(activeInstalls) {
+        do_check_eq(activeInstalls.length, 0);
+        do_check_false(file.exists());
+
+        run_test_10();
+      });
+    });
+  });
 
   install.cancel();
-
-  ensure_test_completed();
-
-  AddonManager.getAllInstalls(function(activeInstalls) {
-    do_check_eq(activeInstalls.length, 0);
-
-    run_test_10();
-  });
 }
 
 // Tests that after cancelling a pending install it is removed from the active
 // installs
 function run_test_10() {
   prepare_test({ }, [
     "onNewInstall"
   ]);
@@ -999,38 +1003,41 @@ function run_test_14() {
     ], check_test_14);
     install.install();
   }, "application/x-xpinstall");
 }
 
 function check_test_14(install) {
   prepare_test({ }, [
     "onDownloadCancelled"
-  ]);
-
-  install.cancel();
-
-  ensure_test_completed();
+  ], function() {
+    let file = install.file;
 
-  install.addListener({
-    onDownloadProgress: function() {
-      do_throw("Download should not have continued");
-    },
-    onDownloadEnded: function() {
-      do_throw("Download should not have continued");
-    }
+    install.addListener({
+      onDownloadProgress: function() {
+        do_throw("Download should not have continued");
+      },
+      onDownloadEnded: function() {
+        do_throw("Download should not have continued");
+      }
+    });
+
+    // Allow the listener to return to see if it continues downloading. The
+    // The listener only really tests if we give it time to see progress, the
+    // file check isn't ideal either
+    do_execute_soon(function() {
+      do_check_false(file.exists());
+
+      run_test_15();
+    });
   });
 
-  // Allow the listener to return to see if it continues downloading. The
-  // The listener only really tests if we give it time to see progress, the
-  // file check isn't ideal either
+  // Wait for the channel to be ready to cancel
   do_execute_soon(function() {
-    do_check_eq(install.file, null);
-
-    run_test_15();
+    install.cancel();
   });
 }
 
 // Checks that cancelling the install from onDownloadEnded actually cancels it
 function run_test_15() {
   prepare_test({ }, [
     "onNewInstall"
   ]);
@@ -1619,17 +1626,20 @@ function check_test_27(aInstall) {
     ]
   }, [
     "onDownloadStarted",
     "onDownloadEnded",
     "onInstallStarted",
     "onInstallEnded"
   ], finish_test_27);
 
+  let file = aInstall.file;
   aInstall.install();
+  do_check_neq(file.path, aInstall.file.path);
+  do_check_false(file.exists());
 }
 
 function finish_test_27(aInstall) {
   prepare_test({
     "addon3@tests.mozilla.org": [
       "onOperationCancelled"
     ]
   }, [