Merge fx-team to m-c.
authorRyan VanderMeulen <ryanvm@gmail.com>
Thu, 29 Aug 2013 20:43:45 -0400
changeset 145024 c7459bc8e449a10da61db2cdb7c3498a55385b06
parent 145015 b46191dc8ad3cb426ddcd5c6f29c8919255a7730 (current diff)
parent 145023 b7df57dc193e4f7b3b7c8b6fd55b211e53000f25 (diff)
child 145025 065727546e13283e83b0ab48763441f81f5e35b2
child 145026 307b9b151f3ff1e9e46a1793bd725d218bc2739a
child 145045 4c2bcf32410d75ac883b235aeda2ecc1320bbe1a
child 145069 9f122ec9a3c9e00ff10cdc9618e1b74819e00e71
child 161423 1b8119764e04bb137285a9a1392e337c6756f913
push id25188
push userryanvm@gmail.com
push dateFri, 30 Aug 2013 00:47:13 +0000
treeherdermozilla-central@c7459bc8e449 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone26.0a1
first release with
nightly linux32
c7459bc8e449 / 26.0a1 / 20130830030205 / files
nightly linux64
c7459bc8e449 / 26.0a1 / 20130830030205 / files
nightly mac
c7459bc8e449 / 26.0a1 / 20130830030205 / files
nightly win32
c7459bc8e449 / 26.0a1 / 20130830030205 / files
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
Merge fx-team to m-c.
widget/gonk/HwcComposer2D.cpp
--- 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/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/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();