Bug 917896 - Replace throbber with progress bar. r=lucasr
authorBrian Nicholson <bnicholson@mozilla.com>
Fri, 17 Jan 2014 20:03:59 -0800
changeset 164181 31688c2aca28ba1014ac5153f4fba25f76107a13
parent 164180 3da20e4c20ec517d07a004fa23879b2726e1757a
child 164182 2235c85f6d6b606f570f38ede90ff66fa9c7d718
push id38648
push userryanvm@gmail.com
push dateSun, 19 Jan 2014 20:41:32 +0000
treeherdermozilla-inbound@10030fadbaaf [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerslucasr
bugs917896
milestone29.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 917896 - Replace throbber with progress bar. r=lucasr
CLOBBER
mobile/android/base/BrowserApp.java
mobile/android/base/Tab.java
mobile/android/base/Tabs.java
mobile/android/base/moz.build
mobile/android/base/resources/drawable-hdpi/progress.9.png
mobile/android/base/resources/drawable-mdpi/progress.9.png
mobile/android/base/resources/drawable-xhdpi/progress.9.png
mobile/android/base/resources/layout-large-v11/browser_toolbar.xml
mobile/android/base/resources/layout/browser_toolbar.xml
mobile/android/base/toolbar/BrowserToolbar.java
mobile/android/base/toolbar/ToolbarDisplayLayout.java
mobile/android/base/toolbar/ToolbarProgressView.java
--- a/CLOBBER
+++ b/CLOBBER
@@ -17,9 +17,9 @@
 #
 # Modifying this file will now automatically clobber the buildbot machines \o/
 #
 
 # Are you updating CLOBBER because you think it's needed for your WebIDL
 # changes to stick? As of bug 928195, this shouldn't be necessary! Please
 # don't change CLOBBER for WebIDL changes any more.
 
-Updates to NSS seem to require a clobber due bug 959928.
+Bug 917896 requires a clobber due to bug 961339.
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -1416,24 +1416,16 @@ abstract public class BrowserApp extends
         maybeCancelFaviconLoad(tab);
 
         final int tabFaviconSize = getResources().getDimensionPixelSize(R.dimen.browser_toolbar_favicon_size);
 
         int flags = (tab.isPrivate() || tab.getErrorType() != Tab.ErrorType.NONE) ? 0 : LoadFaviconTask.FLAG_PERSIST;
         int id = Favicons.getSizedFavicon(tab.getURL(), tab.getFaviconURL(), tabFaviconSize, flags, sFaviconLoadedListener);
 
         tab.setFaviconLoadId(id);
-
-        final Tabs tabs = Tabs.getInstance();
-        if (id != Favicons.LOADED && tabs.isSelectedTab(tab)) {
-            // We're loading the current tab's favicon from somewhere
-            // other than the cache. Display the globe favicon until then.
-            tab.updateFavicon(Favicons.sDefaultFavicon);
-            tabs.notifyListeners(tab, Tabs.TabEvents.FAVICON);
-        }
     }
 
     private void maybeCancelFaviconLoad(Tab tab) {
         int faviconLoadId = tab.getFaviconLoadId();
 
         if (Favicons.NOT_LOADING == faviconLoadId) {
             return;
         }
--- a/mobile/android/base/Tab.java
+++ b/mobile/android/base/Tab.java
@@ -64,16 +64,17 @@ public class Tab {
     private int mBackgroundColor;
     private int mState;
     private Bitmap mThumbnailBitmap;
     private boolean mDesktopMode;
     private boolean mEnteringReaderMode;
     private Context mAppContext;
     private ErrorType mErrorType = ErrorType.NONE;
     private static final int MAX_HISTORY_LIST_SIZE = 50;
+    private int mLoadProgress;
 
     public static final int STATE_DELAYED = 0;
     public static final int STATE_LOADING = 1;
     public static final int STATE_SUCCESS = 2;
     public static final int STATE_ERROR = 3;
 
     private static final int DEFAULT_BACKGROUND_COLOR = Color.WHITE;
 
@@ -759,9 +760,27 @@ public class Tab {
 
     public boolean getDesktopMode() {
         return mDesktopMode;
     }
 
     public boolean isPrivate() {
         return false;
     }
+
+    /**
+     * Sets the tab load progress to the given percentage.
+     *
+     * @param progressPercentage Percentage to set progress to (0-100)
+     */
+    void setLoadProgress(int progressPercentage) {
+        mLoadProgress = progressPercentage;
+    }
+
+    /**
+     * Gets the tab load progress percentage.
+     *
+     * @return Current progress percentage
+     */
+    public int getLoadProgress() {
+        return mLoadProgress;
+    }
 }
--- a/mobile/android/base/Tabs.java
+++ b/mobile/android/base/Tabs.java
@@ -47,16 +47,20 @@ public class Tabs implements GeckoEventL
     private volatile Tab mSelectedTab;
 
     // All accesses to mTabs must be synchronized on the Tabs instance.
     private final HashMap<Integer, Tab> mTabs = new HashMap<Integer, Tab>();
 
     private AccountManager mAccountManager;
     private OnAccountsUpdateListener mAccountListener = null;
 
+    private static final int LOAD_PROGRESS_START = 20;
+    private static final int LOAD_PROGRESS_LOCATION_CHANGE = 60;
+    private static final int LOAD_PROGRESS_LOADED = 80;
+
     public static final int LOADURL_NONE         = 0;
     public static final int LOADURL_NEW_TAB      = 1 << 0;
     public static final int LOADURL_USER_ENTERED = 1 << 1;
     public static final int LOADURL_PRIVATE      = 1 << 2;
     public static final int LOADURL_PINNED       = 1 << 3;
     public static final int LOADURL_DELAY_LOAD   = 1 << 4;
     public static final int LOADURL_DESKTOP      = 1 << 5;
     public static final int LOADURL_BACKGROUND   = 1 << 6;
@@ -430,48 +434,52 @@ public class Tabs implements GeckoEventL
             if (event.startsWith("SessionHistory:")) {
                 event = event.substring("SessionHistory:".length());
                 tab.handleSessionHistoryMessage(event, message);
             } else if (event.equals("Tab:Close")) {
                 closeTab(tab);
             } else if (event.equals("Tab:Select")) {
                 selectTab(tab.getId());
             } else if (event.equals("Content:LocationChange")) {
+                tab.setLoadProgress(LOAD_PROGRESS_LOCATION_CHANGE);
                 tab.handleLocationChange(message);
             } else if (event.equals("Content:SecurityChange")) {
                 tab.updateIdentityData(message.getJSONObject("identity"));
                 notifyListeners(tab, TabEvents.SECURITY_CHANGE);
             } else if (event.equals("Content:ReaderEnabled")) {
                 tab.setReaderEnabled(true);
                 notifyListeners(tab, TabEvents.READER_ENABLED);
             } else if (event.equals("Content:StateChange")) {
                 int state = message.getInt("state");
                 if ((state & GeckoAppShell.WPL_STATE_IS_NETWORK) != 0) {
                     if ((state & GeckoAppShell.WPL_STATE_START) != 0) {
                         boolean showProgress = message.getBoolean("showProgress");
                         tab.handleDocumentStart(showProgress, message.getString("uri"));
+                        tab.setLoadProgress(LOAD_PROGRESS_START);
                         notifyListeners(tab, Tabs.TabEvents.START);
                     } else if ((state & GeckoAppShell.WPL_STATE_STOP) != 0) {
                         tab.handleDocumentStop(message.getBoolean("success"));
                         notifyListeners(tab, Tabs.TabEvents.STOP);
                     }
                 }
             } else if (event.equals("Content:LoadError")) {
+                tab.setLoadProgress(LOAD_PROGRESS_LOADED);
                 notifyListeners(tab, Tabs.TabEvents.LOAD_ERROR);
             } else if (event.equals("Content:PageShow")) {
                 notifyListeners(tab, TabEvents.PAGE_SHOW);
             } else if (event.equals("DOMContentLoaded")) {
                 String backgroundColor = message.getString("bgColor");
                 if (backgroundColor != null) {
                     tab.setBackgroundColor(backgroundColor);
                 } else {
                     // Default to white if no color is given
                     tab.setBackgroundColor(Color.WHITE);
                 }
                 tab.setErrorType(message.optString("errorType"));
+                tab.setLoadProgress(LOAD_PROGRESS_LOADED);
                 notifyListeners(tab, Tabs.TabEvents.LOADED);
             } else if (event.equals("DOMTitleChanged")) {
                 tab.updateTitle(message.getString("title"));
             } else if (event.equals("Link:Favicon")) {
                 tab.updateFaviconURL(message.getString("href"), message.getInt("size"));
                 notifyListeners(tab, TabEvents.LINK_FAVICON);
             } else if (event.equals("Link:Feed")) {
                 tab.setHasFeeds(true);
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -315,16 +315,17 @@ gbjar.sources += [
     'toolbar/ForwardButton.java',
     'toolbar/PageActionLayout.java',
     'toolbar/ShapedButton.java',
     'toolbar/SiteIdentityPopup.java',
     'toolbar/TabCounter.java',
     'toolbar/ToolbarDisplayLayout.java',
     'toolbar/ToolbarEditLayout.java',
     'toolbar/ToolbarEditText.java',
+    'toolbar/ToolbarProgressView.java',
     'toolbar/ToolbarTitlePrefs.java',
     'TouchEventInterceptor.java',
     'updater/UpdateService.java',
     'updater/UpdateServiceHelper.java',
     'VideoPlayer.java',
     'webapp/ApkResources.java',
     'webapp/InstallHelper.java',
     'webapp/InstallListener.java',
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..acc6c89c53d6d128ce76c33e7361bcd43ca738c2
GIT binary patch
literal 300
zc%17D@N?(olHy`uVBq!ia0vp^nn29P!3HGbW^ES*agw~<T^Rm@;DWu&Cj&(|3p^r=
z85p=bL736}<mt6QLG}_)Usv{fjO-#T5*if~IDkSsJzX3_G|r!$yqAl~P@rXhB=e%F
zb_?e6KT5uJgLB(0&F`DVP8ROGb5b_qg@eb9Gdh!Hm6YblJvy(zV9)EL7f@5t&ipKa
zsq(yWBjcrr3rzZ}Z@%ZtIVAa#>rUyOtFoFR!E@S;bp<yF%}{j~%l2aPE#$3Iog6CM
zpu2(1$&yvu^j-XuN5z~feb3YTcRz7Cy;yH<_Hv(Ot!r~G+||~$xL^_TGVWyO9trn0
pu?+=|HVx_*rnqfQV6*>ko-hBv<h>a~rYX=944$rjF6*2UngAM7YTEz+
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..ee2924fb1be9564764540de12228a09ad19f14f9
GIT binary patch
literal 248
zc%17D@N?(olHy`uVBq!ia0vp^GC<7A!3HG5uJjxSQfx`y?k)`f85$TEE(xU<14TFs
zJR*TKcP9ulnx8zq7AVMG;_2(kevgrZK}_w^!e?DTp;k{9#}JM4dnaz>J7mD)a$nUk
zXKL4x`-Yzs|0u;bMrcjb6RG0KoH}WfL4)e)^wZPc=bU(UvObUL!^$YNP}!>Ei@o)p
zOK9!;er01mf6ise9j_`Xnf&CZRt6fqp0?tRqkz*v<MS5}Y&dLkAkA`?+PwSPTe?@h
q*p&0k{D^cAzfG9>-|PPw_A>mtqb4n=@G1xBAO=rYKbLh*2~7Z}30ukl
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..196c95737fe851f3ffd354946a12641d7af3ace8
GIT binary patch
literal 384
zc$@)%0e}99P)<h;3K|Lk000e1NJLTq001@s000OG1^@s6vNa3<00006VoOIv0RI4h
z007bwXLA4m010qNS#tmY3yA;#3yA?Y$;+((000McNliru-T?;`7aFITu(kjI0S!q+
zK~y-)-IPI2#4rp+-*+Q1us~dbn{f$t+!W1<6&obP07=cF>5z7kOvMI)rAkigxc}LS
z#bnOX#2+^S_`?a<Dx$$D)nl}B=Irj1?^@PV#Z$)5Yvp~(4us*6o9lamJrisXyaF#k
z(%sYXPvI=O*BZ)FfvXdfKgq!W)8v<hFcolW=ZJAf<d)GdaV~(sl=Z|yU(pt5`Y8kq
zngkyKvKGq$mVvARHg{TraSXr}Vwd%Y+L5x~y#)I1Qm<R}-RjMPnbv<UZo{lefR*>j
zmtbSjvDV3O;4?uA5$OQD=gebGf7o2vbMMP_ZFo44<|DOADKB6<aA}AH+aun94<Kf+
eX3d@dW%>dcJ5NIZD>PaF0000<MNUMnLSTZ`;GmWO
--- a/mobile/android/base/resources/layout-large-v11/browser_toolbar.xml
+++ b/mobile/android/base/resources/layout-large-v11/browser_toolbar.xml
@@ -100,9 +100,17 @@
 
     <ImageView android:id="@+id/shadow"
                android:layout_width="fill_parent"
                android:layout_height="2dp"
                android:layout_alignParentBottom="true"
                android:background="@color/url_bar_shadow"
                android:contentDescription="@null"/>
 
+    <org.mozilla.gecko.toolbar.ToolbarProgressView android:id="@+id/progress"
+                                                   android:layout_width="fill_parent"
+                                                   android:layout_height="2dp"
+                                                   android:layout_alignBottom="@id/shadow"
+                                                   android:src="@drawable/progress"
+                                                   android:background="@null"
+                                                   android:visibility="gone" />
+
 </merge>
--- a/mobile/android/base/resources/layout/browser_toolbar.xml
+++ b/mobile/android/base/resources/layout/browser_toolbar.xml
@@ -100,9 +100,17 @@
 
     <ImageView android:id="@+id/shadow"
                android:layout_width="fill_parent"
                android:layout_height="2dp"
                android:layout_alignParentBottom="true"
                android:background="@color/url_bar_shadow"
                android:contentDescription="@null"/>
 
+    <org.mozilla.gecko.toolbar.ToolbarProgressView android:id="@+id/progress"
+                                                   android:layout_width="fill_parent"
+                                                   android:layout_height="2dp"
+                                                   android:layout_alignBottom="@id/shadow"
+                                                   android:src="@drawable/progress"
+                                                   android:background="@null"
+                                                   android:visibility="gone" />
+
 </merge>
--- a/mobile/android/base/toolbar/BrowserToolbar.java
+++ b/mobile/android/base/toolbar/BrowserToolbar.java
@@ -118,16 +118,17 @@ public class BrowserToolbar extends Geck
     private ToolbarEditLayout mUrlEditLayout;
     private View mUrlBarEntry;
     private ImageView mUrlBarRightEdge;
     private boolean mSwitchingTabs;
     private ShapedButton mTabs;
     private ImageButton mBack;
     private ImageButton mForward;
 
+    private ToolbarProgressView mProgressBar;
     private TabCounter mTabsCounter;
     private GeckoImageButton mMenu;
     private GeckoImageView mMenuIcon;
     private LinearLayout mActionItemBar;
     private MenuPopup mMenuPopup;
     private List<View> mFocusOrder;
 
     private OnActivateListener mActivateListener;
@@ -199,16 +200,18 @@ public class BrowserToolbar extends Geck
         mForward = (ImageButton) findViewById(R.id.forward);
         setButtonEnabled(mForward, false);
 
         mMenu = (GeckoImageButton) findViewById(R.id.menu);
         mMenuIcon = (GeckoImageView) findViewById(R.id.menu_icon);
         mActionItemBar = (LinearLayout) findViewById(R.id.menu_items);
         mHasSoftMenuButton = !HardwareUtils.hasMenuButton();
 
+        mProgressBar = (ToolbarProgressView) findViewById(R.id.progress);
+
         // We use different layouts on phones and tablets, so adjust the focus
         // order appropriately.
         mFocusOrder = new ArrayList<View>();
         if (HardwareUtils.isTablet()) {
             mFocusOrder.addAll(Arrays.asList(mTabs, mBack, mForward, this));
             mFocusOrder.addAll(mUrlDisplayLayout.getFocusOrder());
             mFocusOrder.addAll(Arrays.asList(mActionItemBar, mMenu));
         } else {
@@ -439,40 +442,57 @@ public class BrowserToolbar extends Geck
                 updateTabCount(tabs.getDisplayCount());
                 break;
             case RESTORED:
                 // TabCount fixup after OOM
             case SELECTED:
                 mUrlDisplayLayout.dismissSiteIdentityPopup();
                 updateTabCount(tabs.getDisplayCount());
                 mSwitchingTabs = true;
-                // Fall through.
+                break;
         }
 
         if (tabs.isSelectedTab(tab)) {
             final EnumSet<UpdateFlags> flags = EnumSet.noneOf(UpdateFlags.class);
 
+            // Progress-related handling
+            switch (msg) {
+                case START:
+                    updateProgressVisibility(tab, 0);
+                    // Fall through.
+                case LOCATION_CHANGE:
+                case LOAD_ERROR:
+                case LOADED:
+                    flags.add(UpdateFlags.PROGRESS);
+                    if (mProgressBar.getVisibility() == View.VISIBLE) {
+                        mProgressBar.animateProgress(tab.getLoadProgress());
+                    }
+                    break;
+
+                case STOP:
+                case SELECTED:
+                    flags.add(UpdateFlags.PROGRESS);
+                    updateProgressVisibility();
+                    break;
+            }
+
             switch (msg) {
                 case TITLE:
                     flags.add(UpdateFlags.TITLE);
                     break;
 
                 case START:
                     updateBackButton(tab);
                     updateForwardButton(tab);
-
-                    flags.add(UpdateFlags.PROGRESS);
                     break;
 
                 case STOP:
                     updateBackButton(tab);
                     updateForwardButton(tab);
 
-                    flags.add(UpdateFlags.PROGRESS);
-
                     // Reset the title in case we haven't navigated
                     // to a new page yet.
                     flags.add(UpdateFlags.TITLE);
                     break;
 
                 case SELECTED:
                 case LOAD_ERROR:
                     flags.add(UpdateFlags.TITLE);
@@ -506,16 +526,30 @@ public class BrowserToolbar extends Geck
         switch (msg) {
             case SELECTED:
             case LOAD_ERROR:
             case LOCATION_CHANGE:
                 mSwitchingTabs = false;
         }
     }
 
+    private void updateProgressVisibility() {
+        final Tab selectedTab = Tabs.getInstance().getSelectedTab();
+        updateProgressVisibility(selectedTab, selectedTab.getLoadProgress());
+    }
+
+    private void updateProgressVisibility(Tab selectedTab, int progress) {
+        if (!mIsEditing && selectedTab.getState() == Tab.STATE_LOADING) {
+            mProgressBar.setProgress(progress);
+            mProgressBar.setVisibility(View.VISIBLE);
+        } else {
+            mProgressBar.setVisibility(View.GONE);
+        }
+    }
+
     public boolean isVisible() {
         return ViewHelper.getTranslationY(this) == 0;
     }
 
     @Override
     public void setNextFocusDownId(int nextId) {
         super.setNextFocusDownId(nextId);
         mTabs.setNextFocusDownId(nextId);
@@ -865,16 +899,18 @@ public class BrowserToolbar extends Geck
             return;
         }
 
         mUrlEditLayout.setText(url != null ? url : "");
 
         setIsEditing(true);
         updateChildrenForEditing();
 
+        updateProgressVisibility();
+
         if (mStartEditingListener != null) {
             mStartEditingListener.onStartEditing();
         }
 
         if (mUrlBarRightEdge != null) {
             mUrlBarRightEdge.setVisibility(View.VISIBLE);
         }
 
@@ -985,16 +1021,18 @@ public class BrowserToolbar extends Geck
         setIsEditing(false);
 
         updateChildrenForEditing();
 
         if (mStopEditingListener != null) {
             mStopEditingListener.onStopEditing();
         }
 
+        updateProgressVisibility();
+
         if (HardwareUtils.isTablet() || Build.VERSION.SDK_INT < 11) {
             hideUrlEditLayout();
 
             if (!HardwareUtils.isTablet()) {
                 updateTabCountAndAnimate(Tabs.getInstance().getDisplayCount());
 
                 if (mUrlBarRightEdge != null) {
                     ViewHelper.setTranslationX(mUrlBarRightEdge, 0);
@@ -1209,17 +1247,16 @@ public class BrowserToolbar extends Geck
         setVisibility(View.GONE);
     }
 
     private void refreshState() {
         Tab tab = Tabs.getInstance().getSelectedTab();
         if (tab != null) {
             updateDisplayLayout(tab, EnumSet.of(UpdateFlags.FAVICON,
                                                 UpdateFlags.SITE_IDENTITY,
-                                                UpdateFlags.PROGRESS,
                                                 UpdateFlags.PRIVATE_MODE));
             updateBackButton(tab);
             updateForwardButton(tab);
 
             final boolean isPrivate = tab.isPrivate();
             setPrivateMode(isPrivate);
             mTabs.setPrivateMode(isPrivate);
             mMenu.setPrivateMode(isPrivate);
--- a/mobile/android/base/toolbar/ToolbarDisplayLayout.java
+++ b/mobile/android/base/toolbar/ToolbarDisplayLayout.java
@@ -112,18 +112,16 @@ public class ToolbarDisplayLayout extend
     private ImageButton mFavicon;
     private int mFaviconSize;
 
     private ImageButton mStop;
     private OnStopListener mStopListener;
 
     private PageActionLayout mPageActionLayout;
 
-    private Animation mProgressSpinner;
-
     private AlphaAnimation mLockFadeIn;
     private TranslateAnimation mTitleSlideLeft;
     private TranslateAnimation mTitleSlideRight;
 
     private SiteIdentityPopup mSiteIdentityPopup;
     private SecurityMode mSecurityMode;
 
     private PropertyAnimator mForwardAnim;
@@ -161,18 +159,16 @@ public class ToolbarDisplayLayout extend
         mFaviconSize = Math.round(res.getDimension(R.dimen.browser_toolbar_favicon_size));
 
         mSiteSecurity = (ImageButton) findViewById(R.id.site_security);
         mSiteSecurityVisible = (mSiteSecurity.getVisibility() == View.VISIBLE);
 
         mSiteIdentityPopup = new SiteIdentityPopup(mActivity);
         mSiteIdentityPopup.setAnchor(mSiteSecurity);
 
-        mProgressSpinner = AnimationUtils.loadAnimation(mActivity, R.anim.progress_spinner);
-
         mStop = (ImageButton) findViewById(R.id.stop);
         mPageActionLayout = (PageActionLayout) findViewById(R.id.page_action_layout);
     }
 
     @Override
     public void onAttachedToWindow() {
         mTitlePrefs = new ToolbarTitlePrefs();
 
@@ -352,36 +348,33 @@ public class ToolbarDisplayLayout extend
     }
 
     private void updateFavicon(Tab tab) {
         if (tab == null) {
             mFavicon.setImageDrawable(null);
             return;
         }
 
-        if (tab.getState() == Tab.STATE_LOADING) {
-            return;
-        }
+        Bitmap image = tab.getFavicon();
 
-        Bitmap image = tab.getFavicon();
-        if (image == mLastFavicon) {
+        if (image != null && image == mLastFavicon) {
             Log.d(LOGTAG, "Ignoring favicon: new image is identical to previous one.");
             return;
         }
 
         // Cache the original so we can debounce without scaling
         mLastFavicon = image;
 
         Log.d(LOGTAG, "updateFavicon(" + image + ")");
 
         if (image != null) {
             image = Bitmap.createScaledBitmap(image, mFaviconSize, mFaviconSize, false);
             mFavicon.setImageBitmap(image);
         } else {
-            mFavicon.setImageDrawable(null);            
+            mFavicon.setImageResource(R.drawable.favicon);
         }
     }
 
     private void updateSiteIdentity(Tab tab, EnumSet<UpdateFlags> flags) {
         final SiteIdentity siteIdentity;
         if (tab == null) {
             siteIdentity = null;
         } else {
@@ -417,27 +410,18 @@ public class ToolbarDisplayLayout extend
         }
 
         mUiMode = uiMode;
 
         // The "Throbber start" and "Throbber stop" log messages in this method
         // are needed by S1/S2 tests (http://mrcote.info/phonedash/#).
         // See discussion in Bug 804457. Bug 805124 tracks paring these down.
         if (mUiMode == UIMode.PROGRESS) {
-            mLastFavicon = null;
-            mFavicon.setImageResource(R.drawable.progress_spinner);
-            mFavicon.setAnimation(mProgressSpinner);
-            mProgressSpinner.start();
-
             Log.i(LOGTAG, "zerdatime " + SystemClock.uptimeMillis() + " - Throbber start");
         } else {
-            updateFavicon(tab);
-            mFavicon.setAnimation(null);
-            mProgressSpinner.cancel();
-
             Log.i(LOGTAG, "zerdatime " + SystemClock.uptimeMillis() + " - Throbber stop");
         }
 
         updatePageActions(flags);
     }
 
     private void updatePageActions(EnumSet<UpdateFlags> flags) {
         final boolean isShowingProgress = (mUiMode == UIMode.PROGRESS);
@@ -567,9 +551,9 @@ public class ToolbarDisplayLayout extend
     boolean dismissSiteIdentityPopup() {
         if (mSiteIdentityPopup != null && mSiteIdentityPopup.isShowing()) {
             mSiteIdentityPopup.dismiss();
             return true;
         }
 
         return false;
     }
-}
\ No newline at end of file
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/toolbar/ToolbarProgressView.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.mozilla.gecko.toolbar;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.Message;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.widget.ImageView;
+import android.view.View;
+
+/**
+ * Progress view used for page loads.
+ *
+ * Because we're given limited information about the page load progress, the
+ * bar also includes incremental animation between each step to improve
+ * perceived performance.
+ */
+public class ToolbarProgressView extends ImageView {
+    public static final int MAX_PROGRESS = 10000;
+    private static final int MSG_UPDATE = 42;
+    private static final int STEPS = 10;
+    private static final int DELAY = 40;
+
+    private int mTargetProgress;
+    private int mIncrement;
+    private Rect mBounds;
+    private Handler mHandler;
+    private int mCurrentProgress;
+
+    public ToolbarProgressView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        init(context);
+    }
+
+    public ToolbarProgressView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(context);
+    }
+
+    public ToolbarProgressView(Context context) {
+        super(context);
+        init(context);
+    }
+
+    private void init(Context ctx) {
+        mBounds = new Rect(0,0,0,0);
+        mTargetProgress = 0;
+
+        mHandler = new Handler() {
+            @Override
+            public void handleMessage(Message msg) {
+                if (msg.what == MSG_UPDATE) {
+                    final int progress = Math.min(mTargetProgress, mCurrentProgress + mIncrement);
+                    mCurrentProgress = progress;
+
+                    updateBounds();
+
+                    if (progress < mTargetProgress) {
+                        sendMessageDelayed(mHandler.obtainMessage(MSG_UPDATE), DELAY);
+                    }
+                }
+            }
+
+        };
+    }
+
+    @Override
+    public void onLayout(boolean f, int l, int t, int r, int b) {
+        mBounds.left = 0;
+        mBounds.right = (r - l) * mCurrentProgress / MAX_PROGRESS;
+        mBounds.top = 0;
+        mBounds.bottom = b - t;
+    }
+
+    @Override
+    public void onDraw(Canvas canvas) {
+        final Drawable d = getDrawable();
+        d.setBounds(mBounds);
+        d.draw(canvas);
+    }
+
+    /**
+     * Immediately sets the progress bar to the given progress percentage.
+     *
+     * @param progress Percentage (0-100) to which progress bar should be set
+     */
+    void setProgress(int progressPercentage) {
+        mCurrentProgress = mTargetProgress = getAbsoluteProgress(progressPercentage);
+        updateBounds();
+
+        mHandler.removeMessages(MSG_UPDATE);
+    }
+
+    /**
+     * Animates the progress bar from the current progress value to the given
+     * progress percentage.
+     *
+     * @param progress Percentage (0-100) to which progress bar should be animated
+     */
+    void animateProgress(int progressPercentage) {
+        final int absoluteProgress = getAbsoluteProgress(progressPercentage);
+        if (absoluteProgress == mTargetProgress) {
+            return;
+        }
+
+        mCurrentProgress = mTargetProgress;
+        mTargetProgress = absoluteProgress;
+        mIncrement = (mTargetProgress - mCurrentProgress) / STEPS;
+
+        mHandler.removeMessages(MSG_UPDATE);
+        mHandler.sendEmptyMessage(MSG_UPDATE);
+    }
+
+    private int getAbsoluteProgress(int progressPercentage) {
+        if (progressPercentage < 0) {
+            return 0;
+        }
+
+        if (progressPercentage > 100) {
+            return 100;
+        }
+
+        return progressPercentage * MAX_PROGRESS / 100;
+    }
+
+    private void updateBounds() {
+        mBounds.right = getWidth() * mCurrentProgress / MAX_PROGRESS;
+        invalidate();
+    }
+}