author | Wes 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 id | 26291 |
push user | kwierso@gmail.com |
push date | Wed, 26 Feb 2014 04:10:11 +0000 |
treeherder | mozilla-central@626d99c084cb [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
milestone | 30.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
30.0a1
/
20140226030202
/
pushlog to previous
nightly linux64
30.0a1
/
20140226030202
/
pushlog to previous
nightly mac
30.0a1
/
20140226030202
/
pushlog to previous
nightly win32
30.0a1
/
20140226030202
/
pushlog to previous
nightly win64
30.0a1
/
20140226030202
/
pushlog to previous
|
browser/metro/base/content/contenthandlers/PluginHelper.js | file | annotate | diff | comparison | revisions |
--- 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 & 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…"> <!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);