author | Wes Kocher <wkocher@mozilla.com> |
Wed, 31 Dec 2014 16:52:30 -0800 | |
changeset 221779 | 3c296aa11c51df594590eccee16c9f639d2b5f1d |
parent 221769 | 271f40e3c3ad2e0ab4839ceac77f887e14de9db4 (current diff) |
parent 221778 | 2e9b1569f4c85cad19ade8903482e3c35a47582f (diff) |
child 221780 | feeb347e67ec566c5fa534f71f825f49a46a248a |
child 221792 | 9261135074f81237a4605abe449e031ca626b2e8 |
child 221810 | f9f2edd5c4ea0632093949edb74ee5bde0d6936d |
push id | 28041 |
push user | kwierso@gmail.com |
push date | Thu, 01 Jan 2015 00:53:12 +0000 |
treeherder | mozilla-central@3c296aa11c51 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | merge |
milestone | 37.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
37.0a1
/
20150101030214
/
pushlog to previous
nightly linux64
37.0a1
/
20150101030214
/
pushlog to previous
nightly mac
37.0a1
/
20150101030214
/
pushlog to previous
nightly win32
37.0a1
/
20150101030214
/
pushlog to previous
nightly win64
37.0a1
/
20150101030214
/
pushlog to previous
|
--- 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" ] }, [