Merge inbound to m-c
authorWes Kocher <wkocher@mozilla.com>
Tue, 25 Feb 2014 20:09:33 -0800
changeset 170571 626d99c084cb8aa80bc58df9949d798edea510e3
parent 170570 a21fc292b2eac11ef03e18675d98c8ceb17adf52 (current diff)
parent 170502 5b1c1dcb9236d080b6334261cc7dd28d0993d9fb (diff)
child 170574 5e514b4b3b91943e5329c16bc7fd067713fbbdbc
child 170616 fd6f16c2f588a1934ef79c1ae126fd7518b867df
child 170644 4fe213a22c59eced4128c9cbc3799b1c4ce857e7
push id26291
push userkwierso@gmail.com
push dateWed, 26 Feb 2014 04:10:11 +0000
treeherdermozilla-central@626d99c084cb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone30.0a1
first release with
nightly linux32
626d99c084cb / 30.0a1 / 20140226030202 / files
nightly linux64
626d99c084cb / 30.0a1 / 20140226030202 / files
nightly mac
626d99c084cb / 30.0a1 / 20140226030202 / files
nightly win32
626d99c084cb / 30.0a1 / 20140226030202 / files
nightly win64
626d99c084cb / 30.0a1 / 20140226030202 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge inbound to m-c
browser/metro/base/content/contenthandlers/PluginHelper.js
--- a/b2g/config/emulator-ics/sources.xml
+++ b/b2g/config/emulator-ics/sources.xml
@@ -7,17 +7,17 @@
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="59605a7c026ff06cc1613af3938579b1dddc6cfe">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="8e21d0a5cdac94f05e9c3623fa3b9ad579ba2967"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="80d6405725788327102cab36e8d8c017cf25fb23"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="d11f524d00cacf5ba0dfbf25e4aa2158b1c3a036"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="022eadd5917615ff00c47eaaafa792b45e9c8a28"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="3d5c964015967ca8c86abe6dbbebee3cb82b1609"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8c449b53328059e9b55bb34baec9b27a15055a7e"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/emulator-jb/sources.xml
+++ b/b2g/config/emulator-jb/sources.xml
@@ -6,17 +6,17 @@
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="97a5b461686757dbb8ecab2aac5903e41d2e1afe">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="8e21d0a5cdac94f05e9c3623fa3b9ad579ba2967"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="80d6405725788327102cab36e8d8c017cf25fb23"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="3d5c964015967ca8c86abe6dbbebee3cb82b1609"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8c449b53328059e9b55bb34baec9b27a15055a7e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="905bfa3548eb75cf1792d0d8412b92113bbd4318"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="c3d7efc45414f1b44cd9c479bb2758c91c4707c0"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
--- a/b2g/config/emulator/sources.xml
+++ b/b2g/config/emulator/sources.xml
@@ -7,17 +7,17 @@
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="59605a7c026ff06cc1613af3938579b1dddc6cfe">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="8e21d0a5cdac94f05e9c3623fa3b9ad579ba2967"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="80d6405725788327102cab36e8d8c017cf25fb23"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="d11f524d00cacf5ba0dfbf25e4aa2158b1c3a036"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="022eadd5917615ff00c47eaaafa792b45e9c8a28"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="3d5c964015967ca8c86abe6dbbebee3cb82b1609"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8c449b53328059e9b55bb34baec9b27a15055a7e"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,4 +1,4 @@
 {
-    "revision": "823616f0af83eca32bceb0367a7a221f8b187110", 
+    "revision": "b2b0a8234336f7004812bf27e4053957cecad492", 
     "repo_path": "/integration/gaia-central"
 }
--- a/b2g/config/hamachi/sources.xml
+++ b/b2g/config/hamachi/sources.xml
@@ -6,17 +6,17 @@
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="59605a7c026ff06cc1613af3938579b1dddc6cfe">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="8e21d0a5cdac94f05e9c3623fa3b9ad579ba2967"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="80d6405725788327102cab36e8d8c017cf25fb23"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="84f2f2fce22605e17d511ff1767e54770067b5b5"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="3d5c964015967ca8c86abe6dbbebee3cb82b1609"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8c449b53328059e9b55bb34baec9b27a15055a7e"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
--- a/b2g/config/helix/sources.xml
+++ b/b2g/config/helix/sources.xml
@@ -5,17 +5,17 @@
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="59605a7c026ff06cc1613af3938579b1dddc6cfe">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="8e21d0a5cdac94f05e9c3623fa3b9ad579ba2967"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="80d6405725788327102cab36e8d8c017cf25fb23"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="84f2f2fce22605e17d511ff1767e54770067b5b5"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="3d5c964015967ca8c86abe6dbbebee3cb82b1609"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
--- a/b2g/config/inari/sources.xml
+++ b/b2g/config/inari/sources.xml
@@ -7,17 +7,17 @@
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="ics_chocolate_rb4.2" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="59605a7c026ff06cc1613af3938579b1dddc6cfe">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="8e21d0a5cdac94f05e9c3623fa3b9ad579ba2967"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="80d6405725788327102cab36e8d8c017cf25fb23"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="84f2f2fce22605e17d511ff1767e54770067b5b5"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="3d5c964015967ca8c86abe6dbbebee3cb82b1609"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8c449b53328059e9b55bb34baec9b27a15055a7e"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="cd5dfce80bc3f0139a56b58aca633202ccaee7f8"/>
--- a/b2g/config/leo/sources.xml
+++ b/b2g/config/leo/sources.xml
@@ -6,17 +6,17 @@
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="59605a7c026ff06cc1613af3938579b1dddc6cfe">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="8e21d0a5cdac94f05e9c3623fa3b9ad579ba2967"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="80d6405725788327102cab36e8d8c017cf25fb23"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="84f2f2fce22605e17d511ff1767e54770067b5b5"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="3d5c964015967ca8c86abe6dbbebee3cb82b1609"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8c449b53328059e9b55bb34baec9b27a15055a7e"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
--- a/b2g/config/mako/sources.xml
+++ b/b2g/config/mako/sources.xml
@@ -6,17 +6,17 @@
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="97a5b461686757dbb8ecab2aac5903e41d2e1afe">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="8e21d0a5cdac94f05e9c3623fa3b9ad579ba2967"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="80d6405725788327102cab36e8d8c017cf25fb23"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="3d5c964015967ca8c86abe6dbbebee3cb82b1609"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8c449b53328059e9b55bb34baec9b27a15055a7e"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="905bfa3548eb75cf1792d0d8412b92113bbd4318"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="c3d7efc45414f1b44cd9c479bb2758c91c4707c0"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
--- a/b2g/config/wasabi/sources.xml
+++ b/b2g/config/wasabi/sources.xml
@@ -6,17 +6,17 @@
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="ics_chocolate_rb4.2" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="59605a7c026ff06cc1613af3938579b1dddc6cfe">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="8e21d0a5cdac94f05e9c3623fa3b9ad579ba2967"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="80d6405725788327102cab36e8d8c017cf25fb23"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="15e8982284c4560f9c74c2b9fe8bb361ebfe0cb6"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="84f2f2fce22605e17d511ff1767e54770067b5b5"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="3d5c964015967ca8c86abe6dbbebee3cb82b1609"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="8c449b53328059e9b55bb34baec9b27a15055a7e"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -587,16 +587,19 @@
 
       <toolbarbutton id="tabs-closebutton"
                      class="close-button tabs-closebutton close-icon"
                      command="cmd_close"
                      label="&closeTab.label;"
                      cui-areatype="toolbar"
                      tooltiptext="&closeTab.label;"/>
 
+#ifdef XP_WIN
+      <hbox id="private-browsing-indicator" skipintoolbarset="true" ordinal="1000"/>
+#endif
 #ifdef CAN_DRAW_IN_TITLEBAR
       <hbox class="titlebar-placeholder" type="caption-buttons"
             id="titlebar-placeholder-on-TabsToolbar-for-captions-buttons" persist="width"
 #ifndef XP_MACOSX
             ordinal="1000"
 #endif
             skipintoolbarset="true"/>
 
--- a/browser/components/customizableui/src/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/src/CustomizableWidgets.jsm
@@ -186,16 +186,29 @@ const CustomizableWidgets = [{
       separator = doc.getElementById("PanelUI-recentlyClosedWindows-separator");
       elementCount = windowsFragment.childElementCount;
       separator.hidden = !elementCount;
       while (--elementCount >= 0) {
         windowsFragment.children[elementCount].classList.add("subviewbutton");
       }
       recentlyClosedWindows.appendChild(windowsFragment);
     },
+    onCreated: function(aNode) {
+      // Middle clicking recently closed items won't close the panel - cope:
+      let onRecentlyClosedClick = function(aEvent) {
+        if (aEvent.button == 1) {
+          CustomizableUI.hidePanelForNode(this);
+        }
+      };
+      let doc = aNode.ownerDocument;
+      let recentlyClosedTabs = doc.getElementById("PanelUI-recentlyClosedTabs");
+      let recentlyClosedWindows = doc.getElementById("PanelUI-recentlyClosedWindows");
+      recentlyClosedTabs.addEventListener("click", onRecentlyClosedClick);
+      recentlyClosedWindows.addEventListener("click", onRecentlyClosedClick);
+    },
     onViewHiding: function(aEvent) {
       LOG("History view is being hidden!");
     }
   }, {
     id: "privatebrowsing-button",
     shortcutId: "key_privatebrowsing",
     defaultArea: CustomizableUI.AREA_PANEL,
     onCommand: function(e) {
--- a/browser/devtools/webconsole/test/browser.ini
+++ b/browser/devtools/webconsole/test/browser.ini
@@ -102,16 +102,18 @@ support-files =
   test_bug_770099_violation.html^headers^
   test-autocomplete-in-stackframe.html
   testscript.js
   test-bug_923281_console_log_filter.html
   test-bug_923281_test1.js
   test-bug_923281_test2.js
   test-bug_939783_console_trace_duplicates.html
   test-bug-952277-highlight-nodes-in-vview.html
+  test-bug-609872-cd-iframe-parent.html
+  test-bug-609872-cd-iframe-child.html
 
 [browser_bug664688_sandbox_update_after_navigation.js]
 [browser_bug_638949_copy_link_location.js]
 [browser_bug_862916_console_dir_and_filter_off.js]
 [browser_bug_865288_repeat_different_objects.js]
 [browser_bug_865871_variables_view_close_on_esc_key.js]
 [browser_bug_869003_inspect_cross_domain_object.js]
 [browser_bug_871156_ctrlw_close_tab.js]
@@ -262,8 +264,9 @@ run-if = os == "mac"
 [browser_console_hide_jsterm_when_devtools_chrome_enabled_false.js]
 [browser_webconsole_output_01.js]
 [browser_webconsole_output_02.js]
 [browser_webconsole_output_03.js]
 [browser_webconsole_output_04.js]
 [browser_webconsole_output_events.js]
 [browser_console_variables_view_highlighter.js]
 [browser_webconsole_console_trace_duplicates.js]
+[browser_webconsole_cd_iframe.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/browser_webconsole_cd_iframe.js
@@ -0,0 +1,110 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that the cd() jsterm helper function works as expected. See bug 609872.
+
+function test() {
+  let hud;
+
+  const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-bug-609872-cd-iframe-parent.html";
+
+  const parentMessages = [{
+    name: "document.title in parent iframe",
+    text: "bug 609872 - iframe parent",
+    category: CATEGORY_OUTPUT,
+  }, {
+    name: "paragraph content",
+    text: "p: test for bug 609872 - iframe parent",
+    category: CATEGORY_OUTPUT,
+  }, {
+    name: "object content",
+    text: "obj: parent!",
+    category: CATEGORY_OUTPUT,
+  }];
+
+  const childMessages = [{
+    name: "document.title in child iframe",
+    text: "bug 609872 - iframe child",
+    category: CATEGORY_OUTPUT,
+  }, {
+    name: "paragraph content",
+    text: "p: test for bug 609872 - iframe child",
+    category: CATEGORY_OUTPUT,
+  }, {
+    name: "object content",
+    text: "obj: child!",
+    category: CATEGORY_OUTPUT,
+  }];
+
+  Task.spawn(runner).then(finishTest);
+
+  function* runner() {
+    const {tab} = yield loadTab(TEST_URI);
+    hud = yield openConsole(tab);
+
+    executeWindowTest();
+
+    yield waitForMessages({ webconsole: hud, messages: parentMessages });
+
+    info("cd() into the iframe using a selector");
+    hud.jsterm.clearOutput();
+    hud.jsterm.execute("cd('iframe')");
+    executeWindowTest();
+
+    yield waitForMessages({ webconsole: hud, messages: childMessages });
+
+    info("cd() out of the iframe, reset to default window");
+    hud.jsterm.clearOutput();
+    hud.jsterm.execute("cd()");
+    executeWindowTest();
+
+    yield waitForMessages({ webconsole: hud, messages: parentMessages });
+
+    info("call cd() with unexpected arguments");
+    hud.jsterm.clearOutput();
+    hud.jsterm.execute("cd(document)");
+
+    yield waitForMessages({
+      webconsole: hud,
+      messages: [{
+        text: "Cannot cd()",
+        category: CATEGORY_OUTPUT,
+        severity: SEVERITY_ERROR,
+      }],
+    });
+
+    hud.jsterm.clearOutput();
+    hud.jsterm.execute("cd('p')");
+
+    yield waitForMessages({
+      webconsole: hud,
+      messages: [{
+        text: "Cannot cd()",
+        category: CATEGORY_OUTPUT,
+        severity: SEVERITY_ERROR,
+      }],
+    });
+
+    info("cd() into the iframe using an iframe DOM element");
+    hud.jsterm.clearOutput();
+    hud.jsterm.execute("cd($('iframe'))");
+    executeWindowTest();
+
+    yield waitForMessages({ webconsole: hud, messages: childMessages });
+
+    info("cd(window.parent)");
+    hud.jsterm.clearOutput();
+    hud.jsterm.execute("cd(window.parent)");
+    executeWindowTest();
+
+    yield waitForMessages({ webconsole: hud, messages: parentMessages });
+
+    yield closeConsole(tab);
+  }
+
+  function executeWindowTest() {
+    hud.jsterm.execute("document.title");
+    hud.jsterm.execute("'p: ' + document.querySelector('p').textContent");
+    hud.jsterm.execute("'obj: ' + window.foobarBug609872");
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/test-bug-609872-cd-iframe-child.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <title>test for bug 609872 - iframe child</title>
+    <!-- Any copyright is dedicated to the Public Domain.
+         http://creativecommons.org/publicdomain/zero/1.0/ -->
+  </head>
+  <body>
+    <p>test for bug 609872 - iframe child</p>
+    <script>window.foobarBug609872 = 'child!';</script>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/test/test-bug-609872-cd-iframe-parent.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <title>test for bug 609872 - iframe parent</title>
+    <!-- Any copyright is dedicated to the Public Domain.
+         http://creativecommons.org/publicdomain/zero/1.0/ -->
+  </head>
+  <body>
+    <p>test for bug 609872 - iframe parent</p>
+    <script>window.foobarBug609872 = 'parent!';</script>
+    <iframe src="test-bug-609872-cd-iframe-child.html"></iframe>
+  </body>
+</html>
--- a/browser/locales/en-US/chrome/browser/devtools/webconsole.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/webconsole.properties
@@ -208,8 +208,12 @@ emptyPropertiesList=No properties to dis
 # LOCALIZATION NOTE (messageRepeats.tooltip2): the tooltip text that is displayed
 # when you hover the red bubble that shows how many times a message is repeated
 # in the web console output.
 # This is a semi-colon list of plural forms.
 # See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
 # #1 number of message repeats
 # example: 3 repeats
 messageRepeats.tooltip2=#1 repeat;#1 repeats
+
+# LOCALIZATION NOTE (cdFunctionInvalidArgument): the text that is displayed when
+# cd() is invoked with an invalid argument.
+cdFunctionInvalidArgument=Cannot cd() to the given window. Invalid argument.
--- a/browser/metro/base/content/browser.js
+++ b/browser/metro/base/content/browser.js
@@ -59,16 +59,17 @@ var Browser = {
     try {
       messageManager.loadFrameScript("chrome://browser/content/Util.js", true);
       messageManager.loadFrameScript("chrome://browser/content/contenthandlers/Content.js", true);
       messageManager.loadFrameScript("chrome://browser/content/contenthandlers/FormHelper.js", true);
       messageManager.loadFrameScript("chrome://browser/content/library/SelectionPrototype.js", true);
       messageManager.loadFrameScript("chrome://browser/content/contenthandlers/SelectionHandler.js", true);
       messageManager.loadFrameScript("chrome://browser/content/contenthandlers/ContextMenuHandler.js", true);
       messageManager.loadFrameScript("chrome://browser/content/contenthandlers/ConsoleAPIObserver.js", true);
+      messageManager.loadFrameScript("chrome://browser/content/contenthandlers/PluginHelper.js", true);
     } catch (e) {
       // XXX whatever is calling startup needs to dump errors!
       dump("###########" + e + "\n");
     }
 
     if (!Services.metro) {
       // Services.metro is only available on Windows Metro. We want to be able
       // to test metro on other platforms, too, so we provide a minimal shim.
--- a/browser/metro/base/content/browser.xul
+++ b/browser/metro/base/content/browser.xul
@@ -1115,16 +1115,19 @@ Desktop browser's sync prefs.
               &clearPrivateData.done;
             </description>
           </deck>
         </hbox>
       </settings>
       <setting pref="signon.rememberSignons"
                title="&optionsHeader.privacy.passwords.label;"
                type="bool"/>
+      <setting pref="browser.display.overlaynavbuttons"
+               title="&optionsHeader.displayOverlayButtons.label;"
+               type="bool"/>
       <settings id="prefs-reporting"
                 label="&optionsHeader.reporting.title;">
         <setting pref="app.crashreporter.autosubmit"
                  type="bool"
                  title="&optionsHeader.reporting.crashes.label;" />
         <checkbox id="prefs-reporting-submitURLs"
                   cropped="end"
                   label="&optionsHeader.reporting.crashes.submitURLs;"
copy from mobile/android/chrome/content/PluginHelper.js
copy to browser/metro/base/content/contenthandlers/PluginHelper.js
--- a/mobile/android/chrome/content/PluginHelper.js
+++ b/browser/metro/base/content/contenthandlers/PluginHelper.js
@@ -1,292 +1,95 @@
 /* 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/. */
 "use strict";
 
-var PluginHelper = {
-  showDoorHanger: function(aTab) {
-    if (!aTab.browser)
-      return;
-
-    // Even though we may not end up showing a doorhanger, this flag
-    // lets us know that we've tried to show a doorhanger.
-    aTab.shouldShowPluginDoorhanger = false;
-
-    let uri = aTab.browser.currentURI;
-
-    // If the user has previously set a plugins permission for this website,
-    // either play or don't play the plugins instead of showing a doorhanger.
-    let permValue = Services.perms.testPermission(uri, "plugins");
-    if (permValue != Services.perms.UNKNOWN_ACTION) {
-      if (permValue == Services.perms.ALLOW_ACTION)
-        PluginHelper.playAllPlugins(aTab.browser.contentWindow);
-
-      return;
-    }
+dump("### PluginHelper.js loaded\n");
 
-    let message = Strings.browser.formatStringFromName("clickToPlayPlugins.message2",
-                                                       [uri.host], 1);
-    let buttons = [
-      {
-        label: Strings.browser.GetStringFromName("clickToPlayPlugins.activate"),
-        callback: function(aChecked) {
-          // If the user checked "Don't ask again", make a permanent exception
-          if (aChecked)
-            Services.perms.add(uri, "plugins", Ci.nsIPermissionManager.ALLOW_ACTION);
-
-          PluginHelper.playAllPlugins(aTab.browser.contentWindow);
-        }
-      },
-      {
-        label: Strings.browser.GetStringFromName("clickToPlayPlugins.dontActivate"),
-        callback: function(aChecked) {
-          // If the user checked "Don't ask again", make a permanent exception
-          if (aChecked)
-            Services.perms.add(uri, "plugins", Ci.nsIPermissionManager.DENY_ACTION);
-
-          // Other than that, do nothing
-        }
-      }
-    ];
-
-    // Add a checkbox with a "Don't ask again" message if the uri contains a
-    // host. Adding a permanent exception will fail if host is not present.
-    let options = uri.host ? { checkbox: Strings.browser.GetStringFromName("clickToPlayPlugins.dontAskAgain") } : {};
-
-    NativeWindow.doorhanger.show(message, "ask-to-play-plugins", buttons, aTab.id, options);
+/**
+ * Handle events generated by plugin click-to-play code.
+ *
+ * This "PluginBindingAttached" fires when a "pluginProblem" XBL binding is
+ * created.  This binding overlays plugin content when the plugin is missing,
+ * blocked, click-to-play, or replaced by a "preview" extension plugin like
+ * Shumway or PDF.js.
+ */
+var PluginHelper = {
+  init: function () {
+    addEventListener("PluginBindingAttached", this, true, true);
   },
 
-  delayAndShowDoorHanger: function(aTab) {
-    // To avoid showing the doorhanger if there are also visible plugin
-    // overlays on the page, delay showing the doorhanger to check if
-    // visible plugins get added in the near future.
-    if (!aTab.pluginDoorhangerTimeout) {
-      aTab.pluginDoorhangerTimeout = setTimeout(function() {
-        if (this.shouldShowPluginDoorhanger) {
-          PluginHelper.showDoorHanger(this);
-        }
-      }.bind(aTab), 500);
-    }
-  },
-
-  playAllPlugins: function(aContentWindow) {
-    let cwu = aContentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
-                            .getInterface(Ci.nsIDOMWindowUtils);
-    // XXX not sure if we should enable plugins for the parent documents...
-    let plugins = cwu.plugins;
-    if (!plugins || !plugins.length)
-      return;
-
-    plugins.forEach(this.playPlugin);
-  },
-
-  playPlugin: function(plugin) {
-    let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
-    if (!objLoadingContent.activated)
-      objLoadingContent.playPlugin();
-  },
-
-  stopPlayPreview: function(plugin, playPlugin) {
-    let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
-    if (objLoadingContent.activated)
-      return;
-
-    if (playPlugin)
-      objLoadingContent.playPlugin();
-    else
-      objLoadingContent.cancelPlayPreview();
-  },
-
-  getPluginPreference: function getPluginPreference() {
-    let pluginDisable = Services.prefs.getBoolPref("plugin.disable");
-    if (pluginDisable)
-      return "0";
-
-    let state = Services.prefs.getIntPref("plugin.default.state");
-    return state == Ci.nsIPluginTag.STATE_CLICKTOPLAY ? "2" : "1";
-  },
-
-  setPluginPreference: function setPluginPreference(aValue) {
-    switch (aValue) {
-      case "0": // Enable Plugins = No
-        Services.prefs.setBoolPref("plugin.disable", true);
-        Services.prefs.clearUserPref("plugin.default.state");
-        break;
-      case "1": // Enable Plugins = Yes
-        Services.prefs.clearUserPref("plugin.disable");
-        Services.prefs.setIntPref("plugin.default.state", Ci.nsIPluginTag.STATE_ENABLED);
-        break;
-      case "2": // Enable Plugins = Tap to Play (default)
-        Services.prefs.clearUserPref("plugin.disable");
-        Services.prefs.clearUserPref("plugin.default.state");
+  handleEvent: function handleEvent(aEvent) {
+    switch (aEvent.type) {
+      case "PluginBindingAttached":
+        this.handlePluginBindingAttached(aEvent);
         break;
     }
   },
 
-  // Copied from /browser/base/content/browser.js
-  isTooSmall : function (plugin, overlay) {
-    // Is the <object>'s size too small to hold what we want to show?
-    let pluginRect = plugin.getBoundingClientRect();
-    // XXX bug 446693. The text-shadow on the submitted-report text at
-    //     the bottom causes scrollHeight to be larger than it should be.
-    let overflows = (overlay.scrollWidth > pluginRect.width) ||
-                    (overlay.scrollHeight - 5 > pluginRect.height);
-
-    return overflows;
-  },
-
   getPluginMimeType: function (plugin) {
     var tagMimetype = plugin.actualType;
 
     if (tagMimetype == "") {
       tagMimetype = plugin.type;
     }
-
     return tagMimetype;
   },
 
-  handlePluginBindingAttached: function (aTab, aEvent) {
+  handlePluginBindingAttached: function (aEvent) {
     let plugin = aEvent.target;
     let doc = plugin.ownerDocument;
     let overlay = doc.getAnonymousElementByAttribute(plugin, "anonid", "main");
     if (!overlay || overlay._bindingHandled) {
       return;
     }
     overlay._bindingHandled = true;
 
     let eventType = PluginHelper._getBindingType(plugin);
     if (!eventType) {
-      // Not all bindings have handlers
       return;
     }
 
     switch  (eventType) {
-      case "PluginClickToPlay": {
-        // Check if plugins have already been activated for this page, or if
-        // the user has set a permission to always play plugins on the site
-        if (aTab.clickToPlayPluginsActivated ||
-            Services.perms.testPermission(aTab.browser.currentURI, "plugins") ==
-            Services.perms.ALLOW_ACTION) {
-          PluginHelper.playPlugin(plugin);
-          return;
-        }
-
-        // If the plugin is hidden, or if the overlay is too small, show a 
-        // doorhanger notification
-        if (PluginHelper.isTooSmall(plugin, overlay)) {
-          PluginHelper.delayAndShowDoorHanger(aTab);
-        } else {
-          // There's a large enough visible overlay that we don't need to show
-          // the doorhanger.
-          aTab.shouldShowPluginDoorhanger = false;
-          overlay.classList.add("visible");
-        }
-
-        // Add click to play listener to the overlay
-        overlay.addEventListener("click", function(e) {
-          if (!e.isTrusted)
-            return;
-          e.preventDefault();
-          let win = e.target.ownerDocument.defaultView.top;
-          let tab = BrowserApp.getTabForWindow(win);
-          tab.clickToPlayPluginsActivated = true;
-          PluginHelper.playAllPlugins(win);
-
-          NativeWindow.doorhanger.hide("ask-to-play-plugins", tab.id);
-        }, true);
-
-        // Add handlers for over- and underflow in case the plugin gets resized
-        plugin.addEventListener("overflow", function(event) {
-          overlay.classList.remove("visible");
-          PluginHelper.delayAndShowDoorHanger(aTab);
-        });
-        plugin.addEventListener("underflow", function(event) {
-          // This is also triggered if only one dimension underflows,
-          // the other dimension might still overflow
-          if (!PluginHelper.isTooSmall(plugin, overlay)) {
-            overlay.classList.add("visible");
-          }
-        });
-
-        break;
-      }
-
       case "PluginPlayPreview": {
+        // Load the "preview" handler (an extension plugin like Shumway or PDF.js).
         let previewContent = doc.getAnonymousElementByAttribute(plugin, "class", "previewPluginContent");
         let pluginHost = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
         let mimeType = PluginHelper.getPluginMimeType(plugin);
         let playPreviewInfo = pluginHost.getPlayPreviewInfo(mimeType);
 
-        if (!playPreviewInfo.ignoreCTP) {
-          // Check if plugins have already been activated for this page, or if
-          // the user has set a permission to always play plugins on the site
-          if (aTab.clickToPlayPluginsActivated ||
-              Services.perms.testPermission(aTab.browser.currentURI, "plugins") ==
-              Services.perms.ALLOW_ACTION) {
-            PluginHelper.playPlugin(plugin);
-            return;
-          }
-
-          // Always show door hanger for play preview plugins
-          PluginHelper.delayAndShowDoorHanger(aTab);
-        }
-
         let iframe = previewContent.getElementsByClassName("previewPluginContentFrame")[0];
         if (!iframe) {
           // lazy initialization of the iframe
           iframe = doc.createElementNS("http://www.w3.org/1999/xhtml", "iframe");
           iframe.className = "previewPluginContentFrame";
           previewContent.appendChild(iframe);
         }
         iframe.src = playPreviewInfo.redirectURL;
-
-        // MozPlayPlugin event can be dispatched from the extension chrome
-        // code to replace the preview content with the native plugin
-        previewContent.addEventListener("MozPlayPlugin", function playPluginHandler(e) {
-          if (!e.isTrusted)
-            return;
-
-          previewContent.removeEventListener("MozPlayPlugin", playPluginHandler, true);
-
-          let playPlugin = !aEvent.detail;
-          PluginHelper.stopPlayPreview(plugin, playPlugin);
-
-          // cleaning up: removes overlay iframe from the DOM
-          let iframe = previewContent.getElementsByClassName("previewPluginContentFrame")[0];
-          if (iframe)
-            previewContent.removeChild(iframe);
-        }, true);
         break;
       }
 
       case "PluginNotFound": {
-        // On devices where we don't support Flash, there will be a
-        // "Learn More..." link in the missing plugin error message.
-        let learnMoreLink = doc.getAnonymousElementByAttribute(plugin, "class", "unsupportedLearnMoreLink");
-        let learnMoreUrl = Services.urlFormatter.formatURLPref("app.support.baseURL");
-        learnMoreUrl += "mobile-flash-unsupported";
-        learnMoreLink.href = learnMoreUrl;
-        overlay.classList.add("visible");
+        // TODO: Display a message about missing plugins (bug 936907)
         break;
       }
     }
   },
 
   // Helper to get the binding handler type from a plugin object
   _getBindingType: function(plugin) {
     if (!(plugin instanceof Ci.nsIObjectLoadingContent))
       return null;
 
     switch (plugin.pluginFallbackType) {
       case Ci.nsIObjectLoadingContent.PLUGIN_UNSUPPORTED:
         return "PluginNotFound";
-      case Ci.nsIObjectLoadingContent.PLUGIN_CLICK_TO_PLAY:
-        return "PluginClickToPlay";
       case Ci.nsIObjectLoadingContent.PLUGIN_PLAY_PREVIEW:
         return "PluginPlayPreview";
       default:
-        // Not all states map to a handler
+        // Metro Firefox does not yet support other fallback types.
         return null;
     }
-  }
+  },
 };
+
+PluginHelper.init();
--- a/browser/metro/base/jar.mn
+++ b/browser/metro/base/jar.mn
@@ -54,16 +54,17 @@ chrome.jar:
   content/helperui/FindHelperUI.js             (content/helperui/FindHelperUI.js)
   content/helperui/ItemPinHelper.js            (content/helperui/ItemPinHelper.js)
 
   content/contenthandlers/ContextMenuHandler.js (content/contenthandlers/ContextMenuHandler.js)
   content/contenthandlers/SelectionHandler.js  (content/contenthandlers/SelectionHandler.js)
   content/contenthandlers/FormHelper.js        (content/contenthandlers/FormHelper.js)
   content/contenthandlers/ConsoleAPIObserver.js (content/contenthandlers/ConsoleAPIObserver.js)
   content/contenthandlers/Content.js           (content/contenthandlers/Content.js)
+  content/contenthandlers/PluginHelper.js      (content/contenthandlers/PluginHelper.js)
 
   content/library/SelectionPrototype.js        (content/library/SelectionPrototype.js)
 
   content/ContentAreaObserver.js               (content/ContentAreaObserver.js)
   content/BrowserTouchHandler.js               (content/BrowserTouchHandler.js)
 * content/WebProgress.js                       (content/WebProgress.js)
   content/pages/config.js                      (content/pages/config.js)
 * content/browser.xul                          (content/browser.xul)
--- a/browser/metro/locales/en-US/chrome/preferences.dtd
+++ b/browser/metro/locales/en-US/chrome/preferences.dtd
@@ -22,16 +22,17 @@
 <!ENTITY clearPrivateData.cache                                  "Cache">
 <!ENTITY clearPrivateData.sitePref                               "Site preferences">
 <!ENTITY clearPrivateData.formSearchHist                         "Form &amp; search history">
 <!ENTITY clearPrivateData.passwords                              "Saved passwords">
 <!ENTITY clearPrivateData.offline                                "Offline website data">
 <!ENTITY clearPrivateData.logins                                 "Active logins">
 
 <!ENTITY optionsHeader.privacy.passwords.label                   "Remember Passwords">
+<!ENTITY optionsHeader.displayOverlayButtons.label               "Show Navigation Buttons">
 <!ENTITY doNotTrack.title                                        "Do Not Track">
 <!ENTITY doNotTrack.options.doNotTrack                           "Tell websites that I do not want to be tracked">
 <!ENTITY doNotTrack.options.doTrack                              "Tell websites that I want to be tracked">
 <!ENTITY doNotTrack.options.default                              "Do not tell websites anything about my tracking preferences">
 <!ENTITY doNotTrack.learnMoreLink                                "Learn more&#x2026;">
 
 <!ENTITY optionsHeader.reporting.title                           "Crash Reporter">
 <!ENTITY optionsHeader.reporting.crashes.label                   "&brandShortName; submits crash reports to help Mozilla make your browser more stable and secure">
--- a/browser/metro/profile/metro.js
+++ b/browser/metro/profile/metro.js
@@ -568,17 +568,22 @@ pref("pdfjs.disabled", true);
 pref("pdfjs.firstRun", true);
 // The values of preferredAction and alwaysAskBeforeHandling before pdf.js
 // became the default.
 pref("pdfjs.previousHandler.preferredAction", 0);
 pref("pdfjs.previousHandler.alwaysAskBeforeHandling", false);
 #endif
 
 #ifdef NIGHTLY_BUILD
+// Shumay is currently experimental.  Toggle this pref to enable Shumway for
+// testing and development.
 pref("shumway.disabled", true);
+// When Shumway is enabled, use it all the time, not only when Flash is set to
+// click-to-play (because Metro doesn't even load the native Flash plugin).
+pref("shumway.ignoreCTP", true);
 #endif
 
 // The maximum amount of decoded image data we'll willingly keep around (we
 // might keep around more than this, but we'll try to get down to this value).
 // (This is intentionally on the high side; see bug 746055.)
 pref("image.mem.max_decoded_image_kb", 256000);
 
 // enable touch events interfaces
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -2504,16 +2504,14 @@ chatbox {
 
 #main-window[customizing] #navigator-toolbox::after {
   margin-left: 2em;
   margin-right: 2em;
 }
 
 /* End customization mode */
 
-#main-window[privatebrowsingmode=temporary] #TabsToolbar::after {
-  content: "";
-  display: -moz-box;
+#main-window[privatebrowsingmode=temporary] #private-browsing-indicator {
   width: 40px;
   background: url("chrome://browser/skin/privatebrowsing-indicator.png") no-repeat center center;
 }
 
 %include ../shared/UITour.inc.css
--- a/dom/camera/DOMCameraControl.cpp
+++ b/dom/camera/DOMCameraControl.cpp
@@ -947,22 +947,16 @@ nsDOMCameraControl::Shutdown()
   mOnShutterCb = nullptr;
   mOnClosedCb = nullptr;
   mOnRecorderStateChangeCb = nullptr;
   mOnPreviewStateChangeCb = nullptr;
 
   mCameraControl->Shutdown();
 }
 
-nsRefPtr<ICameraControl>
-nsDOMCameraControl::GetNativeCameraControl()
-{
-  return mCameraControl;
-}
-
 nsresult
 nsDOMCameraControl::NotifyRecordingStatusChange(const nsString& aMsg)
 {
   NS_ENSURE_TRUE(mWindow, NS_ERROR_FAILURE);
 
   return MediaManager::NotifyRecordingStatusChange(mWindow,
                                                    aMsg,
                                                    true /* aIsAudio */,
@@ -1134,31 +1128,41 @@ nsDOMCameraControl::OnConfigurationChang
     cb->Call(*mCurrentConfiguration, ignored);
   }
 }
 
 void
 nsDOMCameraControl::OnAutoFocusComplete(bool aAutoFocusSucceeded)
 {
   MOZ_ASSERT(NS_IsMainThread());
-  ErrorResult ignored;
 
   nsCOMPtr<CameraAutoFocusCallback> cb = mAutoFocusOnSuccessCb.forget();
   mAutoFocusOnErrorCb = nullptr;
-  cb->Call(aAutoFocusSucceeded, ignored);
+  if (cb) {
+    ErrorResult ignored;
+    cb->Call(aAutoFocusSucceeded, ignored);
+  }
 }
 
 void
 nsDOMCameraControl::OnTakePictureComplete(nsIDOMBlob* aPicture)
 {
   MOZ_ASSERT(NS_IsMainThread());
-  ErrorResult ignored;
 
   nsCOMPtr<CameraTakePictureCallback> cb = mTakePictureOnSuccessCb.forget();
   mTakePictureOnErrorCb = nullptr;
+  if (!cb) {
+    // Warn because it shouldn't be possible to get here without
+    // having passed a success callback into takePicture(), even
+    // though we guard against a nullptr dereference.
+    NS_WARNING("DOM Null success callback in OnTakePictureComplete()");
+    return;
+  }
+
+  ErrorResult ignored;
   cb->Call(aPicture, ignored);
 }
 
 void
 nsDOMCameraControl::OnError(CameraControlListener::CameraErrorContext aContext, const nsAString& aError)
 {
   DOM_CAMERA_LOGI("DOM OnError context=%d, error='%s'\n", aContext,
     NS_LossyConvertUTF16toASCII(aError).get());
--- a/dom/camera/DOMCameraControl.h
+++ b/dom/camera/DOMCameraControl.h
@@ -40,17 +40,16 @@ public:
   NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(nsDOMCameraControl, DOMMediaStream)
   NS_DECL_ISUPPORTS_INHERITED
 
   nsDOMCameraControl(uint32_t aCameraId,
                      const dom::CameraConfiguration& aInitialConfig,
                      dom::GetCameraCallback* aOnSuccess,
                      dom::CameraErrorCallback* aOnError,
                      nsPIDOMWindow* aWindow);
-  nsRefPtr<ICameraControl> GetNativeCameraControl();
 
   void Shutdown();
 
   nsPIDOMWindow* GetParentObject() const { return mWindow; }
 
   // Attributes.
   void GetEffect(nsString& aEffect, ErrorResult& aRv);
   void SetEffect(const nsAString& aEffect, ErrorResult& aRv);
--- a/mobile/android/base/db/BrowserProvider.java
+++ b/mobile/android/base/db/BrowserProvider.java
@@ -267,74 +267,105 @@ public class BrowserProvider extends Tra
     }
 
     protected static void debug(String message) {
         if (logDebug) {
             Log.d(LOGTAG, message);
         }
     }
 
+    /*
+     * This utility is replicated from RepoUtils, which is managed by android-sync.
+     */
+    private static String computeSQLInClause(int items, String field) {
+        final StringBuilder builder = new StringBuilder(field);
+        builder.append(" IN (");
+        int i = 0;
+        for (; i < items - 1; ++i) {
+            builder.append("?, ");
+        }
+        if (i < items) {
+            builder.append("?");
+        }
+        builder.append(")");
+        return builder.toString();
+    }
+
+    /**
+     * Turn a single-column cursor of longs into a single SQL "IN" clause.
+     * We can do this without using selection arguments because Long isn't
+     * vulnerable to injection.
+     */
+    private static String computeSQLInClauseFromLongs(final Cursor cursor, String field) {
+        final StringBuilder builder = new StringBuilder(field);
+        builder.append(" IN (");
+        final int commaLimit = cursor.getCount() - 1;
+        int i = 0;
+        while (cursor.moveToNext()) {
+            builder.append(cursor.getLong(0));
+            if (i++ < commaLimit) {
+                builder.append(", ");
+            }
+        }
+        builder.append(")");
+        return builder.toString();
+    }
+
+    /**
+     * Clean up some deleted records from the specified table.
+     *
+     * If called in an existing transaction, it is the caller's responsibility
+     * to ensure that the transaction is already upgraded to a writer, because
+     * this method issues a read followed by a write, and thus is potentially
+     * vulnerable to an unhandled SQLITE_BUSY failure during the upgrade.
+     *
+     * If not called in an existing transaction, no new explicit transaction
+     * will be begun.
+     */
     private void cleanupSomeDeletedRecords(Uri fromUri, Uri targetUri, String tableName) {
         Log.d(LOGTAG, "Cleaning up deleted records from " + tableName);
 
-        // we cleanup records marked as deleted that are older than a
+        // We clean up records marked as deleted that are older than a
         // predefined max age. It's important not be too greedy here and
         // remove only a few old deleted records at a time.
 
-        // The PARAM_SHOW_DELETED argument is necessary to return the records
-        // that were marked as deleted. We use PARAM_IS_SYNC here to ensure
-        // that we'll be actually deleting records instead of flagging them.
-        Uri.Builder uriBuilder = targetUri.buildUpon()
-                .appendQueryParameter(BrowserContract.PARAM_LIMIT, String.valueOf(DELETED_RECORDS_PURGE_LIMIT))
-                .appendQueryParameter(BrowserContract.PARAM_SHOW_DELETED, "1")
-                .appendQueryParameter(BrowserContract.PARAM_IS_SYNC, "1");
-
-        String profile = fromUri.getQueryParameter(BrowserContract.PARAM_PROFILE);
-        if (!TextUtils.isEmpty(profile))
-            uriBuilder = uriBuilder.appendQueryParameter(BrowserContract.PARAM_PROFILE, profile);
-
-        if (isTest(fromUri))
-            uriBuilder = uriBuilder.appendQueryParameter(BrowserContract.PARAM_IS_TEST, "1");
-
-        Uri uriWithArgs = uriBuilder.build();
+        // Android SQLite doesn't have LIMIT on DELETE. Instead, query for the
+        // IDs of matching rows, then delete them in one go.
+        final long now = System.currentTimeMillis();
+        final String selection = SyncColumns.IS_DELETED + " = 1 AND " +
+                                 SyncColumns.DATE_MODIFIED + " <= " +
+                                 (now - MAX_AGE_OF_DELETED_RECORDS);
 
-        Cursor cursor = null;
-
+        final String profile = fromUri.getQueryParameter(BrowserContract.PARAM_PROFILE);
+        final SQLiteDatabase db = getWritableDatabaseForProfile(profile, isTest(fromUri));
+        final String[] ids;
+        final String limit = Long.toString(DELETED_RECORDS_PURGE_LIMIT, 10);
+        final Cursor cursor = db.query(tableName, new String[] { CommonColumns._ID }, selection, null, null, null, null, limit);
         try {
-            long now = System.currentTimeMillis();
-            String selection = SyncColumns.IS_DELETED + " = 1 AND " +
-                    SyncColumns.DATE_MODIFIED + " <= " + (now - MAX_AGE_OF_DELETED_RECORDS);
-
-            cursor = query(uriWithArgs,
-                           new String[] { CommonColumns._ID },
-                           selection,
-                           null,
-                           null);
-
+            ids = new String[cursor.getCount()];
+            int i = 0;
             while (cursor.moveToNext()) {
-                Uri uriWithId = ContentUris.withAppendedId(uriWithArgs, cursor.getLong(0));
-                delete(uriWithId, null, null);
-
-                debug("Removed old deleted item with URI: " + uriWithId);
+                ids[i++] = Long.toString(cursor.getLong(0), 10);
             }
         } finally {
-            if (cursor != null)
-                cursor.close();
+            cursor.close();
         }
+
+        final String inClause = computeSQLInClause(ids.length,
+                                                   CommonColumns._ID);
+        db.delete(tableName, inClause, ids);
     }
 
     /**
      * Remove enough history items to bring the database count below <code>retain</code>,
      * removing no items with a modified time after <code>keepAfter</code>.
      *
      * Provide <code>keepAfter</code> less than or equal to zero to skip that check.
      *
      * Items will be removed according to an approximate frecency calculation.
-     *
-     * Call this method within a transaction.
      */
     private void expireHistory(final SQLiteDatabase db, final int retain, final long keepAfter) {
         Log.d(LOGTAG, "Expiring history.");
         final long rows = DatabaseUtils.queryNumEntries(db, TABLE_HISTORY);
 
         if (retain >= rows) {
             debug("Not expiring history: only have " + rows + " rows.");
             return;
@@ -353,16 +384,18 @@ public class BrowserProvider extends Tra
                     "ORDER BY " + sortOrder + " LIMIT " + toRemove +
                   ")";
         } else {
             sql = "DELETE FROM " + TABLE_HISTORY + " WHERE " + History._ID + " " +
                   "IN ( SELECT " + History._ID + " FROM " + TABLE_HISTORY + " " +
                   "ORDER BY " + sortOrder + " LIMIT " + toRemove + ")";
         }
         trace("Deleting using query: " + sql);
+
+        beginWrite(db);
         db.execSQL(sql);
     }
 
     /**
      * Remove any thumbnails that for sites that aren't likely to be ever shown.
      * Items will be removed according to a frecency calculation and only if they are not pinned
      *
      * Call this method within a transaction.
@@ -437,16 +470,17 @@ public class BrowserProvider extends Tra
         return null;
     }
 
     @SuppressWarnings("fallthrough")
     @Override
     public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
         trace("Calling delete in transaction on URI: " + uri);
         final SQLiteDatabase db = getWritableDatabase(uri);
+
         final int match = URI_MATCHER.match(uri);
         int deleted = 0;
 
         switch (match) {
             case BOOKMARKS_ID:
                 trace("Delete on BOOKMARKS_ID: " + uri);
 
                 selection = DBUtils.concatenateWhere(selection, TABLE_BOOKMARKS + "._id = ?");
@@ -464,16 +498,17 @@ public class BrowserProvider extends Tra
                 trace("Delete on HISTORY_ID: " + uri);
 
                 selection = DBUtils.concatenateWhere(selection, TABLE_HISTORY + "._id = ?");
                 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
                         new String[] { Long.toString(ContentUris.parseId(uri)) });
                 // fall through
             case HISTORY: {
                 trace("Deleting history: " + uri);
+                beginWrite(db);
                 deleted = deleteHistory(uri, selection, selectionArgs);
                 deleteUnusedImages(uri);
                 break;
             }
 
             case HISTORY_OLD: {
                 String priority = uri.getQueryParameter(BrowserContract.PARAM_EXPIRE_PRIORITY);
                 long keepAfter = System.currentTimeMillis() - DEFAULT_EXPIRY_PRESERVE_WINDOW;
@@ -493,29 +528,31 @@ public class BrowserProvider extends Tra
                 debug("Delete on FAVICON_ID: " + uri);
 
                 selection = DBUtils.concatenateWhere(selection, TABLE_FAVICONS + "._id = ?");
                 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
                         new String[] { Long.toString(ContentUris.parseId(uri)) });
                 // fall through
             case FAVICONS: {
                 trace("Deleting favicons: " + uri);
+                beginWrite(db);
                 deleted = deleteFavicons(uri, selection, selectionArgs);
                 break;
             }
 
             case THUMBNAIL_ID:
                 debug("Delete on THUMBNAIL_ID: " + uri);
 
                 selection = DBUtils.concatenateWhere(selection, TABLE_THUMBNAILS + "._id = ?");
                 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
                         new String[] { Long.toString(ContentUris.parseId(uri)) });
                 // fall through
             case THUMBNAILS: {
                 trace("Deleting thumbnails: " + uri);
+                beginWrite(db);
                 deleted = deleteThumbnails(uri, selection, selectionArgs);
                 break;
             }
 
             default:
                 throw new UnsupportedOperationException("Unknown delete URI " + uri);
         }
 
@@ -572,114 +609,119 @@ public class BrowserProvider extends Tra
     @Override
     public int updateInTransaction(Uri uri, ContentValues values, String selection,
             String[] selectionArgs) {
         trace("Calling update in transaction on URI: " + uri);
 
         int match = URI_MATCHER.match(uri);
         int updated = 0;
 
+        final SQLiteDatabase db = getWritableDatabase(uri);
         switch (match) {
             // We provide a dedicated (hacky) API for callers to bulk-update the positions of
             // folder children by passing an array of GUID strings as `selectionArgs`.
             // Each child will have its position column set to its index in the provided array.
             //
             // This avoids callers having to issue a large number of UPDATE queries through
             // the usual channels. See Bug 728783.
             //
             // Note that this is decidedly not a general-purpose API; use at your own risk.
             // `values` and `selection` are ignored.
             case BOOKMARKS_POSITIONS: {
                 debug("Update on BOOKMARKS_POSITIONS: " + uri);
+
+                // This already starts and finishes its own transaction.
                 updated = updateBookmarkPositions(uri, selectionArgs);
                 break;
             }
 
             case BOOKMARKS_PARENT: {
                 debug("Update on BOOKMARKS_PARENT: " + uri);
-                updated = updateBookmarkParents(uri, values, selection, selectionArgs);
+                beginWrite(db);
+                updated = updateBookmarkParents(db, values, selection, selectionArgs);
                 break;
             }
 
             case BOOKMARKS_ID:
                 debug("Update on BOOKMARKS_ID: " + uri);
 
                 selection = DBUtils.concatenateWhere(selection, TABLE_BOOKMARKS + "._id = ?");
                 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
                         new String[] { Long.toString(ContentUris.parseId(uri)) });
                 // fall through
             case BOOKMARKS: {
                 debug("Updating bookmark: " + uri);
-                if (shouldUpdateOrInsert(uri))
+                if (shouldUpdateOrInsert(uri)) {
                     updated = updateOrInsertBookmark(uri, values, selection, selectionArgs);
-                else
+                } else {
                     updated = updateBookmarks(uri, values, selection, selectionArgs);
+                }
                 break;
             }
 
             case HISTORY_ID:
                 debug("Update on HISTORY_ID: " + uri);
 
                 selection = DBUtils.concatenateWhere(selection, TABLE_HISTORY + "._id = ?");
                 selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
                         new String[] { Long.toString(ContentUris.parseId(uri)) });
                 // fall through
             case HISTORY: {
                 debug("Updating history: " + uri);
-                if (shouldUpdateOrInsert(uri))
+                if (shouldUpdateOrInsert(uri)) {
                     updated = updateOrInsertHistory(uri, values, selection, selectionArgs);
-                else
+                } else {
                     updated = updateHistory(uri, values, selection, selectionArgs);
+                }
                 break;
             }
 
             case FAVICONS: {
                 debug("Update on FAVICONS: " + uri);
 
                 String url = values.getAsString(Favicons.URL);
                 String faviconSelection = null;
                 String[] faviconSelectionArgs = null;
 
                 if (!TextUtils.isEmpty(url)) {
                     faviconSelection = Favicons.URL + " = ?";
                     faviconSelectionArgs = new String[] { url };
                 }
 
-                if (shouldUpdateOrInsert(uri))
+                if (shouldUpdateOrInsert(uri)) {
                     updated = updateOrInsertFavicon(uri, values, faviconSelection, faviconSelectionArgs);
-                else
+                } else {
                     updated = updateExistingFavicon(uri, values, faviconSelection, faviconSelectionArgs);
-
+                }
                 break;
             }
 
             case THUMBNAILS: {
                 debug("Update on THUMBNAILS: " + uri);
 
                 String url = values.getAsString(Thumbnails.URL);
 
                 // if no URL is provided, update all of the entries
-                if (TextUtils.isEmpty(values.getAsString(Thumbnails.URL)))
+                if (TextUtils.isEmpty(values.getAsString(Thumbnails.URL))) {
                     updated = updateExistingThumbnail(uri, values, null, null);
-                else if (shouldUpdateOrInsert(uri))
+                } else if (shouldUpdateOrInsert(uri)) {
                     updated = updateOrInsertThumbnail(uri, values, Thumbnails.URL + " = ?",
                                                       new String[] { url });
-                else
+                } else {
                     updated = updateExistingThumbnail(uri, values, Thumbnails.URL + " = ?",
                                                       new String[] { url });
-
+                }
                 break;
             }
 
             default:
                 throw new UnsupportedOperationException("Unknown update URI " + uri);
         }
 
         debug("Updated " + updated + " rows for URI: " + uri);
-
         return updated;
     }
 
     @Override
     public Cursor query(Uri uri, String[] projection, String selection,
             String[] selectionArgs, String sortOrder) {
         SQLiteDatabase db = getReadableDatabase(uri);
         final int match = URI_MATCHER.match(uri);
@@ -871,50 +913,52 @@ public class BrowserProvider extends Tra
         Cursor cursor = qb.query(db, projection, selection, selectionArgs, groupBy,
                 null, sortOrder, limit);
         cursor.setNotificationUri(getContext().getContentResolver(),
                 BrowserContract.AUTHORITY_URI);
 
         return cursor;
     }
 
-    int getUrlCount(SQLiteDatabase db, String table, String url) {
-        Cursor c = db.query(table, new String[] { "COUNT(*)" },
-                URLColumns.URL + " = ?", new String[] { url }, null, null,
-                null);
-
-        int count = 0;
-
+    private static int getUrlCount(SQLiteDatabase db, String table, String url) {
+        final Cursor c = db.query(table, new String[] { "COUNT(*)" },
+                                  URLColumns.URL + " = ?", new String[] { url },
+                                  null, null, null);
         try {
-            if (c.moveToFirst())
-                count = c.getInt(0);
+            if (c.moveToFirst()) {
+                return c.getInt(0);
+            }
         } finally {
             c.close();
         }
 
-        return count;
+        return 0;
     }
 
     /**
      * Update the positions of bookmarks in batches.
      *
+     * Begins and ends its own transactions.
+     *
      * @see #updateBookmarkPositionsInTransaction(SQLiteDatabase, String[], int, int)
      */
     int updateBookmarkPositions(Uri uri, String[] guids) {
-        if (guids == null)
+        if (guids == null) {
             return 0;
+        }
 
         int guidsCount = guids.length;
-        if (guidsCount == 0)
+        if (guidsCount == 0) {
             return 0;
+        }
 
-        final SQLiteDatabase db = getWritableDatabase(uri);
         int offset = 0;
         int updated = 0;
 
+        final SQLiteDatabase db = getWritableDatabase(uri);
         db.beginTransaction();
 
         while (offset < guidsCount) {
             try {
                 updated += updateBookmarkPositionsInTransaction(db, guids, offset,
                                                                 MAX_POSITION_UPDATES_PER_QUERY);
             } catch (SQLException e) {
                 Log.e(LOGTAG, "Got SQLite exception updating bookmark positions at offset " + offset, e);
@@ -937,18 +981,18 @@ public class BrowserProvider extends Tra
 
         return updated;
     }
 
     /**
      * Construct and execute an update expression that will modify the positions
      * of records in-place.
      */
-    int updateBookmarkPositionsInTransaction(final SQLiteDatabase db, final String[] guids,
-                                             final int offset, final int max) {
+    private static int updateBookmarkPositionsInTransaction(final SQLiteDatabase db, final String[] guids,
+                                                            final int offset, final int max) {
         int guidsCount = guids.length;
         int processCount = Math.min(max, guidsCount - offset);
 
         // Each must appear twice: once in a CASE, and once in the IN clause.
         String[] args = new String[processCount * 2];
         System.arraycopy(guids, offset, args, 0, processCount);
         System.arraycopy(guids, offset, args, processCount, processCount);
 
@@ -964,39 +1008,40 @@ public class BrowserProvider extends Tra
             if (guids[i] == null) {
                 // We don't want to issue the query if not every GUID is specified.
                 debug("updateBookmarkPositions called with null GUID at index " + i);
                 return 0;
             }
             b.append(" WHEN ? THEN " + i);
         }
 
+        // TODO: use computeSQLInClause
         b.append(" END WHERE " + Bookmarks.GUID + " IN (");
         i = 1;
         while (i++ < processCount) {
             b.append("?, ");
         }
         b.append("?)");
         db.execSQL(b.toString(), args);
 
         // We can't easily get a modified count without calling something like changes().
         return processCount;
     }
 
     /**
      * Construct an update expression that will modify the parents of any records
      * that match.
      */
-    int updateBookmarkParents(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+    private int updateBookmarkParents(SQLiteDatabase db, ContentValues values, String selection, String[] selectionArgs) {
         trace("Updating bookmark parents of " + selection + " (" + selectionArgs[0] + ")");
         String where = Bookmarks._ID + " IN (" +
                        " SELECT DISTINCT " + Bookmarks.PARENT +
                        " FROM " + TABLE_BOOKMARKS +
                        " WHERE " + selection + " )";
-        return getWritableDatabase(uri).update(TABLE_BOOKMARKS, values, where, selectionArgs);
+        return db.update(TABLE_BOOKMARKS, values, where, selectionArgs);
     }
 
     long insertBookmark(Uri uri, ContentValues values) {
         // Generate values if not specified. Don't overwrite
         // if specified by caller.
         long now = System.currentTimeMillis();
         if (!values.containsKey(Bookmarks.DATE_CREATED)) {
             values.put(Bookmarks.DATE_CREATED, now);
@@ -1012,165 +1057,151 @@ public class BrowserProvider extends Tra
 
         if (!values.containsKey(Bookmarks.POSITION)) {
             debug("Inserting bookmark with no position for URI");
             values.put(Bookmarks.POSITION,
                        Long.toString(BrowserContract.Bookmarks.DEFAULT_POSITION));
         }
 
         String url = values.getAsString(Bookmarks.URL);
-        Integer type = values.getAsInteger(Bookmarks.TYPE);
 
         debug("Inserting bookmark in database with URL: " + url);
         final SQLiteDatabase db = getWritableDatabase(uri);
+        beginWrite(db);
         return db.insertOrThrow(TABLE_BOOKMARKS, Bookmarks.TITLE, values);
     }
 
 
     int updateOrInsertBookmark(Uri uri, ContentValues values, String selection,
             String[] selectionArgs) {
         int updated = updateBookmarks(uri, values, selection, selectionArgs);
-        if (updated > 0)
+        if (updated > 0) {
             return updated;
+        }
 
+        // Transaction already begun by updateBookmarks.
         if (0 <= insertBookmark(uri, values)) {
             // We 'updated' one row.
             return 1;
         }
 
         // If something went wrong, then we updated zero rows.
         return 0;
     }
 
     int updateBookmarks(Uri uri, ContentValues values, String selection,
             String[] selectionArgs) {
         trace("Updating bookmarks on URI: " + uri);
 
-        final SQLiteDatabase db = getWritableDatabase(uri);
-        int updated = 0;
-
         final String[] bookmarksProjection = new String[] {
                 Bookmarks._ID, // 0
-                Bookmarks.URL, // 1
         };
 
-        trace("Quering bookmarks to update on URI: " + uri);
-
-        Cursor cursor = db.query(TABLE_BOOKMARKS, bookmarksProjection,
-                selection, selectionArgs, null, null, null);
-
-        try {
-            if (!values.containsKey(Bookmarks.DATE_MODIFIED))
-                values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis());
-
-            boolean updatingUrl = values.containsKey(Bookmarks.URL);
-            String url = null;
-
-            if (updatingUrl)
-                url = values.getAsString(Bookmarks.URL);
-
-            while (cursor.moveToNext()) {
-                long id = cursor.getLong(0);
-
-                trace("Updating bookmark with ID: " + id);
-
-                updated += db.update(TABLE_BOOKMARKS, values, "_id = ?",
-                        new String[] { Long.toString(id) });
-            }
-        } finally {
-            if (cursor != null)
-                cursor.close();
+        if (!values.containsKey(Bookmarks.DATE_MODIFIED)) {
+            values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis());
         }
 
-        return updated;
+        trace("Querying bookmarks to update on URI: " + uri);
+        final SQLiteDatabase db = getWritableDatabase(uri);
+
+        // Compute matching IDs.
+        final Cursor cursor = db.query(TABLE_BOOKMARKS, bookmarksProjection,
+                                       selection, selectionArgs, null, null, null);
+
+        // Now that we're done reading, open a transaction.
+        final String inClause;
+        try {
+            inClause = computeSQLInClauseFromLongs(cursor, Bookmarks._ID);
+        } finally {
+            cursor.close();
+        }
+
+        beginWrite(db);
+        return db.update(TABLE_BOOKMARKS, values, inClause, null);
     }
 
     long insertHistory(Uri uri, ContentValues values) {
-        final SQLiteDatabase db = getWritableDatabase(uri);
-
-        long now = System.currentTimeMillis();
+        final long now = System.currentTimeMillis();
         values.put(History.DATE_CREATED, now);
         values.put(History.DATE_MODIFIED, now);
 
         // Generate GUID for new history entry. Don't override specified GUIDs.
         if (!values.containsKey(History.GUID)) {
           values.put(History.GUID, Utils.generateGuid());
         }
 
         String url = values.getAsString(History.URL);
 
         debug("Inserting history in database with URL: " + url);
+        final SQLiteDatabase db = getWritableDatabase(uri);
+        beginWrite(db);
         return db.insertOrThrow(TABLE_HISTORY, History.VISITS, values);
     }
 
     int updateOrInsertHistory(Uri uri, ContentValues values, String selection,
             String[] selectionArgs) {
-        int updated = updateHistory(uri, values, selection, selectionArgs);
-        if (updated > 0)
+        final int updated = updateHistory(uri, values, selection, selectionArgs);
+        if (updated > 0) {
             return updated;
+        }
 
         // Insert a new entry if necessary
-        if (!values.containsKey(History.VISITS))
+        if (!values.containsKey(History.VISITS)) {
             values.put(History.VISITS, 1);
-        if (!values.containsKey(History.TITLE))
+        }
+        if (!values.containsKey(History.TITLE)) {
             values.put(History.TITLE, values.getAsString(History.URL));
+        }
 
         if (0 <= insertHistory(uri, values)) {
             return 1;
         }
 
         return 0;
     }
 
     int updateHistory(Uri uri, ContentValues values, String selection,
             String[] selectionArgs) {
         trace("Updating history on URI: " + uri);
 
-        final SQLiteDatabase db = getWritableDatabase(uri);
         int updated = 0;
 
         final String[] historyProjection = new String[] {
             History._ID,   // 0
             History.URL,   // 1
             History.VISITS // 2
         };
 
-        Cursor cursor = db.query(TABLE_HISTORY, historyProjection, selection,
-                selectionArgs, null, null, null);
+        final SQLiteDatabase db = getWritableDatabase(uri);
+        final Cursor cursor = db.query(TABLE_HISTORY, historyProjection, selection,
+                                       selectionArgs, null, null, null);
 
         try {
             if (!values.containsKey(Bookmarks.DATE_MODIFIED)) {
                 values.put(Bookmarks.DATE_MODIFIED,  System.currentTimeMillis());
             }
 
-            boolean updatingUrl = values.containsKey(History.URL);
-            String url = null;
-
-            if (updatingUrl)
-                url = values.getAsString(History.URL);
-
             while (cursor.moveToNext()) {
                 long id = cursor.getLong(0);
 
                 trace("Updating history entry with ID: " + id);
 
                 if (shouldIncrementVisits(uri)) {
                     long existing = cursor.getLong(2);
                     Long additional = values.getAsLong(History.VISITS);
 
                     // Increment visit count by a specified amount, or default to increment by 1
                     values.put(History.VISITS, existing + ((additional != null) ? additional.longValue() : 1));
                 }
 
                 updated += db.update(TABLE_HISTORY, values, "_id = ?",
-                        new String[] { Long.toString(id) });
+                                     new String[] { Long.toString(id) });
             }
         } finally {
-            if (cursor != null)
-                cursor.close();
+            cursor.close();
         }
 
         return updated;
     }
 
     private void updateFaviconIdsForUrl(SQLiteDatabase db, String pageUrl, Long faviconId) {
         ContentValues updateValues = new ContentValues(1);
         updateValues.put(FaviconColumns.FAVICON_ID, faviconId);
@@ -1188,42 +1219,42 @@ public class BrowserProvider extends Tra
         return insertFavicon(getWritableDatabase(uri), values);
     }
 
     long insertFavicon(SQLiteDatabase db, ContentValues values) {
         // This method is a dupicate of BrowserDatabaseHelper.insertFavicon.
         // If changes are needed, please update both
         String faviconUrl = values.getAsString(Favicons.URL);
         String pageUrl = null;
-        long faviconId;
 
         trace("Inserting favicon for URL: " + faviconUrl);
 
         DBUtils.stripEmptyByteArray(values, Favicons.DATA);
 
         // Extract the page URL from the ContentValues
         if (values.containsKey(Favicons.PAGE_URL)) {
             pageUrl = values.getAsString(Favicons.PAGE_URL);
             values.remove(Favicons.PAGE_URL);
         }
 
         // If no URL is provided, insert using the default one.
         if (TextUtils.isEmpty(faviconUrl) && !TextUtils.isEmpty(pageUrl)) {
             values.put(Favicons.URL, org.mozilla.gecko.favicons.Favicons.guessDefaultFaviconURL(pageUrl));
         }
 
-        long now = System.currentTimeMillis();
+        final long now = System.currentTimeMillis();
         values.put(Favicons.DATE_CREATED, now);
         values.put(Favicons.DATE_MODIFIED, now);
-        faviconId = db.insertOrThrow(TABLE_FAVICONS, null, values);
+
+        beginWrite(db);
+        final long faviconId = db.insertOrThrow(TABLE_FAVICONS, null, values);
 
         if (pageUrl != null) {
             updateFaviconIdsForUrl(db, pageUrl, faviconId);
         }
-
         return faviconId;
     }
 
     int updateOrInsertFavicon(Uri uri, ContentValues values, String selection,
             String[] selectionArgs) {
         return updateFavicon(uri, values, selection, selectionArgs,
                 true /* insert if needed */);
     }
@@ -1234,115 +1265,124 @@ public class BrowserProvider extends Tra
                 false /* only update, no insert */);
     }
 
     int updateFavicon(Uri uri, ContentValues values, String selection,
             String[] selectionArgs, boolean insertIfNeeded) {
         String faviconUrl = values.getAsString(Favicons.URL);
         String pageUrl = null;
         int updated = 0;
-        final SQLiteDatabase db = getWritableDatabase(uri);
-        Cursor cursor = null;
         Long faviconId = null;
         long now = System.currentTimeMillis();
 
         trace("Updating favicon for URL: " + faviconUrl);
 
         DBUtils.stripEmptyByteArray(values, Favicons.DATA);
 
         // Extract the page URL from the ContentValues
         if (values.containsKey(Favicons.PAGE_URL)) {
             pageUrl = values.getAsString(Favicons.PAGE_URL);
             values.remove(Favicons.PAGE_URL);
         }
 
         values.put(Favicons.DATE_MODIFIED, now);
 
+        final SQLiteDatabase db = getWritableDatabase(uri);
+
         // If there's no favicon URL given and we're inserting if needed, skip
         // the update and only do an insert (otherwise all rows would be
-        // updated)
+        // updated).
         if (!(insertIfNeeded && (faviconUrl == null))) {
             updated = db.update(TABLE_FAVICONS, values, selection, selectionArgs);
         }
 
         if (updated > 0) {
             if ((faviconUrl != null) && (pageUrl != null)) {
+                final Cursor cursor = db.query(TABLE_FAVICONS,
+                                               new String[] { Favicons._ID },
+                                               Favicons.URL + " = ?",
+                                               new String[] { faviconUrl },
+                                               null, null, null);
                 try {
-                    cursor = db.query(TABLE_FAVICONS,
-                                      new String[] { Favicons._ID },
-                                      Favicons.URL + " = ?",
-                                      new String[] { faviconUrl },
-                                      null, null, null);
                     if (cursor.moveToFirst()) {
                         faviconId = cursor.getLong(cursor.getColumnIndexOrThrow(Favicons._ID));
                     }
                 } finally {
-                    if (cursor != null)
-                        cursor.close();
+                    cursor.close();
                 }
             }
+            if (pageUrl != null) {
+                beginWrite(db);
+            }
         } else if (insertIfNeeded) {
             values.put(Favicons.DATE_CREATED, now);
 
             trace("No update, inserting favicon for URL: " + faviconUrl);
+            beginWrite(db);
             faviconId = db.insert(TABLE_FAVICONS, null, values);
             updated = 1;
         }
 
         if (pageUrl != null) {
             updateFaviconIdsForUrl(db, pageUrl, faviconId);
         }
 
         return updated;
     }
 
-    long insertThumbnail(Uri uri, ContentValues values) {
-        String url = values.getAsString(Thumbnails.URL);
-        final SQLiteDatabase db = getWritableDatabase(uri);
+    private long insertThumbnail(Uri uri, ContentValues values) {
+        final String url = values.getAsString(Thumbnails.URL);
 
         trace("Inserting thumbnail for URL: " + url);
 
         DBUtils.stripEmptyByteArray(values, Thumbnails.DATA);
 
+        final SQLiteDatabase db = getWritableDatabase(uri);
+        beginWrite(db);
         return db.insertOrThrow(TABLE_THUMBNAILS, null, values);
     }
 
-    int updateOrInsertThumbnail(Uri uri, ContentValues values, String selection,
+    private int updateOrInsertThumbnail(Uri uri, ContentValues values, String selection,
             String[] selectionArgs) {
         return updateThumbnail(uri, values, selection, selectionArgs,
                 true /* insert if needed */);
     }
 
-    int updateExistingThumbnail(Uri uri, ContentValues values, String selection,
+    private int updateExistingThumbnail(Uri uri, ContentValues values, String selection,
             String[] selectionArgs) {
         return updateThumbnail(uri, values, selection, selectionArgs,
                 false /* only update, no insert */);
     }
 
-    int updateThumbnail(Uri uri, ContentValues values, String selection,
+    private int updateThumbnail(Uri uri, ContentValues values, String selection,
             String[] selectionArgs, boolean insertIfNeeded) {
-        String url = values.getAsString(Thumbnails.URL);
-        int updated = 0;
-        final SQLiteDatabase db = getWritableDatabase(uri);
-
+        final String url = values.getAsString(Thumbnails.URL);
         DBUtils.stripEmptyByteArray(values, Thumbnails.DATA);
 
         trace("Updating thumbnail for URL: " + url);
 
-        updated = db.update(TABLE_THUMBNAILS, values, selection, selectionArgs);
+        final SQLiteDatabase db = getWritableDatabase(uri);
+        beginWrite(db);
+        int updated = db.update(TABLE_THUMBNAILS, values, selection, selectionArgs);
 
         if (updated == 0 && insertIfNeeded) {
             trace("No update, inserting thumbnail for URL: " + url);
             db.insert(TABLE_THUMBNAILS, null, values);
             updated = 1;
         }
 
         return updated;
     }
 
+    /**
+     * This method does not create a new transaction. Its first operation is
+     * guaranteed to be a write, which in the case of a new enclosing
+     * transaction will guarantee that a read does not need to be upgraded to
+     * a write.
+     */
     int deleteHistory(Uri uri, String selection, String[] selectionArgs) {
         debug("Deleting history entry for URI: " + uri);
 
         final SQLiteDatabase db = getWritableDatabase(uri);
 
         if (isCallerSync(uri)) {
             return db.delete(TABLE_HISTORY, selection, selectionArgs);
         }
@@ -1355,36 +1395,59 @@ public class BrowserProvider extends Tra
         // Wipe sensitive data.
         values.putNull(History.TITLE);
         values.put(History.URL, "");          // Column is NOT NULL.
         values.put(History.DATE_CREATED, 0);
         values.put(History.DATE_LAST_VISITED, 0);
         values.put(History.VISITS, 0);
         values.put(History.DATE_MODIFIED, System.currentTimeMillis());
 
-        cleanupSomeDeletedRecords(uri, History.CONTENT_URI, TABLE_HISTORY);
-        return db.update(TABLE_HISTORY, values, selection, selectionArgs);
+        // Doing this UPDATE (or the DELETE above) first ensures that the
+        // first operation within a new enclosing transaction is a write.
+        // The cleanup call below will do a SELECT first, and thus would
+        // require the transaction to be upgraded from a reader to a writer.
+        // In some cases that upgrade can fail (SQLITE_BUSY), so we avoid
+        // it if we can.
+        final int updated = db.update(TABLE_HISTORY, values, selection, selectionArgs);
+        try {
+            cleanupSomeDeletedRecords(uri, History.CONTENT_URI, TABLE_HISTORY);
+        } catch (Exception e) {
+            // We don't care.
+            Log.e(LOGTAG, "Unable to clean up deleted history records: ", e);
+        }
+        return updated;
     }
 
     int deleteBookmarks(Uri uri, String selection, String[] selectionArgs) {
         debug("Deleting bookmarks for URI: " + uri);
 
         final SQLiteDatabase db = getWritableDatabase(uri);
 
         if (isCallerSync(uri)) {
+            beginWrite(db);
             return db.delete(TABLE_BOOKMARKS, selection, selectionArgs);
         }
 
         debug("Marking bookmarks as deleted for URI: " + uri);
 
         ContentValues values = new ContentValues();
         values.put(Bookmarks.IS_DELETED, 1);
 
-        cleanupSomeDeletedRecords(uri, Bookmarks.CONTENT_URI, TABLE_BOOKMARKS);
-        return updateBookmarks(uri, values, selection, selectionArgs);
+        // Doing this UPDATE (or the DELETE above) first ensures that the
+        // first operation within this transaction is a write.
+        // The cleanup call below will do a SELECT first, and thus would
+        // require the transaction to be upgraded from a reader to a writer.
+        final int updated = updateBookmarks(uri, values, selection, selectionArgs);
+        try {
+            cleanupSomeDeletedRecords(uri, Bookmarks.CONTENT_URI, TABLE_BOOKMARKS);
+        } catch (Exception e) {
+            // We don't care.
+            Log.e(LOGTAG, "Unable to clean up deleted bookmark records: ", e);
+        }
+        return updated;
     }
 
     int deleteFavicons(Uri uri, String selection, String[] selectionArgs) {
         debug("Deleting favicons for URI: " + uri);
 
         final SQLiteDatabase db = getWritableDatabase(uri);
 
         return db.delete(TABLE_FAVICONS, selection, selectionArgs);
@@ -1425,39 +1488,46 @@ public class BrowserProvider extends Tra
                deleteThumbnails(uri, thumbnailSelection, null);
     }
 
     @Override
     public ContentProviderResult[] applyBatch (ArrayList<ContentProviderOperation> operations)
         throws OperationApplicationException {
         final int numOperations = operations.size();
         final ContentProviderResult[] results = new ContentProviderResult[numOperations];
-        boolean failures = false;
-        SQLiteDatabase db = null;
 
-        if (numOperations >= 1) {
-            // We only have 1 database for all Uri's that we can get
-            db = getWritableDatabase(operations.get(0).getUri());
-        } else {
+        if (numOperations < 1) {
+            debug("applyBatch: no operations; returning immediately.");
             // The original Android implementation returns a zero-length
-            // array in this case, we do the same.
+            // array in this case. We do the same.
             return results;
         }
 
+        boolean failures = false;
+
+        // We only have 1 database for all Uris that we can get.
+        SQLiteDatabase db = getWritableDatabase(operations.get(0).getUri());
+
         // Note that the apply() call may cause us to generate
-        // additional transactions for the invidual operations.
+        // additional transactions for the individual operations.
         // But Android's wrapper for SQLite supports nested transactions,
         // so this will do the right thing.
-        db.beginTransaction();
+        //
+        // Note further that in some circumstances this can result in
+        // exceptions: if this transaction is first involved in reading,
+        // and then (naturally) tries to perform writes, SQLITE_BUSY can
+        // be raised. See Bug 947939 and friends.
+        beginBatch(db);
 
         for (int i = 0; i < numOperations; i++) {
             try {
-                results[i] = operations.get(i).apply(this, results, i);
+                final ContentProviderOperation operation = operations.get(i);
+                results[i] = operation.apply(this, results, i);
             } catch (SQLException e) {
-                Log.w(LOGTAG, "SQLite Exception during applyBatch: ", e);
+                Log.w(LOGTAG, "SQLite Exception during applyBatch.", e);
                 // The Android API makes it implementation-defined whether
                 // the failure of a single operation makes all others abort
                 // or not. For our use cases, best-effort operation makes
                 // more sense. Rolling back and forcing the caller to retry
                 // after it figures out what went wrong isn't very convenient
                 // anyway.
                 // Signal failed operation back, so the caller knows what
                 // went through and what didn't.
@@ -1480,18 +1550,18 @@ public class BrowserProvider extends Tra
                 failures = true;
                 db.setTransactionSuccessful();
                 db.endTransaction();
                 db.beginTransaction();
             }
         }
 
         trace("Flushing DB applyBatch...");
-        db.setTransactionSuccessful();
-        db.endTransaction();
+        markBatchSuccessful(db);
+        endBatch(db);
 
         if (failures) {
             throw new OperationApplicationException();
         }
 
         return results;
     }
 
--- a/mobile/android/base/db/TransactionalProvider.java
+++ b/mobile/android/base/db/TransactionalProvider.java
@@ -52,31 +52,33 @@ public abstract class TransactionalProvi
      * @param values column values to be inserted
      * @return       a URI for the newly inserted item
      */
     abstract protected Uri insertInTransaction(Uri uri, ContentValues values);
 
     /*
      * Deletes items from the database within a DB transaction.
      *
-     * @param uri            query URI
-     * @param selection      An optional filter to match rows to update.
-     * @param selectionArgs  arguments for the selection
-     * @return               number of rows impacted by the deletion
+     * @param uri            Query URI.
+     * @param selection      An optional filter to match rows to delete.
+     * @param selectionArgs  An array of arguments to substitute into the selection.
+     *
+     * @return number of rows impacted by the deletion.
      */
     abstract protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs);
 
     /*
      * Updates the database within a DB transaction.
      *
-     * @param uri            Query URI
+     * @param uri            Query URI.
      * @param values         A set of column_name/value pairs to add to the database.
      * @param selection      An optional filter to match rows to update.
-     * @param selectionArgs  Arguments for the selection
-     * @return               number of rows impacted by the update
+     * @param selectionArgs  An array of arguments to substitute into the selection.
+     *
+     * @return               number of rows impacted by the update.
      */
     abstract protected int updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs);
 
     /*
      * Fetches a readable database based on the profile indicated in the
      * passed URI. If the URI does not contain a profile param, the default profile
      * is used.
      *
@@ -104,114 +106,211 @@ public abstract class TransactionalProvi
         String profile = null;
         if (uri != null) {
             profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
         }
 
         return mDatabases.getDatabaseHelperForProfile(profile, isTest(uri)).getWritableDatabase();
     }
 
+    protected SQLiteDatabase getWritableDatabaseForProfile(String profile, boolean isTest) {
+        return mDatabases.getDatabaseHelperForProfile(profile, isTest).getWritableDatabase();
+    }
+
     @Override
     public boolean onCreate() {
         synchronized (this) {
             mContext = getContext();
             mDatabases = new PerProfileDatabases<T>(
                 getContext(), getDatabaseName(), new DatabaseHelperFactory<T>() {
                     @Override
                     public T makeDatabaseHelper(Context context, String databasePath) {
                         return createDatabaseHelper(context, databasePath);
                     }
                 });
         }
 
         return true;
     }
 
+    /**
+     * Return true if OS version and database parallelism support indicates
+     * that this provider should bundle writes into transactions.
+     */
+    @SuppressWarnings("static-method")
+    protected boolean shouldUseTransactions() {
+        return Build.VERSION.SDK_INT >= 11;
+    }
+
+    /**
+     * Track whether we're in a batch operation.
+     *
+     * When we're in a batch operation, individual write steps won't even try
+     * to start a transaction... and neither will they attempt to finish one.
+     *
+     * Set this to <code>Boolean.TRUE</code> when you're entering a batch --
+     * a section of code in which {@link ContentProvider} methods will be
+     * called, but nested transactions should not be started. Callers are
+     * responsible for beginning and ending the enclosing transaction, and
+     * for setting this to <code>Boolean.FALSE</code> when done.
+     *
+     * This is a ThreadLocal separate from `db.inTransaction` because batched
+     * operations start transactions independent of individual ContentProvider
+     * operations. This doesn't work well with the entire concept of this
+     * abstract class -- that is, automatically beginning and ending transactions
+     * for each insert/delete/update operation -- and doing so without
+     * causing arbitrary nesting requires external tracking.
+     *
+     * Note that beginWrite takes a DB argument, but we don't differentiate
+     * between databases in this tracking flag. If your ContentProvider manages
+     * multiple database transactions within the same thread, you'll need to
+     * amend this scheme -- but then, you're already doing some serious wizardry,
+     * so rock on.
+     */
+    final ThreadLocal<Boolean> isInBatchOperation = new ThreadLocal<Boolean>();
+
+    private boolean isInBatch() {
+        final Boolean isInBatch = isInBatchOperation.get();
+        if (isInBatch == null) {
+            return false;
+        }
+        return isInBatch.booleanValue();
+    }
+
+    /**
+     * If we're not currently in a transaction, and we should be, start one.
+     */
+    protected void beginWrite(final SQLiteDatabase db) {
+        if (isInBatch()) {
+            trace("Not bothering with an intermediate write transaction: inside batch operation.");
+            return;
+        }
+
+        if (shouldUseTransactions() && !db.inTransaction()) {
+            trace("beginWrite: beginning transaction.");
+            db.beginTransaction();
+        }
+    }
+
+    /**
+     * If we're not in a batch, but we are in a write transaction, mark it as
+     * successful.
+     */
+    protected void markWriteSuccessful(final SQLiteDatabase db) {
+        if (isInBatch()) {
+            trace("Not marking write successful: inside batch operation.");
+            return;
+        }
+
+        if (shouldUseTransactions() && db.inTransaction()) {
+            trace("Marking write transaction successful.");
+            db.setTransactionSuccessful();
+        }
+    }
+
+    /**
+     * If we're not in a batch, but we are in a write transaction,
+     * end it.
+     *
+     * @see TransactionalProvider#markWriteSuccessful(SQLiteDatabase)
+     */
+    protected void endWrite(final SQLiteDatabase db) {
+        if (isInBatch()) {
+            trace("Not ending write: inside batch operation.");
+            return;
+        }
+
+        if (shouldUseTransactions() && db.inTransaction()) {
+            trace("endWrite: ending transaction.");
+            db.endTransaction();
+        }
+    }
+
+    protected void beginBatch(final SQLiteDatabase db) {
+        trace("Beginning batch.");
+        isInBatchOperation.set(Boolean.TRUE);
+        db.beginTransaction();
+    }
+
+    protected void markBatchSuccessful(final SQLiteDatabase db) {
+        if (isInBatch()) {
+            trace("Marking batch successful.");
+            db.setTransactionSuccessful();
+            return;
+        }
+        Log.w(LOGTAG, "Unexpectedly asked to mark batch successful, but not in batch!");
+        throw new IllegalStateException("Not in batch.");
+    }
+
+    protected void endBatch(final SQLiteDatabase db) {
+        trace("Ending batch.");
+        db.endTransaction();
+        isInBatchOperation.set(Boolean.FALSE);
+    }
+
     @Override
     public int delete(Uri uri, String selection, String[] selectionArgs) {
-        trace("Calling delete on URI: " + uri);
+        trace("Calling delete on URI: " + uri + ", " + selection + ", " + selectionArgs);
 
         final SQLiteDatabase db = getWritableDatabase(uri);
         int deleted = 0;
 
-        if (Build.VERSION.SDK_INT >= 11) {
-            trace("Beginning delete transaction: " + uri);
-            db.beginTransaction();
-            try {
-                deleted = deleteInTransaction(uri, selection, selectionArgs);
-                db.setTransactionSuccessful();
-                trace("Successful delete transaction: " + uri);
-            } finally {
-                db.endTransaction();
-            }
-        } else {
+        try {
             deleted = deleteInTransaction(uri, selection, selectionArgs);
+            markWriteSuccessful(db);
+        } finally {
+            endWrite(db);
         }
 
         if (deleted > 0) {
             getContext().getContentResolver().notifyChange(uri, null);
         }
 
         return deleted;
     }
 
     @Override
     public Uri insert(Uri uri, ContentValues values) {
         trace("Calling insert on URI: " + uri);
 
         final SQLiteDatabase db = getWritableDatabase(uri);
         Uri result = null;
         try {
-            if (Build.VERSION.SDK_INT >= 11) {
-                trace("Beginning insert transaction: " + uri);
-                db.beginTransaction();
-                try {
-                    result = insertInTransaction(uri, values);
-                    db.setTransactionSuccessful();
-                    trace("Successful insert transaction: " + uri);
-                } finally {
-                    db.endTransaction();
-                }
-            } else {
-                result = insertInTransaction(uri, values);
-            }
+            result = insertInTransaction(uri, values);
+            markWriteSuccessful(db);
         } catch (SQLException sqle) {
             Log.e(LOGTAG, "exception in DB operation", sqle);
         } catch (UnsupportedOperationException uoe) {
             Log.e(LOGTAG, "don't know how to perform that insert", uoe);
+        } finally {
+            endWrite(db);
         }
 
         if (result != null) {
             getContext().getContentResolver().notifyChange(uri, null);
         }
 
         return result;
     }
 
 
     @Override
     public int update(Uri uri, ContentValues values, String selection,
             String[] selectionArgs) {
-        trace("Calling update on URI: " + uri);
+        trace("Calling update on URI: " + uri + ", " + selection + ", " + selectionArgs);
 
         final SQLiteDatabase db = getWritableDatabase(uri);
         int updated = 0;
 
-        if (Build.VERSION.SDK_INT >= 11) {
-            trace("Beginning update transaction: " + uri);
-            db.beginTransaction();
-            try {
-                updated = updateInTransaction(uri, values, selection, selectionArgs);
-                db.setTransactionSuccessful();
-                trace("Successful update transaction: " + uri);
-            } finally {
-                db.endTransaction();
-            }
-        } else {
-            updated = updateInTransaction(uri, values, selection, selectionArgs);
+        try {
+            updated = updateInTransaction(uri, values, selection,
+                                          selectionArgs);
+            markWriteSuccessful(db);
+        } finally {
+            endWrite(db);
         }
 
         if (updated > 0) {
             getContext().getContentResolver().notifyChange(uri, null);
         }
 
         return updated;
     }
@@ -222,27 +321,29 @@ public abstract class TransactionalProvi
             return 0;
         }
 
         int numValues = values.length;
         int successes = 0;
 
         final SQLiteDatabase db = getWritableDatabase(uri);
 
-        db.beginTransaction();
+        debug("bulkInsert: explicitly starting transaction.");
+        beginBatch(db);
 
         try {
             for (int i = 0; i < numValues; i++) {
                 insertInTransaction(uri, values[i]);
                 successes++;
             }
             trace("Flushing DB bulkinsert...");
-            db.setTransactionSuccessful();
+            markBatchSuccessful(db);
         } finally {
-            db.endTransaction();
+            debug("bulkInsert: explicitly ending transaction.");
+            endBatch(db);
         }
 
         if (successes > 0) {
             mContext.getContentResolver().notifyChange(uri, null);
         }
 
         return successes;
     }
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -292,21 +292,24 @@ gbjar.sources += [
     'preferences/PrivateDataPreference.java',
     'preferences/SearchEnginePreference.java',
     'preferences/SearchPreferenceCategory.java',
     'preferences/SyncPreference.java',
     'PrefsHelper.java',
     'PrivateTab.java',
     'prompts/ColorPickerInput.java',
     'prompts/IconGridInput.java',
+    'prompts/IntentChooserPrompt.java',
+    'prompts/IntentHandler.java',
     'prompts/Prompt.java',
     'prompts/PromptInput.java',
     'prompts/PromptListAdapter.java',
     'prompts/PromptListItem.java',
     'prompts/PromptService.java',
+    'prompts/TabInput.java',
     'ReaderModeUtils.java',
     'ReferrerReceiver.java',
     'RemoteTabs.java',
     'Restarter.java',
     'ScrollAnimator.java',
     'ServiceNotificationClient.java',
     'SessionParser.java',
     'SharedPreferencesHelper.java',
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/prompts/IntentChooserPrompt.java
@@ -0,0 +1,157 @@
+/* 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.prompts;
+
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.util.ThreadUtils;
+import org.mozilla.gecko.widget.GeckoActionProvider;
+
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.widget.ListView;
+import android.util.Log;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Shows a prompt letting the user pick from a list of intent handlers for a set of Intents or
+ * for a GeckoActionProvider.  Basic usage:
+ *   IntentChooserPrompt prompt = new IntentChooserPrompt(context, new Intent[] {
+ *      ... // some intents
+ *    });
+ *    prompt.show("Title", context, new IntentHandler() {
+ *        public void onIntentSelected(Intent intent, int position) { }
+ *        public void onCancelled() { }
+ *    });
+ **/
+public class IntentChooserPrompt {
+    private static final String LOGTAG = "GeckoIntentChooser";
+
+    private final ArrayList<PromptListItem> mItems;
+
+    public IntentChooserPrompt(Context context, Intent[] intents) {
+        mItems = getItems(context, intents);
+    }
+
+    public IntentChooserPrompt(Context context, GeckoActionProvider provider) {
+        mItems = getItems(context, provider);
+    }
+
+    /* If an IntentHandler is passed in, will asynchronously call the handler when the dialog is closed
+     * Otherwise, will return the Intent that was chosen by the user. Must be called on the UI thread.
+     */
+    public void show(final String title, final Context context, final IntentHandler handler) {
+        ThreadUtils.assertOnUiThread();
+
+        if (mItems.isEmpty()) {
+            Log.i(LOGTAG, "No activities for the intent chooser!");
+            handler.onCancelled();
+            return;
+        }
+
+        // If there's only one item in the intent list, just return it
+        if (mItems.size() == 1) {
+            handler.onIntentSelected(mItems.get(0).intent, 0);
+            return;
+        }
+
+        final Prompt prompt = new Prompt(context, new Prompt.PromptCallback() {
+            @Override
+            public void onPromptFinished(String promptServiceResult) {
+                if (handler == null) {
+                    return;
+                }
+
+                int itemId = -1;
+                try {
+                    itemId = new JSONObject(promptServiceResult).getInt("button");
+                } catch (JSONException e) {
+                    Log.e(LOGTAG, "result from promptservice was invalid: ", e);
+                }
+
+                if (itemId == -1) {
+                    handler.onCancelled();
+                } else {
+                    handler.onIntentSelected(mItems.get(itemId).intent, itemId);
+                }
+            }
+        });
+
+        PromptListItem[] arrays = new PromptListItem[mItems.size()];
+        mItems.toArray(arrays);
+        prompt.show(title, "", arrays, ListView.CHOICE_MODE_NONE);
+
+        return;
+    }
+
+    // Whether or not any activities were found. Useful for checking if you should try a different Intent set
+    public boolean hasActivities(Context context) {
+        return mItems.isEmpty();
+    }
+
+    // Gets a list of PromptListItems for an Intent array
+    private ArrayList<PromptListItem> getItems(final Context context, Intent[] intents) {
+        final ArrayList<PromptListItem> items = new ArrayList<PromptListItem>();
+
+        // If we have intents, use them to build the initial list
+        for (final Intent intent : intents) {
+            items.addAll(getItemsForIntent(context, intent));
+        }
+
+        return items;
+    }
+
+    // Gets a list of PromptListItems for a GeckoActionProvider
+    private ArrayList<PromptListItem> getItems(final Context context, final GeckoActionProvider provider) {
+        final ArrayList<PromptListItem> items = new ArrayList<PromptListItem>();
+
+        // Add any intents from the provider.
+        final PackageManager packageManager = context.getPackageManager();
+        final ArrayList<ResolveInfo> infos = provider.getSortedActivites();
+
+        for (final ResolveInfo info : infos) {
+            items.add(getItemForResolveInfo(info, packageManager, provider.getIntent()));
+        }
+
+        return items;
+    }
+
+    private PromptListItem getItemForResolveInfo(ResolveInfo info, PackageManager pm, Intent intent) {
+        PromptListItem item = new PromptListItem(info.loadLabel(pm).toString());
+        item.icon = info.loadIcon(pm);
+        item.intent = new Intent(intent);
+
+        // These intents should be implicit.
+        item.intent.setComponent(new ComponentName(info.activityInfo.applicationInfo.packageName,
+                                                   info.activityInfo.name));
+        return item;
+    }
+
+    private ArrayList<PromptListItem> getItemsForIntent(Context context, Intent intent) {
+        ArrayList<PromptListItem> items = new ArrayList<PromptListItem>();
+        PackageManager pm = context.getPackageManager();
+        List<ResolveInfo> lri = pm.queryIntentActivityOptions(GeckoAppShell.getGeckoInterface().getActivity().getComponentName(), null, intent, 0);
+
+        // If we didn't find any activities, just return the empty list
+        if (lri == null) {
+            return items;
+        }
+
+        // Otherwise, convert the ResolveInfo. Note we don't currently check for duplicates here.
+        for (ResolveInfo ri : lri) {
+            items.add(getItemForResolveInfo(ri, pm, intent));
+        }
+
+        return items;
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/prompts/IntentHandler.java
@@ -0,0 +1,12 @@
+/* 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.prompts;
+
+import android.content.Intent;
+
+public interface IntentHandler {
+    public void onIntentSelected(Intent intent, int position);
+    public void onCancelled();
+}
--- a/mobile/android/base/prompts/PromptInput.java
+++ b/mobile/android/base/prompts/PromptInput.java
@@ -71,16 +71,17 @@ public class PromptInput {
                     }
                 });
                 input.requestFocus();
             }
 
             mView = (View)input;
             return mView;
         }
+
         @Override
         public Object getValue() {
             EditText edit = (EditText)mView;
             return edit.getText();
         }
     }
 
     public static class NumberInput extends EditInput {
@@ -106,16 +107,17 @@ public class PromptInput {
 
         public View getView(Context context) throws UnsupportedOperationException {
             EditText input = (EditText) super.getView(context);
             input.setInputType(InputType.TYPE_CLASS_TEXT |
                                InputType.TYPE_TEXT_VARIATION_PASSWORD |
                                InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
             return input;
         }
+
         @Override
         public Object getValue() {
             EditText edit = (EditText)mView;
             return edit.getText();
         }
     }
 
     public static class CheckboxInput extends PromptInput {
@@ -130,16 +132,17 @@ public class PromptInput {
         public View getView(Context context) throws UnsupportedOperationException {
             CheckBox checkbox = new CheckBox(context);
             checkbox.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT));
             checkbox.setText(mLabel);
             checkbox.setChecked(mChecked);
             mView = (View)checkbox;
             return mView;
         }
+
         @Override
         public Object getValue() {
             CheckBox checkbox = (CheckBox)mView;
             return checkbox.isChecked() ? Boolean.TRUE : Boolean.FALSE;
         }
     }
 
     public static class DateTimeInput extends PromptInput {
@@ -208,16 +211,17 @@ public class PromptInput {
                 mView = (View)input;
             }
             return mView;
         }
 
         private static String formatDateString(String dateFormat, Calendar calendar) {
             return new SimpleDateFormat(dateFormat).format(calendar.getTime());
         }
+
         @Override
         public Object getValue() {
             if (Build.VERSION.SDK_INT < 11 && mType.equals("date")) {
                 // We can't use the custom DateTimePicker with a sdk older than 11.
                 // Fallback on the native DatePicker.
                 DatePicker dp = (DatePicker)mView;
                 GregorianCalendar calendar =
                     new GregorianCalendar(dp.getYear(),dp.getMonth(),dp.getDayOfMonth());
@@ -288,16 +292,17 @@ public class PromptInput {
                 container.addView(textView);
 
                 container.addView(spinner);
                 return container;
             }
 
             return spinner;
         }
+
         @Override
         public Object getValue() {
             return new Integer(spinner.getSelectedItemPosition());
         }
     }
 
     public static class LabelInput extends PromptInput {
         public static final String INPUT_TYPE = "label";
@@ -307,20 +312,16 @@ public class PromptInput {
 
         public View getView(Context context) throws UnsupportedOperationException {
             // not really an input, but a way to add labels and such to the dialog
             TextView view = new TextView(context);
             view.setText(Html.fromHtml(mLabel));
             mView = view;
             return mView;
         }
-        @Override
-        public Object getValue() {
-            return "";
-        }
     }
 
     public PromptInput(JSONObject obj) {
         mLabel = obj.optString("label");
         mType = obj.optString("type");
         String id = obj.optString("id");
         mId = TextUtils.isEmpty(id) ? mType : id;
         mValue = obj.optString("value");
@@ -339,16 +340,18 @@ public class PromptInput {
         } else if (MenulistInput.INPUT_TYPE.equals(type)) {
             return new MenulistInput(obj);
         } else if (LabelInput.INPUT_TYPE.equals(type)) {
             return new LabelInput(obj);
         } else if (IconGridInput.INPUT_TYPE.equals(type)) {
             return new IconGridInput(obj);
         } else if (ColorPickerInput.INPUT_TYPE.equals(type)) {
             return new ColorPickerInput(obj);
+        } else if (TabInput.INPUT_TYPE.equals(type)) {
+            return new TabInput(obj);
         } else {
             for (String dtType : DateTimeInput.INPUT_TYPES) {
                 if (dtType.equals(type)) {
                     return new DateTimeInput(obj);
                 }
             }
         }
         return null;
@@ -358,19 +361,19 @@ public class PromptInput {
         return null;
     }
 
     public String getId() {
         return mId;
     }
 
     public Object getValue() {
-        return "";
+        return null;
     }
 
     public boolean getScrollable() {
         return false;
     }
 
     public boolean canApplyInputStyle() {
-	return true;
+        return true;
     }
 }
--- a/mobile/android/base/prompts/PromptListItem.java
+++ b/mobile/android/base/prompts/PromptListItem.java
@@ -1,27 +1,30 @@
 package org.mozilla.gecko.prompts;
 
 import org.json.JSONArray;
 import org.json.JSONObject;
 import org.json.JSONException;
 
+import android.content.Intent;
 import android.graphics.drawable.Drawable;
+
 import java.util.List;
 import java.util.ArrayList;
 
 // This class should die and be replaced with normal menu items
 public class PromptListItem {
     private static final String LOGTAG = "GeckoPromptListItem";
     public final String label;
     public final boolean isGroup;
     public final boolean inGroup;
     public final boolean disabled;
     public final int id;
     public boolean selected;
+    public Intent intent;
 
     public boolean isParent;
     public Drawable icon;
 
     PromptListItem(JSONObject aObject) {
         label = aObject.optString("label");
         isGroup = aObject.optBoolean("isGroup");
         inGroup = aObject.optBoolean("inGroup");
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/prompts/TabInput.java
@@ -0,0 +1,109 @@
+/* -*- 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.prompts;
+
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.json.JSONException;
+
+import android.content.Context;
+import android.view.View;
+import android.view.LayoutInflater;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.TabHost;
+import android.widget.TabWidget;
+import android.widget.FrameLayout;
+import android.widget.ListView;
+import android.widget.LinearLayout.LayoutParams;
+import java.util.LinkedHashMap;
+import android.util.Log;
+import android.widget.AdapterView;
+
+public class TabInput extends PromptInput implements AdapterView.OnItemClickListener {
+    public static final String INPUT_TYPE = "tabs";
+    public static final String LOGTAG = "GeckoTabInput";
+
+    /* Keeping the order of this in sync with the JSON is important. */
+    final private LinkedHashMap<String, PromptListItem[]> mTabs;
+
+    private TabHost mHost;
+    private int mPosition;
+
+    public TabInput(JSONObject obj) {
+        super(obj);
+        mTabs = new LinkedHashMap<String, PromptListItem[]>();
+        try {
+            JSONArray tabs = obj.getJSONArray("items");
+            for (int i = 0; i < tabs.length(); i++) {
+                JSONObject tab = tabs.getJSONObject(i);
+                String title = tab.getString("label");
+                JSONArray items = tab.getJSONArray("items");
+                mTabs.put(title, PromptListItem.getArray(items));
+            }
+        } catch(JSONException ex) {
+            Log.e(LOGTAG, "Exception", ex);
+        }
+    }
+
+    @Override
+    public View getView(final Context context) throws UnsupportedOperationException {
+        LayoutInflater inflater = LayoutInflater.from(context);
+        mHost = (TabHost) inflater.inflate(R.layout.tab_prompt_input, null);
+        mHost.setup();
+
+        for (String title : mTabs.keySet()) {
+            final TabHost.TabSpec spec = mHost.newTabSpec(title);
+            spec.setContent(new TabHost.TabContentFactory() {
+                @Override
+                public View createTabContent(final String tag) {
+                    PromptListAdapter adapter = new PromptListAdapter(context, android.R.layout.simple_list_item_1, mTabs.get(tag));
+                    ListView listView = new ListView(context);
+                    listView.setOnItemClickListener(TabInput.this);
+                    listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
+                    listView.setAdapter(adapter);
+                    return listView;
+                }
+            });
+
+            spec.setIndicator(title);
+            mHost.addTab(spec);
+        }
+        mView = mHost;
+        return mHost;
+    }
+
+    @Override
+    public Object getValue() {
+        JSONObject obj = new JSONObject();
+        try {
+            obj.put("tab", mHost.getCurrentTab());
+            obj.put("item", mPosition);
+        } catch(JSONException ex) { }
+
+        return obj;
+    }
+
+    @Override
+    public boolean getScrollable() {
+        return true;
+    }
+
+    @Override
+    public boolean canApplyInputStyle() {
+        return false;
+    }
+
+    @Override
+    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+        ThreadUtils.assertOnUiThread();
+        mPosition = position;
+    }
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/tab_prompt_input.xml
@@ -0,0 +1,31 @@
+<?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/. -->
+
+<TabHost xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@android:id/tabhost"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent" >
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical"
+        android:minHeight="100dp">
+
+        <TabWidget
+            android:id="@android:id/tabs"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content" >
+        </TabWidget>
+
+        <FrameLayout
+            android:id="@android:id/tabcontent"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent" >
+        </FrameLayout>
+
+    </LinearLayout>
+
+</TabHost>
--- a/mobile/android/base/tests/testBrowserProvider.java
+++ b/mobile/android/base/tests/testBrowserProvider.java
@@ -60,27 +60,51 @@ public class testBrowserProvider extends
                                         BrowserContract.Bookmarks.MOBILE_FOLDER_GUID,
                                         BrowserContract.Bookmarks.MENU_FOLDER_GUID,
                                         BrowserContract.Bookmarks.TAGS_FOLDER_GUID,
                                         BrowserContract.Bookmarks.TOOLBAR_FOLDER_GUID,
                                         BrowserContract.Bookmarks.UNFILED_FOLDER_GUID,
                                         BrowserContract.Bookmarks.READING_LIST_FOLDER_GUID });
 
         c = mProvider.query(appendUriParam(BrowserContract.Bookmarks.CONTENT_URI, BrowserContract.PARAM_SHOW_DELETED, "1"), null, null, null, null);
-        mAsserter.is(c.getCount(), 7, "All non-special bookmarks and folders were deleted");
+        assertCountIsAndClose(c, 7, "All non-special bookmarks and folders were deleted");
 
         mProvider.delete(appendUriParam(BrowserContract.History.CONTENT_URI, BrowserContract.PARAM_IS_SYNC, "1"), null, null);
         c = mProvider.query(appendUriParam(BrowserContract.History.CONTENT_URI, BrowserContract.PARAM_SHOW_DELETED, "1"), null, null, null, null);
-        mAsserter.is(c.getCount(), 0, "All history entries were deleted");
+        assertCountIsAndClose(c, 0, "All history entries were deleted");
+
+        /**
+         * There's no reason why the following two parts should fail.
+         * But sometimes they do, and I'm not going to spend the time
+         * to figure out why in an unrelated bug.
+         */
+
+        mProvider.delete(appendUriParam(BrowserContract.Favicons.CONTENT_URI, BrowserContract.PARAM_IS_SYNC, "1"), null, null);
+        c = mProvider.query(appendUriParam(BrowserContract.Favicons.CONTENT_URI,
+                                           BrowserContract.PARAM_SHOW_DELETED, "1"),
+                                           null, null, null, null);
 
-        mProvider.delete(BrowserContract.Favicons.CONTENT_URI, null, null);
-        mAsserter.is(c.getCount(), 0, "All favicons were deleted");
+        if (c.getCount() > 0) {
+          mAsserter.dumpLog("Unexpected favicons in ensureEmptyDatabase.");
+        }
+        c.close();
+
+        // assertCountIsAndClose(c, 0, "All favicons were deleted");
 
-        mProvider.delete(BrowserContract.Thumbnails.CONTENT_URI, null, null);
-        mAsserter.is(c.getCount(), 0, "All thumbnails were deleted");
+        mProvider.delete(appendUriParam(BrowserContract.Thumbnails.CONTENT_URI, BrowserContract.PARAM_IS_SYNC, "1"), null, null);
+        c = mProvider.query(appendUriParam(BrowserContract.Thumbnails.CONTENT_URI,
+                                           BrowserContract.PARAM_SHOW_DELETED, "1"),
+                                           null, null, null, null);
+
+        if (c.getCount() > 0) {
+          mAsserter.dumpLog("Unexpected thumbnails in ensureEmptyDatabase.");
+        }
+        c.close();
+
+        // assertCountIsAndClose(c, 0, "All thumbnails were deleted");
     }
 
     private ContentValues createBookmark(String title, String url, long parentId,
             int type, int position, String tags, String description, String keyword) throws Exception {
         ContentValues bookmark = new ContentValues();
 
         bookmark.put(BrowserContract.Bookmarks.TITLE, title);
         bookmark.put(BrowserContract.Bookmarks.URL, url);
@@ -321,31 +345,31 @@ public class testBrowserProvider extends
             try {
                 applyResult = mProvider.applyBatch(mOperations);
             } catch (OperationApplicationException ex) {
                 seenException = true;
             }
             mAsserter.is(seenException, false, "Batch updating succeded");
             mOperations.clear();
 
-            // Delte all visits
+            // Delete all visits
             for (int i = 0; i < TESTCOUNT; i++) {
                 builder = ContentProviderOperation.newDelete(BrowserContract.History.CONTENT_URI);
                 builder.withSelection(BrowserContract.History.URL  + " = ?",
                                       new String[] {"http://www.test.org/" + i});
                 builder.withExpectedCount(1);
                 // Queue the operation
                 mOperations.add(builder.build());
             }
             try {
                 applyResult = mProvider.applyBatch(mOperations);
             } catch (OperationApplicationException ex) {
                 seenException = true;
             }
-            mAsserter.is(seenException, false, "Batch deletion succeded");
+            mAsserter.is(seenException, false, "Batch deletion succeeded");
         }
 
         // Force a Constraint error, see if later operations still apply correctly
         public void testApplyBatchErrors() throws Exception {
             ArrayList<ContentProviderOperation> mOperations
                 = new ArrayList<ContentProviderOperation>();
 
             // Test a bunch of inserts with applyBatch
@@ -470,16 +494,18 @@ public class testBrowserProvider extends
                     mAsserter.is(new Integer(id), new Integer(rootId), "The id of places folder is correct");
                 } else if (guid.equals(BrowserContract.Bookmarks.READING_LIST_FOLDER_GUID)) {
                     mAsserter.is(new Integer(id), new Integer(readingListId), "The id of reading list folder is correct");
                 }
 
                 mAsserter.is(new Integer(parentId), new Integer(rootId),
                              "The PARENT of the " + guid + " special folder is correct");
             }
+
+            c.close();
         }
     }
 
     class TestInsertBookmarks extends Test {
         private long insertWithNullCol(String colName) throws Exception {
             ContentValues b = createOneBookmark();
             b.putNull(colName);
             long id = -1;
@@ -544,16 +570,17 @@ public class testBrowserProvider extends
             b.remove(BrowserContract.Bookmarks.TYPE);
             id = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, b));
             c = getBookmarkById(id);
 
             mAsserter.is(c.moveToFirst(), true, "Inserted bookmark found");
 
             mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.TYPE)), String.valueOf(BrowserContract.Bookmarks.TYPE_BOOKMARK),
                          "Inserted bookmark has correct default type");
+            c.close();
         }
     }
 
     class TestInsertBookmarksFavicons extends Test {
         @Override
         public void test() throws Exception {
             ContentValues b = createOneBookmark();
 
@@ -566,83 +593,91 @@ public class testBrowserProvider extends
             mProvider.insert(BrowserContract.Favicons.CONTENT_URI, createFaviconEntry(pageUrl, favicon));
 
             Cursor c = getBookmarkById(id, new String[] { BrowserContract.Bookmarks.FAVICON });
 
             mAsserter.is(c.moveToFirst(), true, "Inserted bookmark found");
 
             mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Bookmarks.FAVICON)), "UTF8"),
                          favicon, "Inserted bookmark has corresponding favicon image");
+            c.close();
 
             c = getFaviconsByUrl(pageUrl);
             mAsserter.is(c.moveToFirst(), true, "Inserted favicon found");
 
             mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Combined.FAVICON)), "UTF8"),
                          favicon, "Inserted favicon has corresponding favicon image");
+            c.close();
         }
     }
 
     class TestDeleteBookmarks extends Test {
         private long insertOneBookmark() throws Exception {
             ContentValues b = createOneBookmark();
             long id = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, b));
 
             Cursor c = getBookmarkById(id);
             mAsserter.is(c.moveToFirst(), true, "Inserted bookmark found");
+            c.close();
 
             return id;
         }
 
         @Override
         public void test() throws Exception {
             long id = insertOneBookmark();
 
             int deleted = mProvider.delete(BrowserContract.Bookmarks.CONTENT_URI,
                                            BrowserContract.Bookmarks._ID + " = ?",
                                            new String[] { String.valueOf(id) });
 
             mAsserter.is((deleted == 1), true, "Inserted bookmark was deleted");
 
             Cursor c = getBookmarkById(appendUriParam(BrowserContract.Bookmarks.CONTENT_URI, BrowserContract.PARAM_SHOW_DELETED, "1"), id);
             mAsserter.is(c.moveToFirst(), true, "Deleted bookmark was only marked as deleted");
+            c.close();
 
             deleted = mProvider.delete(appendUriParam(BrowserContract.Bookmarks.CONTENT_URI, BrowserContract.PARAM_IS_SYNC, "1"),
                                        BrowserContract.Bookmarks._ID + " = ?",
                                        new String[] { String.valueOf(id) });
 
             mAsserter.is((deleted == 1), true, "Inserted bookmark was deleted");
 
             c = getBookmarkById(appendUriParam(BrowserContract.Bookmarks.CONTENT_URI, BrowserContract.PARAM_SHOW_DELETED, "1"), id);
             mAsserter.is(c.moveToFirst(), false, "Inserted bookmark is now actually deleted");
+            c.close();
 
             id = insertOneBookmark();
 
             deleted = mProvider.delete(ContentUris.withAppendedId(BrowserContract.Bookmarks.CONTENT_URI, id), null, null);
             mAsserter.is((deleted == 1), true,
                          "Inserted bookmark was deleted using URI with id");
 
             c = getBookmarkById(id);
             mAsserter.is(c.moveToFirst(), false,
                          "Inserted bookmark can't be found after deletion using URI with ID");
+            c.close();
 
             if (Build.VERSION.SDK_INT >= 8 &&
                 Build.VERSION.SDK_INT < 16) {
                 ContentValues b = createBookmark("Folder", null, mMobileFolderId,
                         BrowserContract.Bookmarks.TYPE_FOLDER, 0, "folderTags", "folderDescription", "folderKeyword");
 
                 long parentId = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, b));
                 c = getBookmarkById(parentId);
                 mAsserter.is(c.moveToFirst(), true, "Inserted bookmarks folder found");
+                c.close();
 
                 b = createBookmark("Example", "http://example.com", parentId,
                         BrowserContract.Bookmarks.TYPE_BOOKMARK, 0, "tags", "description", "keyword");
 
                 id = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, b));
                 c = getBookmarkById(id);
                 mAsserter.is(c.moveToFirst(), true, "Inserted bookmark found");
+                c.close();
 
                 deleted = 0;
                 try {
                     Uri uri = ContentUris.withAppendedId(BrowserContract.Bookmarks.CONTENT_URI, parentId);
                     deleted = mProvider.delete(appendUriParam(uri, BrowserContract.PARAM_IS_SYNC, "1"), null, null);
                 } catch(Exception e) {}
 
                 mAsserter.is((deleted == 0), true,
@@ -659,21 +694,23 @@ public class testBrowserProvider extends
             final String pageUrl = b.getAsString(BrowserContract.Bookmarks.URL);
             long id = ContentUris.parseId(mProvider.insert(BrowserContract.Bookmarks.CONTENT_URI, b));
 
             // Insert the favicon into the favicons table
             mProvider.insert(BrowserContract.Favicons.CONTENT_URI, createFaviconEntry(pageUrl, "FAVICON"));
 
             Cursor c = getFaviconsByUrl(pageUrl);
             mAsserter.is(c.moveToFirst(), true, "Inserted favicon found");
+            c.close();
 
             mProvider.delete(ContentUris.withAppendedId(BrowserContract.Bookmarks.CONTENT_URI, id), null, null);
 
             c = getFaviconsByUrl(pageUrl);
             mAsserter.is(c.moveToFirst(), false, "Favicon is deleted with last reference to it");
+            c.close();
         }
     }
 
     class TestUpdateBookmarks extends Test {
         private int updateWithNullCol(long id, String colName) throws Exception {
             ContentValues u = new ContentValues();
             u.putNull(colName);
 
@@ -708,16 +745,17 @@ public class testBrowserProvider extends
             u.put(BrowserContract.Bookmarks.TYPE, BrowserContract.Bookmarks.TYPE_FOLDER);
             u.put(BrowserContract.Bookmarks.POSITION, 10);
 
             int updated = mProvider.update(BrowserContract.Bookmarks.CONTENT_URI, u,
                                            BrowserContract.Bookmarks._ID + " = ?",
                                            new String[] { String.valueOf(id) });
 
             mAsserter.is((updated == 1), true, "Inserted bookmark was updated");
+            c.close();
 
             c = getBookmarkById(id);
             mAsserter.is(c.moveToFirst(), true, "Updated bookmark found");
 
             mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.TITLE)), u.getAsString(BrowserContract.Bookmarks.TITLE),
                          "Inserted bookmark has correct title");
             mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.URL)), u.getAsString(BrowserContract.Bookmarks.URL),
                          "Inserted bookmark has correct URL");
@@ -747,22 +785,24 @@ public class testBrowserProvider extends
             updated = updateWithNullCol(id, BrowserContract.Bookmarks.TYPE);
             mAsserter.is((updated > 0), false,
                          "Should not be able to update bookmark with null type");
 
             u = new ContentValues();
             u.put(BrowserContract.Bookmarks.URL, "http://examples2.com");
 
             updated = mProvider.update(ContentUris.withAppendedId(BrowserContract.Bookmarks.CONTENT_URI, id), u, null, null);
+            c.close();
 
             c = getBookmarkById(id);
             mAsserter.is(c.moveToFirst(), true, "Updated bookmark found");
 
             mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Bookmarks.URL)), u.getAsString(BrowserContract.Bookmarks.URL),
                          "Updated bookmark has correct URL using URI with id");
+            c.close();
         }
     }
 
     class TestUpdateBookmarksFavicons extends Test {
         @Override
         public void test() throws Exception {
             ContentValues b = createOneBookmark();
 
@@ -779,22 +819,24 @@ public class testBrowserProvider extends
             Cursor c = getFaviconsByUrl(pageUrl);
             mAsserter.is(c.moveToFirst(), true, "Inserted favicon found");
 
             mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Combined.FAVICON)), "UTF8"),
                          favicon, "Inserted favicon has corresponding favicon image");
 
             ContentValues u = createFaviconEntry(pageUrl, newFavicon);
             mProvider.update(BrowserContract.Favicons.CONTENT_URI, u, null, null);
+            c.close();
 
             c = getFaviconsByUrl(pageUrl);
             mAsserter.is(c.moveToFirst(), true, "Updated favicon found");
 
             mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Combined.FAVICON)), "UTF8"),
                          newFavicon, "Updated favicon has corresponding favicon image");
+            c.close();
         }
     }
 
     /**
      * Create a folder of one thousand and one bookmarks, then impose an order
      * on them.
      *
      * Verify that the reordering worked by querying.
@@ -820,16 +862,17 @@ public class testBrowserProvider extends
                     mAsserter.is(pos, (long) i, "Position matches sequence.");
                     mAsserter.is(guid, items[i], "GUID matches sequence.");
                 }
                 ++i;
                 c.moveToNext();
             }
 
             mAsserter.is(i, count, "Folder has the right number of children.");
+            c.close();
         }
 
         public static final int NUMBER_OF_CHILDREN = 1001;
         @Override
         public void test() throws Exception {
             // Create the containing folder.
             ContentValues folder = createBookmark("FolderFolder", "", mMobileFolderId,
                                                   BrowserContract.Bookmarks.TYPE_FOLDER, 0, "",
@@ -916,16 +959,17 @@ public class testBrowserProvider extends
 
             id = insertWithNullCol(BrowserContract.History.URL);
             mAsserter.is(new Long(id), new Long(-1),
                          "Should not be able to insert history with null URL");
 
             id = insertWithNullCol(BrowserContract.History.VISITS);
             mAsserter.is(new Long(id), new Long(-1),
                          "Should not be able to insert history with null number of visits");
+            c.close();
         }
     }
 
     class TestInsertHistoryFavicons extends Test {
         @Override
         public void test() throws Exception {
             ContentValues h = createOneHistoryEntry();
 
@@ -938,32 +982,35 @@ public class testBrowserProvider extends
             mProvider.insert(BrowserContract.Favicons.CONTENT_URI, createFaviconEntry(pageUrl, favicon));
 
             Cursor c = getHistoryEntryById(id, new String[] { BrowserContract.History.FAVICON });
 
             mAsserter.is(c.moveToFirst(), true, "Inserted history entry found");
 
             mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.History.FAVICON)), "UTF8"),
                          favicon, "Inserted history entry has corresponding favicon image");
+            c.close();
 
             c = getFaviconsByUrl(pageUrl);
             mAsserter.is(c.moveToFirst(), true, "Inserted favicon found");
 
             mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Combined.FAVICON)), "UTF8"),
                          favicon, "Inserted favicon has corresponding favicon image");
+            c.close();
         }
     }
 
     class TestDeleteHistory extends Test {
         private long insertOneHistoryEntry() throws Exception {
             ContentValues h = createOneHistoryEntry();
             long id = ContentUris.parseId(mProvider.insert(BrowserContract.History.CONTENT_URI, h));
 
             Cursor c = getHistoryEntryById(id);
             mAsserter.is(c.moveToFirst(), true, "Inserted history entry found");
+            c.close();
 
             return id;
         }
 
         @Override
         public void test() throws Exception {
             long id = insertOneHistoryEntry();
 
@@ -976,29 +1023,32 @@ public class testBrowserProvider extends
             Cursor c = getHistoryEntryById(appendUriParam(BrowserContract.History.CONTENT_URI, BrowserContract.PARAM_SHOW_DELETED, "1"), id);
             mAsserter.is(c.moveToFirst(), true, "Deleted history entry was only marked as deleted");
 
             deleted = mProvider.delete(appendUriParam(BrowserContract.History.CONTENT_URI, BrowserContract.PARAM_IS_SYNC, "1"),
                                        BrowserContract.History._ID + " = ?",
                                        new String[] { String.valueOf(id) });
 
             mAsserter.is((deleted == 1), true, "Inserted history entry was deleted");
+            c.close();
 
             c = getHistoryEntryById(appendUriParam(BrowserContract.History.CONTENT_URI, BrowserContract.PARAM_SHOW_DELETED, "1"), id);
             mAsserter.is(c.moveToFirst(), false, "Inserted history is now actually deleted");
 
             id = insertOneHistoryEntry();
 
             deleted = mProvider.delete(ContentUris.withAppendedId(BrowserContract.History.CONTENT_URI, id), null, null);
             mAsserter.is((deleted == 1), true,
                          "Inserted history entry was deleted using URI with id");
+            c.close();
 
             c = getHistoryEntryById(id);
             mAsserter.is(c.moveToFirst(), false,
                          "Inserted history entry can't be found after deletion using URI with ID");
+            c.close();
         }
     }
 
     class TestDeleteHistoryFavicons extends Test {
         @Override
         public void test() throws Exception {
             ContentValues h = createOneHistoryEntry();
 
@@ -1007,19 +1057,21 @@ public class testBrowserProvider extends
 
             // Insert the favicon into the favicons table
             mProvider.insert(BrowserContract.Favicons.CONTENT_URI, createFaviconEntry(pageUrl, "FAVICON"));
 
             Cursor c = getFaviconsByUrl(pageUrl);
             mAsserter.is(c.moveToFirst(), true, "Inserted favicon found");
 
             mProvider.delete(ContentUris.withAppendedId(BrowserContract.History.CONTENT_URI, id), null, null);
+            c.close();
 
             c = getFaviconsByUrl(pageUrl);
             mAsserter.is(c.moveToFirst(), false, "Favicon is deleted with last reference to it");
+            c.close();
         }
     }
 
     class TestUpdateHistory extends Test {
         private int updateWithNullCol(long id, String colName) throws Exception {
             ContentValues u = new ContentValues();
             u.putNull(colName);
 
@@ -1051,16 +1103,17 @@ public class testBrowserProvider extends
             u.put(BrowserContract.History.TITLE, h.getAsString(BrowserContract.History.TITLE) + "CHANGED");
             u.put(BrowserContract.History.URL, h.getAsString(BrowserContract.History.URL) + "/more/stuff");
 
             int updated = mProvider.update(BrowserContract.History.CONTENT_URI, u,
                                            BrowserContract.History._ID + " = ?",
                                            new String[] { String.valueOf(id) });
 
             mAsserter.is((updated == 1), true, "Inserted history entry was updated");
+            c.close();
 
             c = getHistoryEntryById(id);
             mAsserter.is(c.moveToFirst(), true, "Updated history entry found");
 
             mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.TITLE)), u.getAsString(BrowserContract.History.TITLE),
                          "Updated history entry has correct title");
             mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.URL)), u.getAsString(BrowserContract.History.URL),
                          "Updated history entry has correct URL");
@@ -1084,22 +1137,24 @@ public class testBrowserProvider extends
             updated = updateWithNullCol(id, BrowserContract.History.VISITS);
             mAsserter.is((updated > 0), false,
                          "Should not be able to update history with null number of visits");
 
             u = new ContentValues();
             u.put(BrowserContract.History.URL, "http://examples2.com");
 
             updated = mProvider.update(ContentUris.withAppendedId(BrowserContract.History.CONTENT_URI, id), u, null, null);
+            c.close();
 
             c = getHistoryEntryById(id);
             mAsserter.is(c.moveToFirst(), true, "Updated history entry found");
 
             mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.URL)), u.getAsString(BrowserContract.History.URL),
                          "Updated history entry has correct URL using URI with id");
+            c.close();
         }
     }
 
     class TestUpdateHistoryFavicons extends Test {
         @Override
         public void test() throws Exception {
             ContentValues h = createOneHistoryEntry();
 
@@ -1116,22 +1171,24 @@ public class testBrowserProvider extends
             mAsserter.is(c.moveToFirst(), true, "Inserted favicon found");
 
             mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Combined.FAVICON)), "UTF8"),
                          favicon, "Inserted favicon has corresponding favicon image");
 
             ContentValues u = createFaviconEntry(pageUrl, newFavicon);
 
             mProvider.update(BrowserContract.Favicons.CONTENT_URI, u, null, null);
+            c.close();
 
             c = getFaviconsByUrl(pageUrl);
             mAsserter.is(c.moveToFirst(), true, "Updated favicon found");
 
             mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Combined.FAVICON)), "UTF8"),
                          newFavicon, "Updated favicon has corresponding favicon image");
+            c.close();
         }
     }
 
     class TestUpdateOrInsertHistory extends Test {
         private final String TEST_URL_1 = "http://example.com";
         private final String TEST_URL_2 = "http://example.org";
         private final String TEST_TITLE = "Example";
 
@@ -1190,16 +1247,17 @@ public class testBrowserProvider extends
             values = new ContentValues();
             values.put(BrowserContract.History.DATE_LAST_VISITED, System.currentTimeMillis());
             values.put(BrowserContract.History.TITLE, TEST_TITLE);
 
             updated = mProvider.update(updateOrInsertHistoryUri, values,
                                        BrowserContract.History._ID + " = ?",
                                        new String[] { String.valueOf(id) });
             mAsserter.is((updated == 1), true, "Inserted history entry was updated");
+            c.close();
 
             c = getHistoryEntryById(id);
             mAsserter.is(c.moveToFirst(), true, "Updated history entry found");
 
             mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.TITLE)), TEST_TITLE,
                          "Updated history entry has correct title");
             mAsserter.is(new Long(c.getLong(c.getColumnIndex(BrowserContract.History.VISITS))), new Long(2),
                          "Updated history entry has correct number of visits");
@@ -1215,16 +1273,18 @@ public class testBrowserProvider extends
             values.put(BrowserContract.History.VISITS, 10);
 
             updated = mProvider.update(updateOrInsertHistoryUri, values,
                                            BrowserContract.History.URL + " = ?",
                                            new String[] { values.getAsString(BrowserContract.History.URL) });
             mAsserter.is((updated == 1), true, "History entry was inserted");
 
             id = getHistoryEntryIdByUrl(TEST_URL_2);
+            c.close();
+
             c = getHistoryEntryById(id);
             mAsserter.is(c.moveToFirst(), true, "History entry was inserted");
 
             dateCreated = c.getLong(c.getColumnIndex(BrowserContract.History.DATE_CREATED));
             dateModified = c.getLong(c.getColumnIndex(BrowserContract.History.DATE_MODIFIED));
 
             mAsserter.is(new Long(c.getLong(c.getColumnIndex(BrowserContract.History.VISITS))), new Long(10),
                          "Inserted history entry has correct specified number of visits");
@@ -1234,30 +1294,32 @@ public class testBrowserProvider extends
             // Update the history entry, specifying additional visit count
             values = new ContentValues();
             values.put(BrowserContract.History.VISITS, 10);
 
             updated = mProvider.update(updateOrInsertHistoryUri, values,
                                        BrowserContract.History._ID + " = ?",
                                        new String[] { String.valueOf(id) });
             mAsserter.is((updated == 1), true, "Inserted history entry was updated");
+            c.close();
 
             c = getHistoryEntryById(id);
             mAsserter.is(c.moveToFirst(), true, "Updated history entry found");
 
             mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.TITLE)), TEST_TITLE,
                          "Updated history entry has correct unchanged title");
             mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.History.URL)), TEST_URL_2,
                          "Updated history entry has correct unchanged URL");
             mAsserter.is(new Long(c.getLong(c.getColumnIndex(BrowserContract.History.VISITS))), new Long(20),
                          "Updated history entry has correct number of visits");
             mAsserter.is(new Long(c.getLong(c.getColumnIndex(BrowserContract.History.DATE_CREATED))), new Long(dateCreated),
                          "Updated history entry has same creation date");
             mAsserter.isnot(new Long(c.getLong(c.getColumnIndex(BrowserContract.History.DATE_MODIFIED))), new Long(dateModified),
                             "Updated history entry has new modification date");
+            c.close();
 
         }
     }
 
     class TestInsertHistoryThumbnails extends Test {
         @Override
         public void test() throws Exception {
             ContentValues h = createOneHistoryEntry();
@@ -1270,16 +1332,17 @@ public class testBrowserProvider extends
             // Insert the thumbnail into the thumbnails table
             mProvider.insert(BrowserContract.Thumbnails.CONTENT_URI, createThumbnailEntry(pageUrl, thumbnail));
 
             Cursor c = getThumbnailByUrl(pageUrl);
             mAsserter.is(c.moveToFirst(), true, "Inserted thumbnail found");
 
             mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Thumbnails.DATA)), "UTF8"),
                          thumbnail, "Inserted thumbnail has corresponding thumbnail image");
+            c.close();
         }
     }
 
     class TestUpdateHistoryThumbnails extends Test {
         @Override
         public void test() throws Exception {
             ContentValues h = createOneHistoryEntry();
 
@@ -1296,22 +1359,24 @@ public class testBrowserProvider extends
             mAsserter.is(c.moveToFirst(), true, "Inserted thumbnail found");
 
             mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Thumbnails.DATA)), "UTF8"),
                          thumbnail, "Inserted thumbnail has corresponding thumbnail image");
 
             ContentValues u = createThumbnailEntry(pageUrl, newThumbnail);
 
             mProvider.update(BrowserContract.Thumbnails.CONTENT_URI, u, null, null);
+            c.close();
 
             c = getThumbnailByUrl(pageUrl);
             mAsserter.is(c.moveToFirst(), true, "Updated thumbnail found");
 
             mAsserter.is(new String(c.getBlob(c.getColumnIndex(BrowserContract.Thumbnails.DATA)), "UTF8"),
                          newThumbnail, "Updated thumbnail has corresponding thumbnail image");
+            c.close();
         }
     }
 
     class TestDeleteHistoryThumbnails extends Test {
         @Override
         public void test() throws Exception {
             ContentValues h = createOneHistoryEntry();
 
@@ -1320,19 +1385,21 @@ public class testBrowserProvider extends
 
             // Insert the thumbnail into the thumbnails table
             mProvider.insert(BrowserContract.Thumbnails.CONTENT_URI, createThumbnailEntry(pageUrl, "THUMBNAIL"));
 
             Cursor c = getThumbnailByUrl(pageUrl);
             mAsserter.is(c.moveToFirst(), true, "Inserted thumbnail found");
 
             mProvider.delete(ContentUris.withAppendedId(BrowserContract.History.CONTENT_URI, id), null, null);
+            c.close();
 
             c = getThumbnailByUrl(pageUrl);
             mAsserter.is(c.moveToFirst(), false, "Thumbnail is deleted with last reference to it");
+            c.close();
         }
     }
 
     class TestCombinedView extends Test {
         @Override
         public void test() throws Exception {
             final String TITLE_1 = "Test Page 1";
             final String TITLE_2 = "Test Page 2";
@@ -1430,16 +1497,17 @@ public class testBrowserProvider extends
             mAsserter.is(new Long(c.getLong(c.getColumnIndex(BrowserContract.Combined.HISTORY_ID))), new Long(combinedHistoryId),
                          "Combined entry has correct history id");
             mAsserter.is(c.getString(c.getColumnIndex(BrowserContract.Combined.URL)), URL_3,
                          "Combined entry has correct url");
             mAsserter.is(c.getInt(c.getColumnIndex(BrowserContract.Combined.VISITS)), VISITS,
                          "Combined entry has correct number of visits");
             mAsserter.is(new Long(c.getLong(c.getColumnIndex(BrowserContract.Combined.DATE_LAST_VISITED))), new Long(LAST_VISITED),
                          "Combined entry has correct last visit time");
+            c.close();
         }
     }
 
     class TestCombinedViewDisplay extends Test {
         @Override
         public void test() throws Exception {
             final String TITLE_1 = "Test Page 1";
             final String TITLE_2 = "Test Page 2";
@@ -1490,16 +1558,17 @@ public class testBrowserProvider extends
                 long id = c.getLong(c.getColumnIndex(BrowserContract.Combined.BOOKMARK_ID));
 
                 int display = c.getInt(c.getColumnIndex(BrowserContract.Combined.DISPLAY));
                 int expectedDisplay = (id == readingListItemId || id == readingListItemId2 ? BrowserContract.Combined.DISPLAY_READER : BrowserContract.Combined.DISPLAY_NORMAL);
 
                 mAsserter.is(new Integer(display), new Integer(expectedDisplay),
                                  "Combined display column should always be DISPLAY_READER for the reading list item");
             }
+            c.close();
         }
     }
 
     class TestCombinedViewWithDeletedBookmark extends Test {
         @Override
         public void test() throws Exception {
             final String TITLE = "Test Page 1";
             final String URL = "http://example.com";
@@ -1522,23 +1591,25 @@ public class testBrowserProvider extends
             mAsserter.is(new Long(c.getLong(c.getColumnIndex(BrowserContract.Combined.BOOKMARK_ID))), new Long(combinedBookmarkId),
                          "Bookmark id should be set correctly on combined entry");
 
             int deleted = mProvider.delete(BrowserContract.Bookmarks.CONTENT_URI,
                                            BrowserContract.Bookmarks._ID + " = ?",
                                            new String[] { String.valueOf(combinedBookmarkId) });
 
             mAsserter.is((deleted == 1), true, "Inserted combined bookmark was deleted");
+            c.close();
 
             c = mProvider.query(BrowserContract.Combined.CONTENT_URI, null, "", null, null);
             mAsserter.is(c.getCount(), 1, "1 combined entry found");
 
             mAsserter.is(c.moveToFirst(), true, "Found combined entry without bookmark id");
             mAsserter.is(new Long(c.getLong(c.getColumnIndex(BrowserContract.Combined.BOOKMARK_ID))), new Long(0),
                          "Bookmark id should not be set to removed bookmark id");
+            c.close();
         }
     }
 
     class TestCombinedViewWithDeletedReadingListItem extends Test {
         @Override
         public void test() throws Exception {
             final String TITLE = "Test Page 1";
             final String URL = "http://example.com";
@@ -1564,25 +1635,27 @@ public class testBrowserProvider extends
             mAsserter.is(new Long(c.getLong(c.getColumnIndex(BrowserContract.Combined.DISPLAY))), new Long(BrowserContract.Combined.DISPLAY_READER),
                          "Combined entry should have reader display type");
 
             int deleted = mProvider.delete(BrowserContract.Bookmarks.CONTENT_URI,
                                            BrowserContract.Bookmarks._ID + " = ?",
                                            new String[] { String.valueOf(combinedReadingListItemId) });
 
             mAsserter.is((deleted == 1), true, "Inserted combined reading list item was deleted");
+            c.close();
 
             c = mProvider.query(BrowserContract.Combined.CONTENT_URI, null, "", null, null);
             mAsserter.is(c.getCount(), 1, "1 combined entry found");
 
             mAsserter.is(c.moveToFirst(), true, "Found combined entry without bookmark id");
             mAsserter.is(new Long(c.getLong(c.getColumnIndex(BrowserContract.Combined.BOOKMARK_ID))), new Long(0),
                          "Bookmark id should not be set to removed bookmark id");
             mAsserter.is(new Long(c.getLong(c.getColumnIndex(BrowserContract.Combined.DISPLAY))), new Long(BrowserContract.Combined.DISPLAY_NORMAL),
                          "Combined entry should have reader display type");
+            c.close();
         }
     }
 
     class TestExpireHistory extends Test {
         private void createFakeHistory(long timeShift, int count) {
             // Insert a bunch of very new entries
             ContentValues[] allVals = new ContentValues[count];
             long time = System.currentTimeMillis() - timeShift;
@@ -1603,96 +1676,101 @@ public class testBrowserProvider extends
                 ContentValues cv = new ContentValues();
                 cv.put(BrowserContract.History.DATE_CREATED, time);
                 cv.put(BrowserContract.History.DATE_MODIFIED, time);
                 mProvider.update(BrowserContract.History.CONTENT_URI, cv, BrowserContract.History.URL + " = ?",
                                  new String[] { "http://www.test.org/" + i });
             }
 
             Cursor c = mProvider.query(BrowserContract.History.CONTENT_URI, null, "", null, null);
-            mAsserter.is(c.getCount(), count, count + " history entries found");
+
+            assertCountIsAndClose(c, count, count + " history entries found");
 
             // add thumbnails for each entry
             allVals = new ContentValues[count];
             for (int i = 0; i < count; i++) {
                 allVals[i] = new ContentValues();
                 allVals[i].put(BrowserContract.Thumbnails.DATA, i);
                 allVals[i].put(BrowserContract.Thumbnails.URL, "http://www.test.org/" + i);
             }
 
             inserts = mProvider.bulkInsert(BrowserContract.Thumbnails.CONTENT_URI, allVals);
             mAsserter.is(inserts, count, "Expected number of inserts matches");
 
             c = mProvider.query(BrowserContract.Thumbnails.CONTENT_URI, null, null, null, null);
-            mAsserter.is(c.getCount(), count, count + " thumbnails entries found");
+            assertCountIsAndClose(c, count, count + " thumbnails entries found");
         }
 
         @Override
         public void test() throws Exception {
             final int count = 3000;
             final int thumbCount = 15;
 
             // insert a bunch of new entries
             createFakeHistory(0, count);
 
             // expiring with a normal priority should not delete new entries
             Uri url = appendUriParam(BrowserContract.History.CONTENT_OLD_URI, BrowserContract.PARAM_EXPIRE_PRIORITY, "NORMAL");
             mProvider.delete(url, null, null);
             Cursor c = mProvider.query(BrowserContract.History.CONTENT_URI, null, "", null, null);
-            mAsserter.is(c.getCount(), count, count + " history entries found");
+            assertCountIsAndClose(c, count, count + " history entries found");
 
             // expiring with a normal priority should delete all but 10 thumbnails
             c = mProvider.query(BrowserContract.Thumbnails.CONTENT_URI, null, null, null, null);
-            mAsserter.is(c.getCount(), thumbCount, thumbCount + " thumbnails found");
+            assertCountIsAndClose(c, thumbCount, thumbCount + " thumbnails found");
 
             ensureEmptyDatabase();
-            // insert a bunch of new entries
+
+            // Insert a bunch of new entries.
             createFakeHistory(0, count);
 
-            // expiring with a aggressive priority should leave 500 entries
+            // Expiring with a aggressive priority should leave 500 entries.
             url = appendUriParam(BrowserContract.History.CONTENT_OLD_URI, BrowserContract.PARAM_EXPIRE_PRIORITY, "AGGRESSIVE");
             mProvider.delete(url, null, null);
+
             c = mProvider.query(BrowserContract.History.CONTENT_URI, null, "", null, null);
-            mAsserter.is(c.getCount(), 500, "500 history entries found");
+            assertCountIsAndClose(c, 500, "500 history entries found");
 
-            // expiring with a aggressive priority should delete all but 10 thumbnails
+            // Expiring with a aggressive priority should delete all but 10 thumbnails.
             c = mProvider.query(BrowserContract.Thumbnails.CONTENT_URI, null, null, null, null);
-            mAsserter.is(c.getCount(), thumbCount, thumbCount + " thumbnails found");
+            assertCountIsAndClose(c, thumbCount, thumbCount + " thumbnails found");
 
             ensureEmptyDatabase();
-            // insert a bunch of entries with an old time created/modified
+
+            // Insert a bunch of entries with an old time created/modified.
             long time = 1000L * 60L * 60L * 24L * 30L * 3L;
             createFakeHistory(time, count);
 
-            // expiring with an normal priority should remove at most 1000 entries
-            // entries leaving at least 2000
+            // Expiring with an normal priority should remove at most 1000 entries,
+            // entries leaving at least 2000.
             url = appendUriParam(BrowserContract.History.CONTENT_OLD_URI, BrowserContract.PARAM_EXPIRE_PRIORITY, "NORMAL");
             mProvider.delete(url, null, null);
+
             c = mProvider.query(BrowserContract.History.CONTENT_URI, null, "", null, null);
-            mAsserter.is(c.getCount(), 2000, "2000 history entries found");
+            assertCountIsAndClose(c, 2000, "2000 history entries found");
 
-            // expiring with a normal priority should delete all but 10 thumbnails
+            // Expiring with a normal priority should delete all but 10 thumbnails.
             c = mProvider.query(BrowserContract.Thumbnails.CONTENT_URI, null, null, null, null);
-            mAsserter.is(c.getCount(), thumbCount, thumbCount + " thumbnails found");
+            assertCountIsAndClose(c, thumbCount, thumbCount + " thumbnails found");
 
             ensureEmptyDatabase();
             // insert a bunch of entries with an old time created/modified
             time = 1000L * 60L * 60L * 24L * 30L * 3L;
             createFakeHistory(time, count);
 
-            // expiring with an agressive priority should remove old
-            // entries leaving at least 500
+            // Expiring with an aggressive priority should remove old
+            // entries, leaving at least 500.
             url = appendUriParam(BrowserContract.History.CONTENT_OLD_URI, BrowserContract.PARAM_EXPIRE_PRIORITY, "AGGRESSIVE");
             mProvider.delete(url, null, null);
             c = mProvider.query(BrowserContract.History.CONTENT_URI, null, "", null, null);
-            mAsserter.is(c.getCount(), 500, "500 history entries found");
+            assertCountIsAndClose(c, 500, "500 history entries found");
 
             // expiring with an aggressive priority should delete all but 10 thumbnails
             c = mProvider.query(BrowserContract.Thumbnails.CONTENT_URI, null, null, null, null);
-            mAsserter.is(c.getCount(), thumbCount, thumbCount + " thumbnails found");
+            assertCountIsAndClose(c, thumbCount, thumbCount + " thumbnails found");
         }
     }
 
     /*
      * Verify that insert, update, delete, and bulkInsert operations
      * notify the ambient content resolver.  Each operation calls the
      * content resolver notifyChange method synchronously, so it is
      * okay to test sequentially.
@@ -1771,9 +1849,21 @@ public class testBrowserProvider extends
 
             mAsserter.is(Long.valueOf(numBulkInserted),
                          Long.valueOf(1),
                          "Correct number of items are bulkInserted");
 
             ensureOnlyChangeNotifiedStartsWith(BrowserContract.History.CONTENT_URI, "bulkInsert");
         }
     }
+
+    /**
+     * Assert that the provided cursor has the expected number of rows,
+     * closing the cursor afterwards.
+     */
+    private void assertCountIsAndClose(Cursor c, int expectedCount, String message) {
+        try {
+            mAsserter.is(c.getCount(), expectedCount, message);
+        } finally {
+            c.close();
+        }
+    }
 }
--- a/mobile/android/base/widget/GeckoActionProvider.java
+++ b/mobile/android/base/widget/GeckoActionProvider.java
@@ -14,16 +14,18 @@ import android.content.pm.ResolveInfo;
 import android.graphics.drawable.Drawable;
 import android.view.ActionProvider;
 import android.view.MenuItem;
 import android.view.MenuItem.OnMenuItemClickListener;
 import android.view.SubMenu;
 import android.view.View;
 import android.view.View.OnClickListener;
 
+import java.util.ArrayList;
+
 public class GeckoActionProvider extends ActionProvider {
     private static int MAX_HISTORY_SIZE = 2;
 
     /**
      * A listener to know when a target was selected.
      * When setting a provider, the activity can listen to this,
      * to close the menu.
      */
@@ -120,16 +122,30 @@ public class GeckoActionProvider extends
             mOnTargetListener.onTargetSelected();
         }
     }
 
     public void setOnTargetSelectedListener(OnTargetSelectedListener listener) {
         mOnTargetListener = listener;
     }
 
+    public ArrayList<ResolveInfo> getSortedActivites() {
+        ArrayList<ResolveInfo> infos = new ArrayList<ResolveInfo>();
+
+        ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mHistoryFileName);
+        PackageManager packageManager = mContext.getPackageManager();
+
+        // Populate the sub-menu with a sub set of the activities.
+        final int count = dataModel.getActivityCount();
+        for (int i = 0; i < count; i++) {
+            infos.add(dataModel.getActivity(i));
+        }
+        return infos;
+    }
+
     /**
      * Listener for handling default activity / menu item clicks.
      */
     private class Callbacks implements OnMenuItemClickListener,
                                        OnClickListener {
         private void chooseActivity(int index) { 
             ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mHistoryFileName);
             Intent launchIntent = dataModel.chooseActivity(index);
--- a/toolkit/devtools/server/actors/webbrowser.js
+++ b/toolkit/devtools/server/actors/webbrowser.js
@@ -1040,16 +1040,18 @@ BrowserAddonActor.prototype = {
  * is resumed before the navigation begins.
  *
  * @param BrowserTabActor aBrowserTabActor
  *        The tab actor associated with this listener.
  */
 function DebuggerProgressListener(aBrowserTabActor) {
   this._tabActor = aBrowserTabActor;
   this._tabActor._tabbrowser.addProgressListener(this);
+  let EventEmitter = devtools.require("devtools/shared/event-emitter");
+  EventEmitter.decorate(this);
 }
 
 DebuggerProgressListener.prototype = {
   onStateChange:
   makeInfallible(function DPL_onStateChange(aProgress, aRequest, aFlag, aStatus) {
     let isStart = aFlag & Ci.nsIWebProgressListener.STATE_START;
     let isStop = aFlag & Ci.nsIWebProgressListener.STATE_STOP;
     let isDocument = aFlag & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT;
@@ -1067,38 +1069,42 @@ DebuggerProgressListener.prototype = {
       // Proceed normally only if the debuggee is not paused.
       if (this._tabActor.threadActor.state == "paused") {
         aRequest.suspend();
         this._tabActor.threadActor.onResume();
         this._tabActor.threadActor.dbg.enabled = false;
         this._tabActor._pendingNavigation = aRequest;
       }
 
-      this._tabActor.threadActor.disableAllBreakpoints();
-      this._tabActor.conn.send({
+      let packet = {
         from: this._tabActor.actorID,
         type: "tabNavigated",
         url: aRequest.URI.spec,
         nativeConsoleAPI: true,
         state: "start"
-      });
+      };
+      this._tabActor.threadActor.disableAllBreakpoints();
+      this._tabActor.conn.send(packet);
+      this.emit("will-navigate", packet);
     } else if (isStop) {
       if (this._tabActor.threadActor.state == "running") {
         this._tabActor.threadActor.dbg.enabled = true;
       }
 
       let window = this._tabActor.window;
-      this._tabActor.conn.send({
+      let packet = {
         from: this._tabActor.actorID,
         type: "tabNavigated",
         url: this._tabActor.url,
         title: this._tabActor.title,
         nativeConsoleAPI: this._tabActor.hasNativeConsoleAPI(window),
         state: "stop"
-      });
+      };
+      this._tabActor.conn.send(packet);
+      this.emit("navigate", packet);
     }
   }, "DebuggerProgressListener.prototype.onStateChange"),
 
   /**
    * Destroy the progress listener instance.
    */
   destroy: function DPL_destroy() {
     if (this._tabActor._tabbrowser.removeProgressListener) {
--- a/toolkit/devtools/server/actors/webconsole.js
+++ b/toolkit/devtools/server/actors/webconsole.js
@@ -54,20 +54,20 @@ function WebConsoleActor(aConnection, aP
 
   this._actorPool = new ActorPool(this.conn);
   this.conn.addActorPool(this._actorPool);
 
   this._prefs = {};
 
   this.dbg = new Debugger();
 
-  this._protoChains = new Map();
   this._netEvents = new Map();
   this._gripDepth = 0;
 
+  this._onWillNavigate = this._onWillNavigate.bind(this);
   this._onObserverNotification = this._onObserverNotification.bind(this);
   if (this.parentActor.isRootActor) {
     Services.obs.addObserver(this._onObserverNotification,
                              "last-pb-context-exited", false);
   }
 }
 
 WebConsoleActor.l10n = new WebConsoleUtils.l10n("chrome://global/locale/console.properties");
@@ -108,26 +108,16 @@ WebConsoleActor.prototype =
    * created with sendHTTPRequest.
    *
    * @private
    * @type Map
    */
   _netEvents: null,
 
   /**
-   * A cache of prototype chains for objects that have received a
-   * prototypeAndProperties request.
-   *
-   * @private
-   * @type Map
-   * @see dbg-script-actors.js, ThreadActor._protoChains
-   */
-  _protoChains: null,
-
-  /**
    * The debugger server connection instance.
    * @type object
    */
   conn: null,
 
   /**
    * The window we work with.
    * @type nsIDOMWindow
@@ -204,16 +194,41 @@ WebConsoleActor.prototype =
   /**
    * A weak reference to the last chrome window we used to work with.
    *
    * @private
    * @type nsIWeakReference
    */
   _lastChromeWindow: null,
 
+  // The evalWindow is used at the scope for JS evaluation.
+  _evalWindow: null,
+  get evalWindow() {
+    return this._evalWindow || this.window;
+  },
+
+  set evalWindow(aWindow) {
+    this._evalWindow = aWindow;
+
+    if (!this._progressListenerActive && this.parentActor._progressListener) {
+      this.parentActor._progressListener.once("will-navigate", this._onWillNavigate);
+      this._progressListenerActive = true;
+    }
+  },
+
+  /**
+   * Flag used to track if we are listening for events from the progress
+   * listener of the tab actor. We use the progress listener to clear
+   * this.evalWindow on page navigation.
+   *
+   * @private
+   * @type boolean
+   */
+  _progressListenerActive: false,
+
   /**
    * The ConsoleServiceListener instance.
    * @type object
    */
   consoleServiceListener: null,
 
   /**
    * The ConsoleAPIListener instance.
@@ -290,18 +305,19 @@ WebConsoleActor.prototype =
     }
     this.conn.removeActorPool(this._actorPool);
     if (this.parentActor.isRootActor) {
       Services.obs.removeObserver(this._onObserverNotification,
                                   "last-pb-context-exited");
     }
     this._actorPool = null;
 
+    this._jstermHelpersCache = null;
+    this._evalWindow = null;
     this._netEvents.clear();
-    this._protoChains.clear();
     this.dbg.enabled = false;
     this.dbg = null;
     this.conn = null;
   },
 
   /**
    * Create and return an environment actor that corresponds to the provided
    * Debugger.Environment. This is a straightforward clone of the ThreadActor's
@@ -720,17 +736,17 @@ WebConsoleActor.prototype =
       }
       else {
         Cu.reportError("Web Console Actor: the frame actor was not found: " +
                        frameActorId);
       }
     }
     // This is the general case (non-paused debugger)
     else {
-      dbgObject = this.dbg.makeGlobalObjectReference(this.window);
+      dbgObject = this.dbg.makeGlobalObjectReference(this.evalWindow);
     }
 
     let result = JSPropertyProvider(dbgObject, environment, aRequest.text,
                                     aRequest.cursor, frameActorId) || {};
     let matches = result.matches || [];
     let reqText = aRequest.text.substr(0, aRequest.cursor);
 
     // We consider '$' as alphanumerc because it is used in the names of some
@@ -817,22 +833,23 @@ WebConsoleActor.prototype =
    * @return object
    *         The same object as |this|, but with an added |sandbox| property.
    *         The sandbox holds methods and properties that can be used as
    *         bindings during JS evaluation.
    */
   _getJSTermHelpers: function WCA__getJSTermHelpers(aDebuggerGlobal)
   {
     let helpers = {
-      window: this.window,
+      window: this.evalWindow,
       chromeWindow: this.chromeWindow.bind(this),
       makeDebuggeeValue: aDebuggerGlobal.makeDebuggeeValue.bind(aDebuggerGlobal),
       createValueGrip: this.createValueGrip.bind(this),
       sandbox: Object.create(null),
       helperResult: null,
+      consoleActor: this,
     };
     JSTermHelpers(helpers);
 
     // Make sure the helpers can be used during eval.
     for (let name in helpers.sandbox) {
       let desc = Object.getOwnPropertyDescriptor(helpers.sandbox, name);
       if (desc.get || desc.set) {
         continue;
@@ -919,22 +936,22 @@ WebConsoleActor.prototype =
 
     // If we've been given a frame actor in whose scope we should evaluate the
     // expression, be sure to use that frame's Debugger (that is, the JavaScript
     // debugger's Debugger) for the whole operation, not the console's Debugger.
     // (One Debugger will treat a different Debugger's Debugger.Object instances
     // as ordinary objects, not as references to be followed, so mixing
     // debuggers causes strange behaviors.)
     let dbg = frame ? frameActor.threadActor.dbg : this.dbg;
-    let dbgWindow = dbg.makeGlobalObjectReference(this.window);
+    let dbgWindow = dbg.makeGlobalObjectReference(this.evalWindow);
 
     // If we have an object to bind to |_self|, create a Debugger.Object
     // referring to that object, belonging to dbg.
     let bindSelf = null;
-    let dbgWindow = dbg.makeGlobalObjectReference(this.window);
+    let dbgWindow = dbg.makeGlobalObjectReference(this.evalWindow);
     if (aOptions.bindObjectActor) {
       let objActor = this.getActorByID(aOptions.bindObjectActor);
       if (objActor) {
         let jsObj = objActor.obj.unsafeDereference();
         // If we use the makeDebuggeeValue method of jsObj's own global, then
         // we'll get a D.O that sees jsObj as viewed from its own compartment -
         // that is, without wrappers. The evalWithBindings call will then wrap
         // jsObj appropriately for the evaluation compartment.
@@ -1284,17 +1301,27 @@ WebConsoleActor.prototype =
     switch (aTopic) {
       case "last-pb-context-exited":
         this.conn.send({
           from: this.actorID,
           type: "lastPrivateContextExited",
         });
         break;
     }
-  }
+  },
+
+  /**
+   * The "will-navigate" progress listener. This is used to clear the current
+   * eval scope.
+   */
+  _onWillNavigate: function WCA__onWillNavigate()
+  {
+    this._evalWindow = null;
+    this._progressListenerActive = false;
+  },
 };
 
 WebConsoleActor.prototype.requestTypes =
 {
   startListeners: WebConsoleActor.prototype.onStartListeners,
   stopListeners: WebConsoleActor.prototype.onStopListeners,
   getCachedMessages: WebConsoleActor.prototype.onGetCachedMessages,
   evaluateJS: WebConsoleActor.prototype.onEvaluateJS,
--- a/toolkit/devtools/webconsole/test/chrome.ini
+++ b/toolkit/devtools/webconsole/test/chrome.ini
@@ -16,8 +16,9 @@ support-files =
 [test_network_longstring.html]
 [test_network_post.html]
 [test_nsiconsolemessage.html]
 [test_object_actor.html]
 [test_object_actor_native_getters.html]
 [test_object_actor_native_getters_lenient_this.html]
 [test_page_errors.html]
 [test_throw.html]
+[test_jsterm_cd_iframe.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/webconsole/test/test_jsterm_cd_iframe.html
@@ -0,0 +1,154 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<head>
+  <meta charset="utf8">
+  <title>Test for the cd() function</title>
+  <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript;version=1.8" src="common.js"></script>
+  <!-- Any copyright is dedicated to the Public Domain.
+     - http://creativecommons.org/publicdomain/zero/1.0/ -->
+</head>
+<body>
+<p>Test for the cd() function</p>
+
+<script class="testbody" type="text/javascript;version=1.8">
+SimpleTest.waitForExplicitFinish();
+
+let gState;
+
+function startTest()
+{
+  removeEventListener("load", startTest);
+
+  attachConsole([], onAttach, true);
+}
+
+function onAttach(aState, aResponse)
+{
+  top.foobarObject = Object.create(null);
+  top.foobarObject.bug609872 = "parent";
+
+  window.foobarObject = Object.create(null);
+  window.foobarObject.bug609872 = "child";
+
+  gState = aState;
+
+  let tests = [doCheckParent, doCdIframe, doCheckIframe, doCdParent,
+      doCheckParent2];
+  runTests(tests, testEnd);
+}
+
+function doCheckParent()
+{
+  info("check parent window");
+  gState.client.evaluateJS("window.foobarObject.bug609872",
+      onFooObjectFromParent);
+}
+
+function onFooObjectFromParent(aResponse)
+{
+  checkObject(aResponse, {
+    from: gState.actor,
+    input: "window.foobarObject.bug609872",
+    result: "parent",
+  });
+
+  ok(!aResponse.exception, "no eval exception");
+  ok(!aResponse.helperResult, "no helper result");
+
+  nextTest();
+}
+
+function doCdIframe()
+{
+  info("test cd('iframe')");
+  gState.client.evaluateJS("cd('iframe')", onCdIframe);
+}
+
+function onCdIframe(aResponse)
+{
+  checkObject(aResponse, {
+    from: gState.actor,
+    input: "cd('iframe')",
+    result: { type: "undefined" },
+    helperResult: { type: "cd" },
+  });
+
+  ok(!aResponse.exception, "no eval exception");
+
+  nextTest();
+}
+
+function doCheckIframe()
+{
+  info("check foobarObject from the iframe");
+  gState.client.evaluateJS("window.foobarObject.bug609872",
+                           onFooObjectFromIframe);
+}
+
+function onFooObjectFromIframe(aResponse)
+{
+  checkObject(aResponse, {
+    from: gState.actor,
+    input: "window.foobarObject.bug609872",
+    result: "child",
+  });
+
+  ok(!aResponse.exception, "no js eval exception");
+  ok(!aResponse.helperResult, "no helper result");
+
+  nextTest();
+}
+
+function doCdParent()
+{
+  info("test cd() back to parent");
+  gState.client.evaluateJS("cd()", onCdParent);
+}
+
+function onCdParent(aResponse)
+{
+  checkObject(aResponse, {
+    from: gState.actor,
+    input: "cd()",
+    result: { type: "undefined" },
+    helperResult: { type: "cd" },
+  });
+
+  ok(!aResponse.exception, "no eval exception");
+
+  nextTest();
+}
+
+function doCheckParent2()
+{
+  gState.client.evaluateJS("window.foobarObject.bug609872",
+                           onFooObjectFromParent2);
+}
+
+function onFooObjectFromParent2(aResponse)
+{
+  checkObject(aResponse, {
+    from: gState.actor,
+    input: "window.foobarObject.bug609872",
+    result: "parent",
+  });
+
+  ok(!aResponse.exception, "no eval exception");
+  ok(!aResponse.helperResult, "no helper result");
+
+  nextTest();
+}
+
+function testEnd()
+{
+  closeDebugger(gState, function() {
+    gState = null;
+    SimpleTest.finish();
+  });
+}
+
+addEventListener("load", startTest);
+</script>
+</body>
+</html>
--- a/toolkit/devtools/webconsole/utils.js
+++ b/toolkit/devtools/webconsole/utils.js
@@ -1508,16 +1508,50 @@ function JSTermHelpers(aOwner)
    * Opens a help window in MDN.
    */
   aOwner.sandbox.help = function JSTH_help()
   {
     aOwner.helperResult = { type: "help" };
   };
 
   /**
+   * Change the JS evaluation scope.
+   *
+   * @param DOMElement|string|window aWindow
+   *        The window object to use for eval scope. This can be a string that
+   *        is used to perform document.querySelector(), to find the iframe that
+   *        you want to cd() to. A DOMElement can be given as well, the
+   *        .contentWindow property is used. Lastly, you can directly pass
+   *        a window object. If you call cd() with no arguments, the current
+   *        eval scope is cleared back to its default (the top window).
+   */
+  aOwner.sandbox.cd = function JSTH_cd(aWindow)
+  {
+    if (!aWindow) {
+      aOwner.consoleActor.evalWindow = null;
+      aOwner.helperResult = { type: "cd" };
+      return;
+    }
+
+    if (typeof aWindow == "string") {
+      aWindow = aOwner.window.document.querySelector(aWindow);
+    }
+    if (aWindow instanceof Ci.nsIDOMElement && aWindow.contentWindow) {
+      aWindow = aWindow.contentWindow;
+    }
+    if (!(aWindow instanceof Ci.nsIDOMWindow)) {
+      aOwner.helperResult = { type: "error", message: "cdFunctionInvalidArgument" };
+      return;
+    }
+
+    aOwner.consoleActor.evalWindow = aWindow;
+    aOwner.helperResult = { type: "cd" };
+  };
+
+  /**
    * Inspects the passed aObject. This is done by opening the PropertyPanel.
    *
    * @param object aObject
    *        Object to inspect.
    */
   aOwner.sandbox.inspect = function JSTH_inspect(aObject)
   {
     let dbgObj = aOwner.makeDebuggeeValue(aObject);