Merge m-c to inbound.
authorRyan VanderMeulen <ryanvm@gmail.com>
Thu, 29 Aug 2013 20:48:34 -0400
changeset 153053 9f122ec9a3c9e00ff10cdc9618e1b74819e00e71
parent 153052 47180a284986e5bd17fbde8ac9ed4aff30c27a91 (current diff)
parent 153008 c7459bc8e449a10da61db2cdb7c3498a55385b06 (diff)
child 153054 9ff6833625a2cb250428cd11474527b7164d57b6
push idunknown
push userunknown
push dateunknown
milestone26.0a1
Merge m-c to inbound.
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,4 +1,4 @@
 {
-    "revision": "ba5f179a8e557690746e88ffc8b41a8151f65b06", 
+    "revision": "9adc8de2a121d92d43a4669c4326695fe703eb71", 
     "repo_path": "/integration/gaia-central"
 }
--- a/browser/components/sessionstore/src/SessionStore.jsm
+++ b/browser/components/sessionstore/src/SessionStore.jsm
@@ -4648,17 +4648,19 @@ let TabStateCache = {
    *
    * @param {XULElement} aKey The tab or the associated browser.
    *
    * @return {TabData|undefined} The data if available, |undefined|
    * otherwise.
    */
   get: function(aKey) {
     let key = this._normalizeToBrowser(aKey);
-    return this._data.get(key);
+    let result = this._data.get(key);
+    TabStateCacheTelemetry.recordAccess(!!result);
+    return result;
   },
 
   /**
    * Delete the tab data associated with a tab.
    *
    * @param {XULElement} aKey The tab or the associated browser.
    *
    * Noop of there is no tab data associated with the tab.
@@ -4667,16 +4669,17 @@ let TabStateCache = {
     let key = this._normalizeToBrowser(aKey);
     this._data.delete(key);
   },
 
   /**
    * Delete all tab data.
    */
   clear: function() {
+    TabStateCacheTelemetry.recordClear();
     this._data.clear();
   },
 
   /**
    * Update in place a piece of data.
    *
    * @param {XULElement} aKey The tab or the associated browser.
    * If the tab/browser is not present, do nothing.
@@ -4684,21 +4687,87 @@ let TabStateCache = {
    * @param {*} aValue The new value to place in the field.
    */
   update: function(aKey, aField, aValue) {
     let key = this._normalizeToBrowser(aKey);
     let data = this._data.get(key);
     if (data) {
       data[aField] = aValue;
     }
+    TabStateCacheTelemetry.recordAccess(!!data);
   },
 
   _normalizeToBrowser: function(aKey) {
     let nodeName = aKey.localName;
     if (nodeName == "tab") {
       return aKey.linkedBrowser;
     }
     if (nodeName == "browser") {
       return aKey;
     }
     throw new TypeError("Key is neither a tab nor a browser: " + nodeName);
   }
 };
+
+let TabStateCacheTelemetry = {
+  // Total number of hits during the session
+  _hits: 0,
+  // Total number of misses during the session
+  _misses: 0,
+  // Total number of clears during the session
+  _clears: 0,
+  // |true| once we have been initialized
+  _initialized: false,
+
+  /**
+   * Record a cache access.
+   *
+   * @param {boolean} isHit If |true|, the access was a hit, otherwise
+   * a miss.
+   */
+  recordAccess: function(isHit) {
+    this._init();
+    if (isHit) {
+      ++this._hits;
+    } else {
+      ++this._misses;
+    }
+  },
+
+  /**
+   * Record a cache clear
+   */
+  recordClear: function() {
+    this._init();
+    ++this._clears;
+  },
+
+  /**
+   * Initialize the telemetry.
+   */
+  _init: function() {
+    if (this._initialized) {
+      // Avoid double initialization
+      return;
+    }
+    Services.obs.addObserver(this, "profile-before-change", false);
+  },
+
+  observe: function() {
+    Services.obs.removeObserver(this, "profile-before-change");
+
+    // Record hit/miss rate
+    let accesses = this._hits + this._misses;
+    if (accesses == 0) {
+      return;
+    }
+
+    this._fillHistogram("HIT_RATE", this._hits, accesses);
+    this._fillHistogram("CLEAR_RATIO", this._clears, accesses);
+  },
+
+  _fillHistogram: function(suffix, positive, total) {
+    let PREFIX = "FX_SESSION_RESTORE_TABSTATECACHE_";
+    let histo = Services.telemetry.getHistogramById(PREFIX + suffix);
+    let rate = Math.floor( ( positive * 100 ) / total );
+    histo.add(rate);
+  }
+};
--- a/browser/metro/base/content/input.js
+++ b/browser/metro/base/content/input.js
@@ -89,16 +89,17 @@ var TouchModule = {
     this._targetScrollInterface = null;
 
     this._kinetic = new KineticController(this._dragBy.bind(this),
                                           this._kineticStop.bind(this));
 
     // capture phase events
     window.addEventListener("CancelTouchSequence", this, true);
     window.addEventListener("dblclick", this, true);
+    window.addEventListener("keydown", this, true);
 
     // bubble phase
     window.addEventListener("contextmenu", this, false);
     window.addEventListener("touchstart", this, false);
     window.addEventListener("touchmove", this, false);
     window.addEventListener("touchend", this, false);
 
     try {
@@ -151,21 +152,51 @@ var TouchModule = {
                 !SelectionHelperUI.isActive &&
                 !FindHelperUI.isActive) {
               setTimeout(function () {
                 SelectionHelperUI.attachEditSession(Browser.selectedTab.browser,
                                                     aEvent.clientX, aEvent.clientY);
               }, 50);
             }
             break;
+          case "keydown":
+            this._handleKeyDown(aEvent);
+            break;
         }
       }
     }
   },
 
+  _handleKeyDown: function _handleKeyDown(aEvent) {
+    const TABKEY = 9;
+    if (aEvent.keyCode == TABKEY && !InputSourceHelper.isPrecise) {
+      if (Util.isEditable(aEvent.target) &&
+          aEvent.target.selectionStart != aEvent.target.selectionEnd) {
+        SelectionHelperUI.closeEditSession(false);
+      }
+      setTimeout(function() {
+        let element = Browser.selectedBrowser.contentDocument.activeElement;
+        // We only want to attach monocles if we have an input, text area,
+        // there is selection, and the target element changed.
+        // Sometimes the target element won't change even though selection is
+        // cleared because of focus outside the browser.
+        if (Util.isEditable(element) &&
+            !SelectionHelperUI.isActive &&
+            element.selectionStart != element.selectionEnd &&
+            // not e10s friendly
+            aEvent.target != element) {
+              let rect = element.getBoundingClientRect();
+              SelectionHelperUI.attachEditSession(Browser.selectedBrowser,
+                                                  rect.left + rect.width / 2,
+                                                  rect.top + rect.height / 2);
+        }
+      }, 50);
+    }
+  },
+
   sample: function sample(aTimeStamp) {
     this._waitingForPaint = false;
   },
 
   /**
    * This gets invoked by the input handler if another module grabs.  We should
    * reset our state or something here.  This is probably doing the wrong thing
    * in its current form.
--- a/dom/network/src/TCPSocket.js
+++ b/dom/network/src/TCPSocket.js
@@ -29,16 +29,17 @@ const InputStreamPump = CC(
         '@mozilla.org/io/multiplex-input-stream;1', 'nsIMultiplexInputStream');
 const TCPServerSocket = CC(
         "@mozilla.org/tcp-server-socket;1", "nsITCPServerSocketInternal", "init");
 
 const kCONNECTING = 'connecting';
 const kOPEN = 'open';
 const kCLOSING = 'closing';
 const kCLOSED = 'closed';
+const kRESUME_ERROR = 'Calling resume() on a connection that was not suspended.';
 
 const BUFFER_SIZE = 65536;
 
 // XXX we have no TCPError implementation right now because it's really hard to
 // do on b2g18.  On mozilla-central we want a proper TCPError that ideally
 // sub-classes DOMError.  Bug 867872 has been filed to implement this and
 // contains a documented TCPError.webidl that maps all the error codes we use in
 // this file to slightly more readable explanations.
@@ -554,16 +555,18 @@ TCPSocket.prototype = {
   resume: function ts_resume() {
     if (this._inChild) {
       this._socketBridge.resume();
       return;
     }
 
     if (this._inputStreamPump) {
       this._inputStreamPump.resume();
+    } else if (this._suspendCount < 1) {
+      throw new Error(kRESUME_ERROR);
     } else {
       --this._suspendCount;
     }
   },
 
   _maybeReportErrorAndCloseIfOpen: function(status) {
     // If we're closed, we've already reported the error or just don't need to
     // report the error.
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -1436,19 +1436,21 @@ abstract public class BrowserApp extends
         animateHideHomePager();
         hideBrowserSearch();
 
         return true;
     }
 
     void filterEditingMode(String searchTerm, AutocompleteHandler handler) {
         if (TextUtils.isEmpty(searchTerm)) {
+            mHomePager.setVisibility(View.VISIBLE);
             hideBrowserSearch();
         } else {
             showBrowserSearch();
+            mHomePager.setVisibility(View.INVISIBLE);
             mBrowserSearch.filter(searchTerm, handler);
         }
     }
 
     private void showHomePager(HomePager.Page page) {
         showHomePagerWithAnimator(page, null);
     }
 
--- a/mobile/android/base/GeckoAccessibility.java
+++ b/mobile/android/base/GeckoAccessibility.java
@@ -296,17 +296,17 @@ public class GeckoAccessibility {
                                 onInitializeAccessibilityNodeInfo(host, info);
                                 info.addChild(host, VIRTUAL_CURSOR_PREVIOUS);
                                 info.addChild(host, VIRTUAL_CURSOR_POSITION);
                                 info.addChild(host, VIRTUAL_CURSOR_NEXT);
                                 break;
                             default:
                                 info.setParent(host);
                                 info.setSource(host, virtualDescendantId);
-                                info.setVisibleToUser(true);
+                                info.setVisibleToUser(host.isFocused());
                                 info.setPackageName(GeckoAppShell.getContext().getPackageName());
                                 info.setClassName(host.getClass().getName());
                                 info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
                                 info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
                                 info.addAction(AccessibilityNodeInfo.ACTION_CLICK);
                                 info.addAction(AccessibilityNodeInfo.ACTION_LONG_CLICK);
                                 info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
                                 info.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -222,16 +222,17 @@ FENNEC_JAVA_FILES = \
   home/BookmarkThumbnailView.java \
   home/BrowserSearch.java \
   home/HistoryPage.java \
   home/HomeCursorLoaderCallbacks.java \
   home/HomeFragment.java \
   home/HomeListView.java \
   home/HomePager.java \
   home/HomePagerTabStrip.java \
+  home/HomeBanner.java \
   home/FadedTextView.java \
   home/FaviconsLoader.java \
   home/LastTabsPage.java \
   home/MostRecentPage.java \
   home/MostVisitedPage.java \
   home/MultiTypeCursorAdapter.java \
   home/PinBookmarkDialog.java \
   home/ReadingListPage.java \
@@ -476,16 +477,17 @@ RES_LAYOUT = \
   res/layout/home_history_tabs_indicator.xml \
   res/layout/home_last_tabs_page.xml \
   res/layout/home_history_list.xml \
   res/layout/home_most_recent_page.xml \
   res/layout/home_most_visited_page.xml \
   res/layout/home_pager.xml \
   res/layout/home_reading_list_page.xml \
   res/layout/home_search_item_row.xml \
+  res/layout/home_banner.xml \
   res/layout/home_suggestion_prompt.xml \
   res/layout/web_app.xml \
   res/layout/launch_app_list.xml \
   res/layout/launch_app_listitem.xml \
   res/layout/menu_action_bar.xml \
   res/layout/menu_item_action_view.xml \
   res/layout/menu_popup.xml \
   res/layout/notification_icon_text.xml \
@@ -1108,16 +1110,17 @@ RES_DRAWABLE += \
   res/drawable/bookmark_folder.xml                    \
   res/drawable/divider_horizontal.xml                 \
   res/drawable/divider_vertical.xml                   \
   res/drawable/favicon_bg.xml                         \
   res/drawable/handle_end_level.xml                   \
   res/drawable/handle_start_level.xml                 \
   res/drawable/home_history_tabs_indicator.xml        \
   res/drawable/home_page_title_background.xml         \
+  res/drawable/home_banner.xml                        \
   res/drawable/ic_menu_back.xml                       \
   res/drawable/ic_menu_desktop_mode_off.xml           \
   res/drawable/ic_menu_desktop_mode_on.xml            \
   res/drawable/ic_menu_quit.xml                       \
   res/drawable/menu_item_state.xml                    \
   res/drawable/menu_level.xml                         \
   res/drawable/remote_tabs_child_divider.xml          \
   res/drawable/shaped_button.xml                      \
--- a/mobile/android/base/home/BookmarksPage.java
+++ b/mobile/android/base/home/BookmarksPage.java
@@ -3,16 +3,19 @@
  * 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.home;
 
 import org.mozilla.gecko.Favicons;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Tabs;
+import org.mozilla.gecko.animation.PropertyAnimator;
+import org.mozilla.gecko.animation.PropertyAnimator.Property;
+import org.mozilla.gecko.animation.ViewHelper;
 import org.mozilla.gecko.db.BrowserContract.Bookmarks;
 import org.mozilla.gecko.db.BrowserContract.Thumbnails;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.BrowserDB.URLColumns;
 import org.mozilla.gecko.gfx.BitmapUtils;
 import org.mozilla.gecko.home.BookmarksListAdapter.OnRefreshFolderListener;
 import org.mozilla.gecko.home.HomeListView.HomeContextMenuInfo;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
@@ -36,17 +39,19 @@ import android.support.v4.content.AsyncT
 import android.support.v4.content.Loader;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.ContextMenu;
 import android.view.ContextMenu.ContextMenuInfo;
 import android.view.LayoutInflater;
 import android.view.MenuInflater;
 import android.view.MenuItem;
+import android.view.MotionEvent;
 import android.view.View;
+import android.view.View.OnTouchListener;
 import android.view.ViewGroup;
 import android.widget.Toast;
 
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Map;
 
 /**
@@ -71,65 +76,84 @@ public class BookmarksPage extends HomeF
     private static final String THUMBNAILS_URLS_KEY = "urls";
 
     // List of bookmarks.
     private BookmarksListView mList;
 
     // Grid of top bookmarks.
     private TopBookmarksView mTopBookmarks;
 
+    // Banner to show snippets.
+    private HomeBanner mBanner;
+
     // Adapter for list of bookmarks.
     private BookmarksListAdapter mListAdapter;
 
     // Adapter for grid of bookmarks.
     private TopBookmarksAdapter mTopBookmarksAdapter;
 
     // Callback for cursor loaders.
     private CursorLoaderCallbacks mLoaderCallbacks;
 
     // Callback for thumbnail loader.
     private ThumbnailsLoaderCallbacks mThumbnailsLoaderCallbacks;
 
     // Listener for pinning bookmarks.
     private PinBookmarkListener mPinBookmarkListener;
 
+    // Raw Y value of the last event that happened on the list view.
+    private float mListTouchY = -1;
+
+    // Scrolling direction of the banner.
+    private boolean mSnapBannerToTop;
+
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
-        BookmarksListView list = (BookmarksListView) inflater.inflate(R.layout.home_bookmarks_page, container, false);
+        final View view = inflater.inflate(R.layout.home_bookmarks_page, container, false);
+
+        mList = (BookmarksListView) view.findViewById(R.id.bookmarks_list);
 
         mTopBookmarks = new TopBookmarksView(getActivity());
-        list.addHeaderView(mTopBookmarks);
+        mList.addHeaderView(mTopBookmarks);
 
-        return list;
+        return view;
     }
 
     @Override
     public void onViewCreated(View view, Bundle savedInstanceState) {
         super.onViewCreated(view, savedInstanceState);
 
         OnUrlOpenListener listener = null;
         try {
             listener = (OnUrlOpenListener) getActivity();
         } catch (ClassCastException e) {
             throw new ClassCastException(getActivity().toString()
                     + " must implement HomePager.OnUrlOpenListener");
         }
 
         mPinBookmarkListener = new PinBookmarkListener();
 
-        mList = (BookmarksListView) view.findViewById(R.id.bookmarks_list);
         mList.setTag(HomePager.LIST_TAG_BOOKMARKS);
         mList.setOnUrlOpenListener(listener);
         mList.setHeaderDividersEnabled(false);
 
         mTopBookmarks.setOnUrlOpenListener(listener);
         mTopBookmarks.setOnPinBookmarkListener(mPinBookmarkListener);
 
         registerForContextMenu(mList);
         registerForContextMenu(mTopBookmarks);
+
+        mBanner = (HomeBanner) view.findViewById(R.id.home_banner);
+        mList.setOnTouchListener(new OnTouchListener() {
+            @Override
+            public boolean onTouch(View v, MotionEvent event) {
+                BookmarksPage.this.handleListTouchEvent(event);
+                return false;
+            }
+        });
     }
 
     @Override
     public void onActivityCreated(Bundle savedInstanceState) {
         super.onActivityCreated(savedInstanceState);
 
         final Activity activity = getActivity();
 
@@ -185,16 +209,70 @@ public class BookmarksPage extends HomeF
         if (isVisible()) {
             getFragmentManager().beginTransaction()
                                 .detach(this)
                                 .attach(this)
                                 .commitAllowingStateLoss();
         }
     }
 
+    private void handleListTouchEvent(MotionEvent event) {
+        // Ignore the event if the banner is hidden for this session.
+        if (mBanner.isDismissed()) {
+            return;
+        }
+
+        switch (event.getActionMasked()) {
+            case MotionEvent.ACTION_DOWN: {
+                mListTouchY = event.getRawY();
+                break;
+             }
+
+            case MotionEvent.ACTION_MOVE: {
+                // There is a chance that we won't receive ACTION_DOWN, if the touch event
+                // actually started on the Grid instead of the List. Treat this as first event.
+                if (mListTouchY == -1) {
+                    mListTouchY = event.getRawY();
+                    return;
+                }
+
+                final float curY = event.getRawY();
+                final float delta = mListTouchY - curY;
+                mSnapBannerToTop = (delta > 0.0f) ? false : true;
+
+                final float height = mBanner.getHeight();
+                float newTranslationY = ViewHelper.getTranslationY(mBanner) + delta;
+
+                // Clamp the values to be between 0 and height.
+                if (newTranslationY < 0.0f) {
+                    newTranslationY = 0.0f;
+                } else if (newTranslationY > height) {
+                    newTranslationY = height;
+                }
+
+                ViewHelper.setTranslationY(mBanner, newTranslationY);
+                mListTouchY = curY;
+                break;
+            }
+
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL: {
+                mListTouchY = -1;
+                final float y = ViewHelper.getTranslationY(mBanner);
+                final float height = mBanner.getHeight();
+                if (y > 0.0f && y < height) {
+                    final PropertyAnimator animator = new PropertyAnimator(100);
+                    animator.attach(mBanner, Property.TRANSLATION_Y, mSnapBannerToTop ? 0 : height);
+                    animator.start();
+                }
+                break;
+            }
+        }
+    }
+
     @Override
     public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
         if (menuInfo == null) {
             return;
         }
 
         // HomeFragment will handle the default case.
         if (menuInfo instanceof HomeContextMenuInfo) {
--- a/mobile/android/base/home/BrowserSearch.java
+++ b/mobile/android/base/home/BrowserSearch.java
@@ -877,12 +877,12 @@ public class BrowserSearch extends HomeF
 
         @Override
         public boolean onInterceptTouchEvent(MotionEvent event) {
             if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
                 // Dismiss the soft keyboard.
                 requestFocus();
             }
 
-            return false;
+            return super.onInterceptTouchEvent(event);
         }
     }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/home/HomeBanner.java
@@ -0,0 +1,51 @@
+/* -*- 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.home;
+
+import org.mozilla.gecko.R;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+
+public class HomeBanner extends LinearLayout {
+
+    public HomeBanner(Context context) {
+        this(context, null);
+    }
+
+    public HomeBanner(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        LayoutInflater.from(context).inflate(R.layout.home_banner, this);
+    }
+
+    @Override
+    public void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        // Tapping on the close button will ensure that the banner is never
+        // showed again on this session.
+        final ImageButton closeButton = (ImageButton) findViewById(R.id.close);
+
+        // The drawable should have 50% opacity.
+        closeButton.getDrawable().setAlpha(127);
+
+        closeButton.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View view) {
+                HomeBanner.this.setVisibility(View.GONE);
+            }
+        });
+    }
+
+    public boolean isDismissed() {
+        return (getVisibility() == View.GONE);
+    }
+}
--- a/mobile/android/base/home/HomeListView.java
+++ b/mobile/android/base/home/HomeListView.java
@@ -94,16 +94,30 @@ public class HomeListView extends ListVi
         }
     }
 
     @Override
     public ContextMenuInfo getContextMenuInfo() {
         return mContextMenuInfo;
     }
 
+    @Override
+    public void setOnItemClickListener(final AdapterView.OnItemClickListener listener) {
+        super.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+            @Override
+            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+                if (mShowTopDivider) {
+                    position--;
+                }
+
+                listener.onItemClick(parent, view, position, id);
+            }
+        });
+    }
+
     public OnUrlOpenListener getOnUrlOpenListener() {
         return mUrlOpenListener;
     }
 
     public void setOnUrlOpenListener(OnUrlOpenListener listener) {
         mUrlOpenListener = listener;
     }
 
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable/home_banner.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <item android:state_pressed="true">
+        <layer-list>
+            <item android:left="-2dp"
+                  android:right="-2dp"
+                  android:bottom="-2dp">
+
+                <shape android:shape="rectangle" >
+                    <stroke android:width="2dp"
+                            android:color="#FFE0E4E7" />
+                    <solid android:color="#FFC5D0DA" />
+                </shape>
+            </item>
+        </layer-list>
+    </item>
+
+    <item>
+        <layer-list>
+            <item android:left="-2dp"
+                  android:right="-2dp"
+                  android:bottom="-2dp">
+
+                <shape android:shape="rectangle" >
+                    <stroke android:width="2dp"
+                            android:color="#FFE0E4E7" />
+                    <solid android:color="@color/background_light" />
+                </shape>
+            </item>
+        </layer-list>
+    </item>
+
+</selector>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/home_banner.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+
+     <ImageView android:id="@+id/icon"
+                android:layout_width="48dip"
+                android:layout_height="48dip"
+                android:layout_marginLeft="10dp"
+                android:layout_marginRight="10dp"
+                android:scaleType="centerInside"/>
+
+     <TextView android:id="@+id/text"
+               android:layout_width="0dip"
+               android:layout_height="fill_parent"
+               android:layout_weight="1"
+               android:paddingTop="7dp"
+               android:paddingBottom="7dp"
+               android:textAppearance="@style/TextAppearance.Widget.HomeBanner"
+               android:layout_gravity="bottom"
+               android:singleLine="false"
+               android:maxLines="3"
+               android:ellipsize="end"
+               android:gravity="center_vertical"/>
+
+    <ImageButton android:id="@+id/close"
+                 android:layout_width="34dip"
+                 android:layout_height="fill_parent"
+                 android:background="@drawable/home_banner"
+                 android:scaleType="center"
+                 android:contentDescription="@string/close_tab"
+                 android:src="@drawable/tab_close"/>
+
+</merge>
--- a/mobile/android/base/resources/layout/home_bookmarks_page.xml
+++ b/mobile/android/base/resources/layout/home_bookmarks_page.xml
@@ -1,9 +1,26 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- 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/. -->
 
-<org.mozilla.gecko.home.BookmarksListView xmlns:android="http://schemas.android.com/apk/res/android"
-        android:id="@+id/bookmarks_list"
-        android:layout_width="fill_parent"
-        android:layout_height="fill_parent"/>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+             android:layout_width="fill_parent"
+             android:layout_height="fill_parent">
+
+    <org.mozilla.gecko.home.BookmarksListView
+            android:id="@+id/bookmarks_list"
+            android:layout_width="fill_parent"
+            android:layout_height="fill_parent"/>
+
+    <org.mozilla.gecko.home.HomeBanner android:id="@+id/home_banner"
+                                       style="@style/Widget.HomeBanner"
+                                       android:layout_width="fill_parent"
+                                       android:layout_height="@dimen/home_banner_height"
+                                       android:background="@drawable/home_banner"
+                                       android:layout_gravity="bottom"
+                                       android:gravity="center_vertical"
+                                       android:visibility="gone"
+                                       android:clickable="true"
+                                       android:focusable="true"/>
+
+</FrameLayout>
--- a/mobile/android/base/resources/values-large-land-v11/styles.xml
+++ b/mobile/android/base/resources/values-large-land-v11/styles.xml
@@ -45,9 +45,20 @@
 
     <style name="Widget.Home.HistoryTabWidget">
         <item name="android:showDividers">beginning|middle|end</item>
         <item name="android:dividerPadding">0dp</item>
         <item name="android:paddingLeft">100dp</item>
         <item name="android:paddingTop">30dp</item>
     </style>
 
+    <!--
+         The content of the banner should align with the Grid/List views
+         in BookmarksPage. BookmarksListView has a 120dp padding and
+         the TwoLinePageRows have a 50dp padding. Hence HomeBanner should
+         have 170dp padding.
+    -->
+    <style name="Widget.HomeBanner">
+        <item name="android:paddingLeft">170dp</item>
+        <item name="android:paddingRight">170dp</item>
+    </style>
+
 </resources>
--- a/mobile/android/base/resources/values-large-v11/styles.xml
+++ b/mobile/android/base/resources/values-large-v11/styles.xml
@@ -55,9 +55,14 @@
         <item name="android:verticalSpacing">10dp</item>
     </style>
 
     <style name="Widget.ReadingListView" parent="Widget.BookmarksListView">
         <item name="android:paddingTop">30dp</item>
         <item name="topDivider">true</item>
     </style>
 
+    <style name="Widget.HomeBanner">
+        <item name="android:paddingLeft">32dp</item>
+        <item name="android:paddingRight">32dp</item>
+    </style>
+
 </resources>
--- a/mobile/android/base/resources/values/dimens.xml
+++ b/mobile/android/base/resources/values/dimens.xml
@@ -82,9 +82,12 @@
     <!-- We need to maintain height for the tab widget on History Page
          since android does not add footer/header divider height to its
          calculation for wrap_content in LinearLayout.
          50dp * 3 Views + 30dp padding + 4dp dividers-->
     <dimen name="history_tab_widget_height">184dp</dimen>
 
     <!-- PageActionButtons dimensions -->
     <dimen name="page_action_button_width">32dp</dimen>
+
+    <!-- Banner -->
+    <dimen name="home_banner_height">72dp</dimen>
 </resources>
--- a/mobile/android/base/resources/values/styles.xml
+++ b/mobile/android/base/resources/values/styles.xml
@@ -155,16 +155,18 @@
     </style>
 
     <style name="Widget.HomeListView" parent="Widget.ListView">
         <item name="android:divider">#E7ECF0</item>
     </style>
 
     <style name="Widget.ReadingListView" parent="Widget.BookmarksListView"/>
 
+    <style name="Widget.HomeBanner"/>
+
     <style name="Widget.Home" />
 
     <style name="Widget.Home.HeaderItem">
         <item name="android:layout_width">fill_parent</item>
         <item name="android:layout_height">32dp</item>
         <item name="android:textAppearance">@style/TextAppearance.Widget.Home.Header</item>
         <item name="android:background">#fff5f7f9</item>
         <item name="android:focusable">false</item>
@@ -313,16 +315,20 @@
     </style>
 
     <style name="TextAppearance.Widget.Home.PageTitle" parent="TextAppearance.Medium" />
 
     <style name="TextAppearance.Widget.Home.PageAction" parent="TextAppearance.Small">
         <item name="android:textColor">#00ACFF</item>
     </style>
 
+    <style name="TextAppearance.Widget.HomeBanner" parent="TextAppearance.Small">
+        <item name="android:textColor">?android:attr/textColorHint</item>
+    </style>
+
     <!-- BrowserToolbar -->
     <style name="BrowserToolbar">
         <item name="android:layout_width">fill_parent</item>
         <item name="android:layout_height">@dimen/browser_toolbar_height</item>
         <item name="android:orientation">horizontal</item>
     </style>
 
     <style name="UrlBar.ImageButton.TabCount">
--- a/testing/marionette/client/docs/index.rst
+++ b/testing/marionette/client/docs/index.rst
@@ -67,32 +67,37 @@ Script Execution
 
 Debugging
 `````````
 .. autoattribute:: Marionette.page_source
 .. automethod:: Marionette.log
 .. automethod:: Marionette.get_logs
 .. automethod:: Marionette.screenshot
 
-HTMLElement Objects
--------------------
+Querying and Modifying Document Content
+---------------------------------------
 .. autoclass:: HTMLElement
 
 .. autoattribute:: HTMLElement.text
 .. autoattribute:: HTMLElement.location
 .. autoattribute:: HTMLElement.size
 .. autoattribute:: HTMLElement.tag_name
 .. automethod:: HTMLElement.send_keys
 .. automethod:: HTMLElement.clear
 .. automethod:: HTMLElement.click
 .. automethod:: HTMLElement.is_selected
 .. automethod:: HTMLElement.is_enabled
 .. automethod:: HTMLElement.is_displayed
 .. automethod:: HTMLElement.value_of_css_property
 
+.. autoclass:: DateTimeValue
+
+.. autoattribute:: DateTimeValue.date
+.. autoattribute:: DateTimeValue.time
+
 Action Objects
 --------------
 
 Action Sequences
 ````````````````
 .. autoclass:: Actions
 
 .. automethod:: Actions.press
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/date_time_value.py
@@ -0,0 +1,49 @@
+# 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/.
+
+class DateTimeValue(object):
+    """
+    Interface for setting the value of HTML5 "date" and "time" input elements.
+
+    Simple usage example:
+
+    ::
+
+        element = marionette.find_element("id", "date-test")
+        dt_value = DateTimeValue(element)
+        dt_value.date = datetime(1998, 6, 2)
+
+    """
+
+    def __init__(self, element):
+        self.element = element
+
+    @property
+    def date(self):
+        """
+        Retrieve the element's string value
+        """
+        return self.element.get_attribute('value')
+
+    # As per the W3C "date" element specification
+    # (http://dev.w3.org/html5/markup/input.date.html), this value is formatted
+    # according to RFC 3339: http://tools.ietf.org/html/rfc3339#section-5.6
+    @date.setter
+    def date(self, date_value):
+        self.element.send_keys(date_value.strftime('%Y-%m-%d'))
+
+    @property
+    def time(self):
+        """
+        Retrieve the element's string value
+        """
+        return self.element.get_attribute('value')
+
+    # As per the W3C "time" element specification
+    # (http://dev.w3.org/html5/markup/input.time.html), this value is formatted
+    # according to RFC 3339: http://tools.ietf.org/html/rfc3339#section-5.6
+    @time.setter
+    def time(self, time_value):
+        self.element.send_keys(time_value.strftime('%H:%M:%S'))
+
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/tests/unit/test_date_time_value.py
@@ -0,0 +1,27 @@
+# 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/.
+
+from marionette_test import MarionetteTestCase
+from datetime import datetime
+from date_time_value import DateTimeValue
+
+class TestDateTime(MarionetteTestCase):
+
+    def test_set_date(self):
+        test_html = self.marionette.absolute_url("datetimePage.html")
+        self.marionette.navigate(test_html)
+
+        element = self.marionette.find_element("id", "date-test")
+        dt_value = DateTimeValue(element)
+        dt_value.date = datetime(1998, 6, 2)
+        self.assertEqual(element.get_attribute("value"), "1998-06-02")
+
+    def test_set_time(self):
+        test_html = self.marionette.absolute_url("datetimePage.html")
+        self.marionette.navigate(test_html)
+
+        element = self.marionette.find_element("id", "time-test")
+        dt_value = DateTimeValue(element)
+        dt_value.time = datetime(1998, 11, 19, 9, 8, 7)
+        self.assertEqual(element.get_attribute("value"), "09:08:07")
--- a/testing/marionette/client/marionette/tests/unit/unit-tests.ini
+++ b/testing/marionette/client/marionette/tests/unit/unit-tests.ini
@@ -86,8 +86,9 @@ b2g = false
 [test_cookies.py]
 b2g = false
 [test_window_title.py]
 b2g = false
 [test_window_type.py]
 b2g = false
 
 [test_implicit_waits.py]
+[test_date_time_value.py]
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/www/datetimePage.html
@@ -0,0 +1,15 @@
+<?xml version="1.0"?>
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+  <!-- 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/. -->
+<head>
+    <title>Testing date and time inputs</title>
+</head>
+<body>
+    <form>
+        <input id="date-test" type="date"/>
+        <input id="time-test" type="time"/>
+    </form>
+</body>
+</html>
--- a/testing/xpcshell/runxpcshelltests.py
+++ b/testing/xpcshell/runxpcshelltests.py
@@ -81,24 +81,26 @@ def parse_json(j):
 
 """ Control-C handling """
 gotSIGINT = False
 def markGotSIGINT(signum, stackFrame):
     global gotSIGINT
     gotSIGINT = True
 
 class XPCShellTestThread(Thread):
-    def __init__(self, test_object, event, cleanup_dir_list, tests_root_dir=None,
-            app_dir_key=None, interactive=False, verbose=False, pStdout=None,
-            pStderr=None, keep_going=False, log=None, **kwargs):
+    def __init__(self, test_object, event, cleanup_dir_list, retry=True,
+            tests_root_dir=None, app_dir_key=None, interactive=False,
+            verbose=False, pStdout=None, pStderr=None, keep_going=False,
+            log=None, **kwargs):
         Thread.__init__(self)
         self.daemon = True
 
         self.test_object = test_object
         self.cleanup_dir_list = cleanup_dir_list
+        self.retry = retry
 
         self.appPath = kwargs.get('appPath')
         self.xrePath = kwargs.get('xrePath')
         self.testingModulesDir = kwargs.get('testingModulesDir')
         self.debuggerInfo = kwargs.get('debuggerInfo')
         self.pluginsPath = kwargs.get('pluginsPath')
         self.httpdManifest = kwargs.get('httpdManifest')
         self.httpdJSPath = kwargs.get('httpdJSPath')
@@ -138,16 +140,19 @@ class XPCShellTestThread(Thread):
         try:
             self.run_test()
         except Exception as e:
             self.exception = e
             self.traceback = traceback.format_exc()
         else:
             self.exception = None
             self.traceback = None
+        if self.retry:
+            self.log.info("TEST-INFO | %s | Test failed or timed out, will retry."
+                          % self.test_object['name'])
         self.event.set()
 
     def kill(self, proc):
         """
           Simple wrapper to kill a process.
           On a remote system, this is overloaded to handle remote process communication.
         """
         return proc.kill()
@@ -206,17 +211,18 @@ class XPCShellTestThread(Thread):
         self.log.info("TEST-INFO | %s | current directory: %r" % (name, testdir))
         # Show only those environment variables that are changed from
         # the ambient environment.
         changedEnv = (set("%s=%s" % i for i in self.env.iteritems())
                       - set("%s=%s" % i for i in os.environ.iteritems()))
         self.log.info("TEST-INFO | %s | environment: %s" % (name, list(changedEnv)))
 
     def testTimeout(self, test_file, processPID):
-        self.log.error("TEST-UNEXPECTED-FAIL | %s | Test timed out" % test_file)
+        if not self.retry:
+            self.log.error("TEST-UNEXPECTED-FAIL | %s | Test timed out" % test_file)
         Automation().killAndGetStackNoScreenshot(processPID, self.appPath, self.debuggerInfo)
 
     def buildCmdTestFile(self, name):
         """
           Build the command line arguments for the test file.
           On a remote system, this may be overloaded to use a remote path structure.
         """
         return ['-e', 'const _TEST_FILE = ["%s"];' %
@@ -346,16 +352,19 @@ class XPCShellTestThread(Thread):
         if not self.pluginsPath:
             self.pluginsPath = os.path.join(self.appPath, 'plugins')
 
         self.pluginsDir = self.setupPluginsDir()
         if self.pluginsDir:
             self.xpcsCmd.extend(['-p', self.pluginsDir])
 
     def cleanupDir(self, directory, name, xunit_result):
+        if not os.path.exists(directory):
+            return
+
         TRY_LIMIT = 25 # up to TRY_LIMIT attempts (one every second), because
                        # the Windows filesystem is slow to react to the changes
         try_count = 0
         while try_count < TRY_LIMIT:
             try:
                 self.removeDir(directory)
             except OSError:
                 self.log.info("TEST-INFO | Failed to remove directory: %s. Waiting." % directory)
@@ -369,30 +378,44 @@ class XPCShellTestThread(Thread):
 
         # we try again later at the end of the run for plugin dirs (because windows!)
         if directory == self.pluginsDir:
             self.cleanup_dir_list.append(directory)
             return
 
         # we couldn't clean up the directory, and it's not the plugins dir,
         # which means that something wrong has probably happened
+        if self.retry:
+            return
+
         with LOG_MUTEX:
             message = "TEST-UNEXPECTED-FAIL | %s | Failed to clean up directory: %s" % (name, sys.exc_info()[1])
             self.log.error(message)
             self.log_output(self.output_lines)
             self.log_output(traceback.format_exc())
 
         self.failCount += 1
         xunit_result["passed"] = False
         xunit_result["failure"] = {
             "type": "TEST-UNEXPECTED-FAIL",
             "message": message,
-            "text": "%s\n%s" % (stdout, traceback.format_exc())
+            "text": "%s\n%s" % ("\n".join(self.output_lines), traceback.format_exc())
         }
 
+    def clean_temp_dirs(self, name, stdout):
+        # We don't want to delete the profile when running check-interactive
+        # or check-one.
+        if self.profileDir and not self.interactive and not self.singleFile:
+            self.cleanupDir(self.profileDir, name, self.xunit_result)
+
+        self.cleanupDir(self.tempDir, name, self.xunit_result)
+
+        if self.pluginsDir:
+            self.cleanupDir(self.pluginsDir, name, self.xunit_result)
+
     def append_message_from_line(self, line):
         """Given a line of raw output, convert to message and append to
         output buffer."""
         if isinstance(line, basestring):
             # This function has received unstructured output.
             if line:
                 self.output_lines.append(line)
                 if 'TEST-UNEXPECTED-' in line:
@@ -483,16 +506,17 @@ class XPCShellTestThread(Thread):
             self.xunit_result['classname'] = relpath.replace('/', '.').replace('\\', '.')
 
         # Check for skipped tests
         if 'disabled' in self.test_object:
             self.log.info('TEST-INFO | skipping %s | %s' %
                 (name, self.test_object['disabled']))
 
             self.xunit_result['skipped'] = True
+            self.retry = False
 
             self.keep_going = True
             return
 
         # Check for known-fail tests
         expected = self.test_object['expected'] == 'pass'
 
         # By default self.appPath will equal the gre dir. If specified in the
@@ -554,16 +578,20 @@ class XPCShellTestThread(Thread):
             if testTimer:
                 testTimer.cancel()
 
             self.parse_output(stdout)
             result = not (self.has_failure_output or
                           (self.getReturnCode(proc) != 0))
 
             if result != expected:
+                if self.retry:
+                    self.clean_temp_dirs(name, stdout)
+                    return
+
                 failureType = "TEST-UNEXPECTED-%s" % ("FAIL" if expected else "PASS")
                 message = "%s | %s | test failed (with xpcshell return code: %d), see following log:" % (
                               failureType, name, self.getReturnCode(proc))
 
                 with LOG_MUTEX:
                     self.log.error(message)
                     self.log_output(self.output_lines)
 
@@ -588,64 +616,65 @@ class XPCShellTestThread(Thread):
                 self.xunit_result["time"] = now - startTime
 
                 with LOG_MUTEX:
                     self.log.info("TEST-%s | %s | test passed (time: %.3fms)" % ("PASS" if expected else "KNOWN-FAIL", name, timeTaken))
                     if self.verbose:
                         self.log_output(self.output_lines)
 
                 self.xunit_result["passed"] = True
+                self.retry = False
 
                 if expected:
                     self.passCount = 1
                 else:
                     self.todoCount = 1
                     self.xunit_result["todo"] = True
 
             if mozcrash.check_for_crashes(self.tempDir, self.symbolsPath, test_name=name):
+                if self.retry:
+                    self.clean_temp_dirs(name, stdout)
+                    return
+
                 message = "PROCESS-CRASH | %s | application crashed" % name
                 self.failCount = 1
                 self.xunit_result["passed"] = False
                 self.xunit_result["failure"] = {
                     "type": "PROCESS-CRASH",
                     "message": message,
                     "text": stdout
                 }
 
             if self.logfiles and stdout:
                 self.createLogFile(name, stdout)
 
         finally:
             # We can sometimes get here before the process has terminated, which would
             # cause removeDir() to fail - so check for the process & kill it it needed.
             if proc and self.poll(proc) is None:
+                self.kill(proc)
+
+                if self.retry:
+                    self.clean_temp_dirs(name, stdout)
+                    return
+
                 with LOG_MUTEX:
                     message = "TEST-UNEXPECTED-FAIL | %s | Process still running after test!" % name
                     self.log.error(message)
                     self.log_output(self.output_lines)
 
                 self.failCount = 1
                 self.xunit_result["passed"] = False
                 self.xunit_result["failure"] = {
                   "type": "TEST-UNEXPECTED-FAIL",
                   "message": message,
                   "text": stdout
                 }
-                self.kill(proc)
 
-
-            # We don't want to delete the profile when running check-interactive
-            # or check-one.
-            if self.profileDir and not self.interactive and not self.singleFile:
-                self.cleanupDir(self.profileDir, name, self.xunit_result)
-
-            self.cleanupDir(self.tempDir, name, self.xunit_result)
-
-            if self.pluginsDir:
-                self.cleanupDir(self.pluginsDir, name, self.xunit_result)
+            self.clean_temp_dirs(name, stdout)
 
         if gotSIGINT:
             self.xunit_result["passed"] = False
             self.xunit_result["time"] = "0.0"
             self.xunit_result["failure"] = {
                 "type": "SIGINT",
                 "message": "Received SIGINT",
                 "text": "Received SIGINT (control-C) during test execution."
@@ -1212,16 +1241,17 @@ class XPCShellTests(object):
         if self.singleFile:
             self.sequential = True
 
         if shuffle:
             random.shuffle(self.alltests)
 
         self.xunitResults = []
         self.cleanup_dir_list = []
+        self.try_again_list = []
 
         kwargs = {
             'appPath': self.appPath,
             'xrePath': self.xrePath,
             'testingModulesDir': self.testingModulesDir,
             'debuggerInfo': self.debuggerInfo,
             'pluginsPath': self.pluginsPath,
             'httpdManifest': self.httpdManifest,
@@ -1306,16 +1336,21 @@ class XPCShellTests(object):
             self.event.clear()
 
             # find what tests are done (might be more than 1)
             done_tests = set()
             for test in running_tests:
                 if not test.is_alive():
                     done_tests.add(test)
                     test.join()
+                    # if the test had trouble, we will try running it again
+                    # at the end of the run
+                    if test.retry:
+                        self.try_again_list.append(test.test_object)
+                        continue
                     # did the test encounter any exception?
                     if test.exception:
                         exceptions.append(test.exception)
                         tracebacks.append(test.traceback)
                         # we won't add any more tests, will just wait for
                         # the currently running ones to finish
                         keep_going = False
                     keep_going = keep_going and test.keep_going
@@ -1326,26 +1361,48 @@ class XPCShellTests(object):
 
         if keep_going:
             # run the other tests sequentially
             for test in sequential_tests:
                 if not keep_going:
                     self.log.error("TEST-UNEXPECTED-FAIL | Received SIGINT (control-C), so stopped run. " \
                                    "(Use --keep-going to keep running tests after killing one with SIGINT)")
                     break
+                # we don't want to retry these tests
+                test.retry = False
                 test.start()
                 test.join()
                 self.addTestResults(test)
                 # did the test encounter any exception?
                 if test.exception:
                     exceptions.append(test.exception)
                     tracebacks.append(test.traceback)
                     break
                 keep_going = test.keep_going
 
+        # retry tests that failed when run in parallel
+        if self.try_again_list:
+            self.log.info("Retrying tests that failed when run in parallel.")
+        for test_object in self.try_again_list:
+            test = testClass(test_object, self.event, self.cleanup_dir_list,
+                    retry=False, tests_root_dir=testsRootDir,
+                    app_dir_key=appDirKey, interactive=interactive,
+                    verbose=verbose, pStdout=pStdout, pStderr=pStderr,
+                    keep_going=keepGoing, log=self.log, mobileArgs=mobileArgs,
+                    **kwargs)
+            test.start()
+            test.join()
+            self.addTestResults(test)
+            # did the test encounter any exception?
+            if test.exception:
+                exceptions.append(test.exception)
+                tracebacks.append(test.traceback)
+                break
+            keep_going = test.keep_going
+
         # restore default SIGINT behaviour
         signal.signal(signal.SIGINT, signal.SIG_DFL)
 
         self.shutdownNode()
         # Clean up any slacker directories that might be lying around (Windows).
         # Some might fail because of windows taking too long to unlock them.
         # We don't do anything if this fails because the test slaves will have
         # their $TEMP dirs cleaned up on reboot anyway.
@@ -1364,16 +1421,17 @@ class XPCShellTests(object):
         if self.testCount == 0:
             self.log.error("TEST-UNEXPECTED-FAIL | runxpcshelltests.py | No tests run. Did you pass an invalid --test-path?")
             self.failCount = 1
 
         self.log.info("INFO | Result summary:")
         self.log.info("INFO | Passed: %d" % self.passCount)
         self.log.info("INFO | Failed: %d" % self.failCount)
         self.log.info("INFO | Todo: %d" % self.todoCount)
+        self.log.info("INFO | Retried: %d" % len(self.try_again_list))
 
         if autolog:
             self.post_to_autolog(self.xunitResults, xunitName)
 
         if xunitFilename is not None:
             self.writeXunitResults(filename=xunitFilename, results=self.xunitResults,
                                    name=xunitName)
 
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -2558,16 +2558,26 @@
     "description": "Session restore: Time spent blocking the main thread while restoring a window state (ms)"
   },
   "FX_SESSION_RESTORE_SESSION_LENGTH": {
     "kind": "exponential",
     "high": "365",
     "n_buckets": 15,
     "description": "Session restore: Days elapsed since the session was first started"
   },
+  "FX_SESSION_RESTORE_TABSTATECACHE_HIT_RATE": {
+     "kind": "enumerated",
+     "n_values": 101,
+     "description": "Session restore: Percentage of tab state cache hits in all tab state cache accesses"
+  },
+  "FX_SESSION_RESTORE_TABSTATECACHE_CLEAR_RATIO": {
+     "kind": "enumerated",
+     "n_values": 101,
+     "description": "Session restore: Number of times the tab state cache has been cleared during a session divided by number of total accesses during the session (percentage)"
+  },
   "INNERWINDOWS_WITH_MUTATION_LISTENERS": {
     "kind": "boolean",
     "description": "Deleted or to-be-reused innerwindow which has had mutation event listeners."
   },
   "CHARSET_OVERRIDE_SITUATION": {
     "kind": "enumerated",
     "n_values": 7,
     "description": "Labeling status of top-level page when overriding charset (unlabeled file URL without detection, unlabeled non-file URL without detection, unlabeled file URL with detection, unlabeled non-file URL with detection, labeled, already overridden, bug)"
--- a/toolkit/components/thumbnails/content/backgroundPageThumbsContent.js
+++ b/toolkit/components/thumbnails/content/backgroundPageThumbsContent.js
@@ -2,16 +2,18 @@
  * 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/. */
 
 (function () { // bug 673569 workaround :(
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 Cu.import("resource://gre/modules/PageThumbs.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
 
 const backgroundPageThumbsContent = {
 
   init: function () {
     // Arrange to prevent (most) popup dialogs for this window - popups done
     // in the parent (eg, auth) aren't prevented, but alert() etc are.
     let dwu = content.
                 QueryInterface(Ci.nsIInterfaceRequestor).
@@ -21,69 +23,86 @@ const backgroundPageThumbsContent = {
     // We want a low network priority for this service - lower than b/g tabs
     // etc - so set it to the lowest priority available.
     this._webNav.QueryInterface(Ci.nsIDocumentLoader).
       loadGroup.QueryInterface(Ci.nsISupportsPriority).
       priority = Ci.nsISupportsPriority.PRIORITY_LOWEST;
 
     docShell.allowMedia = false;
     docShell.allowPlugins = false;
+    docShell.allowContentRetargeting = false;
 
     addMessageListener("BackgroundPageThumbs:capture",
                        this._onCapture.bind(this));
+    docShell.
+      QueryInterface(Ci.nsIInterfaceRequestor).
+      getInterface(Ci.nsIWebProgress).
+      addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_WINDOW);
   },
 
   get _webNav() {
     return docShell.QueryInterface(Ci.nsIWebNavigation);
   },
 
   _onCapture: function (msg) {
-    this._webNav.stop(Ci.nsIWebNavigation.STOP_NETWORK);
-    if (this._onLoad)
-      removeEventListener("load", this._onLoad, true);
+    this._webNav.loadURI(msg.json.url,
+                         Ci.nsIWebNavigation.LOAD_FLAGS_STOP_CONTENT,
+                         null, null, null);
+    // If a page was already loading, onStateChange is synchronously called at
+    // this point by loadURI.
+    this._requestID = msg.json.id;
+    this._requestDate = new Date();
+  },
 
-    this._onLoad = function onLoad(event) {
-      if (event.target != content.document)
-        return;
-      let pageLoadTime = new Date() - loadDate;
-      removeEventListener("load", this._onLoad, true);
-      delete this._onLoad;
+  onStateChange: function (webProgress, req, flags, status) {
+    if (!webProgress.isTopLevel ||
+        !(flags & Ci.nsIWebProgressListener.STATE_STOP) ||
+        req.name == "about:blank")
+      return;
 
-      let canvas = PageThumbs._createCanvas(content);
-      let captureDate = new Date();
-      PageThumbs._captureToCanvas(content, canvas);
-      let captureTime = new Date() - captureDate;
+    let requestID = this._requestID;
+    let pageLoadTime = new Date() - this._requestDate;
+    delete this._requestID;
+
+    let canvas = PageThumbs._createCanvas(content);
+    let captureDate = new Date();
+    PageThumbs._captureToCanvas(content, canvas);
+    let captureTime = new Date() - captureDate;
 
-      let channel = docShell.currentDocumentChannel;
-      let isErrorResponse = PageThumbs._isChannelErrorResponse(channel);
-      let finalURL = this._webNav.currentURI.spec;
-      let fileReader = Cc["@mozilla.org/files/filereader;1"].
-                       createInstance(Ci.nsIDOMFileReader);
-      fileReader.onloadend = function onArrayBufferLoad() {
-        sendAsyncMessage("BackgroundPageThumbs:didCapture", {
-          id: msg.json.id,
-          imageData: fileReader.result,
-          finalURL: finalURL,
-          telemetry: {
-            CAPTURE_PAGE_LOAD_TIME_MS: pageLoadTime,
-            CAPTURE_CANVAS_DRAW_TIME_MS: captureTime,
-          },
-          wasErrorResponse: isErrorResponse,
-        });
-      };
-      canvas.toBlob(blob => fileReader.readAsArrayBuffer(blob));
+    let channel = docShell.currentDocumentChannel;
+    let isErrorResponse = PageThumbs._isChannelErrorResponse(channel);
+    let finalURL = this._webNav.currentURI.spec;
+    let fileReader = Cc["@mozilla.org/files/filereader;1"].
+                     createInstance(Ci.nsIDOMFileReader);
+    fileReader.onloadend = () => {
+      sendAsyncMessage("BackgroundPageThumbs:didCapture", {
+        id: requestID,
+        imageData: fileReader.result,
+        finalURL: finalURL,
+        wasErrorResponse: isErrorResponse,
+        telemetry: {
+          CAPTURE_PAGE_LOAD_TIME_MS: pageLoadTime,
+          CAPTURE_CANVAS_DRAW_TIME_MS: captureTime,
+        },
+      });
+    };
+    canvas.toBlob(blob => fileReader.readAsArrayBuffer(blob));
 
-      // Load about:blank to cause the captured window to be collected...
-      // eventually.
-      this._webNav.loadURI("about:blank", Ci.nsIWebNavigation.LOAD_FLAGS_NONE,
-                           null, null, null);
-    }.bind(this);
+    // If no other pages are loading, load about:blank to cause the captured
+    // window to be collected... eventually.  Calling loadURI at this point
+    // trips an assertion in nsLoadGroup::Cancel, so do it on another stack.
+    Services.tm.mainThread.dispatch(() => {
+      if (!("_requestID" in this))
+        this._webNav.loadURI("about:blank",
+                             Ci.nsIWebNavigation.LOAD_FLAGS_STOP_CONTENT,
+                             null, null, null);
+    }, Ci.nsIEventTarget.DISPATCH_NORMAL);
+  },
 
-    addEventListener("load", this._onLoad, true);
-    this._webNav.loadURI(msg.json.url, Ci.nsIWebNavigation.LOAD_FLAGS_NONE,
-                         null, null, null);
-    let loadDate = new Date();
-  },
+  QueryInterface: XPCOMUtils.generateQI([
+    Ci.nsIWebProgressListener,
+    Ci.nsISupportsWeakReference,
+  ]),
 };
 
 backgroundPageThumbsContent.init();
 
 })();
--- a/toolkit/components/thumbnails/test/browser_thumbnails_background.js
+++ b/toolkit/components/thumbnails/test/browser_thumbnails_background.js
@@ -49,17 +49,17 @@ let tests = [
       "http://www.example.com/1",
       // an item that will timeout to ensure timeouts work and we resume.
       testPageURL({ wait: 2002 }),
       "http://www.example.com/2",
     ];
     let files = urls.map(fileForURL);
     files.forEach(f => ok(!f.exists(), "Thumbnail should not be cached yet."));
     urls.forEach(function (url) {
-      let isTimeoutTest = url.indexOf("?wait") >= 0;
+      let isTimeoutTest = url.indexOf("wait") >= 0;
       imports.BackgroundPageThumbs.capture(url, {
         timeout: isTimeoutTest ? 100 : 30000,
         onDone: function onDone(capturedURL) {
           ok(urls.length > 0, "onDone called, so URLs should still remain");
           is(capturedURL, urls.shift(),
              "Captured URL should be currently expected URL (i.e., " +
              "capture() callbacks should be called in the correct order)");
           let file = files.shift();